# The value pipeline

> The typed conversion every tag shares, from the raw reading to the engineering value, the two invalid-reading policies, the out-of-range actions, the Boolean and String transforms, and how a write reverses the stages.

Every tag shares one typed conversion pipeline that turns the raw reading into the engineering value. The pipeline lives in the tag itself: the Conversion card on the [Tags](connector-tags#the-conversion-card) page and the device grid both show the same stages, and the value that lands in Logic, on dashboards, in the recorder and in what the station publishes is the one that leaves the last stage. The public OPC UA node carries the raw value.

## Raw first {#raw-first}

The pipeline starts from the **raw value** coerced to the tag's data type, not from the driver's own object: a driver that hands 3.7 to an Int32 tag reads 4 at the top of the card, and every stage continues from that 4. A reading the declared type cannot hold at all (text where a number is declared) is an empty reading, never a stand-in zero. The stages compute in real numbers, and at the far end the engineering value takes its type:

| Tag | Engineering value |
| --- | --- |
| Integer data type, Scale and Calibration off, or on at Angular 1 and Linear 0 (how every tag is born) | The whole number itself, in the tag's data type. |
| Integer data type, Scale or Calibration changing the value | A real number (Double), rounded by the Decimals stage: an Int16 tag holding a temperature times ten reads 235 on the device and 23.5 with Scale at Angular 0.1. |
| Float or Double | The tag's data type, so a Float tag rounds to what a float can represent. |

The data type stays what the device and the public OPC UA node carry. Turning Scale or Calibration on or off changes the type of the engineering value from that moment on: what the recorder already recorded stays as recorded, and a channel keeps the type it declares, so an Integer channel records the value rounded to a whole number and a Decimal channel keeps the decimals. The Conversion card closes on the delivered value, and says when an integer type that Scale and Calibration leave as it is rounds away the decimals of a Limits bound.

## The stages {#the-stages}

For numeric types the pipeline is a chain of stages, each with its own checkbox; a disabled stage passes its input through, and the card shows the running value at the end of every stage so you can follow a live reading through the math:

| Stage | Default | What it does |
| --- | --- | --- |
| **Scale** | on | `scaled = raw × Angular + Linear`. Angular defaults to 1, Linear to 0. An Angular of 0 is refused where it is typed ("A scale factor of 0 would zero every reading. Enter the factor to multiply by (1 keeps the value)."), because it would zero every reading and the preview and the persisted pipeline must never disagree. |
| **Calibration** | off | A second linear pair applied to the scaled value: `calibrated = scaled × Angular + Linear`. Kept separate from Scale so an as-found/as-left adjustment does not disturb the range math, and gated by its own permission; see [Calibration](connector-calibration). An Angular of 0 is refused the same way. |
| **Invalid reading** | always shown | Two policies for readings that carry no usable number (below). Not a checkbox: its "off" is itself one of the answers. |
| **Limits** | off | Judges the value against **Min** (default 0) and **Max** (default 100) and applies the chosen out-of-range action (below). The bounds are normalized, so Min above Max never fails: the lower of the two is the floor. |
| **Decimals** | on | Rounds to **Decimals** places, default 2, accepted from 0 to 15. |

Both linear stages carry a **Two-point** assistant that computes the pair from two reference points; it is described step by step on [Calibration](connector-calibration#the-two-point-assistant).

## The device answered without a number {#the-device-answered-without-a-number}

An open thermocouple, a channel with no sensor: the reading arrived, the equipment is talking, but the measurement is NaN. Numeric tags only, and the quality stays good either way:

| Policy | Effect |
| --- | --- |
| **Pass it through** (default) | The NaN travels on untouched and skips the Limits stage entirely, whatever action that stage carries: limits judge numbers, and there is none here. Infinity is not NaN and still saturates on the range. |
| **Replace with a value** | The declared number stands in from this stage on and enters Limits, Decimals and the final coercion like any reading. |

## The device did not answer {#the-device-did-not-answer}

A timeout, a severed cable, equipment switched off, a driver exception. Every type, though a
tag that reads text, a timestamp, an identifier or a byte block is offered only the first two
policies: a declared stand-in is a number, and a reading of those types is not one.

| Policy | Effect |
| --- | --- |
| **Show the error** (default) | The tag reads empty and its quality goes bad, so the failure surfaces everywhere at once. |
| **Hold the last good value** | The last reading that landed stays on display, with no expiry, until the equipment answers again. The timestamps keep pointing at that reading, because that is when the number was measured. Nothing is held before the first reading lands; the tag reads empty then. |
| **Replace with a value** | The declared number is presented as declared: there is no reading to walk through the stages, so it is not scaled, calibrated or rounded, only coerced to the type of the engineering value. Offered on the numeric types and on Boolean, whose reading a number can become; not on String, DateTime, Guid or ByteString. |

A stand-in is never silent: the Live value card prints why ("Last good value, held while the device is not answering."), the live dot turns amber, the embedded OPC UA server publishes the value with a substitute status instead of plain Good, and an agent reading the tag with `tag_read` is told which stand-in it is (`substitution`). It stays published for as long as the policy calls for it: a device that goes on not answering does not make an external client alternate between the stand-in and a read failure, so what that client reads is what the panel shows. The reading itself takes the point back the moment the device answers, and so does turning the stand-in off. Choosing a stand-in is choosing availability, so alarms and expressions go on judging the point as if it had a value; what that costs is paid back in the open, because the journal still records the failed read and device health still reports the device offline. A stand-in answers "what should this point show", never "is the equipment there". A declared replacement outside the enabled limits is warned about where it is typed: "Outside the limits (0 to 100): the value replacing a failed read."

A reading that goes silent counts as a failed read. When no new reading arrives for the tag's poll interval plus 10 seconds, as when a device accepts the connection and then stops answering, the policy above applies as if the read had failed, for everything that reads the tag: Logic expressions, dashboards, what is recorded and agents. With **Show the error** the tag reads empty and its quality goes bad, while the embedded OPC UA server keeps the device's last value on the node under `UncertainLastUsableValue` instead of Good, so an external client sees the number it had and that nothing renews it; with a stand-in, the stand-in is presented and published as above. Alarms hold their state while the quality is bad, as they always do. The Live value card says why ("No new reading has arrived since the application timestamp. The tag counts this as a failed read."), the timestamps keep dating the last reading that landed, and the next reading that lands brings the tag back.

A Write only tag has none of these policies. The station never reads it, so no read of it can fail: until the station writes it, the tag holds no value and says so, with no stand-in, no amber dot and nothing published in its place over OPC UA, and the panel does not draw this row for it. A policy chosen while the tag was read stays stored and is ignored while the tag is Write only; it applies again as soon as the access is Read only or Read and write.

## Out-of-range values {#out-of-range-values}

**Limits** actions for a value outside Min and Max:

| Action | Value becomes | Quality |
| --- | --- | --- |
| **Null** | A gap (no value). | good |
| **Clamp** (default) | The nearest bound. | good |
| **Error and null** | A gap. | bad (limit fault) |
| **Error and clamp** | The nearest bound. | bad (limit fault) |
| **NaN** | NaN. | good. Offered only for Float and Double tags, whose type can represent it; it lets a Process channel classify the invalid value. A NaN action left on an integer tag by an agent write or a restored backup degrades to the Null gap. |

A limit fault folds into the tag's effective quality, so the live dot, property expressions and alarms all see the out-of-limits value as bad with no extra wiring, and the Conversion card prints "Value 130 is outside the limits (0 to 100)." It is still a process condition rather than a connection problem, and [device health](connector-health) does not count the device offline for it.

## Boolean, String and other types {#boolean-string-and-other-types}

Non-numeric types have their own transforms in the same Conversion card:

- **Boolean**: **Invert** reports the logical negation of the raw value.
- **String**: **Substring start** (0-based) and **Length (blank = to end)** cut a window out of the raw string; a blank length runs to the end, and out-of-range bounds are clamped rather than failing.
- Every other type (DateTime, Guid, ByteString) passes through unchanged. Its Conversion card holds the Invalid reading stage alone, with the policy for a device that did not answer, and a Write only tag of those types shows no card.

Showing the error and holding the last good value apply to every type; replacing with a value is
offered on numeric and Boolean tags only, so a String, DateTime, Guid or ByteString tag does not
list it. A replacement already stored on a tag of one of those types, as an earlier version could
save it, is obeyed as showing the error, and the next save of the tag, from the panel or by an
agent, stores showing the error in its place. The NaN policy applies to numeric tags only.

## Writes reverse the pipeline {#writes-reverse-the-pipeline}

The write field on the Live value card takes an engineering value and reverses the enabled, invertible stages in order: clamp to Min and Max when Limits is enabled (a write is always clamped, never dropped, whatever the limit action), de-calibrate (`(value − Linear) ÷ Angular`), de-scale the same way, then coerce to the tag's type. An integer tag whose Scale or Calibration changes the value takes a fraction: 123.4 on an Int16 tag at Angular 0.1 writes 1234. With no such stage, a fraction written to an integer tag is rounded to the nearest whole number, halves away from zero (2.5 writes 3). Rounding is lossy and not reversed. A Boolean input re-applies Invert. A String substring is display-only, so a string write sends the whole value verbatim; DateTime, Guid and ByteString pass through. A value written by an external OPC UA client on the public node is already raw and crosses none of this.
