Files
lmxopcua/docs/v2/modbus-addressing.md
Joseph Doherty 5ea57d2d70 Task #138 — Modbus addressing grammar docs + e2e
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)
2026-04-25 00:32:27 -04:00

7.0 KiB
Raw Blame History

Modbus tag-addressing reference

Foundational doc for the Modbus addressing grammar shipped across #136#144. Covers the address-string parser (ModbusAddressParser) that the wire driver and the Admin UI both consume, the per-tag suffix modifiers, and the family- native branch.

Grammar

<region><offset>[.<bit>][:<type>[<len>]][:<order>][:<count>]

Each field is optional from left to right; the parser fills defaults.

Region + offset

Three accepted forms — pick whichever matches your tag spreadsheet's convention. All three resolve to the same (Region, ushort PduOffset) on the wire.

Form Example Means
Modicon 5-digit 40001 Holding register 1 (PDU 0)
Modicon 6-digit 400001 Holding register 1 (PDU 0); supports up to 465536 (PDU 65535)
Mnemonic HR1, IR1, C100, DI1 Same regions; 1-based register number

Modicon leading-digit → region:

Digit Region OPC UA wire FC
0 Coils FC01 / FC05 / FC15
1 DiscreteInputs FC02 (read-only)
3 InputRegisters FC04 (read-only)
4 HoldingRegisters FC03 / FC06 / FC16

Bit suffix .N

40001.5 = bit 5 (LSB-first) of HR[0]. Implies DataType=BitInRegister; mixing with an explicit type or array-count is rejected.

Type code :T

Code Type Registers
BOOL Boolean 1 (region must be Coils / DiscreteInputs)
I Int16 1
UI UInt16 1
DI, L Int32 2
UDI, UL UInt32 2
LI Int64 4
ULI UInt64 4
F Float32 2
D Float64 4
BCD 16-bit BCD 1
LBCD 32-bit BCD 2
STR<len> ASCII string, len chars (2 chars / register) ceil(len/2)

Default when omitted:

  • Coils / DiscreteInputs → BOOL
  • HoldingRegisters / InputRegisters → I (Int16)

Byte order :O

Mnemonic Meaning Wire
ABCD Big-endian (Modbus spec default) [A,B,C,D]
CDAB Word swap (Siemens, several AB) [C,D,A,B]
BADC Byte swap (legacy little-endian-internal devices) [B,A,D,C]
DCBA Full reverse (some EtherNet/IP gateways) [D,C,B,A]

For 8-byte values (Int64 / Float64) the same labels apply pairwise.

Array count :N

40001:F:5 = Float32[5] (consumes HR[0..9]). Array + bit suffix is rejected. Strings are not arrays.

Composition

The 3-field shorthand 40001:F:5 is parsed as (type=F, count=5) because 5 isn't a valid byte-order mnemonic. Use the explicit 4-field form 40001:F:CDAB:5 when you need a non-default order.

Family-native syntax (#144)

When the driver instance has Family != Generic, the parser tries the family's native syntax FIRST, then falls back to Modicon / mnemonic.

DL205 (AutomationDirect DirectLOGIC)

Form Example Mapping
Vnnnn (octal) V2000 HoldingRegisters[1024] (octal 2000 = decimal 1024)
Ynn (octal) Y17 Coils[2048 + 15] (Y-output base + offset)
Cnn (octal) C100 Coils[3072 + 64] (C-relay base + offset)
Xnn (octal) X17 DiscreteInputs[15]
SPnn (octal) SP10 DiscreteInputs[1024 + 8]

Cross-family ambiguity: C100 means Coils[99] under Generic (mnemonic) but Coils[3136] under DL205. Per-driver Family choice disambiguates.

MELSEC (Mitsubishi)

Form Example Mapping (sub-family Q_L_iQR / F_iQF)
Dnnn (decimal) D100 HoldingRegisters[100]
Mnnn (decimal) M50 Coils[50]
Xnn X20 DiscreteInputs[32 hex / 16 octal]
Ynn Y20 Coils[32 hex / 16 octal]

X / Y digit interpretation depends on MelsecSubFamily:

  • Q_L_iQR → hex (default)
  • F_iQF → octal

Bank-base offsets default to 0 in the grammar string. Sites with non-zero "Modbus Device Assignment" bases use the structured tag form.

Driver-instance options

Beyond per-tag addressing, ModbusDriverOptions exposes (#139#143):

Connection (#139)

  • KeepAlive { Enabled, Time, Interval, RetryCount } — TCP-level probes. Defaults match the historical PR 53 wire output (Enabled=true, Time=30s, Interval=10s, RetryCount=3).
  • IdleDisconnectTimeout — proactively close + reconnect after this much socket idle time. Default null = disabled.
  • Reconnect { InitialDelay, MaxDelay, BackoffMultiplier } — geometric backoff for the post-drop reconnect loop. Default (0, 30s, 2.0) = immediate first retry, geometric thereafter.

Protocol (#140)

  • MaxCoilsPerRead (default 2000) — separate cap for FC01/FC02 coil reads.
  • 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).

Subscribe (#141)

  • Per-tag Deadband — suppress sub-threshold publishes on numeric tags.
  • WriteOnChangeOnly (driver-level) — short-circuit identical-value writes. Cache invalidates on read-divergence.

Multi-unit (#142)

  • Per-tag UnitId — overrides the driver-level UnitId in the MBAP header. Required for one-Ethernet-gateway / N-RTU-slave deployments.
  • IPerCallHostResolver.ResolveHost returns host:port/unitN per tag so per-PLC circuit breakers fire per slave.
  • Per-tag CoalesceProhibited — escape hatch for #143's planner (read 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.

JSON DTO shape

The factory accepts both the structured form (legacy) and the new AddressString form per-tag. Mix freely — newer pasted rows use the grammar string; legacy rows keep the structured fields.

{
  "host": "10.1.2.3",
  "port": 502,
  "unitId": 1,
  "family": "DL205",
  "keepAlive": { "enabled": true, "timeMs": 30000, "intervalMs": 10000, "retryCount": 3 },
  "idleDisconnectMs": 120000,
  "reconnect": { "initialDelayMs": 0, "maxDelayMs": 30000, "backoffMultiplier": 2.0 },
  "maxCoilsPerRead": 2000,
  "writeOnChangeOnly": false,
  "maxReadGap": 8,
  "tags": [
    { "name": "Temp",        "addressString": "V2000:F:CDAB" },
    { "name": "Setpoint",    "addressString": "40001:I" },
    { "name": "Outputs",     "addressString": "Y0:5" },
    { "name": "AlarmCount",  "region": "HoldingRegisters", "address": 200, "dataType": "Int16", "deadband": 5.0 }
  ]
}

Vendor compatibility caveat

The exact spelling of type codes (e.g. I vs INT, BCD vs L_BCD) and the byte-order mnemonics were synthesised from training-era vendor docs (Wonderware DASMBTCP, Kepware KEPServerEX, Ignition, Matrikon, OAS). Before locking the grammar for a production deployment, verify against 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.