Auto: s7-b2 — block-read coalescing for contiguous DBs

Closes #293
This commit is contained in:
Joseph Doherty
2026-04-25 21:23:06 -04:00
parent 5432c49364
commit 17faf76ea7
7 changed files with 976 additions and 11 deletions

View File

@@ -450,6 +450,104 @@ Test names:
- **ET 200SP CPU (1510SP / 1512SP)**: behaves as S7-1500 from `MB_SERVER`
perspective. No known deltas [3].
## Performance (native S7comm driver)
This section covers the native S7comm driver (`ZB.MOM.WW.OtOpcUa.Driver.S7`),
not the Modbus-on-S7 quirks above. Both share a CPU but use different ports,
different libraries, and different optimization levers.
### Block-read coalescing
The S7 driver runs a coalescing planner before every read pass: same-area /
same-DB tags are sorted by byte offset and merged into single
`Plc.ReadBytesAsync` requests when the gap between them is small. Reading
`DB1.DBW0`, `DB1.DBW2`, `DB1.DBW4` issues **one** 6-byte byte-range read
covering offsets 0..6, sliced client-side instead of three multi-var items
(let alone three individual `Plc.ReadAsync` round-trips). On a 50-tag
contiguous workload this reduces wire traffic from 50 single reads (or 3
multi-var batches at the 19-item PDU ceiling) to **1 byte-range PDU**.
#### Default 16-byte gap-merge threshold
The planner merges two adjacent ranges when the gap between them is at most
16 bytes. The default reflects the cost arithmetic on a 240-byte default
PDU: an S7 request frame is ~30 bytes and a per-item response header is
~12 bytes, so over-fetching 16 bytes (which decode-time discards) is
cheaper than paying for one extra PDU round-trip.
The math also holds for 480/960-byte PDUs but the relative cost flips —
on a 960-byte PDU you can fit a much larger request and the over-fetch
ceiling is less of a concern. Sites running the extended PDU on S7-1500
can safely raise the threshold (see operator guidance below).
#### Opaque-size opt-out for STRING / array / structured-timestamp tags
Variable-width and header-prefixed tag types **never** participate in
coalescing:
- **STRING / WSTRING** carry a 2-byte (or 4-byte) length header, and the
per-tag width depends on the configured `StringLength`.
- **CHAR / WCHAR** are routed through the dedicated `S7StringCodec` decode
path, which expects an exact byte slice, not an offset into a larger
buffer.
- **DTL / DT / S5TIME / TIME / TOD / DATE-as-DateTime** route through
`S7DateTimeCodec` for the same reason.
- **Arrays** (`ElementCount > 1`) carry a per-tag width of `N × elementBytes`
and would silently mis-decode if the slice landed mid-block.
Each opaque-size tag emits its own standalone `Plc.ReadBytesAsync` call.
A STRING in the middle of a contiguous run of DBWs will split the
neighbour reads into "before STRING" and "after STRING" merged ranges
without straddling the STRING's bytes — verified by the
`S7BlockCoalescingPlannerTests` unit suite.
#### Operator tuning: `BlockCoalescingGapBytes`
Surface knob in the driver options:
```jsonc
{
"Host": "10.0.0.50",
"Port": 102,
"CpuType": "S71500",
"BlockCoalescingGapBytes": 16, // default
// ...
}
```
Tuning guidance:
- **Raise the threshold (32-64 bytes)** when the PLC has chatty firmware
(S7-1200 with default 240-byte PDU and many DBs scattered every few
bytes). One fewer PDU round-trip beats over-fetching a kilobyte.
- **Lower the threshold (4-8 bytes)** when DBs are sparsely populated
with hot tags far apart — over-fetching dead bytes wastes the PDU
envelope and the saved round-trip never materialises.
- **Set to 0** to disable gap merging entirely (only literally adjacent
ranges with `gap == 0` coalesce). Useful as a debugging knob: if a
driver is misreading values you can flip the threshold to 0 to confirm
the slice math isn't the culprit.
- **Per-DB tuning isn't supported yet** — the knob is global per driver
instance. If a site needs different policies for two DBs they live in
different drivers (different `Host:Port` rows in the config DB).
#### Diagnostics counters
The driver surfaces three coalescing counters via `DriverHealth.Diagnostics`
under the standard `<DriverType>.<Counter>` naming convention:
- `S7.TotalBlockReads` — number of `Plc.ReadBytesAsync` calls issued by
the coalesced path. A fully-coalesced contiguous workload bumps this
by 1 per `ReadAsync`.
- `S7.TotalMultiVarBatches` — `Plc.ReadMultipleVarsAsync` batches issued
for residual singletons that didn't merge. With perfect coalescing this
stays at 0.
- `S7.TotalSingleReads` — per-tag fallbacks (strings, dates, arrays,
64-bit ints, anything that bypasses both the coalescer and the packer).
Observe via the `driver-diagnostics` RPC (`/api/v2/drivers/{id}/diagnostics`)
or the Admin UI's per-driver dashboard.
## References
1. Siemens Industry Online Support, *Modbus/TCP Communication between SIMATIC S7-1500 / S7-1200 and Modbus/TCP Controllers with Instructions `MB_CLIENT` and `MB_SERVER`*, Entry ID 102020340, V6 (Feb 2021). https://cache.industry.siemens.com/dl/files/340/102020340/att_118119/v6/net_modbus_tcp_s7-1500_s7-1200_en.pdf