From b7dfb5aff2a7e964084d54206631df5a67a413f4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 06:22:36 -0400 Subject: [PATCH] =?UTF-8?q?docs(s7):=20wide-type/Timer-Counter=20support?= =?UTF-8?q?=20=E2=80=94=20CLI=20help=20+=20driver-specs=20+=20S7=20driver?= =?UTF-8?q?=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/drivers/S7.md | 96 ++++++++++++++++--- docs/v2/driver-specs.md | 68 ++++++++++++- .../Commands/ReadCommand.cs | 10 +- .../Commands/SubscribeCommand.cs | 8 +- .../Commands/WriteCommand.cs | 11 +-- 5 files changed, 160 insertions(+), 33 deletions(-) diff --git a/docs/drivers/S7.md b/docs/drivers/S7.md index c10a27bf..492f2faa 100644 --- a/docs/drivers/S7.md +++ b/docs/drivers/S7.md @@ -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 8–15 | +| `UInt64` | same | 8 | `DB2.DBB16` → ULINT at bytes 16–23 | +| `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 | + +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 (0–999), 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. diff --git a/docs/v2/driver-specs.md b/docs/v2/driver-specs.md index b7cb1325..4365114e 100644 --- a/docs/v2/driver-specs.md +++ b/docs/v2/driver-specs.md @@ -395,13 +395,73 @@ Pure managed .NET, MIT license. No native dependencies. | Area | Address Syntax | Area Code | Examples | |------|---------------|-----------|----------| -| Data Block | `DB{n}.DB{X\|B\|W\|D}{offset}[.bit]` | 0x84 | `DB1.DBX0.0`, `DB1.DBW0`, `DB1.DBD4` | -| Merkers | `M{B\|W\|D}{offset}` or `M{offset}.{bit}` | 0x83 | `M0.0`, `MW0`, `MD4` | -| Inputs | `I{B\|W\|D}{offset}` or `I{offset}.{bit}` | 0x81 | `I0.0`, `IW0`, `ID0` | -| Outputs | `Q{B\|W\|D}{offset}` or `Q{offset}.{bit}` | 0x82 | `Q0.0`, `QW0`, `QD0` | +| Data Block | `DB{n}.DB{X\|B\|W\|D}{offset}[.bit]` | 0x84 | `DB1.DBX0.0`, `DB1.DBW0`, `DB1.DBD4`, `DB1.DBB8` | +| Merkers | `M{B\|W\|D}{offset}` or `M{offset}.{bit}` | 0x83 | `M0.0`, `MW0`, `MD4`, `MB4` | +| Inputs | `I{B\|W\|D}{offset}` or `I{offset}.{bit}` | 0x81 | `I0.0`, `IW0`, `ID0`, `IB0` | +| Outputs | `Q{B\|W\|D}{offset}` or `Q{offset}.{bit}` | 0x82 | `Q0.0`, `QW0`, `QD0`, `QB0` | | Timers | `T{n}` | 0x1D | `T0`, `T15` | | Counters | `C{n}` | 0x1C | `C0`, `C10` | +### Supported Data Types + +| DataType | S7 Type | Width | Read | Write | Notes | +|----------|---------|-------|------|-------|-------| +| `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 | Byte-anchored — see below | +| `UInt64` | ULINT/LWORD | 8 bytes | yes | yes | Byte-anchored — see below | +| `Float64` | LREAL | 8 bytes | yes | yes | Byte-anchored — see below | +| `String` | STRING | `StringLength + 2` bytes | yes | yes | Byte-anchored; default `StringLength`=254 | +| `DateTime` | DATE_AND_TIME | 8 bytes | yes | yes | Byte-anchored — see below | +| Timer | TIME | — | yes | **no** | `T{n}` address only; DataType must be `Float64`; value = seconds | +| Counter | COUNTER | — | yes | **no** | `C{n}` address only; DataType must be `Int32`; value = raw word | + +### Wide / Structured Types — Byte-Anchored Addressing + +Wide types (`Int64`, `UInt64`, `Float64`/LReal, `String`, `DateTime`) are **byte-anchored**: the +address must use the `B` suffix to specify the start byte; the driver reads the +correct number of bytes based on the DataType. + +| DataType | Required address form | Example | +|----------|-----------------------|---------| +| `Int64` | `DB{n}.DBB{offset}` / `MB{offset}` / `IB{offset}` / `QB{offset}` | `DB1.DBB8` → LINT at bytes 8–15 | +| `UInt64` | same | `DB2.DBB16` → ULINT at bytes 16–23 | +| `Float64` | same | `DB1.DBB8` → LREAL at bytes 8–15 | +| `DateTime` | same | `DB3.DBB0` → DATE_AND_TIME at bytes 0–7 | +| `String` | same | `DB4.DBB0` DataType=String, StringLength=40 → reads 42 bytes | + +Using a `W` (word) or `D` (dword) suffix with a wide DataType produces an address parse +error at `InitializeAsync`. + +### Timer / Counter Read + +**Timer (`T{n}`)** — reads the timer's current value as elapsed **seconds** (double). The OPC UA +node DataType is `Float64`. Timers are **read-only** this phase; writes return `BadNotWritable`. + +**Counter (`C{n}`)** — reads the counter current value as a raw count (`int`). The OPC UA node +DataType is `Int32`. Counters are **read-only** this phase; writes return `BadNotWritable`. + +> **Counter known-limitation**: `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 (0–999), +> 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 return `BadNotSupported` or a config error if attempted: + +- **Wide-type arrays** — array tags (`isArray: true`) with element types `Int64`, `UInt64`, + `Float64`, `String`, or `DateTime`. +- **`S7WString`** (2-byte characters; distinct from `STRING`). +- **`DTL` / `DateTimeLong`** (12-byte Siemens date-and-time-long). +- **Timer / Counter writes** — surfaced as `BadNotWritable` until full write support lands. + ### PDU Size & Optimization | PLC | PDU Size | Max Data per Read | diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs index 2cda2f1e..57a26c72 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs @@ -20,13 +20,13 @@ public sealed class ReadCommand : S7CommandBase IsRequired = true)] public string Address { get; init; } = default!; - // Driver.S7.Cli-002: help text trimmed to the types the driver actually implements. - // Int64 / UInt64 / Float64 / String / DateTime are defined in S7DataType but the driver - // raises NotSupportedException (→ BadNotSupported) on reads of those types. + // Driver.S7.Cli-002: help text reflects the types the driver currently implements. /// Gets the data type to interpret the address as. [CommandOption("type", 't', Description = - "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 (default Int16). " + - "Int64, UInt64, Float64, String, and DateTime are not yet implemented and will return BadNotSupported.")] + "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " + + "Wide types (Int64, UInt64, Float64/LReal, String, DateTime) are byte-anchored: address must use the B suffix " + + "(e.g. DB1.DBB8, MB4) pointing at the first byte; width comes from the DataType. " + + "Timer (T{n}) reads elapsed seconds as Float64 (read-only). Counter (C{n}) reads the count as Int32 (read-only).")] public S7DataType DataType { get; init; } = S7DataType.Int16; /// Gets the maximum string length for string-type reads. diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs index 24b7e9eb..dbbf68a3 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs @@ -17,10 +17,12 @@ public sealed class SubscribeCommand : S7CommandBase public string Address { get; init; } = default!; /// Gets the data type of the address. - // Driver.S7.Cli-002: help text trimmed to the types the driver actually implements. + // Driver.S7.Cli-002: help text reflects the types the driver currently implements. [CommandOption("type", 't', Description = - "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 (default Int16). " + - "Int64, UInt64, Float64, String, and DateTime are not yet implemented and will return BadNotSupported.")] + "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " + + "Wide types (Int64, UInt64, Float64/LReal, String, DateTime) are byte-anchored: address must use the B suffix " + + "(e.g. DB1.DBB8, MB4) pointing at the first byte; width comes from the DataType. " + + "Timer (T{n}) reads elapsed seconds as Float64 (read-only). Counter (C{n}) reads the count as Int32 (read-only).")] public S7DataType DataType { get; init; } = S7DataType.Int16; /// Gets the polling interval in milliseconds. diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs index 2ccb1d10..90287597 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs @@ -20,13 +20,12 @@ public sealed class WriteCommand : S7CommandBase public string Address { get; init; } = default!; /// Gets or sets the data type of the value to write. - // Driver.S7.Cli-002: help text trimmed to the types the driver actually implements. - // Int64 / UInt64 / Float64 / String / DateTime are defined in S7DataType but the driver - // raises NotSupportedException (→ BadNotSupported) on any read/write of those types; - // advertising them misleads operators who then see BadNotSupported with no explanation. + // Driver.S7.Cli-002: help text reflects the types the driver currently implements. [CommandOption("type", 't', Description = - "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 (default Int16). " + - "Int64, UInt64, Float64, String, and DateTime are not yet implemented and will return BadNotSupported.")] + "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " + + "Wide types (Int64, UInt64, Float64/LReal, String, DateTime) are byte-anchored: address must use the B suffix " + + "(e.g. DB1.DBB8, MB4) pointing at the first byte; width comes from the DataType. " + + "Timer (T{n}) and Counter (C{n}) are read-only — writes to them return BadNotWritable.")] public S7DataType DataType { get; init; } = S7DataType.Int16; /// Gets or sets the value to write.