Adds the full Wonderware/Kepware/Ignition-style address suffix grammar so
users paste tag spreadsheets without per-tag manual translation:
<region><offset>[.<bit>][:<type>[<len>]][:<order>][:<count>]
Examples that now parse end-to-end:
40001 HoldingRegisters[0], Int16
400001 same, 6-digit form
40001.5 bit 5 of HR[0]
40001:F Float32 (HR[0..1])
40001:F:CDAB word-swapped Float32
40001:STR20 20-char ASCII string
HR1:DI Int32 via mnemonic region
C100 Coils[99] (mnemonic)
40001:F:5 Float32[5] array (3-field shorthand)
40001:I:CDAB:10 Int16[10] word-swapped (4-field strict)
Driver-side plumbing:
- ModbusAddressParser + ParsedModbusAddress in the shared Addressing
assembly. 91 parser tests (every grammar variant + malformed shapes).
- ModbusDataType / ModbusByteOrder moved to shared (with the same namespace
so callers compile unchanged). ModbusByteOrder gains ByteSwap (BADC) and
FullReverse (DCBA) alongside the existing BigEndian (ABCD) and WordSwap
(CDAB).
- NormalizeWordOrder extended to honor all four orders for both 4-byte and
8-byte values. Old WordSwap behavior preserved bit-for-bit.
- ModbusTagDefinition gains optional ArrayCount.
- ReadOneAsync / WriteOneAsync handle array fan-out: one FC03/04 read covers
N consecutive register-typed elements, decoded into a typed array (short[],
float[], etc.). Coil arrays use FC01 reads + FC15 writes (FakeTransport
in tests gains FC15 support to match).
- DriverAttributeInfo IsArray / ArrayDim flow from ArrayCount so the OPC UA
address space surfaces ValueRank=1 + ArrayDimensions to clients.
- ModbusDriverFactoryExtensions gains AddressString DTO field. When
present, the parser drives Region/Address/DataType/ByteOrder/Bit/
StringLength/ArrayCount; structured fields (Writable, WriteIdempotent,
StringByteOrder) still come from the DTO. Existing structured tag rows
keep working unchanged.
Tests: 91 parser unit tests (Driver.Modbus.Addressing.Tests, all green) +
204 driver tests including new ModbusByteOrderTests (BADC/DCBA roundtrips
across Int32/Float32/Float64) and ModbusArrayTests (Int16[5], Float32[3]
CDAB, Coil[10], length-mismatch error, IsArray/ArrayDim discovery).
Solution-wide build clean.
Caveat: grammar names (type codes, byte-order mnemonics, the :count
shorthand) were synthesized from training-era vendor docs. Verify against
current Kepware Modbus Ethernet Driver Help and Ignition Modbus Addressing
manuals before freezing for production deployments — naming may need a
back-compat layer if vendor wording has shifted.
Per-tag opt-in for write-retry per docs/v2/plan.md decisions #44, #45, #143.
Default is false — writes never auto-retry unless the driver author has marked
the tag as safe to replay.
Core.Abstractions:
- DriverAttributeInfo gains `bool WriteIdempotent = false` at the end of the
positional record (back-compatible; every existing call site uses the default).
Driver.Modbus:
- ModbusTagDefinition gains `bool WriteIdempotent = false`. Safe candidates
documented in the param XML: holding-register set-points, configuration
registers. Unsafe: edge-triggered coils, counter-increment addresses.
- ModbusDriver.DiscoverAsync propagates t.WriteIdempotent into
DriverAttributeInfo.WriteIdempotent.
Driver.S7:
- S7TagDefinition gains `bool WriteIdempotent = false`. Safe candidates:
DB word/dword set-points, configuration DBs. Unsafe: M/Q bits that drive
edge-triggered program routines.
- S7Driver.DiscoverAsync propagates the flag.
Stream A.5 integration tests (FlakeyDriverIntegrationTests, 4 new) exercise
the invoker + flaky-driver contract the plan enumerates:
- Read with 5 transient failures succeeds on the 6th attempt (RetryCount=10).
- Non-idempotent write with RetryCount=5 configured still fails on the first
failure — no replay (decision #44 guard at the ExecuteWriteAsync surface).
- Idempotent write with 2 transient failures succeeds on the 3rd attempt.
- Two hosts on the same driver have independent breakers — dead-host trips
its breaker but live-host's first call still succeeds.
Propagation tests:
- ModbusDriverTests: SetPoint WriteIdempotent=true flows into
DriverAttributeInfo; PulseCoil default=false.
- S7DiscoveryAndSubscribeTests: same pattern for DBx SetPoint vs M-bit.
Full solution dotnet test: 947 passing (baseline 906, +41 net across Stream A
so far). Pre-existing Client.CLI Subscribe flake unchanged.
Stream A's remaining work (wiring CapabilityInvoker into DriverNodeManager's
OnReadValue / OnWriteValue / History / Subscribe dispatch paths) is the
server-side integration piece + needs DI wiring for the pipeline builder —
lands in the next PR on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>