Auto-prohibited ranges (#148) were previously visible only through an
internal AutoProhibitedRangeCount accessor used by tests. Production
operators had no way to see what the planner had learned without pulling
logs or inspecting driver state.
Changes:
- New public record `ModbusAutoProhibition(UnitId, Region, StartAddress,
EndAddress, LastProbedUtc, BisectionPending)` — operator-facing snapshot
shape. Lives in the addressing assembly's logical namespace alongside
the other public types.
- `ModbusDriver.GetAutoProhibitedRanges()` returns
`IReadOnlyList<ModbusAutoProhibition>` — a copy of the live prohibition
map. Lock-protected snapshot so consumers don't race with the re-probe
loop.
- RecordAutoProhibition tracks first-fire vs re-fire via the dictionary
insert path, leaving a hook to add structured logging once an ILogger
is plumbed through (currently elided to keep the constructor minimal
for testability — a future change can wire ILogger and emit a single
warning per first-fire).
Tests (1 new, additive to the 6 in ModbusCoalescingAutoRecoveryTests):
- GetAutoProhibitedRanges_Surfaces_Operator_Visible_Snapshot — confirms
the snapshot shape: empty before any failure, populated with correct
UnitId/Region/Start/End/BisectionPending after a failed coalesced read,
LastProbedUtc within the recent past.
Docs:
- docs/v2/modbus-addressing.md — new "Coalescing auto-recovery" subsection
consolidates the #148/#150/#151/#152 surface in one place. Documents
the diagnostic accessor + flags the in-process consumption pattern
(Server health endpoints today; Admin UI when an RPC channel exists).
239 + 1 = 240 unit tests green.
Caveat: the Admin UI surfacing (table render, "clear all prohibitions"
button) is intentionally NOT shipped here. Admin can't reach a live
ModbusDriver instance without a driver-diagnostics RPC channel that
doesn't exist yet — that's a larger architectural piece. For now the
data is queryable in-process by the Server's health endpoints; once an
RPC channel lands, Admin can wire the existing GetAutoProhibitedRanges
into a Blazor table without further driver changes.
Web verification (2026-04-25) against current vendor docs surfaced concrete
grammar conflicts in the v1 suffix grammar shipped in #137. Hard cutover
before the Admin UI rolls out widely so users don't paste `:I` from a
Wonderware spreadsheet and silently get wrong-typed reads.
Sources:
- Wonderware DASMBTCP user guide
https://cdn.logic-control.com/media/DASMBTCP.pdf
- Ignition Modbus addressing (8.1)
https://www.docs.inductiveautomation.com/docs/8.1/ignition-modules/opc-ua/opc-ua-drivers/modbus/modbus-addressing
Type-code changes:
| Code | Pre-#146 | Post-#146 | Vendor reference |
|--------|----------|------------|------------------------------|
| `:S` | (n/a) | Int16 | Wonderware DASMBTCP `S` |
| `:US` | (n/a) | UInt16 | Ignition `HRUS` |
| `:I` | Int16 | **Int32** | Wonderware `I` + Ignition `HRI` |
| `:UI` | UInt16 | **UInt32** | Ignition `HRUI` |
| `:I_64` | (n/a) | Int64 | Ignition `HRI_64` |
| `:UI_64` | (n/a) | UInt64 | Ignition `HRUI_64` |
| `:BCD_32`| (n/a) | BCD32 | Ignition `HRBCD_32` |
Codes REMOVED (no clear vendor precedent + conflict with the new mapping):
`:DI`, `:L`, `:UDI`, `:UL`, `:LI`, `:ULI`, `:LBCD`. Pre-#146 configs that
use them get an "Unknown type code" diagnostic at parse time so users get
a fast surface-level error rather than silent wrong-typed reads.
Codes UNCHANGED (already vendor-aligned): `:BOOL`, `:F`, `:D`, `:BCD`,
`:STR<n>`. Modicon 5/6-digit + mnemonic regions (HR/IR/C/DI) + bit suffix
`.N` are also unchanged.
Defaults:
- Coils / DiscreteInputs → `BOOL` (unchanged)
- HoldingRegisters / InputRegisters with no explicit type → Int16 (matches
Ignition's bare `HR` default)
Byte-order mnemonics (`:ABCD` / `:CDAB` / `:BADC` / `:DCBA`) are kept but
documented as OtOpcUa-specific — they aren't in any major vendor's per-tag
address string. Ignition uses a `-R` suffix per prefix; Wonderware
configures word-order at the topic level.
Tests:
- 12 Type_Codes_Parse rows updated to assert the new mappings.
- New Removed_Aliases_Are_Rejected (×7) confirms each pre-#146 alias now
fails fast with "Unknown type code".
- Worked_Example_Int16_Array uses the new `:S` code.
- New Worked_Example_Int32_Array_Via_I_Code documents the `:I = Int32`
vendor-alignment intent so a future "fix" doesn't accidentally regress.
- Unknown_Type_Code_Rejected_With_Catalog updated to match the new error
message ("Valid: BOOL, S, US, I, ...").
Docs:
- docs/v2/modbus-addressing.md — table replaced with the post-#146 codes,
each row cites its Wonderware / Ignition reference. New "Codes removed
in #146" subsection documents the cutover.
- docs/Driver.Modbus.Cli.md — example grammar list updated; explicit
type-code reminder appended.
114 addressing tests + 231 driver tests still green. Solution build clean.
Closes the docs/e2e end of the Modbus addressing line shipped across
#136-#145.
Docs:
- docs/v2/modbus-addressing.md (new) — full grammar reference.
Region+offset (Modicon 5-digit / 6-digit / mnemonic), bit suffix,
type codes (BOOL / I / UI / DI / UDI / LI / ULI / F / D / BCD / LBCD /
STR<n>), all four byte-order mnemonics (ABCD / CDAB / BADC / DCBA),
array-count semantics, family-native syntax (DL205 V/Y/C/X/SP and
MELSEC D/M/X/Y with hex-vs-octal sub-family selection), driver-instance
options (KeepAlive / Reconnect / IdleDisconnect, MaxCoilsPerRead and
FC15/16 forcing, Deadband + WriteOnChangeOnly, MaxReadGap +
CoalesceProhibited, multi-unit IPerCallHostResolver). Includes a worked
JSON DTO example mixing AddressString + structured tag forms.
- docs/Driver.Modbus.Cli.md — appended a "v2 addressing grammar" section
pointing users at the full reference, with quick-reference examples.
- Vendor-compatibility caveat documented: type codes and byte-order
mnemonics were synthesised from training-era vendor docs (Wonderware
DASMBTCP, Kepware KEPServerEX, Ignition, Matrikon, OAS) and should be
verified against current vendor manuals before locking for production.
E2E tests (4 new AddressingGrammarTests in IntegrationTests):
- Modicon 5-digit and 6-digit forms map to identical wire offsets.
- Float32 + WordSwap (CDAB) round-trips end-to-end through the
pymodbus simulator.
- Int16[5] array round-trips as a typed short[] surface.
- Block-read coalescing produces a wire-acceptable PDU when MaxReadGap=5
bridges three nearby tags.
All tests skip gracefully when the pymodbus simulator at localhost:5020
is unreachable (matches the existing ModbusSimulatorFixture pattern).
Final test count across the Modbus addressing surface:
- 107 ModbusAddressing.Tests (parser + family + Modicon)
- 231 Driver.Modbus.Tests (driver, byte order, array, multi-unit, coalescing,
protocol, subscribe, connection options)
- 110 Admin.Tests (incl. ModbusOptionsViewModel defaults pinning)
- 4 new AddressingGrammar integration tests (skip when sim down)