100
docs/v2/s7.md
100
docs/v2/s7.md
@@ -668,6 +668,106 @@ tick rate). Tests can call the internal helpers `S7Driver.GetPartitionCount`
|
||||
and `S7Driver.GetPartitionSummary` to inspect the resolved partitioning of
|
||||
a live subscription handle.
|
||||
|
||||
### Deadband / on-change
|
||||
|
||||
Before PR-S7-C4 the subscription poll loop emitted `OnDataChange` whenever
|
||||
the freshly-read value differed from the last cached one — a strict
|
||||
`!Equals(prev, current)` test. That's correct for booleans and discrete
|
||||
state, but for analog tags (Float32 / Float64 / scaled integer set-points)
|
||||
it floods the OPC UA subscription queue with insignificant noise: the last
|
||||
counts of an ADC's least-significant-bit jitter, sub-percent setpoint drift,
|
||||
sensor-grade flutter on a flow rate. PR-S7-C4 lets the operator configure
|
||||
**per-tag deadband thresholds** so the driver suppresses uninteresting
|
||||
publishes at source, before they cross the OPC UA boundary.
|
||||
|
||||
Two knobs, both optional, both per-tag:
|
||||
|
||||
- `DeadbandAbsolute` (`double?`) — minimum value change in raw units.
|
||||
Suppress when `|new - prev| < DeadbandAbsolute`.
|
||||
- `DeadbandPercent` (`double?`, 0..100) — minimum value change as a
|
||||
percentage of the previous published value. Suppress when
|
||||
`|new - prev| < |prev| * DeadbandPercent / 100`.
|
||||
|
||||
When both knobs are set the filters are **OR'd** — the value publishes if
|
||||
**either** threshold says publish. This matches Kepware's documented
|
||||
"either threshold triggers" semantics and mirrors the AbLegacy driver's
|
||||
shipped behaviour for cross-driver consistency.
|
||||
|
||||
#### JSON config example
|
||||
|
||||
```json
|
||||
{
|
||||
"Host": "10.0.0.50",
|
||||
"Tags": [
|
||||
{ "Name": "BoilerPressure", "Address": "DB1.DBD0", "DataType": "Float32",
|
||||
"DeadbandAbsolute": 0.5 },
|
||||
|
||||
{ "Name": "FlowRate", "Address": "DB1.DBD4", "DataType": "Float32",
|
||||
"DeadbandPercent": 1.0 },
|
||||
|
||||
{ "Name": "Temperature", "Address": "DB1.DBD8", "DataType": "Float32",
|
||||
"DeadbandAbsolute": 0.1, "DeadbandPercent": 0.5 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`BoilerPressure` only republishes after a 0.5-bar change; `FlowRate` only
|
||||
when the rate moves by more than 1% of its last published value;
|
||||
`Temperature` whenever **either** `0.1 °C absolute` **or** `0.5% of last`
|
||||
is satisfied.
|
||||
|
||||
#### Edge cases
|
||||
|
||||
- **First sample.** `PollOnceAsync` gates `forceRaise` and the
|
||||
no-prior-value case ahead of the deadband filter — the first sample for
|
||||
a tag always publishes (otherwise an OPC UA subscription would never see
|
||||
an initial-data push).
|
||||
- **Status-code change.** Any transition in the OPC UA `StatusCode` channel
|
||||
(`Bad → Good`, `Good → Bad`, etc.) bypasses deadband and publishes,
|
||||
because quality is a semantically different signal from value.
|
||||
- **Non-numeric types.** `String` / `WString` / `Char` / `WChar` /
|
||||
`DateTime` / byte-array tags ignore deadband entirely and keep the
|
||||
legacy `!Equals` semantics. Configuring `DeadbandAbsolute` on a
|
||||
`String` tag is harmless — the filter just doesn't engage.
|
||||
- **`NaN` samples.** If either `prev` or `current` is `NaN`, the filter
|
||||
publishes. NaN never equals NaN; treating it as "changed" surfaces the
|
||||
degenerate float to the client rather than hiding it.
|
||||
- **`±Infinity` samples.** Same rationale as NaN — degenerate values are
|
||||
always published, never deadbanded.
|
||||
- **Sign flip.** A tag swinging `+10 → -10` produces `|delta|=20`; the
|
||||
deadband math operates on the **absolute** delta so a sign flip with
|
||||
`DeadbandAbsolute=1` always publishes. This is the right answer for
|
||||
bidirectional set-points (positive / negative torque, valve-direction
|
||||
flags encoded as signed scalars).
|
||||
- **Near-zero baseline (`|prev| < 1e-6`).** A percent threshold against a
|
||||
zero or near-zero baseline diverges (any tiny change is "infinity
|
||||
percent"), so the driver falls back to absolute when `|prev| < 1e-6`:
|
||||
- If `DeadbandAbsolute` is also configured, that threshold takes over.
|
||||
- If only `DeadbandPercent` is set (no absolute fallback), the sample
|
||||
publishes — there's no usable threshold and silently dropping changes
|
||||
against a near-zero baseline would mask a genuine signal.
|
||||
|
||||
The `1e-6` cutoff is a deliberately conservative floor: floats below
|
||||
`~1e-7` are already in denormal-precision territory; anything above
|
||||
`~1e-6` carries enough magnitude that `|prev| * pct / 100` produces a
|
||||
meaningful threshold.
|
||||
|
||||
#### Implementation notes
|
||||
|
||||
- The filter is the pure-function helper `S7Driver.ShouldPublish(tag,
|
||||
prev, current)`. It's exposed at `internal` scope so unit tests can
|
||||
drive every decision branch (NaN, ±Inf, sign flip, near-zero baseline,
|
||||
both-set OR semantics) without spinning up a partition or poll loop.
|
||||
- `LastValues` continues to cache the **last published** snapshot, not
|
||||
the last polled one. After a deadband suppression the next sample
|
||||
compares against the cached (previously published) value, so a slow
|
||||
drift that never crosses the threshold in any single tick still gets
|
||||
caught the moment cumulative drift exceeds the threshold.
|
||||
- Deadband is a **publish-time** filter, not a wire-level one — every
|
||||
configured tag is still read every tick, the filter only decides
|
||||
whether to invoke `OnDataChange`. The mailbox / PDU / coalescing path
|
||||
is untouched.
|
||||
|
||||
## TSAP / Connection Type
|
||||
|
||||
S7comm runs on top of ISO-on-TCP (RFC 1006), and the COTP connection-request
|
||||
|
||||
Reference in New Issue
Block a user