feat(s7): STRING/WSTRING/CHAR/WCHAR and DTL/DATE/TOD/TIME/S5TIME codecs, TSAP/connection-type override, negotiated PDU surfaced (#632 #288 #289 #294 #295)

New S7DataType members WString/Char/WChar/Dtl/Date/Tod/Time/S5Time mapped onto existing
DriverDataType (String / DateTime / Int32 ms); writes refuse out-of-range with BadOutOfRange.
Optional localTsap/remoteTsap/connectionType through options, factory JSON, probe and AdminUI form.
NegotiatedPduSize snapshotted at connect and exposed via GetDiagnostics (DriverHealth has no bag).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 14:31:44 -04:00
parent d898262c4a
commit 34c52defb3
18 changed files with 1430 additions and 31 deletions
+65 -8
View File
@@ -65,6 +65,23 @@ 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).
## Address forms
Addresses use Siemens TIA-Portal / STEP 7 Classic syntax, parsed by
@@ -102,19 +119,50 @@ 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 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 +173,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 +205,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`