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.
8.1 KiB
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-HRdefault
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.ResolveHostreturnshost:port/unitNper tag so per-PLC circuit breakers fire per slave.- Per-tag
CoalesceProhibited— escape hatch for #143's planner (read this tag in isolation regardless ofMaxReadGap).
Block-read coalescing (#143)
MaxReadGap(default 0 = off) — gap budget the planner is willing to bridge between adjacent register tags. WithMaxReadGap=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.