// tags & structured text

The Modbus register
your meter doesn't have

A three-phase meter will happily give you line voltage, current and power factor. Ask it for kilowatts and there is no register to ask — the number was never stored, so no amount of polling will find it. ModbusManager Pro lets you name the registers you do have, compute the one you want in IEC 61131-3 Structured Text, and then treat the result as if the device had reported it all along.

Download free trial See pricing

Windows 10/11 · Pro edition · one-time license · 14-day trial

// five minutes, on a real meter

Watch it happen

ModbusManager Pro Tags tab: eight tags with live values, a Structured Text script computing kilowatts, and the script log showing the two values it wrote ▶ 5:34
Naming registers, the 32-bit energy counter read as one number instead of two, and a Structured Text loop producing kilowatts from a meter that reports none.

Nothing is requested from YouTube until you press play.

// the two kinds of tag

A name instead of an address

A register map is a list of numbers, and numbers are a poor thing to build on. Holding register 3928 on slave 12 is correct and unmemorable; Line_Voltage is neither ambiguous nor forgettable. Tags put a name on the address once, and everything downstream — scripts, dashboard elements, the Historian, alarms — refers to the name.

// REG

Register tag. Bound to a real register by slave ID, register type and address. Its value tracks the live device, and it is read-only as far as a script is concerned — it is whatever the last poll returned.

// INT

Internal tag. Exists only inside the tool. It holds a computed or stored value and is normally written by a Structured Text script. This is where the kilowatts end up.

The division is the whole model: scripts read REG tags and write INT tags. An internal tag is then indistinguishable from a real one everywhere else in the application, which is the point — you can put computed kilowatts on a dashboard gauge, log them to the Historian, and raise an alarm on them, exactly as if the meter had a kW register.

// the example everybody needs first

Kilowatts from a meter that reports none

Three registers you have, one number you want. Power is not stored on most meters because it is derivable, and the derivation is cheap — so the meter's designer left it to you.

Name the three inputs as REG tags, add Power_kW and Overload as INT tags, set a loop interval, and start the loop. From that moment Power_kW updates like any polled value.

Note the two operators that catch out anyone arriving from C or JavaScript: := assigns, a single = compares. That is correct Structured Text and the interpreter follows it, so A = B in an IF is a comparison, not a mistake.

(* Three-phase power the meter *) (* never reports. V is line-to-line, *) (* PF is 0..1, not a percentage. *) Power_kW := 1.732 * Line_V * Amps_Avg * PF / 1000.0; (* Latch an overload flag with a *) (* deadband so it cannot chatter. *) IF Power_kW > 75.0 THEN Overload := TRUE; ELSIF Power_kW < 60.0 THEN Overload := FALSE; END_IF;

The ELSIF at 60 rather than 75 is deliberate: a single threshold on a value that hovers around it produces an alarm that turns on and off several times a minute. Two thresholds with a gap between them is the same deadband idea as in logging intervals and deadbands, applied to a boolean.

// what the interpreter actually does

The supported syntax, in full

Structured Text is a large language and this is a subset of it. Publishing the exact boundary is more useful than implying the whole standard is there and letting you find the edge with a script that silently fails, so here is the complete list.

ConstructStatus
(* comment *)✓ supported
:=✓ assignment
=✓ equality (not assignment)
<>   <=   >=   <   >✓ comparisons
TRUE   FALSE✓ supported
AND   OR   NOT✓ supported
IF / THEN / ELSIF / ELSE / END_IF✓ the full chain
FOR … TO … DO / END_FOR✓ supported
WHILE / END_WHILE— not translated
REPEAT / UNTIL / END_REPEAT— not translated
CASE / OF / END_CASE— not translated
MOD   XOR— not translated
FOR … BY <step>— step not translated
// the escape hatch

The script box also accepts plain JavaScript, and the two mix freely in one script because Structured Text is translated into JavaScript before it runs. So a lower-case while loop, a switch, the % operator and the whole Math library all work, even though their ST spellings do not. If you need MOD, write %. If you need CASE, write switch. That is also why the reserved-word list below includes JavaScript keywords as well as ST ones.

// what happens when a script misbehaves

A runaway loop cannot take the tool with it

Every script runs in a separate worker thread with a 2000 ms timeout. A loop whose exit condition is wrong gets terminated and reported as a timeout, and polling, the dashboard, the Historian and the user interface carry on untouched.

This is worth stating plainly because the obvious implementation does not behave that way. Running user script on the main thread means a bad loop blocks everything: polling stops, the connection to the interface dies, and the only way out is killing the process. Moving the script onto its own thread is what makes an endless loop a message in the log instead of a lost session.

Dangerous globals are also hidden from the script, which stops a formula from reaching out into the operating system by accident. Being straight about the limit: that is protection against mistakes, not a security sandbox, and it is not a reason to paste in a script from a stranger.

# what you get back timeout Script timed out after 2000 ms - check for a loop that never exits # and the line number, because a # stack trace of generated JS is # no use for finding ST errors error line 7
// one rule that is stricter than it looks

Why some tag names are refused

A tag name becomes a variable name in the compiled script, so it has to be a valid identifier: no spaces, no hyphens, not starting with a digit, and not one of the reserved words. Line Voltage and 3PH-Amps are rejected; Line_Voltage and Amps_3PH are fine.

The reserved list covers Structured Text keywords (AND, TRUE, END_IF…), JavaScript keywords, and a set of global names the sandbox hides. The reason the rule is enforced at the point of naming rather than at the point of running is worth knowing: a colliding name does not break the one script that uses it, it breaks every script. Compilation fails for all of them at once, and the resulting error points at generated code rather than anything you typed. Refusing the name is much kinder than explaining that later.

// frequently asked questions

Tags and Structured Text: common questions

How do I calculate power from Modbus registers when the meter doesn't report it?+
Name the registers you do have as tags, then write a Structured Text script that computes the value into an internal tag. A three-phase meter that reports line voltage, current and power factor gives you kilowatts as 1.732 * V * I * PF / 1000. The internal tag then behaves like any other tag: it can be shown on a dashboard, logged by the Historian and used in an alarm.
Which Structured Text constructs are supported?+
Comments in (* *), assignment with :=, equality with a single = and inequality with <>, the comparison operators, TRUE and FALSE, AND, OR and NOT, the full IF / THEN / ELSIF / ELSE / END_IF chain, and FOR … TO … DO … END_FOR. WHILE, REPEAT, CASE, MOD and XOR are reserved words but are not translated, so they will not run as Structured Text. The script box also accepts plain JavaScript, which is the practical way to use a while loop or a switch.
What is the difference between a register tag and an internal tag?+
A register tag (REG) is bound to a real register by slave ID, register type and address, and its value tracks the live device. An internal tag (INT) exists only inside the tool and holds a computed or stored value, normally written by a Structured Text script. Scripts read register tags and write internal tags.
Can a script with an endless loop freeze the application?+
No. Scripts run in a separate worker thread with a 2000 ms timeout. A loop that never exits is terminated and reported as a timeout, while polling, the dashboard and the rest of the application keep running. This was a real failure mode in an earlier design where the script ran on the main thread and a bad loop hung the whole tool.
Why won't it let me name a tag AND, TRUE or process?+
Tag names are used directly as variable names in the compiled script, so a name that collides with a Structured Text keyword or with a hidden global breaks compilation — and it breaks every script, not only the one using that tag. Names must be valid identifiers: no spaces or hyphens, not starting with a digit, and not a reserved word.
How often does the script run?+
You set a loop interval in milliseconds and start the loop. The minimum is 100 ms; anything shorter is clamped. Pick an interval that matches how fast the underlying registers are actually polled — running the script ten times between two polls just recomputes the same inputs.
// where the computed value goes next

Once the number exists

An internal tag is a first-class value everywhere else in the tool. The usual next steps:

Put it on a dashboard Log it to the Historian Alarm on it Export to CSV Manual: Tags & scripting

Compute the missing register

Tags and Structured Text are part of the Pro edition. The trial is the full thing for 14 days — long enough to find out whether the number you need was derivable all along.

Download free trial See pricing