Merge lane Modbus: Modbus driver remediation (#741)

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 16:53:49 -04:00
41 changed files with 4156 additions and 390 deletions
+52 -4
View File
@@ -149,7 +149,11 @@ Beyond per-tag addressing, `ModbusDriverOptions` exposes (#139–#143):
- `UseFC15ForSingleCoilWrites` — force FC15 (write multiple coils
qty=1) for single-coil writes. Safety/audit PLCs may require this.
- `UseFC16ForSingleRegisterWrites` — same for FC16 vs FC06.
- `DisableFC23` — kill switch for FC23 (currently unused; reserved).
- `UseFC23ForMultiRegisterWrites` (default `false`) — opt-in: a 2–121-register
holding write is sent as one FC23 (write + read-back). A unit answering
exception 01 moves to FC16 until the next re-init. Without the key the
wire stays FC16 (#741).
- `DisableFC23` — hard kill switch: never send FC23, even when opted in.
### Subscribe (#141)
- Per-tag `Deadband` — suppress sub-threshold publishes on numeric tags.
@@ -165,9 +169,13 @@ Beyond per-tag addressing, `ModbusDriverOptions` exposes (#139–#143):
this tag in isolation regardless of `MaxReadGap`).
### Block-read coalescing (#143)
- `MaxReadGap` (default 0 = off) — gap budget the planner is willing to
bridge between adjacent register tags. With `MaxReadGap=10`, three tags
at HR 100/102/110 collapse into one FC03 of quantity 11.
- `MaxReadGap` — gap budget the planner is willing to bridge between
adjacent tags of one (unit, region). With `MaxReadGap=10`, three tags at
HR 100/102/110 collapse into one FC03 of quantity 11. An explicit `0` =
coalescing off (its historical meaning); the default `8` applies only when
the key is absent (#741). Coils / DiscreteInputs (gap in bits), `String`
scalars and same-word `BitInRegister` tags coalesce too.
- `DisableReadCoalescing` — turn coalescing off regardless of the gap.
### Coalescing auto-recovery (#148 / #150 / #151 / #152)
- A coalesced read that fails with a Modbus exception (write-only or
@@ -231,3 +239,43 @@ the current Kepware "Modbus Ethernet Driver Help" PDF and Ignition's
"Modbus Addressing" user-manual page — if a critical tool's mnemonics
have shifted, add aliases in `ModbusAddressParser.TryParseType` rather
than asking users to rewrite spreadsheets.
## Address strings, families and region/type rules (#741)
- **One authority: `ModbusTagAddressResolution` (Driver.Modbus.Contracts).** The
driver's tag reader, the deploy gate (`Inspect`), the AdminUI editor model
(load / save / `Validate` / the hint text) and the CSV import/export all ask
it which part of a blob is the address; none restates the rule. Its
agreement is pinned by one table-driven test that runs every persisted shape
through every consumer (`ModbusAddressAuthorityAgreementTests`).
- **Whenever a tag carries both `region` and `address`, those keys are its
address** — a classic tag, an editor-pinned tag, and a pre-#741 row that
carried an `addressString` beside its keys (the old driver never read the
string) all load from the keys, exactly as before, and a family change never
remaps them. Only a **string-only** blob (an `addressString` with NO
`region`/`address` — CSV / API authoring) is resolved through
`ModbusAddressParser` with the **driver's** `Family` / `MelsecSubFamily`; a
family that cannot parse it refuses the tag (BadNodeIdUnknown). It is never
served as HR[0].
- `"addressResolved": true` is editor display / CSV state only: it never makes
keys authoritative, and a marker on a blob WITHOUT keys is ignored (the blob
is string-only) and dropped on the next editor save.
- The AdminUI never writes default keys it did not resolve: the editor model
stamps `region`/`address`/type only when the loaded blob had the keys, or when
a field is set (hand edit, CSV address cell, Resolve). A string-only,
non-string-`addressString` or address-less blob keeps its keys unwritten, and
its CSV export leaves the address cells blank.
- The editor's "Resolve with driver family" persists the resolved
`region`/`address`/type fields plus `"addressResolved": true` (the pinned
state). Editing a resolved field by hand in the editor drops the string and
the marker. A typed CSV address cell keeps them only while the string still
describes the cells under some family (so an unchanged export → import of a
pinned row is lossless); a contradicting cell drops them, and the import
review grid reports that column as an override.
- Coils / DiscreteInputs carry only Bool. A bit-region tag persisted with a
register `dataType` (the pre-#741 editor saved `Int16`) is coerced to Bool,
never refused. A register region with `Bool` is refused (use
`BitInRegister`).
- Exception 01/02/03 are device refusals: per-tag Bad, one Warning per tag,
no driver Degraded; reads and writes share one comms-fault health slot.