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)
7.0 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
| 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.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.