This chapter collects the idioms that recur in working plants' configurations, with their codes replaced by invented ones, and the functions that complete them. Plant formulas need few functions: across more than a thousand of them, eleven cover everything — if, evl, to_double, or, to_int, format, not, scale, toLocale, now and clamp.
The examples run against the sample variables. Formulas that read a plant's own variables are shown as plain text.
A signed register in engineering units
A Modbus master gives each register as a whole number from 0 to 65535. A device that sends a signed 16-bit number, such as a temperature below zero, sends a negative one as a value above 32767. Subtract 65536 from those:
REGISTERS[2] → 65535
if(REGISTERS[2] > 32767, REGISTERS[2] - 65536, REGISTERS[2]) → -1.0
if(REGISTERS[0] > 32767, REGISTERS[0] - 65536, REGISTERS[0]) → 16256
if(REGISTERS[2] > 32767, REGISTERS[2] - 65536, REGISTERS[2]) / 10 → -0.1
scale then maps the raw range onto engineering units:
scale(value, rawMin, rawMax, unitMin, unitMax). For a transmitter whose register runs from 0 to 1000 over 0 to
14 bar:
scale(500, 0, 1000, 0, 14) → 7.0
scale(if(REGISTERS[2] > 32767, REGISTERS[2] - 65536, REGISTERS[2]), 0, 1000, 0, 14) → -0.014
scalein(1500, 0, 1000, 0, 14) → 14.0
The plant form reads one register of an array collector's list, here AI_REGS:
scale(if(AI_REGS[4] > 32767, AI_REGS[4] - 65536, AI_REGS[4]), 0, 1000, 0, 14)
scale does not hold its result within the unit range: a reading past the raw range gives a value past the unit
range. scalein holds it within. For values that span two registers, see
Modbus decoding.
Guarding a reading that may not exist yet
After a start, a variable that is neither persistent nor given an initial value exists only once something has
written it. Until a Modbus poll has answered, reading its variable fails, and so does every formula that reads it: see
Never-written variables. evl
gives a default in place of the failure; nvl does not help, because the read
fails before nvl sees a value.
The plant form converts at the same time: evl(to_double(PT_101), 0.0) gives the reading as a decimal, and 0.0
while there is none, or when it holds text that is not a number.
evl(to_double(LEVEL), 0.0) → 42.5
evl(to_double(PT_101), 0.0) → 0.0
nvl(PT_101, 0.0) → error
evl(to_double(TAG), 0.0) → 0.0
evl(PT_101 > 4.5, false) → false
evl(LEVEL != null, false) → true
evl(PT_101 != null, false) → false
- "Is there a reading?" is
evl(PT_101 != null, false):trueonly when the variable exists and holds a value. It gives a widget a grey "no data" state; see Multi-state indicators. - The default is a value like any other. A pressure shown as
0.0because nothing has arrived looks like a real zero. Where the difference matters, show "no data" on its own. - Keep the guarded expression small.
evlhides every error, a misspelt code included, which would then give the default for ever.
Formatting a value for a label
format lays a value out with Java's format specifiers: %.1f for a decimal with one
digit after the point, %d for a whole number, %s for any value. The plant form guards the reading too:
"%.1f bar".format(evl(to_double(PT_101), 0.0))
- The decimal separator of
%ffollows the instance's language,application.locale: a point withen, the default, and a comma withes. See the Configuration reference. A style needs a point whatever the language, so give a style numbers, not formatted text. %drefuses a decimal and%fa whole number. Arithmetic always gives a decimal, so convert with to_int or to_double first.%sand+write a number as the language holds it, always with a point, and without rounding.+joins text only when its left side is text. Start with the text, or convert the number with to_string.
"%d".format(COUNT) → "3"
"%s: %d pumps".format(TAG, COUNT) → "P-101: 3 pumps"
"%d".format(COUNT + 1) → error
"%d".format(to_int(COUNT + 1)) → "4"
"%.1f".format(COUNT) → error
"%s m".format(LEVEL) → "42.5 m"
"Level " + LEVEL + " m" → "Level 42.5 m"
to_string(LEVEL) + " m" → "42.5 m"
LEVEL + " m" → error
"%tT".format(START) → "08:00:00"
"%tF %<tR".format(START) → "2026-09-29 08:00"
%tT writes a time as hours, minutes and seconds, %tF a date, and %tR hours and minutes; %< reuses the previous
argument. To show a time in the viewer's zone, convert it in the widget: "%tT".format(toLocale(TIME)).
A clock for dashboards
A widget's formula runs again when a variable it names changes. now() is not a variable, so a label that reads only
now() does not move by itself: it changes only when something else redraws the view. The plants keep the time in a
variable instead, and let a Scheduler rewrite it:
| Variable | Formula | Why |
|---|---|---|
TIME |
now() |
The current time in UTC, rewritten on every tick |
| Scheduler lists | Cron | Runs |
|---|---|---|
TIME |
* * * * * ? |
Every second. 0 * * * * ?, every minute, is enough for a clock without seconds |
A label then shows it in the viewer's time zone:
"%tT".format(toLocale(TIME))
Every view that shows TIME redraws on each tick, and a redraw evaluates every formula of the view. Tick no more
often than the display needs.
Colour by state or threshold
A widget's styles formula gives a record, and each of its keys becomes a style property, with _ written as -:
stroke_width is stroke-width. Styling widgets has the rules; the plant patterns
are these:
- One
ifper key. Each key chooses its own value from the state. - Every key in every state. Give each key a value in every state, so that no state depends on what the previous one left.
- Colours as text. A style takes a colour as text, such as
"#00AA44". The colour functions give a number, which a style cannot use: write it as text with"#%06X".format(…). - Sizes and opacities as numbers, such as
stroke_width: 2oropacity: 0.5, not as formatted text.
{fill: if(PUMP_ON, "#00AA44", "#BBBBBB"), stroke: if(PUMP_ON, "#006622", "#888888"), stroke_width: 2} → {fill: "#00AA44", stroke: "#006622", stroke_width: 2}
if(LEVEL > SETPOINTS.high, "#DD2222", if(LEVEL < SETPOINTS.low, "#FFAA00", "#00AA44")) → "#00AA44"
rgb(1, 0, 0) → 16711680
"#%06X".format(rgb(1, 0, 0)) → "#FF0000"
"#%06X".format(rgb(0, 0.5, 0)) → "#008000"
A threshold on a plant reading, with its guard, and a colour that follows the level through one of the engine's gradients:
{fill: if(evl(to_double(LIT_301), 0.0) > 4.2, "#DD2222", "#C4CCD8"), stroke: "#52606D", stroke_width: 1}
{fill: "#%06X".format(gradient(red2green, clamp(LIT_301_PCT / 100, 0.0, 1.0)))}
gradient takes the bare name of a gradient, white2black, rainbow,
black2green, black2red, red2gray2green, red2green or red2white2green, and a value from 0 to 1. A name the
engine does not provide fails when the formula runs, with Unknown gradient 'NAME'.
Multi-state indicators
For several boolean signals, nest the ifs with the most urgent state first. A signal that may not exist yet goes
through evl:
if(evl(P_101_TRIP, false), "#DD2222", if(PUMP_ON, "#00AA44", "#C4CCD8")) → "#00AA44"
or(PUMP_ON, COUNT > 5) → true
bit(REGISTERS[3], 0) → true
bit(REGISTERS[3], 1) → false
The plant form of a pump symbol adds a grey state for "no data" in front of the others:
{fill: if(not(evl(P_101_RUN != null, false)), "#D9DEE4", if(evl(P_101_TRIP, false), "#D62828", if(P_101_RUN, "#00AA44", "#9AA5B1"))), stroke: "#52606D", stroke_width: 1}
A packed status word from a device is split with bit, counting from bit 0, the least
significant: {fill: if(bit(P_101_STATUS, 3), "#DD2222", "#C4CCD8")}.
For a numeric state code, case is a lookup table: the code, then pairs of candidate and
result, then a default. It matches only a value of the same kind, and a whole number never matches a decimal. A code
read from a Modbus register is a whole number, but one that a formula computes with arithmetic, or that a dashboard's
number field writes, is a decimal: convert it with to_int.
case(COUNT, 0, "#9AA5B1", 1, "#00AA44", 2, "#FFAA00", 3, "#DD2222", "#D9DEE4") → "#DD2222"
case(COUNT + 0, 3, "#DD2222", "#D9DEE4") → "#D9DEE4"
case(to_int(COUNT + 0), 3, "#DD2222", "#D9DEE4") → "#DD2222"
Decimals and whole numbers
A number written without a point is a whole number, and every result of +, -, *, / and ^ is a decimal (see
Arithmetic). Most functions take either. Three places care:
| Where | What happens | Write |
|---|---|---|
format |
%d refuses a decimal, %f a whole number |
to_int or to_double first |
case |
A whole number never matches a decimal | to_int on the code |
clamp, max, min |
They give back the chosen argument as it was, so a whole-number limit comes back whole | Limits written as decimals |
COUNT + 1 → 4.0
type_of(COUNT + 1) → "Double"
clamp(LEVEL, 0, 40) → 40
clamp(LEVEL, 0.0, 40.0) → 40.0
clamp(LEVEL, 0.0, 100.0) → 42.5
clamp(evl(to_double(PT_101), 0.0), 0.0, 10.0) → 0.0
The plant form writes every argument as a decimal: clamp(evl(to_double(PT_101), 0.0), 0.0, 10.0). The result is
then a decimal whichever limit applies, and a label that formats it with %.1f always works. A variable whose Type is
double also stores a whole-number result as a decimal.
Names
The names the engine and the dashboards use for themselves, ORCHESTER, E, PI, DEBUG, _VALUE_, _VIEW_ and
_CALLER_, are listed in Names to avoid, with the names loops bind. Beyond
those:
- A code a formula can read is made of the letters A to Z, digits and underscores, and does not start with a
digit.
P-101reads asPminus101. true,falseandnullare lower case. Written in capitals,TRUEis a name, and reads a variable of that code.- In a panel, parameters are replaced as text. A parameter named like a style key, such as
FILLorSTROKE, is replaced inside the panel's style records too, and a parameter namedEorPIhides the constant. Give parameters names that nothing else in the panel uses; see Regions and panels.
PI → 3.141592653589793
TRUE → error
P-101 → error
Spaces and line breaks
Spaces around operators are a matter of taste: a formula compiles the same, and as quickly, with or without them. A formula may also break across lines anywhere between two names, numbers or symbols, which keeps a long style record readable:
LEVEL+1 → 43.5
LEVEL + 1 → 43.5
LEVEL * 2 → 85.0
1 + 2 * 3 → 7.0
{
fill: if(PUMP_ON, "#00AA44", "#BBBBBB"),
stroke: if(PUMP_ON, "#006622", "#888888"),
stroke_width: 2
}
Next steps
This page describes Amtiri Script 5.4.1 as shipped with Data Orchester engine 6.12.1.