Merge #602(S7)/#610.2: S7 connect backoff, write-path worst-state-wins, Volatile health, factory logging, driver-side array writes

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 07:01:32 -04:00
12 changed files with 845 additions and 75 deletions
+27 -1
View File
@@ -195,6 +195,21 @@ Portal under *Protection & Security* for the CPU.
| PUT/GET denied | `BadNotSupported` | `Faulted` (config alert) |
| CPU / hardware fault | `BadDeviceFailure` | `Degraded` |
| Socket / timeout | `BadCommunicationError` | `Degraded` |
| Reconnect suppressed (backoff window) | `BadCommunicationError` | `Degraded` |
| Array write of the wrong shape / length / element type | `BadTypeMismatch` | unchanged |
| Bool / wide-type array write | `BadNotSupported` | unchanged |
**Health is decided once per pass, worst state wins.** A read pass and a write batch each
accumulate their outcomes and write health once (`Healthy < Degraded < Faulted`), so a PUT/GET-denied
`Faulted` item is not erased by a later `Degraded` or successful sibling. The write-side `Faulted` is
**sticky against reads**: a clean read pass (reads often work against a write-protected CPU) leaves
`Faulted` in place and only advances `LastSuccessfulRead`; the next write that succeeds clears it. A
transient `Degraded` write failure is still cleared by a clean read, as before.
**Connect backoff.** A dead PLC costs one connect attempt per backoff window (1 s doubling to a 30 s
cap), not one per read / poll tick / probe tick: inside the window `EnsureConnectedAsync` fails fast
(`BadCommunicationError`, `Degraded`) without a connect. A successful connect clears the window, so a
fresh drop after a healthy period reconnects immediately; worst-case recovery lag is the 30 s cap.
## Testing
@@ -228,7 +243,18 @@ 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,
**Array writes (S7-016)** — a writable `Byte` / `Int16` / `UInt16` / `Int32` / `UInt32` / `Float32` array
is written as ONE contiguous `WriteBytesAsync` of `count x element-bytes` (the inverse of the read
decode), and the value must be an array of exactly `arrayLength` elements (else `BadTypeMismatch`, no
health impact). **Writable `Bool` arrays are refused at init** (`NotSupportedException`): the array
read is byte-granular, so a matching write would overwrite the 7 neighbouring bits of every byte it
touches, and a safe bit-packed write needs read-modify-write. Mark Bool arrays `writable: false`.
Note the *server* still presents every array node read-only to OPC UA clients ([Uns.md §Array
tags](../Uns.md#array-tags-1-d), #610 item 1) — the driver path is reachable today through the Client
CLI / driver-level callers and is proven by the unit suite plus the snap7 fixture test
(`S7_1500ArrayWriteTests`); the AdminUI tag editor rejects wide-type and writable-Bool arrays at author time.
**Deferrals** — server-side exposure of array writes, multi-dimensional arrays, per-element historization,
wide-type array elements (`Int64`, `UInt64`, `Float64`, `String`, `DateTime`), and
Timer/Counter array elements.