Merge b626-s7 (batch #626+)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 14:43:07 -04:00
18 changed files with 1599 additions and 32 deletions
+74 -8
View File
@@ -65,6 +65,30 @@ hardware. A wrong slot causes a connection refusal during the handshake. See
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs` for the
per-field defaults.
### TSAP / connection-type override
By default S7.Net derives both ISO-TSAPs from `CpuType` / `Rack` / `Slot` (local `01.00`; remote
`03.<rack×0x20+slot>` for the S7-300/400/1200/1500 families). Three optional keys override that
for CPUs and gateways that hand out connection resources by TSAP (S7-400H, CP343, LOGO!):
| Key | Form | Effect |
|---|---|---|
| `localTsap` | `"01.00"` (also `"0100"`, `"0x0100"`, or a 16-bit number) | replaces the local TSAP |
| `remoteTsap` | `"03.01"` (same forms) | replaces the whole remote TSAP |
| `connectionType` | `"PG"` \| `"OP"` \| `"S7Basic"` (string) | writes `1` / `2` / `3` into the **first** remote-TSAP byte, keeping `rack×0x20+slot` as the second |
`remoteTsap` and `connectionType` are mutually exclusive (a config carrying both fails to load); a
malformed TSAP fails to load rather than silently falling back to the slot-derived default. With none of
the three set the connection is built exactly as before. The override applies to the driver, the AdminUI
Test Connect probe and the AdminUI S7 driver form (Connection type / Local TSAP / Remote TSAP fields).
### Test Connect and PUT/GET
After the handshake, Test Connect reads 2 bytes from `MB0`. A CPU with PUT/GET disabled (the S7-1200/1500
default) completes the handshake but denies every read, so the probe fails with an actionable "enable
PUT/GET communication" message instead of a false green tick. A CPU that merely rejects that address (no
`MB0`) still passes — the connection is fine; any other read fault fails the probe with its message.
## Address forms
Addresses use Siemens TIA-Portal / STEP 7 Classic syntax, parsed by
@@ -102,19 +126,52 @@ extra PLC round-trip.
| `UInt64` | ULINT/LWORD | 8 bytes | yes | yes |
| `Float64` | LREAL | 8 bytes | yes | yes |
| `String` | STRING | `StringLength + 2` bytes | yes | yes |
| `DateTime` | DATE_AND_TIME | 8 bytes | yes | yes |
| `WString` | WSTRING | `4 + 2 × StringLength` bytes | yes | yes |
| `Char` | CHAR | 1 byte | yes | yes |
| `WChar` | WCHAR | 2 bytes | yes | yes |
| `DateTime` | DATE_AND_TIME (DT) | 8 bytes (BCD) | yes | yes |
| `Dtl` | DTL | 12 bytes | yes | yes |
| `Date` | DATE | 2 bytes | yes | yes |
| `Tod` | TIME_OF_DAY | 4 bytes | yes | yes |
| `Time` | TIME | 4 bytes | yes | yes |
| `S5Time` | S5TIME | 2 bytes | yes | yes |
| Timer (`T{n}`) | TIME | — | yes | **read-only** |
| Counter (`C{n}`) | COUNTER | — | yes | **read-only** |
Wide types (`Int64`, `UInt64`, `Float64`, `String`, `DateTime`) and Timer/Counter require
byte-anchored addressing — see the section below.
Wide types (`Int64`, `UInt64`, `Float64`, `String`, `DateTime`, and the string / date-time
types below) and Timer/Counter require byte-anchored addressing — see the section below.
### String, character and date/time/duration types
These add no new OPC UA data types — they map onto existing ones:
| DataType | OPC UA type | Representation | Write refuses (`BadOutOfRange`, nothing sent) |
|---|---|---|---|
| `String` | String | S7 `[maxLen][curLen][chars]`, ASCII | over `StringLength` (S7.Net behaviour) |
| `WString` | String | `[maxLen UInt16][curLen UInt16][UTF-16BE]`; `stringLength` is the declared max in characters, **0–16382** (default 254) | more UTF-16 code units than `StringLength` |
| `Char` | String (1 char) | 1 byte, Latin-1 | a character above U+00FF |
| `WChar` | String (1 char) | UTF-16BE code unit | — (a string whose length is not 1 is `BadTypeMismatch`) |
| `DateTime` | DateTime | DT, 8-byte BCD (S7.Net codec; 1990–2089) | out-of-range year |
| `Dtl` | DateTime | year UInt16 + month/day/weekday/h/m/s + UInt32 nanoseconds; 100 ns resolution | year outside 1970–2262 |
| `Date` | DateTime (midnight) | UInt16 days since 1990-01-01 (max 2169-06-06) | before 1990-01-01 or after 2169-06-06 |
| `Tod` | **Int32 ms** since midnight | UInt32 ms | outside 0–86 399 999 |
| `Time` | **Int32 ms** (signed duration) | Int32 ms | outside the Int32 range |
| `S5Time` | **Int32 ms** | 2-bit time base (10 ms / 100 ms / 1 s / 10 s) + 3 BCD digits | above 9 990 000 ms, negative, or not exactly representable (resolution is 10 ms up to 9 990 ms, then coarser) — **never rounded** |
A fractional millisecond `Double` is rounded to the nearest whole millisecond (banker's rounding), exactly
as the driver does for every other Int32 tag write; only values off the S5TIME time-base grid are refused.
A write of a `TimeSpan` is also accepted for `Tod` / `Time` / `S5Time`. A PLC buffer the codec cannot
decode — an uninitialised (all-zero) `Dtl`, a non-BCD `S5Time` digit, a `WString` whose current length
exceeds its reserved length — reads `BadOutOfRange` for that tag only, without degrading driver health
or condemning the connection. `S5Time` writes pick the finest time base that holds the value. The
`Timer` area (`T{n}`) keeps its own S5TIME-to-**seconds** (`Float64`) decode.
## Wide types & Timer/Counter
### Byte-anchored addressing for wide / structured types
Wide types (`Int64`, `UInt64`, `Float64`/LReal, `String`, `DateTime`) are
**byte-anchored**: the address must use the `B` suffix pointing at the **first
Wide types (`Int64`, `UInt64`, `Float64`/LReal, `String`, `DateTime`, `WString`, `Char`,
`WChar`, `Dtl`, `Date`, `Tod`, `Time`, `S5Time`) are **byte-anchored**: the address must use the `B` suffix pointing at the **first
byte** of the value; the driver reads the correct number of contiguous bytes
based on the DataType.
@@ -125,6 +182,8 @@ based on the DataType.
| `Float64` | same | 8 | `DB1.DBB8` → LREAL at bytes 8–15 |
| `DateTime` | same | 8 | `DB3.DBB0` → DATE_AND_TIME at bytes 0–7 |
| `String` | same | `StringLength + 2` | `DB4.DBB0`, StringLength=40 → 42 bytes |
| `WString` | same | `4 + 2 × StringLength` | `DB5.DBB0`, StringLength=10 → 24 bytes |
| `Dtl` | same | 12 | `DB6.DBB0` → DTL at bytes 0–11 |
Using a `W` or `D` suffix with a wide DataType is a config error caught at
`InitializeAsync`. For array tags (`isArray: true`), wide-type element arrays
@@ -155,11 +214,18 @@ The following are explicitly deferred and will produce a config error or
`BadNotSupported` if attempted:
- **Wide-type arrays** — array tags (`isArray: true`) with element types `Int64`,
`UInt64`, `Float64`, `String`, or `DateTime`.
- **`S7WString`** (2-byte character strings; distinct from classic `STRING`).
- **`DTL` / `DateTimeLong`** (12-byte Siemens date-and-time-long).
`UInt64`, `Float64`, `String`, `DateTime`, or any of the string / date-time types above.
- **Timer / Counter writes** — surfaced as `BadNotWritable` until write support lands.
## Negotiated PDU size
The PDU size the CPU negotiates during the connection handshake (typically 240 bytes; 480 / 960 with
extended PDU) is snapshotted at every (re)connect and exposed as `S7Driver.NegotiatedPduSize` /
`S7Driver.GetDiagnostics()["S7.NegotiatedPduSize"]` (0 before the first connect and after shutdown). It is
also written to the `S7Driver connected` / `reconnected` log lines and to the AdminUI Test Connect
message (`S7 connected (CPU S71500, PDU 240 bytes)`). `DriverHealth` has no diagnostics bag in this
codebase, so the value is not yet on the `/hosts` health snapshot — adding one is a Core change.
## Capability surface
`S7Driver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe`