# 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 ``` [.][:[]][:][:] ``` 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](https://cdn.logic-control.com/media/DASMBTCP.pdf) and the [Ignition Modbus addressing manual](https://www.docs.inductiveautomation.com/docs/8.1/ignition-modules/opc-ua/opc-ua-drivers/modbus/modbus-addressing). 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` | ASCII string, `len` chars (2 chars / register) | `ceil(len/2)` | analogous to Ignition `HRS:` | 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. ### Coalescing auto-recovery (#148 / #150 / #151 / #152) - A coalesced read that fails with a Modbus exception (write-only or protected register mid-block) records the failed range as auto-prohibited. The planner stops re-coalescing across the range; the per-tag fallback path keeps healthy members working in the same scan. - **Bisection (#150)**: every re-probe pass narrows multi-register prohibitions by trying the two halves separately. Over log2(span) ticks the prohibition pins at the actual offending register(s); intermediate halves that succeed get cleared. - **Periodic re-probe (#151)**: opt in via `AutoProhibitReprobeInterval` (TimeSpan?). Default null = disabled (prohibitions persist for the driver lifetime; clear on `ReinitializeAsync`). - **Per-tag escape hatch**: `CoalesceProhibited` (bool, default false) on `ModbusTagDefinition`. The planner reads such tags in isolation regardless of `MaxReadGap`. Use for known-bad addresses you want to exclude from the auto-discovery loop. - **Diagnostics (#152)**: `ModbusDriver.GetAutoProhibitedRanges()` returns a snapshot of every active prohibition as `ModbusAutoProhibition` records (UnitId / Region / StartAddress / EndAddress / LastProbedUtc / BisectionPending). Surface in the driver-diagnostics RPC channel when that wiring lands; for now consumable by in-process callers (Server health endpoints, log aggregation). ## 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. ```json { "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.