docs(audit): G2 completeness — Modbus/AbCip/AbLegacy driver overview pages
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# AB CIP Driver
|
||||
|
||||
In-process native-protocol driver that exposes Allen-Bradley CIP / EtherNet-IP
|
||||
controllers as OPC UA nodes. It runs inside the OtOpcUa server's .NET 10 AnyCPU
|
||||
process and talks to the PLC through the libplctag.NET wrapper — no gateway, no
|
||||
sidecar. One driver instance can serve many devices; per-device routing is keyed
|
||||
on the canonical `ab://gateway[:port]/cip-path` host-address string.
|
||||
|
||||
Supported families: **ControlLogix**, **CompactLogix**, **Micro800**, and
|
||||
**GuardLogix**. CIP has no native push model, so subscriptions are a polling
|
||||
overlay on top of `IReadable`.
|
||||
|
||||
For the driver spec (capability surface, config shape, type mapping), see
|
||||
[docs/v2/driver-specs.md §3](../v2/driver-specs.md). For the manual test client,
|
||||
see [Driver.AbCip.Cli.md](../Driver.AbCip.Cli.md). For the integration fixture
|
||||
coverage map, see [AbServer-Test-Fixture.md](AbServer-Test-Fixture.md).
|
||||
|
||||
## Project Layout
|
||||
|
||||
| Project | Role |
|
||||
|---------|------|
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/` | The driver — `AbCipDriver`, the libplctag runtime/enumerator/template-reader wrappers, the UDT read planner + template decoders, the host-address parser, and the ALMD alarm projection. |
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/` | `AbCipDriverOptions`, `AbCipDeviceOptions`, `AbCipTagDefinition` / `AbCipStructureMember`, and the `AbCipDataType` / `AbCipPlcFamily` enums bound from the driver's `DriverConfig` JSON. |
|
||||
|
||||
Per family the `AbCipPlcFamilyProfile` (`PlcFamilies/AbCipPlcFamilyProfile.cs`)
|
||||
supplies the libplctag `plc` attribute, default CIP path, ConnectionSize, and
|
||||
request-packing / connected-messaging quirks — ControlLogix is the baseline and
|
||||
each other family is a delta (Micro800 is unconnected-only with no backplane
|
||||
routing; GuardLogix shares the ControlLogix wire protocol with a tag-level safety
|
||||
partition).
|
||||
|
||||
## Capability Surface
|
||||
|
||||
`AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource, IDisposable, IAsyncDisposable`
|
||||
(`Driver.AbCip/AbCipDriver.cs`). It adds **`IAlarmSource`** over the Modbus /
|
||||
AB Legacy surface.
|
||||
|
||||
| Capability | Implementation entry point | Notes |
|
||||
|------------|---------------------------|-------|
|
||||
| `ITagDiscovery` | `DiscoverAsync` | Emits pre-declared tags under per-device folders; UDT tags with declared `Members` fan out into a sub-folder + one variable per member. With `EnableControllerBrowse` the `@tags` symbol table is walked into a `Discovered/` folder (system/module/routine tags filtered out). |
|
||||
| `IReadable` | `ReadAsync` → `ReadGroupAsync` / `ReadSingleAsync` | Per-tag reads; opt-in whole-UDT grouping (`EnableDeclarationOnlyUdtGrouping`) collapses N member reads into one. |
|
||||
| `IWritable` | `WriteAsync` | BOOL-within-DINT writes do a per-parent read-modify-write under a lock; `SafetyTag` and non-writable tags return `BadNotWritable`. |
|
||||
| `ISubscribable` | `SubscribeAsync` driven by the shared `PollGroupEngine` | CIP has no push model — subscriptions become polling groups. |
|
||||
| `IHostConnectivityProbe` | `ProbeLoopAsync` + `GetHostStatuses` | One probe loop per device reading `Probe.ProbeTagPath`; no path configured ⇒ a warning is logged and the device stays `Unknown`. |
|
||||
| `IPerCallHostResolver` | `ResolveHost` | Routes each call to the tag's `DeviceHostAddress`, the breaker key for the resilience pipeline so one dead PLC trips only its own breaker. |
|
||||
| `IAlarmSource` | `AbCipAlarmProjection` (ALMD) | Opt-in via `EnableAlarmProjection`; off by default the subscribe path is a no-op so capability negotiation still works. |
|
||||
|
||||
## Addressing Model
|
||||
|
||||
Per-device host addresses are the canonical `ab://gateway[:port]/cip-path` form
|
||||
parsed by `AbCipHostAddress.TryParse` (`AbCipHostAddress.cs`). The parsed
|
||||
`CipPath` is handed to libplctag verbatim, so no wire-layer translation is
|
||||
needed:
|
||||
|
||||
| Form | Meaning |
|
||||
|------|---------|
|
||||
| `ab://10.0.0.5/1,0` | Single-chassis ControlLogix, CPU in slot 0 |
|
||||
| `ab://10.0.0.5/1,2,2,192.168.50.20,1,0` | Bridged ControlLogix (routed path) |
|
||||
| `ab://10.0.0.5/` | Micro800 / no-backplane device (empty path) |
|
||||
| `ab://10.0.0.5:44818/1,0` | Explicit EIP port (default 44818) |
|
||||
|
||||
Tags carry a Logix symbolic `TagPath` (controller or program scope). UDT-typed
|
||||
tags are declared as `AbCipDataType.Structure` with a `Members` list; discovery
|
||||
fans each member out as `{tag.Name}.{member.Name}`, and the read planner can
|
||||
collapse a batch of members into one whole-UDT read when
|
||||
`EnableDeclarationOnlyUdtGrouping` is set. The whole-UDT fast path is opt-in
|
||||
because Studio 5000 may reorder members vs declaration order; decoding at
|
||||
declaration-order offsets against a reordered layout yields silently-plausible
|
||||
wrong numbers.
|
||||
|
||||
## Configuration
|
||||
|
||||
`AbCipDriverOptions` (`Driver.AbCip.Contracts/AbCipDriverOptions.cs`) binds from
|
||||
the driver's `DriverConfig` JSON. Key fields:
|
||||
|
||||
- **`Devices`** — one `AbCipDeviceOptions` per PLC (`HostAddress`, `PlcFamily`, optional `DeviceName`, per-device `AllowPacking` / `ConnectionSize` overrides).
|
||||
- **`Tags`** — pre-declared `AbCipTagDefinition` list; `Members` for UDT fan-out, `SafetyTag` for GuardLogix safety-partition tags.
|
||||
- **`Probe`** — connectivity-probe `Enabled` / `Interval` / `Timeout` / `ProbeTagPath`.
|
||||
- **Discovery** — `EnableControllerBrowse` (`@tags` walk) and `EnableDeclarationOnlyUdtGrouping` (whole-UDT read fast path).
|
||||
- **Alarms** — `EnableAlarmProjection` + `AlarmPollInterval` for the ALMD projection.
|
||||
|
||||
Full per-field descriptions live in `AbCipDriverOptions.cs`. The JSON skeleton is
|
||||
reproduced in [docs/v2/driver-specs.md §3](../v2/driver-specs.md).
|
||||
|
||||
## Alarm Projection
|
||||
|
||||
`IAlarmSource` is served by `AbCipAlarmProjection`, which polls each subscribed
|
||||
ALMD UDT's `InFaulted` + `Severity` members at `AlarmPollInterval` and fires
|
||||
`OnAlarmEvent` on raise/clear transitions. It is **ALMD-only** in this pass (ALMA
|
||||
analog alarms are a follow-up) and **disabled by default** — shops running FT
|
||||
Alarm & Events should keep it off and take alarms through the native route, since
|
||||
the projection semantics don't exactly mirror Rockwell FT A&E.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/` cover the driver, host-address parser, UDT planner, and alarm projection via fake tag runtimes.
|
||||
- **Integration tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/` run against the `ab_server` Docker fixture. See [AbServer-Test-Fixture.md](AbServer-Test-Fixture.md) for the coverage map and the `AB_SERVER_ENDPOINT` wiring.
|
||||
- **Manual client** — [Driver.AbCip.Cli.md](../Driver.AbCip.Cli.md).
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- **Native heap is invisible to the GC.** `GetMemoryFootprint()` reports CLR allocations only; libplctag's native `Tag` heap does not show up there. Watch whole-process RSS, and use `ReinitializeAsync` (tears down + re-creates every device's libplctag handles) as the remediation for native-heap growth.
|
||||
- **Handle eviction on failure** — a non-zero libplctag status or a transport exception evicts the cached tag runtime so the next read/write re-creates a fresh handle, mirroring the probe loop's recreate-on-failure behaviour.
|
||||
- **Declaration-only UDT grouping is a footgun unless verified** — only enable `EnableDeclarationOnlyUdtGrouping` when every UDT's member declaration order has been hand-verified against the controller's compiled layout.
|
||||
@@ -0,0 +1,100 @@
|
||||
# AB Legacy Driver
|
||||
|
||||
In-process native-protocol driver that exposes legacy Allen-Bradley PLCs —
|
||||
**SLC 500**, **MicroLogix**, **PLC-5**, and Logix-via-PCCC — as OPC UA nodes. It
|
||||
runs inside the OtOpcUa server's .NET 10 AnyCPU process and speaks PCCC over
|
||||
EtherNet/IP through the same libplctag.NET wrapper as the AB CIP driver, but
|
||||
addresses data by **file** (data-table) rather than by symbolic tag. One driver
|
||||
instance can serve many devices; per-device routing is keyed on the canonical
|
||||
`ab://gateway[:port]/cip-path` host-address string. PCCC has no native push
|
||||
model, so subscriptions are a polling overlay on top of `IReadable`.
|
||||
|
||||
For the driver spec (capability surface, config shape, payload limits), see
|
||||
[docs/v2/driver-specs.md §4](../v2/driver-specs.md). For the manual test client,
|
||||
see [Driver.AbLegacy.Cli.md](../Driver.AbLegacy.Cli.md). For the integration
|
||||
fixture coverage map, see [AbLegacy-Test-Fixture.md](AbLegacy-Test-Fixture.md).
|
||||
|
||||
## Project Layout
|
||||
|
||||
| Project | Role |
|
||||
|---------|------|
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/` | The driver — `AbLegacyDriver`, the libplctag runtime wrapper, the PCCC file-address parser (`AbLegacyAddress`), the host-address parser, and the status mapper. |
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/` | `AbLegacyDriverOptions`, `AbLegacyDeviceOptions`, `AbLegacyTagDefinition`, and the `AbLegacyDataType` / `AbLegacyPlcFamily` / `AbLegacyPlcFamilyProfile` records bound from the driver's `DriverConfig` JSON. |
|
||||
|
||||
Per family the `AbLegacyPlcFamilyProfile` supplies the libplctag `plc` attribute,
|
||||
default CIP path, max-payload bytes, and the `SupportsStringFile` /
|
||||
`SupportsLongFile` capability flags. MicroLogix uses direct EIP (empty default
|
||||
path); MicroLogix and PLC-5 don't ship L-files; PLC-5 predates them entirely.
|
||||
Tag types are validated against the device's profile at init time — declaring a
|
||||
`Long` or `String` tag on a family that can't support it fails fast with a clear
|
||||
message.
|
||||
|
||||
## Capability Surface
|
||||
|
||||
`AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable`
|
||||
(`Driver.AbLegacy/AbLegacyDriver.cs`). There is **no `IAlarmSource`** — unlike the
|
||||
AB CIP driver, PCCC has no ALMD instruction to project, so alarms are out of
|
||||
scope.
|
||||
|
||||
| Capability | Implementation entry point | Notes |
|
||||
|------------|---------------------------|-------|
|
||||
| `ITagDiscovery` | `DiscoverAsync` | Emits pre-declared tags under per-device folders. Tags are single-element today (`IsArray` hard-wired false); multi-element file ranges are a tracked follow-up. |
|
||||
| `IReadable` | `ReadAsync` | Per-tag reads serialized per cached runtime under a lock (a libplctag `Tag` handle is not concurrency-safe across the server read path + poll loop). |
|
||||
| `IWritable` | `WriteAsync` | Bit-within-word writes (N-file `N7:0/3`, B-file bits) do a per-parent-word read-modify-write under a lock. Non-writable tags return `BadNotWritable`. |
|
||||
| `ISubscribable` | `SubscribeAsync` driven by the shared `PollGroupEngine` | No push model — subscriptions become polling groups. |
|
||||
| `IHostConnectivityProbe` | `ProbeLoopAsync` + `GetHostStatuses` | One probe loop per device reading `Probe.ProbeAddress`; transitions log Warning (down) / Information (recover). |
|
||||
| `IPerCallHostResolver` | `ResolveHost` | Routes each call to the tag's `DeviceHostAddress`; unknown references fall back to the first device, never throwing (per the interface contract). |
|
||||
|
||||
## Addressing Model
|
||||
|
||||
Per-device host addresses are the canonical `ab://gateway[:port]/cip-path` form
|
||||
parsed by `AbLegacyHostAddress.TryParse`. When the parsed CIP path is empty the
|
||||
family profile's default path is used (e.g. SLC 500 gets `1,0`; MicroLogix stays
|
||||
empty for direct EIP).
|
||||
|
||||
Tags carry a PCCC **file address** parsed by `AbLegacyAddress` (`AbLegacyAddress.cs`)
|
||||
— file letter + file number + word number, with an optional bit index (`/N`) or
|
||||
structured sub-element (`.ACC`, `.PRE`, …). The string is passed straight through
|
||||
to libplctag's `name=` attribute; the parser validates shape and surfaces the
|
||||
pieces for driver-side routing (e.g. deciding a bit needs read-modify-write):
|
||||
|
||||
| Form | Meaning |
|
||||
|------|---------|
|
||||
| `N7:0` | Integer file 7, word 0 (signed 16-bit) |
|
||||
| `F8:0` | Float file 8, word 0 (32-bit IEEE-754) |
|
||||
| `B3:0/0` | Bit file 3, word 0, bit 0 |
|
||||
| `L9:0` | Long-integer file (SLC 5/05+, 32-bit) |
|
||||
| `ST9:0` | String file (82-byte fixed-length) |
|
||||
| `T4:0.ACC` / `C5:0.PRE` | Timer / counter sub-element |
|
||||
| `I:0/0` / `O:1/2` / `S:1` | Input / output / status system files (no file number) |
|
||||
|
||||
`AbLegacyDataType` covers the corresponding PCCC types: `Bit`, `Int` (N), `Long`
|
||||
(L), `Float` (F), `AnalogInt` (A), `String` (ST), and the `TimerElement` /
|
||||
`CounterElement` / `ControlElement` sub-element families. The parser enforces
|
||||
PCCC structural rules — bit-addressing only on 16/32-bit element files,
|
||||
sub-elements only on T/C/R files, no file number on I/O/S — rejecting malformed
|
||||
addresses before they reach libplctag.
|
||||
|
||||
## Configuration
|
||||
|
||||
`AbLegacyDriverOptions` (`Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs`)
|
||||
binds from the driver's `DriverConfig` JSON:
|
||||
|
||||
- **`Devices`** — one `AbLegacyDeviceOptions` per PLC (`HostAddress`, `PlcFamily`, optional `DeviceName`).
|
||||
- **`Tags`** — pre-declared `AbLegacyTagDefinition` list (`Name`, `DeviceHostAddress`, `Address`, `DataType`, `Writable`, `WriteIdempotent`).
|
||||
- **`Probe`** — connectivity-probe `Enabled` / `Interval` / `Timeout` / `ProbeAddress`.
|
||||
|
||||
Full per-field descriptions live in the contracts assembly. The JSON skeleton is
|
||||
reproduced in [docs/v2/driver-specs.md §4](../v2/driver-specs.md).
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` cover the driver, the PCCC address parser, and the host-address parser via fake tag runtimes.
|
||||
- **Integration tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` run against the AB Legacy Docker fixture. See [AbLegacy-Test-Fixture.md](AbLegacy-Test-Fixture.md) for the coverage map.
|
||||
- **Manual client** — [Driver.AbLegacy.Cli.md](../Driver.AbLegacy.Cli.md).
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- **Native heap is invisible to the GC.** As with AB CIP, `GetMemoryFootprint()` reports CLR allocations only; watch whole-process RSS and use `ReinitializeAsync` to recycle libplctag handles.
|
||||
- **PCCC reconnect is more expensive than CIP** — legacy PLCs have no connection multiplexing, so the resilience pipeline should use longer backoff than for AB CIP (see [docs/v2/driver-specs.md §4](../v2/driver-specs.md)).
|
||||
- **Single-element addressing today** — a PCCC file is inherently an array (an N7 file is up to 256 words), but the current tag surface addresses one element per tag; range-spanning tags must be enumerated element-by-element until multi-element addressing lands.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Modbus Driver
|
||||
|
||||
In-process native-protocol driver that exposes Modbus-TCP devices as OPC UA
|
||||
variable nodes. It runs inside the OtOpcUa server's .NET 10 AnyCPU process and
|
||||
speaks Modbus-TCP directly over a socket — no gateway, no sidecar, no bitness
|
||||
constraint. Modbus has no discovery protocol and no native push model, so the
|
||||
address space is built entirely from pre-declared tags and subscriptions are a
|
||||
polling overlay on top of `IReadable`.
|
||||
|
||||
For the driver spec (capability surface, config shape, byte-order matrix), see
|
||||
[docs/v2/driver-specs.md §2](../v2/driver-specs.md). For the manual test client,
|
||||
see [Driver.Modbus.Cli.md](../Driver.Modbus.Cli.md). For the integration fixture
|
||||
coverage map, see [Modbus-Test-Fixture.md](Modbus-Test-Fixture.md).
|
||||
|
||||
## Project Layout
|
||||
|
||||
| Project | Role |
|
||||
|---------|------|
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/` | The driver — `ModbusDriver` plus the `ModbusTcpTransport` socket layer, the connectivity probe, and the auto-prohibition planner. |
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/` | Shared address grammar — `ModbusAddressParser` and the `ModbusRegion` / `ModbusDataType` / `ModbusByteOrder` / `ModbusFamily` enums. Lives in its own assembly so the Admin UI and the parser can speak about addresses without a transport dependency. |
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/` | `ModbusDriverOptions` + `ModbusTagDefinition` config records bound from the driver's `DriverConfig` JSON. |
|
||||
|
||||
## Capability Surface
|
||||
|
||||
`ModbusDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable`
|
||||
(`Driver.Modbus/ModbusDriver.cs`). There is **no `IAlarmSource`** and no
|
||||
`IHistoryProvider` — the Modbus protocol expresses neither, so those capabilities
|
||||
are out of scope by design.
|
||||
|
||||
| Capability | Implementation entry point | Notes |
|
||||
|------------|---------------------------|-------|
|
||||
| `ITagDiscovery` | `DiscoverAsync` | Emits one `Modbus/{tag}` variable per pre-declared tag; Modbus has no browse protocol, so the driver returns exactly the configured `Tags`. |
|
||||
| `IReadable` | `ReadAsync` → `ReadOneAsync` / `ReadCoalescedAsync` | FC01/FC02 for coils, FC03/FC04 for registers; auto-chunks reads past the per-device cap. |
|
||||
| `IWritable` | `WriteAsync` → `WriteOneAsync` | FC05/FC15 for coils, FC06/FC16 for registers; `BitInRegister` writes do a per-register read-modify-write under a lock. `DiscreteInputs` / `InputRegisters` are read-only and return `BadNotWritable`. |
|
||||
| `ISubscribable` | `SubscribeAsync` driven by the shared `PollGroupEngine` | No native push — subscriptions become per-tag polling groups with an optional per-tag `Deadband` filter. |
|
||||
| `IHostConnectivityProbe` | `ProbeLoopAsync` + `GetHostStatuses` | Periodic cheap FC03 at `Probe.ProbeAddress`; `HostName` is the `Host:Port` string surfaced to the Admin UI. |
|
||||
| `IPerCallHostResolver` | `ResolveHost` | Routes each call to a per-slave breaker key (`Host:Port/unit{UnitId}`) so a dead RTU slave behind a multi-unit gateway opens its own breaker. |
|
||||
|
||||
## Addressing Model
|
||||
|
||||
Every exposed register is a pre-declared `ModbusTagDefinition` (Region, Address,
|
||||
DataType, ByteOrder, …). Tag spreadsheets are typically authored as address
|
||||
strings parsed by `ModbusAddressParser` at config-bind time; the grammar is
|
||||
`<region><offset>[.<bit>][:<type>[<len>]][:<order>][:<count>]`:
|
||||
|
||||
| Form | Example | Meaning |
|
||||
|------|---------|---------|
|
||||
| Modicon digits | `40001` / `400001` | Holding register 0 (5- or 6-digit form), default Int16 |
|
||||
| Mnemonic prefix | `HR1` / `IR1` / `C100` / `DI5` | Region prefix + 1-based register number |
|
||||
| Bit suffix | `40001.5` | Bit 5 of holding register 0 (`BitInRegister`) |
|
||||
| Explicit type | `40001:F` / `40001:STR20` | Float32 / 20-char ASCII string |
|
||||
| Word order | `40001:F:CDAB` | Float32 with word-swap byte order |
|
||||
| Array | `40001:F:5` | Float32[5] (consumes HR[0..9]) |
|
||||
|
||||
The four regions (`Coils`, `DiscreteInputs`, `InputRegisters`,
|
||||
`HoldingRegisters`) map directly to function-code selection. The type codes are
|
||||
aligned with Wonderware DASMBTCP and the Ignition Modbus driver so pasted tag
|
||||
sheets translate without manual rewriting.
|
||||
|
||||
**Byte/word order** is the most common production misconfiguration. The four
|
||||
`ModbusByteOrder` mnemonics — `ABCD` (BigEndian, spec default), `CDAB`
|
||||
(WordSwap), `BADC` (ByteSwap), `DCBA` (FullReverse) — describe how bytes A/B/C/D
|
||||
appear across consecutive registers when decoding a multi-register value.
|
||||
|
||||
## Device Profiles
|
||||
|
||||
`ModbusDriverOptions.Family` selects a parser family-native branch
|
||||
(`ModbusFamily`):
|
||||
|
||||
- **`Generic`** (default) — only Modicon (`4xxxx`) and mnemonic (`HR1`, `C100`) forms are accepted.
|
||||
- **`DL205`** — AutomationDirect DirectLOGIC. V-memory (octal) → HoldingRegisters, `Y`/`C` → Coils, `X`/`SP` → DiscreteInputs. Strings can be packed low-byte-first via `ModbusTagDefinition.StringByteOrder` (the grammar can't express this — see `ModbusStringByteOrder`).
|
||||
- **`MELSEC`** — Mitsubishi. D-registers → HoldingRegisters, `X` → DiscreteInputs, `Y`/`M` → Coils; the `MelsecSubFamily` selector switches Q/L/iQR (hex) vs FX (octal) X/Y interpretation.
|
||||
|
||||
Per-family register caps are honoured through `MaxRegistersPerRead` /
|
||||
`MaxRegistersPerWrite` / `MaxCoilsPerRead` (e.g. DL205/DL260 cap reads at 128,
|
||||
Mitsubishi Q at 64); the driver auto-chunks larger reads into consecutive
|
||||
requests.
|
||||
|
||||
## Coalesced Reads + Auto-Prohibition
|
||||
|
||||
When `MaxReadGap > 0` the read planner (`ReadCoalescedAsync`) groups tags in the
|
||||
same `(UnitId, Region)`, sorts by address, and merges near-adjacent register
|
||||
spans (gap ≤ `MaxReadGap`, total span ≤ the read cap) into a single FC03/FC04
|
||||
PDU, then slices the response back into per-tag values. If a coalesced read hits
|
||||
a Modbus exception (illegal/protected register), the offending range is recorded
|
||||
as **auto-prohibited** so the planner stops re-coalescing across it; the
|
||||
surviving members fall back to per-tag reads in the same scan. Setting
|
||||
`AutoProhibitReprobeInterval` starts a background loop that periodically retries
|
||||
prohibited ranges and uses bisection to narrow a multi-register prohibition down
|
||||
to the actual offending register(s). Per-tag escape hatch:
|
||||
`ModbusTagDefinition.CoalesceProhibited`.
|
||||
|
||||
## Configuration
|
||||
|
||||
`ModbusDriverOptions` (`Driver.Modbus.Contracts/ModbusDriverOptions.cs`) binds
|
||||
from the driver's `DriverConfig` JSON. Key fields:
|
||||
|
||||
- **Endpoint** — `Host`, `Port` (default 502), `UnitId`, `Timeout`. Per-tag `UnitId` overrides drive multi-slave gateway topology.
|
||||
- **`Tags`** — the pre-declared `ModbusTagDefinition` list; this *is* the address space.
|
||||
- **`Probe`** — connectivity-probe interval / timeout / probe register (default register 0).
|
||||
- **Read/write caps** — `MaxRegistersPerRead` (125), `MaxRegistersPerWrite` (123), `MaxCoilsPerRead` (2000), plus `MaxReadGap` and `AutoProhibitReprobeInterval` for coalescing.
|
||||
- **Function-code overrides** — `UseFC15ForSingleCoilWrites`, `UseFC16ForSingleRegisterWrites` for PLCs that only accept multi-write codes.
|
||||
- **Resilience** — `AutoReconnect`, `KeepAlive`, `IdleDisconnectTimeout`, `Reconnect` backoff, and `WriteOnChangeOnly` redundant-write suppression.
|
||||
|
||||
Full per-field descriptions live in `ModbusDriverOptions.cs`. The JSON skeleton
|
||||
is reproduced in [docs/v2/driver-specs.md §2](../v2/driver-specs.md).
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/` (driver behaviour via a fake transport) and `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests/` (the address grammar).
|
||||
- **Integration tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/` run against the Docker Modbus simulator fixture. See [Modbus-Test-Fixture.md](Modbus-Test-Fixture.md) for the coverage map and the `MODBUS_SIM_ENDPOINT` wiring.
|
||||
- **Manual client** — [Driver.Modbus.Cli.md](../Driver.Modbus.Cli.md).
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- **Wrong-endian readings are silently plausible.** A byte-order misconfiguration produces a wrong number, not a Bad quality code — surface byte-order mismatches as data-validation alerts, not status codes (see [docs/v2/driver-specs.md §2](../v2/driver-specs.md)).
|
||||
- **`WriteOnChangeOnly` + write-only tags** — the suppression cache is only invalidated by a read that returns a divergent value. A tag that is never subscribed/polled never refreshes its cache entry, so a re-asserted value can be suppressed indefinitely. Subscribe every tag that needs deterministic re-writes, or leave the option off.
|
||||
- **Auto-prohibited ranges** are visible via `GetAutoProhibitedRanges` and logged on first occurrence / on clear — use them to find protected register holes in a device's map.
|
||||
Reference in New Issue
Block a user