Files
lmxopcua/docs/v2/modbus-addressing.md
Joseph Doherty dfd027ebca Task #146 — Modbus addressing: align type codes with Wonderware DASMBTCP + Ignition
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.
2026-04-25 00:51:50 -04:00

8.1 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

Codes verified 2026-04-25 against Wonderware DASMBTCP user guide and the Ignition Modbus addressing manual. The I / UI / I_64 / UI_64 / BCD_32 shapes match Wonderware's suffix convention and Ignition's underscore-N prefix variants where those vendors agree.

Code Type Registers Vendor reference
BOOL Boolean 1 (region must be Coils / DiscreteInputs) universal
S Int16 1 Wonderware DASMBTCP S = 16-bit signed
US UInt16 1 Ignition HRUS = Unsigned Short
I Int32 2 Wonderware DASMBTCP I = 32-bit signed; Ignition HRI
UI UInt32 2 Ignition HRUI
I_64 Int64 4 Ignition HRI_64
UI_64 UInt64 4 Ignition HRUI_64
F Float32 2 Wonderware F; Ignition HRF
D Float64 4 Ignition HRD
BCD 16-bit BCD 1 Ignition HRBCD
BCD_32 32-bit BCD 2 Ignition HRBCD_32
STR<len> ASCII string, len chars (2 chars / register) ceil(len/2) analogous to Ignition HRS<addr>:<len>

Default when omitted:

  • Coils / DiscreteInputs → BOOL
  • HoldingRegisters / InputRegisters → S (Int16) — matches Ignition's bare-HR default

Codes removed in #146 (silent wrong-data risk, never compatible with the two reference vendors): :DI, :L, :UDI, :UL, :LI, :ULI, :LBCD. Pre-#146 configs that use these get a clear "Unknown type code" diagnostic at parse time; rewrite to the post-#146 codes per the table above.

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.