Commit Graph

20 Commits

Author SHA1 Message Date
Joseph Doherty a0b635f673 fix(s7): probe gate contention is no verdict; Test Connect honours timeoutMs (R07:H7)
- The background probe reported the host Stopped whenever a read pass held
  _gate past the probe deadline: contention is the connection being used,
  not lost. A contended tick now gives no verdict (counted for tests); a
  real probe failure still reports Stopped. A tick that races a teardown no
  longer reopens a connection on an uninitialised driver.
- S7DriverProbe deserialised S7DriverOptions directly, whose Timeout is a
  TimeSpan no config names, so "timeoutMs" was ignored and every handshake
  waited the whole Test Connect budget. The handshake and test read are now
  bounded by timeoutMs, capped by the caller's timeout.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:33:38 -04:00
Joseph Doherty 1f7ab80b81 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
2026-10-22 15:30:12 -04:00
Joseph Doherty 1d83cde6bc perf(s7): fail the rest of a pass fast once the handle is dead (R07:P1)
No read or write loop checked _plcDead between items: on a frozen-but-
established peer the first op blew its deadline and marked the handle dead,
and every remaining span / tag / write still went to the wire and could wait
its own deadline under _gate (a 20-span pass ~100 s), with Degraded and the
comms-loss watchdog's clock deferred until the pass ended.

- The span loop, the multi-var chunk loop, the per-tag loop and the write
  loop stamp BadCommunicationError without I/O once _plcDead is set.
- A multi-var chunk that dies on a transport loss (socket / IO / timeout /
  ConnectionError) marks the handle dead instead of falling back to one
  doomed per-tag read per member. The framing class is deliberately excluded
  there: snap7/S7.Net report a multi-var with one refused item as
  ReadData(WrongNumberReceivedBytes) over a healthy stream.
- Live gate via an in-test freezable TCP proxy (counts request frames, not
  seconds). Mutation shows S7.Net 0.20 already self-closes on a cancelled op,
  so the gate pins the end-to-end property; the driver guard itself is proven
  by the unit tests (an op that ignores its token keeps the socket open).

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:26:14 -04:00
Joseph Doherty a1f20e78f9 fix(s7): a CPU refusal is per-tag Bad and counts as answered (R07:H4)
A deterministic PLC refusal (object does not exist, address out of range,
type inconsistent...) degraded driver health, and an all-refused driver (a
wrong DB number) sat Degraded with LastSuccessfulRead frozen, so the 90 s
comms-loss watchdog reconnected it forever against a reachable PLC.

- IsRefusal / IsAccessDenied classify over the whole exception chain. Live
  probe on the snap7 fixture: S7.Net's READ paths raise the item error as a
  BARE Exception ("Received error from PLC: ..."), its write paths as
  PlcException(WriteData). The old PlcException-only match missed the read
  shape entirely, so a real read access-denied was BadCommunicationError +
  Degraded instead of the Faulted PUT/GET alert; the Test Connect probe had
  the same blind spot.
- A refusal (read or write) is BadDeviceFailure with a once-per-tag Warning,
  no health write and no reopen; it counts as answered, so it advances
  LastSuccessfulRead and restores Healthy after a comms fault. An undecodable
  buffer (S7ValueRangeException) also counts as answered.
- A write batch the CPU answered without a failure advances
  LastSuccessfulRead and retires a Degraded the write side itself published.
- Tests that used ReadData as their generic "device fault" now use SendData
  so their Degraded leg is still exercised.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:20:51 -04:00
Joseph Doherty 87a7414df7 fix(s7): refuse unservable tag shapes per tag at every gate (R07:H3, R07:U4)
The init-time tag guards THREW: one mis-authored tag (DB1.DBZ0, DB1.DBW0 typed
Float64, T5 typed Int32, a writable Bool array) faulted the whole driver and the
actor retried the same init every 10 s forever, darkening every tag on the PLC.
Neither the editor nor the deploy gate checked any of it.

- S7AddressParser moves to S7.Contracts (same namespace) and a new
  S7TagShapeRules there is the single authority on servable shapes.
- S7TagDefinitionFactory.FromTagConfig applies it, so the AdminUI editor, the
  deploy gate (Inspect, with the specific reason) and the driver init refuse
  the same tags. Init now skips a refused tag (ISkippedTagReporter) instead of
  throwing; the throwing RejectUnsupportedTagConfigs is gone.
- New rules on top of the old guards: a narrow type at the wrong access width
  (every read failed) and a Bool array not starting at bit 0 (U4: DB1.DBX0.3
  silently read bit 0 of bytes 0..n).
- AdminUI test fixtures that used addresses the driver could never serve
  (DB1.DBS0, a browse "SomeAddress", Int32/Float32 on DBW) now use real ones.

Refs #742
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:04:19 -04:00
Joseph Doherty cd50db136e docs: clarify S7 counter evidence and fixture limits 2026-10-13 11:34:59 -04:00
Joseph Doherty 9b74a65b25 Merge b640-640: driver diagnostics surface on /hosts (#640)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 17:13:34 -04:00
Joseph Doherty c2bdb7d5de feat(hosts): driver diagnostics channel on /hosts, S7 negotiated PDU size first (#640)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 16:57:38 -04:00
Joseph Doherty 2aae3c094f test(s7): live python-snap7 coverage for the #632/#638 codecs, TSAP override and PDU size (#632)
Extends the s7_1500 profile (DB1 2048 bytes, 'hex' seed type) with distinct
non-zero seeds for STRING/WSTRING/CHAR/WCHAR/DTL/DATE/TOD/TIME/S5TIME plus
undecodable buffers; S7_1500CodecTests covers reads, BadOutOfRange paths,
write round-trips (S7_SIM_ALLOW_WRITES=1), TSAP keys and negotiated PDU size.
Skips when S7_SIM_ENDPOINT is unreachable.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 16:53:02 -04:00
Joseph Doherty ff055ebd79 fix(s7): numeric TSAP loads in form, shared TSAP conflict error, Test Connect read detects PUT/GET disabled, pin Int32 ms rounding (#632 #638)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:38:49 -04:00
Joseph Doherty 34c52defb3 feat(s7): STRING/WSTRING/CHAR/WCHAR and DTL/DATE/TOD/TIME/S5TIME codecs, TSAP/connection-type override, negotiated PDU surfaced (#632 #288 #289 #294 #295)
New S7DataType members WString/Char/WChar/Dtl/Date/Tod/Time/S5Time mapped onto existing
DriverDataType (String / DateTime / Int32 ms); writes refuse out-of-range with BadOutOfRange.
Optional localTsap/remoteTsap/connectionType through options, factory JSON, probe and AdminUI form.
NegotiatedPduSize snapshotted at connect and exposed via GetDiagnostics (DriverHealth has no bag).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:31:44 -04:00
Joseph Doherty b7a48e2b93 Merge #610: server-side array writes gated per driver capability (DriverArrayWriteSupport); ArrayWritable on the surgical whitelist + UNS Backing; shape/IndexRange refusal; ValueRank>1 out of scope
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe

# Conflicts:
#	docs/drivers/S7.md
2026-10-08 07:39:40 -04:00
Joseph Doherty 18f8eb5d2f feat(address-space): server-side array writes gated per driver capability (#610)
Array nodes were forced read-only for every driver. A new single authority,
Core.Abstractions DriverArrayWriteSupport, says which (driver, element type)
pairs encode a whole-array write: Modbus (Coils / numeric HoldingRegisters),
S7 (Byte/Int16/UInt16/Int32/UInt32/Float32), OpcUaClient (pass-through) and
Galaxy (Boolean/Int32/Int64/Float/Double/String/DateTime). Everything else
stays read-only, so the server never advertises a write the driver refuses.

- Composer derives RawTagPlan.ArrayWritable + UnsReferenceVariable
  .BackingArrayWritable at compose time (both seams, byte-parity); the
  applier's one writability rule is Writable && (!IsArray || ArrayWritable)
  for materialise and surgical paths, raw and UNS alike.
- ArrayWritable is on the classifier's surgical raw-tag whitelist and
  mirrored on the UNS record (UnsReferenceBackingParityTests updated).
- Node manager refuses a wrong-length / non-array value on an array node
  (BadTypeMismatch) and any IndexRange write (BadWriteNotSupported) inline,
  before dispatch; WriteOperate/ACL gates and the revert are unchanged.
- Per-driver parity tests pin the table against the real Modbus write path,
  S7 EncodeArrayBlock and Galaxy MxValueEncoder (UInt32/UInt64 excluded:
  array covariance reinterprets their bits).
- Over-the-wire proof in OpcUaServer.IntegrationTests (ArrayWriteWireTests).
- ValueRank > 1 documented as out of scope by decision (docs/Uns.md).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:39:05 -04:00
Joseph Doherty e77bca385a fix(galaxy,s7): R2 review minors — subscribe-once deadline, probe vs backoff, write-fault ownership, null array elements, write opt-in
- Galaxy: deadline bounds the wait, not the subscribe RPC; BadTimeout for all tags and late handles unsubscribed; doc comment relocated
- S7: probe connect runs on the connect deadline so a timeout opens the backoff window
- S7: a successful write only retires a Faulted the write side owns
- S7: null array elements rejected (BadTypeMismatch); multi-PDU array writes documented non-atomic
- S7 integration: array write test gated on S7_SIM_ALLOW_WRITES=1

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:31:22 -04:00
Joseph Doherty 75c66c16ef Merge #625: deferred code-review findings resolved or ruled (S7.Cli verdict, browser CP release, Cluster member events, Sql Detach hoist, docs)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:01:32 -04:00
Joseph Doherty 3fb6549209 fix(review): resolve deferred code-review findings; S7.Cli verdict, browser CP release, MemberLeft/Exited/Downed, Sql Detach hoist (#625)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:59:53 -04:00
Joseph Doherty 82372f1672 fix(s7): connect backoff, worst-state write health, Volatile health, array writes (#602, #610)
STAB-21 ConnectionBackoff in EnsureConnectedAsync; STAB-22 write-pass accumulator with a
write-side Faulted that a clean read does not erase; _health on the Volatile idiom;
S7-016 contiguous array write (Bool arrays stay refused).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:59:29 -04:00
Joseph Doherty b7dfb5aff2 docs(s7): wide-type/Timer-Counter support — CLI help + driver-specs + S7 driver doc
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.
2026-06-17 06:22:36 -04:00
Joseph Doherty 05c7e86f0c docs(phase4c): cross-driver 1-D array support + isArray/arrayLength keys + coverage matrix 2026-06-16 22:18:55 -04:00
Joseph Doherty f21883d607 docs(audit): G2 completeness — S7/TwinCAT/OpcUaClient/Historian overview pages 2026-06-03 16:14:40 -04:00