# 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`](../v2/driver-specs.md),
for hands-on CLI testing read [Driver.S7.Cli.md](../Driver.S7.Cli.md), and for
the test-harness map read [S7-Test-Fixture.md](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](https://github.com/S7NetPlus/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:

```jsonc
"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`'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

`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](S7-Test-Fixture.md) for the coverage map and endpoint.
- **CLI** — [Driver.S7.Cli.md](../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](../Uns.md#array-tags-1-d) for the cross-driver coverage matrix
and the UI authoring flow.

## Further reading

- [`docs/v2/driver-specs.md §5`](../v2/driver-specs.md) — full per-field spec,
  DriverConfig JSON shape, and operational stability notes
- [Driver.S7.Cli.md](../Driver.S7.Cli.md) — standalone S7 driver CLI
- [S7-Test-Fixture.md](S7-Test-Fixture.md) — simulator + test-harness map
