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.
12 KiB
Siemens S7 Driver
Getting-started guide for the Siemens S7 driver. This is the short path — for
the full per-field spec read docs/v2/driver-specs.md §5,
for hands-on CLI testing read Driver.S7.Cli.md, and for
the test-harness map read S7-Test-Fixture.md.
What it talks to
Siemens S7 PLCs — S7-300, S7-400, S7-1200, S7-1500, plus S7-200 / S7-200 Smart
/ LOGO! 0BA8 — over the native S7comm protocol on ISO-on-TCP, TCP port
102. The wire is spoken by the pure-managed S7netplus
(S7.Net) library: no native DLL, no P/Invoke, no out-of-process isolation. The
driver runs in-process in the OtOpcUa server's .NET 10 AnyCPU host on every OS
the server runs on.
This is the leanest OtOpcUa driver — read/write/subscribe/discover plus a connectivity probe, and nothing else. It implements no alarm source and no per-call host resolver (a single S7 instance targets a single CPU).
Project split
| Project | Target | Role |
|---|---|---|
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/ |
net10.0 | In-process driver — hosts the S7.Net.Plc connection and the address parser |
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/ |
net10.0 | Dependency-free config records + enums (S7DriverOptions, S7CpuType, S7DataType) bound from DriverConfig JSON |
Minimum deployment
Register the driver instance in the central config DB (or appsettings.json).
No separate service, no DLL deployment:
"Drivers": {
"s7-line-1": {
"Type": "S7",
"Config": {
"Host": "10.20.30.40",
"CpuType": "S71500",
"Rack": 0,
"Slot": 0,
"Tags": [
{ "Name": "Running", "Address": "DB1.DBX0.0", "DataType": "Bool", "Writable": false },
{ "Name": "Speed", "Address": "DB1.DBD4", "DataType": "Float32", "Writable": true }
]
}
}
}
S7 exposes a symbol table, but S7.Net does not surface it — so the driver
operates off a static, per-site tag list, not live symbol discovery.
Rack / slot / CPU family
CpuType selects the ISO-TSAP slot byte used during the connection handshake;
pick the family that matches the PLC exactly. Rack is almost always 0
(relevant only for distributed S7-400 racks). Slot conventions per family:
S7-300 = slot 2, S7-400 = slot 2 or 3, S7-1200 / S7-1500 = slot 0 (onboard PN).
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.
Address forms
Addresses use Siemens TIA-Portal / STEP 7 Classic syntax, parsed by
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7AddressParser.cs:
| Area | Example | Meaning |
|---|---|---|
| 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.
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.
| 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'sCounter.FromByteArrayreturns 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 typesInt64,UInt64,Float64,String, orDateTime. S7WString(2-byte character strings; distinct from classicSTRING).DTL/DateTimeLong(12-byte Siemens date-and-time-long).- Timer / Counter writes — surfaced as
BadNotWritableuntil write support lands.
Capability surface
S7Driver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe
(src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs).
| Capability | Path | Notes |
|---|---|---|
IReadable |
ReadAsync → S7.Net.Plc.ReadAsync |
One request/response per tag, serialized on a per-PLC semaphore |
IWritable |
WriteAsync → S7.Net.Plc.WriteAsync |
Read-only tags (Writable=false) return BadNotWritable |
ITagDiscovery |
DiscoverAsync |
Emits a flat S7/ folder of the configured tags — no live browse |
ISubscribable |
per-tag poll loop with capped exponential backoff | S7 has no push model; floor is 100 ms (the CPU services the comms mailbox once per scan) |
IHostConnectivityProbe |
periodic S7.Net.Plc.ReadStatusAsync (CPU-status PDU) |
host:port host key; Running/Stopped transitions raise OnHostStatusChanged |
Single-connection policy
One S7.Net.Plc instance per PLC, serialized with a SemaphoreSlim.
Parallelising reads against a single CPU doesn't help — the CPU scans its
comms mailbox at most once per cycle and queues concurrent requests wire-side
anyway, while wasting the CPU's 8-64 connection-resource budget.
PUT/GET communication
S7-1200 / S7-1500 ship with PUT/GET access disabled by default. A driver
pointed at a freshly-flashed CPU sees a hard access-denied fault. The driver
maps it specifically to BadNotSupported, flags the instance Faulted (a
configuration alert, not a transient fault), and does not blind-retry —
because the CPU will keep refusing. Fix: enable PUT/GET communication in TIA
Portal under Protection & Security for the CPU.
Error mapping
| Condition | StatusCode | Health |
|---|---|---|
| Tag not in config | BadNodeIdUnknown |
unchanged |
| Read-only tag written | BadNotWritable |
unchanged |
| Unimplemented data type | BadNotSupported |
unchanged |
| PUT/GET denied | BadNotSupported |
Faulted (config alert) |
| CPU / hardware fault | BadDeviceFailure |
Degraded |
| Socket / timeout | BadCommunicationError |
Degraded |
Testing
- Unit tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/cover the address parser, the reinterpret/box conversions, and the driver lifecycle. - Integration fixture — a Docker S7 simulator on the shared docker host; see S7-Test-Fixture.md for the coverage map and endpoint.
- CLI — Driver.S7.Cli.md documents the standalone read/write/probe CLI for manual checks against a real or simulated CPU.
1-D array support
An S7 tag becomes a 1-D OPC UA array node when its TagConfig JSON carries
"isArray": true and "arrayLength": N (N ≥ 1). The canonical rule:
isArray: true + arrayLength >= 1 → array; isArray: false (any length) → scalar.
Read mechanism — the driver issues a single ReadBytesAsync call over the contiguous
memory span starting at the declared address for N × (bytes per element) bytes, then
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 — 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
the contiguous-block read and per-element decode loop. Integration fixture is down during
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,
wide-type array elements (Int64, UInt64, Float64, String, DateTime), and
Timer/Counter array elements.
See Uns.md §Array tags for the cross-driver coverage matrix and the UI authoring flow.
Further reading
docs/v2/driver-specs.md §5— full per-field spec, DriverConfig JSON shape, and operational stability notes- Driver.S7.Cli.md — standalone S7 driver CLI
- S7-Test-Fixture.md — simulator + test-harness map