fix(s7): a refused span falls back per member and is not regrouped (R07:P2)

A span's block is the union of its members' ranges, so one member that runs
past the DB's real end (or over-declares a STRING) made the CPU refuse the
whole block, and every neighbour within it was Bad on every poll. The
planner's stated invariant (a grouped read cannot fault where per-tag reads
would succeed) was false; the multi-var path already fell back, spans did not.

- A refused multi-member span hands its members back; they are read one at a
  time for that pass, bypassing the multi-var pass.
- _noGroup remembers the members refused on their own (or every member when
  none was) and keeps them out of later plans, so the rest group again. It is
  cleared by every initialize publish. Non-refusal span failures are
  unchanged (every member stamped).
- The once-per-tag refusal Warning memo (R07:H4) is now also cleared by the
  publish, and shutdown resets the write-side Degraded owner, as documented.
- Live gate: MW10 + MD254 (2 bytes past the 256-byte M area) merged into one
  refused block; MW10 keeps reading 4097 on every pass.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 15:30:12 -04:00
parent 1d83cde6bc
commit 1f7ab80b81
5 changed files with 295 additions and 6 deletions
+7
View File
@@ -344,6 +344,13 @@ to one doomed per-tag read per member. Without this an op that ignores its cance
one full deadline per remaining item under the gate, and `Degraded` (and so the comms-loss watchdog's
clock) only became visible after the whole pass.
**A refused span falls back per member (R07:P2).** A grouped block read covers the union of its members'
ranges, so one member the CPU refuses (running past the DB's real end, an over-declared `stringLength`)
used to make every neighbour in the block Bad on every poll. When the CPU refuses a multi-member span the
driver re-reads its members one at a time for that pass and remembers which tags must not be grouped —
the members refused on their own, or every member when none was — so later passes group only the rest.
The memo lasts until the next initialize. A span failure that is not a refusal still stamps every member.
**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