Auto: s7-c5 — pre-flight PUT/GET enablement test

Closes #298
This commit is contained in:
Joseph Doherty
2026-04-26 01:31:48 -04:00
parent 4bc8aa2478
commit 64a11ef285
9 changed files with 625 additions and 3 deletions

View File

@@ -34,6 +34,28 @@ Enable it in TIA Portal: *Device config → Protection & Security → Connection
mechanisms → "Permit access with PUT/GET communication from remote partner"*.
Without it the CLI's first read will surface `BadNotSupported`.
### Pre-flight PUT/GET enablement (PR-S7-C5)
The driver issues a tiny 2-byte read against `Probe.ProbeAddress` (default
`MW0`) immediately after `OpenAsync` and **fails `InitializeAsync` with a
typed `S7PutGetDisabledException`** when the PLC rejects the read with the
wire-level "function not allowed" response. The exception message names the
exact TIA Portal toggle to flip — operators see the configuration fix at
init time, not after the first per-tag read produces `BadDeviceFailure`.
Two opt-out knobs on the JSON `Probe` block:
- `ProbeAddress` — set to `""` (empty string) to skip the pre-flight read
entirely. Useful when no fingerprint address has been wired.
- `SkipPreflight` — set to `true` to defer the check to runtime while
keeping the background liveness loop. Per-tag reads still surface
`BadDeviceFailure` until PUT/GET is enabled, but Init succeeds and the
driver becomes visible in the Admin UI.
See [s7.md "Pre-flight PUT/GET enablement"](v2/s7.md#pre-flight-putget-enablement)
for the full rationale, classifier behaviour, and the wire-level
`ErrorCode` matching.
## S7 address grammar cheat sheet
| Form | Meaning |

View File

@@ -113,6 +113,18 @@ arrays of structs — not covered.
lab rig but not CI.
3. **Real S7 lab rig** — cheapest physical PLC (CPU 1212C) on a dedicated
network port, wired via self-hosted runner.
4. **PR-S7-C5 — PUT/GET-disabled pre-flight rejection.** Snap7 does *not*
model the hardened-CPU PUT/GET response (it accepts every read once the
COTP handshake completes), so the **failure** path of the pre-flight
probe — `S7PutGetDisabledException` thrown from `InitializeAsync` when
the PLC rejects the probe read with `ErrorCode.WrongCPU_Type` /
`ErrorCode.ReadData` — needs a real S7-1500 with PUT/GET disabled in TIA
Portal. The integration suite covers the *happy* path
(`Driver_preflight_passes_when_probe_address_seeded`); the failure path
should be added as a `--with-real-plc` opt-in test that the self-hosted
runner with the lab rig executes. The classifier branch
(`S7PreflightClassifier.IsPutGetDisabled`) is unit-tested without a
network in `S7PreflightTests.Classifier_matches_only_PUT_GET_disabled_error_codes`.
Without any of these, S7 driver correctness against real hardware is trusted
from field deployments, not from the test suite.

View File

@@ -768,6 +768,98 @@ is satisfied.
whether to invoke `OnDataChange`. The mailbox / PDU / coalescing path
is untouched.
## Pre-flight PUT/GET enablement
S7-1200 / S7-1500 CPUs ship with **PUT/GET communication disabled by
default**. The COTP / S7comm handshake itself succeeds against these
locked-down CPUs (you can `OpenAsync` / negotiate PDU size cleanly), so
the failure surfaces only on the *first* `Plc.ReadAsync` — at which
point the driver is already past `InitializeAsync`, has flipped to
`DriverState.Healthy`, and dependent code (subscriptions, Admin UI) is
binding against a connection it can't actually use. Operators see
`BadDeviceFailure` per tag instead of a single, actionable
configuration error.
PR-S7-C5 adds a **post-`OpenAsync` pre-flight probe**: a tiny 2-byte
read against `Probe.ProbeAddress` (default `MW0`). If the PLC rejects
that read with the wire-level "function not allowed in current
operating state" response (S7 error family `D6 05` / `85 00`),
S7netplus surfaces the rejection as `PlcException` with one of
`ErrorCode.WrongCPU_Type` (CPU drops the connection mid-response) or
`ErrorCode.ReadData` (CPU sends an S7-level error byte). The driver
classifies that pair as "PUT/GET disabled" and throws a typed
`S7PutGetDisabledException` from `InitializeAsync` so the operator sees
the TIA-Portal fix path immediately:
> PUT/GET communication is disabled on the PLC. Enable it in TIA Portal:
> *Device → Properties → Protection & Security → Connection mechanisms →
> "Permit access with PUT/GET communication from remote partner"*.
> Re-deploy the hardware config and restart the S7 driver.
`S7PreflightClassifier.IsPutGetDisabled(PlcException)` is the pure
function that decides whether a given `PlcException` qualifies; it
matches **only** `WrongCPU_Type` and `ReadData`. Other error codes
(`ConnectionError`, `IPAddressNotAvailable`, `WrongVarFormat`, …)
indicate transport / framing faults rather than PUT/GET gating, so the
driver re-throws the original `PlcException` unchanged and the existing
`DriverState.Faulted` path takes over with the original message.
### Knobs
Two opt-out knobs on `S7ProbeOptions`:
- `ProbeAddress` (`string?`, default `"MW0"`) — address probed by both
the background liveness loop and the pre-flight read. Set to `null`
(or empty string in JSON) to skip the pre-flight entirely. Useful
for sites where no fingerprint address has been wired and an arbitrary
read at `MW0` would itself be misleading.
- `SkipPreflight` (`bool`, default `false`) — opt out of the pre-flight
read while keeping the background probe. Init succeeds against a
PUT/GET-disabled CPU; per-tag reads still surface `BadDeviceFailure`
at runtime. Useful for staged deployments where the operator hasn't
enabled PUT/GET yet but wants the driver visible in the Admin UI.
### Why `MW0`?
The convention from `Driver.S7.Cli.md`'s `probe` command. `MW0` exists
on every S7 CPU regardless of project — Merker memory is universal —
so it's a safe default that doesn't require a per-site DB to be wired.
Sites with a dedicated fingerprint DB can override to e.g.
`DB1.DBW0`.
### JSON config example
```json
{
"Host": "10.0.0.50",
"Probe": {
"Enabled": true,
"IntervalMs": 5000,
"TimeoutMs": 2000,
"ProbeAddress": "DB1.DBW0",
"SkipPreflight": false
}
}
```
To skip the pre-flight (defer the check to first read):
```json
{
"Host": "10.0.0.50",
"Probe": { "SkipPreflight": true }
}
```
To skip the probe entirely (no pre-flight, no liveness loop):
```json
{
"Host": "10.0.0.50",
"Probe": { "Enabled": false, "ProbeAddress": "" }
}
```
## TSAP / Connection Type
S7comm runs on top of ISO-on-TCP (RFC 1006), and the COTP connection-request