docs(s7): wide-type/Timer-Counter support — CLI help + driver-specs + S7 driver doc

Drop the "not yet implemented / BadNotSupported" stale note from all three
S7 CLI --type option descriptions (ReadCommand, WriteCommand, SubscribeCommand)
and replace with accurate help listing the full supported type set, byte-anchored
addressing for wide types, and Timer/Counter read-only status.

docs/v2/driver-specs.md §5: add Supported Data Types table, Byte-Anchored
Addressing table (DBB/MB/IB/QB + examples), Timer/Counter read section with
the Counter-BCD known-limitation, and Deferrals list.

docs/drivers/S7.md: expand Data types to a full table, add "Wide types &
Timer/Counter" section (byte-anchored addressing, Timer/Counter read-only,
Counter BCD known-limitation, deferrals), update Address forms table and
1-D array Deferrals note.
This commit is contained in:
Joseph Doherty
2026-06-17 06:22:36 -04:00
parent 11e8e4302d
commit b7dfb5aff2
5 changed files with 160 additions and 33 deletions
+81 -15
View File
@@ -68,28 +68,93 @@ Addresses use Siemens TIA-Portal / STEP 7 Classic syntax, parsed by
| Area | Example | Meaning |
|------|---------|---------|
| Data block | `DB1.DBX0.0` / `DB1.DBW0` / `DB1.DBD4` | DB number + size suffix `X`(bit) / `B`(byte) / `W`(word) / `D`(dword), optional `.bit` for `DBX` |
| Data block | `DB1.DBX0.0` / `DB1.DBW0` / `DB1.DBD4` / `DB1.DBB8` | DB number + size suffix `X`(bit) / `B`(byte) / `W`(word) / `D`(dword), optional `.bit` for `DBX` |
| Merker (M) | `MB0` / `MW0` / `MD4` / `M0.0` | Marker byte; size prefix `B`/`W`/`D`, or bare offset `.bit` for bit access |
| Input (I) | `IB0` / `IW0` / `I0.0` | Process-image input |
| Output (Q) | `QB0` / `QW0` / `Q0.0` | Process-image output |
| Timer | `T0` / `T15` | Timer area (read-only — see Wide types section) |
| Counter | `C0` / `C10` | Counter area (read-only — see Wide types section) |
Parsing is strict and runs once at `InitializeAsync` so a config typo fails fast
at load instead of surfacing as `BadInternalError` on every read. Bit offsets
must be 0-7, byte offsets non-negative, DB numbers >= 1.
> **Timer (`T{n}`) and Counter (`C{n}`)** addresses parse cleanly but the read
> path has no decode case for them yet — the driver rejects them at init with an
> explicit error rather than letting them surface a misleading type-mismatch.
## Data types
`S7DataType` declares the **semantic** type; `S7.Net` returns an unsigned boxed
value (bool / byte / ushort / uint) that the driver reinterprets without an
extra PLC round-trip. Wired through today: `Bool`, `Byte`, `Int16`, `UInt16`,
`Int32`, `UInt32`, `Float32`. `Int64`, `UInt64`, `Float64`, `String`, and
`DateTime` are declared in the enum but **rejected at init** — half-implemented
types must not create OPC UA nodes that then return `BadNotSupported` on every
access.
extra PLC round-trip.
| DataType | S7 Type | Width | Read | Write |
|----------|---------|-------|------|-------|
| `Bool` | BOOL | 1 bit | yes | yes |
| `Byte` | BYTE | 1 byte | yes | yes |
| `Int16` | INT | 2 bytes | yes | yes |
| `UInt16` | WORD | 2 bytes | yes | yes |
| `Int32` | DINT | 4 bytes | yes | yes |
| `UInt32` | DWORD | 4 bytes | yes | yes |
| `Float32` | REAL | 4 bytes | yes | yes |
| `Int64` | LINT | 8 bytes | yes | yes |
| `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 |
| 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 & 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
byte** of the value; the driver reads the correct number of contiguous bytes
based on the DataType.
| DataType | Address form | Bytes read | Example |
|----------|-------------|------------|---------|
| `Int64` | `DB{n}.DBB{offset}` / `MB{offset}` / `IB{offset}` / `QB{offset}` | 8 | `DB1.DBB8` → LINT at bytes 815 |
| `UInt64` | same | 8 | `DB2.DBB16` → ULINT at bytes 1623 |
| `Float64` | same | 8 | `DB1.DBB8` → LREAL at bytes 815 |
| `DateTime` | same | 8 | `DB3.DBB0` → DATE_AND_TIME at bytes 07 |
| `String` | same | `StringLength + 2` | `DB4.DBB0`, StringLength=40 → 42 bytes |
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
are deferred — see Deferrals below.
### Timer read
**Address**: `T{n}` (e.g. `T0`, `T15`). **DataType must be `Float64`**. The OPC UA node
is a scalar `Float64` whose value is the timer's elapsed time in **seconds** (as a double).
Timers are **read-only** this phase; a write attempt returns `BadNotWritable`.
### Counter read
**Address**: `C{n}` (e.g. `C0`, `C10`). **DataType must be `Int32`**. The OPC UA node is
a scalar `Int32` whose value is the current **count** (raw word from the PLC).
Counters are **read-only** this phase; a write attempt returns `BadNotWritable`.
> **Known limitation — Counter BCD encoding on S7-300/400**: `S7.Net`'s
> `Counter.FromByteArray` returns the raw big-endian word without BCD decode.
> On **classic S7-300/400** the C-area word is BCD-encoded (0999), so the
> surfaced value can differ from the actual count on that hardware. S7-1200/1500
> use IEC/DB counters (plain integers) where the raw word is correct. BCD
> reinterpretation for legacy C-area is a live-hardware-gated follow-up.
### Deferrals (not yet implemented)
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).
- **Timer / Counter writes** — surfaced as `BadNotWritable` until write support lands.
## Capability surface
@@ -152,8 +217,9 @@ loops over the response buffer decoding each element individually using the same
reinterpret/box logic as scalar reads. This keeps wire round-trips at 1 per array tag
regardless of N.
**Supported element types**any `S7DataType` that has a scalar decode path today
(`Bool`, `Byte`, `Int16`, `UInt16`, `Int32`, `UInt32`, `Float32`). The `ReadBytesAsync`
**Supported element types**the narrow scalar types (`Bool`, `Byte`, `Int16`, `UInt16`,
`Int32`, `UInt32`, `Float32`). Wide types (`Int64`, `UInt64`, `Float64`, `String`,
`DateTime`) and Timer/Counter are deferred for array contexts. The `ReadBytesAsync`
network half is a thin `S7.Net` call; the decode half is fully unit-tested.
**Unit test coverage**`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` covers
@@ -162,9 +228,9 @@ normal dev (the S7 sim is on the shared Docker host but currently offline).
**Live-verify** — integration-fixture-gated (not Mac-verifiable without the S7 sim up).
**Deferrals** — array *writes*, multi-dimensional arrays, per-element historization. The
`Timer` / `Counter` address types that are rejected at init for scalars are also excluded
for arrays.
**Deferrals** — array *writes*, multi-dimensional arrays, per-element historization,
wide-type array elements (`Int64`, `UInt64`, `Float64`, `String`, `DateTime`), and
Timer/Counter array elements.
See [Uns.md §Array tags](../Uns.md#array-tags-1-d) for the cross-driver coverage matrix
and the UI authoring flow.