Formula Usage Guide
If you enter an arithmetic expression in the fomula field when registering a tag, the driver post-processes the raw value read from the PLC and uses the converted value for the cache / Sparkplug / REST responses.
| Item | Value |
|---|---|
| Implementation class | plantpulse.app.edge.component.collector.plc.PLCValueFomula |
| Call site | Step 3 of PLCValueReader.read() (raw → format → fomula → cache update) |
| Library | plantpulse-edge-fomula.jar (plantpulse.edge.fomula.FormulaEngine) |
| Variable substitution | ${VALUE} (current tag value) / ${tag_id} (last value of another tag, LastValueMap cache) |
Applicable data_type | Applied only to Float / Double / Integer / Long. Other types (String, Boolean, …) pass through unchanged |
| Result | BigDecimal of Formula.evaluate(...) → rendered per type (integer types as integers, Float/Double as decimals) |
Processing Flow
-
The driver receives the raw string value (e.g.
"16384"). -
PLCValueFomula.fomulaValue(address, value)is called. -
If
address.getFomula()is empty, the value is returned as is. -
If
data_typeis not Float / Double / Integer / Long, the value is returned as is. -
The names referenced by the formula are resolved —
VALUEis the value just read, the rest are the last values of other tags fromLastValueMap. -
If a referenced tag has never been collected, an exception is raised:
Variable value referenced by the formula is not yet in cache : formula=[`<fomula>`], variable=[`<name>`] -
Only the names referenced by the formula are bound to values and evaluated →
BigDecimal→ a string is returned per type.
Variable Types
| Notation | Meaning | Source |
|---|---|---|
${VALUE} | Raw value of the current tag | The value just read by the driver |
${TAG_ID_X} | Last value of another tag | LastValueMap (cache shared by all tags) |
LastValueMap holds the last raw / post-processed value of every tag. You can reference not only other tags in the same OPC but also tags in other OPCs (provided that tag has been collected at least once and exists in the cache).
If you reference ${TAG_ZERO} but TAG_ZERO has never been collected, the value cannot be bound and an exception occurs. Set a shorter timecycle on the referenced tag, or register the referenced tag under auto_collect=true so it is collected first. (The syntax check performed at save time passes in this case — it is a collection-order issue, not a formula error.)
Expression Syntax
The syntax is the same as Excel. Function names and semantics match Excel, so you can port an Excel expression almost verbatim. Case is not significant (IF = if).
| Category | Tokens | Notes |
|---|---|---|
| Arithmetic | +, -, *, /, ^ | ^ is exponentiation |
| Comparison | >, <, >=, <=, =, <> | = is equal, <> is not equal (same as Excel) |
| Grouping | (, ) | Parentheses take precedence |
| Constants | PI, E | Pi, Euler's number |
| Conditional | IF, SWITCH, AND, OR, NOT | IF(조건, 참일때, 거짓일때) |
| Rounding | ROUND, ROUNDUP, ROUNDDOWN, CEILING, FLOOR, INT, TRUNC | ROUND(값, 자리수) |
| Numeric | ABS, SIGN, MOD, POWER, SQRT, CBRT, EXP, FACT | The sign of the remainder in MOD follows the divisor |
| Aggregation | MIN, MAX, SUM, AVERAGE, COALESCE | Multiple arguments |
| Logarithm | LOG, LOG10, LN | LOG = common logarithm (base 10), LN = natural logarithm |
| Trigonometric | SIN, COS, TAN, ASIN, ACOS, ATAN, ATAN2 | Based on radians (same as Excel) |
| Hyperbolic | SINH, COSH, TANH, ASINH, ACOSH, ATANH | |
| Angle conversion | DEGREES, RADIANS | Radians ↔ degrees |
- Bitwise operations (
&,|) - User-defined functions
- String operations / date operations If you need these, handle them in a later stage (external function / post-processing node).
A formula with invalid syntax is rejected at save time (UI save, CSV upload, and backup restore alike). You can enter a sample value in the tag settings modal to preview the result.
Failures during evaluation (division by zero, a referenced value that is not numeric, etc.) are recorded as a collection error and degrade the quality of that tag. A plausible-looking number is never stored in its place.
Variable Notation
Both ${VALUE} and the bare name VALUE are accepted. The same applies to other tags —
${TAG_ZERO} = TAG_ZERO. Existing saved ${...} formulas continue to work.
${VALUE}*0.1 기존 표기
VALUE*0.1 같은 뜻
IF(VALUE>100, 100, VALUE*0.1) 엑셀식
Extended Example Set
1. Simple Scaling (×0.1)
| Item | Value |
|---|---|
data_type | Float |
fomula | ${VALUE}*0.1 |
| Input → Output | 16384 → 1638.4 |
Use case: integer raw → real number with 1 decimal place. For temperature / pressure / flow sensors that send values as integers.
2. Unit Conversion (mV → V)
| Item | Value |
|---|---|
data_type | Float |
fomula | ${VALUE}/1000 |
| Input → Output | 3300 → 3.3 |
3. Offset Correction (Celsius → Kelvin)
| Item | Value |
|---|---|
data_type | Float |
fomula | ${VALUE}+273.15 |
| Input → Output | 25 → 298.15 |
4. Referencing Another Tag (Zero Correction)
| Item | Value |
|---|---|
data_type | Float |
fomula | ${VALUE}-${TAG_ZERO} |
| Input → Output | VALUE=1024, TAG_ZERO=24 → 1000 |
TAG_ZERO is the ID of another tag. Used for zero / tare correction.
5. Polynomial (Square)
| Item | Value |
|---|---|
data_type | Float |
fomula | ${VALUE}*${VALUE}*0.001 |
| Input → Output | 100 → 10.0 |
6. Function — Square Root
| Item | Value |
|---|---|
data_type | Float |
fomula | sqrt(${VALUE}) |
| Input → Output | 144 → 12.0 |
7. Function — Trigonometric (sin, radians)
| Item | Value |
|---|---|
data_type | Float |
fomula | sin(${VALUE}) |
| Input → Output | 1.5708 (≈π/2) → 1.0 |
For degree input, use sin(${VALUE}*pi/180).
8. Multiple Tags — Calibration (gain × x + offset)
| Item | Value |
|---|---|
data_type | Float |
fomula | ${VALUE}*${TAG_GAIN}+${TAG_OFFSET} |
| Input → Output | VALUE=100, GAIN=0.05, OFFSET=2 → 7.0 |
A pattern in which per-device correction coefficients are managed as separate tags (or HTTP-bind tags).
9. Type Conversion / Forced Float
| Item | Value |
|---|---|
data_type | Float |
fomula | ${VALUE}*1.0 |
| Input → Output | 123 → 123.0 |
For when you only want to cast a value received as Integer into Float.
10. Exponentiation / Exponential
| Item | Value |
|---|---|
data_type | Float |
fomula | ${VALUE}^2 |
| Input → Output | 5 → 25.0 |
11. Logarithm
| Item | Value |
|---|---|
data_type | Float |
fomula | log(${VALUE}) |
| Input → Output | 100 → 2.0 |
ln(...) is also available (natural logarithm).
12. Composite — dB Conversion of RMS
| Item | Value |
|---|---|
data_type | Float |
fomula | 20*log(${VALUE}) |
| Input → Output | 1000 → 60.0 |
REST Registration Example (curl)
Registering a single tag:
curl -X POST http://<edge-host>/api/v1/opc/OPC_LS_XBM_0001/tag \
-H "Content-Type: application/json" \
-d '{
"tag_id": "OPC_LS_XBM_0001_TAG_PRESS",
"tag_name": "Pressure (kPa)",
"plc_address": "D00100",
"data_type": "Float",
"format": "REAL",
"fomula": "${VALUE}*0.1",
"description": "스케일 ×0.1 적용"
}'
When registering an OPC together with a set of tags, include fomula inside tag_list[].
Reading a value (the post-processed value is returned):
curl -s http://<edge-host>/api/v1/tag/OPC_LS_XBM_0001_TAG_PRESS/value | jq
# {
# "result": "OK",
# "data": {
# "tag_id": "OPC_LS_XBM_0001_TAG_PRESS",
# "value": "1638.4",
# ...
# }
# }
Common Errors and Fixes
| Message / Symptom | Cause | Fix |
|---|---|---|
계산식에 해당하는 변수값이 아직 캐시에 없습니다 : 계산식=[${VALUE}-${TAG_ZERO}] | The referenced tag (TAG_ZERO) has never been collected | Register the referenced tag first and let it run one cycle at auto_collect=true |
계산식이 참조하는 값이 숫자가 아닙니다 | The referenced tag's value is not numeric (e.g. referencing a tag that is data_type=String) | Change it to reference a numeric-type tag |
Closing brace not found / Missing second operand | Unbalanced parentheses or a missing operand after an operator | Rejected at save time, so the modal's error message pinpoints the position |
| Post-processing is not applied (raw value returned as is) | data_type is String/Boolean | Change it to Float/Double/Integer/Long, or use a separate processing stage if post-processing is required |
Result is always 0 | The raw value really is 0 | Check the raw read value with GET /api/v1/tag/.../value first. (The old parser silently evaluated incomplete expressions such as ${VALUE}* as 0, but such expressions are now rejected at save time.) |
Collection error FORMAT_FAILED + quality degradation | Evaluation failure such as division by 0 or a negative sqrt | Infinity/NaN is not stored; the failure is surfaced as an error instead. Add a guard to the formula — e.g. IF(TAG_ZERO=0, 0, VALUE/TAG_ZERO) |
Behavioral Notes
LastValueMapis a singleton (in-memory). It is cleared when the gateway restarts → references to other tags may fail on the first cycle.- Evaluation uses
BigDecimalthroughout, so large integers inLong/QWordare preserved down to the last digit (no loss as in the olddoubleevaluation). - The result is
Double.toString()→ cached as a string. It is cast back todata_typefor display and Sparkplug publication (see SparkplugDataTypeMapper). - If the formulas of several tags within the same OPC reference one another, the first cycle may partially fail depending on the collection order. This is not a significant operational problem (it works from the next cycle on), but for a clean setup place the referenced tags in a separate OPC with a shorter
timecycle.