5ed26d2ec651e6c78b3547b5c49123dc0219c16e
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b8df230eb8 |
Task #152 — Modbus coalescing: surface auto-prohibitions through diagnostics
Auto-prohibited ranges (#148) were previously visible only through an internal AutoProhibitedRangeCount accessor used by tests. Production operators had no way to see what the planner had learned without pulling logs or inspecting driver state. Changes: - New public record `ModbusAutoProhibition(UnitId, Region, StartAddress, EndAddress, LastProbedUtc, BisectionPending)` — operator-facing snapshot shape. Lives in the addressing assembly's logical namespace alongside the other public types. - `ModbusDriver.GetAutoProhibitedRanges()` returns `IReadOnlyList<ModbusAutoProhibition>` — a copy of the live prohibition map. Lock-protected snapshot so consumers don't race with the re-probe loop. - RecordAutoProhibition tracks first-fire vs re-fire via the dictionary insert path, leaving a hook to add structured logging once an ILogger is plumbed through (currently elided to keep the constructor minimal for testability — a future change can wire ILogger and emit a single warning per first-fire). Tests (1 new, additive to the 6 in ModbusCoalescingAutoRecoveryTests): - GetAutoProhibitedRanges_Surfaces_Operator_Visible_Snapshot — confirms the snapshot shape: empty before any failure, populated with correct UnitId/Region/Start/End/BisectionPending after a failed coalesced read, LastProbedUtc within the recent past. Docs: - docs/v2/modbus-addressing.md — new "Coalescing auto-recovery" subsection consolidates the #148/#150/#151/#152 surface in one place. Documents the diagnostic accessor + flags the in-process consumption pattern (Server health endpoints today; Admin UI when an RPC channel exists). 239 + 1 = 240 unit tests green. Caveat: the Admin UI surfacing (table render, "clear all prohibitions" button) is intentionally NOT shipped here. Admin can't reach a live ModbusDriver instance without a driver-diagnostics RPC channel that doesn't exist yet — that's a larger architectural piece. For now the data is queryable in-process by the Server's health endpoints; once an RPC channel lands, Admin can wire the existing GetAutoProhibitedRanges into a Blazor table without further driver changes. |
||
|
|
9e4aae350b |
Task #151 — Modbus coalescing: periodic re-probe of auto-prohibitions
#148 introduced auto-prohibited coalesced ranges that persist for the driver lifetime. Long-running deployments with transient PLC permission changes (firmware update unlocking a previously-protected register, operator reconfiguring the device) had no recovery short of operator restart. Adds an opt-in background loop that re-probes each prohibition periodically: - ModbusDriverOptions.AutoProhibitReprobeInterval (TimeSpan?, default null = disabled). Set to e.g. TimeSpan.FromHours(1) to opt in. - _autoProhibited refactored from HashSet<key> to Dictionary<key, DateTime> so each entry tracks its last failure / last re-probe timestamp. - ReprobeLoopAsync runs on the same Task.Run pattern as ProbeLoopAsync; cancelled by ShutdownAsync. Each tick snapshots the prohibition set and issues a one-shot coalesced read per range. Successful re-probes drop the prohibition; failed ones bump the timestamp + leave the prohibition in place. - Communication failures during re-probe (transport-level) are treated the same as PLC-exception failures — the prohibition stays, but isn't upgraded to "permanent" since transports recover. The driver-instance health surface picks up the failure separately. - ShutdownAsync explicitly clears the prohibition set so a manual restart via ReinitializeAsync starts with a clean slate (matches the old "restart to clear" semantics). - Factory DTO + JSON binding extended with AutoProhibitReprobeMs field. Tests (2 new, additive to the 3 in ModbusCoalescingAutoRecoveryTests): - Reprobe_Clears_Prohibition_When_Range_Becomes_Healthy — protected register at 102 records prohibition; clearing the simulated protection + invoking the re-probe drops the prohibition. - Reprobe_Leaves_Prohibition_When_Range_Is_Still_Bad — re-probe on a still-failing range keeps the prohibition in place. Tests use a new internal RunReprobeOnceForTestAsync helper to fire one re-probe pass synchronously, so the suite doesn't have to wait on the background timer (the loop's timer behaviour is exercised implicitly via the InitializeAsync wire-up + the synchronous helper sharing the actual re-probe code path). 234 + 2 = 236 unit tests green. |
||
|
|
3b0e093002 |
Task #148 — Modbus block-coalescing: auto-recover from protected register holes
Pre-#148 behaviour: a coalesced FC03/FC04 read that crossed a write-only or PLC-fault register marked every member tag Bad until the operator manually flagged the offending tag with CoalesceProhibited. Healthy tags around the hole stayed broken indefinitely. Post-#148: two-stage recovery, no operator intervention needed. 1. Same-scan fallback: when a coalesced read fails with a Modbus exception (IllegalDataAddress, SlaveDeviceFailure, etc.), the planner does NOT mark members handled. The per-tag fallback in the same scan reads each member individually — non-protected members surface Good values immediately, and only the actual protected register stays Bad. 2. Cross-scan prohibition: the failed range (Unit, Region, Start, End) is recorded in a per-driver `_autoProhibited` set. On subsequent scans the planner checks each candidate merge against the set and refuses to re-form any block that overlaps a known-bad range. Net effect: after one scan with a failure, the protected range goes "per-tag mode" indefinitely while ranges around it keep coalescing normally. Communication failures (timeouts, socket drops) are NOT auto-prohibited — they're transport-level, not structural. The same coalesced read can succeed once the transport recovers; recording it as "permanently bad" would defeat coalescing for the whole driver instance. Auto-prohibition state lives for the driver lifetime and clears on ReinitializeAsync (operator restart). A periodic re-probe is a follow-up if deployments need it without a restart. Implementation: - Added `_autoProhibited` HashSet<(byte, ModbusRegion, ushort, ushort)> + `_autoProhibitedLock` on ModbusDriver. - `RangeIsAutoProhibited(unit, region, start, end)` overlap check called from the planner when forming blocks. - `RecordAutoProhibition(...)` called from the catch (ModbusException) branch. - The catch (Exception) branch (non-Modbus failures) keeps the pre-#148 "mark all Bad in this scan, don't auto-prohibit" behaviour. - Internal `AutoProhibitedRangeCount` accessor for tests. Tests (3 new ModbusCoalescingAutoRecoveryTests): - First_Failure_Falls_Back_To_PerTag_Same_Scan — three tags around a protected register at 102: T100 + T104 surface Good values via the per-tag fallback in the SAME scan; T102 surfaces the exception. - Second_Scan_Skips_Coalesced_Read_Of_Prohibited_Range — confirms scan 2 doesn't re-attempt the failed merge (no FC03 with quantity > 1 at the prohibited start). - Tags_Outside_Prohibited_Range_Still_Coalesce — separate cluster at HR 200..202 keeps coalescing normally even after the 100..104 cluster is prohibited. 234/234 unit tests green. Follow-ups intentionally NOT shipped (smaller, independent changes): - Bisection-style range narrowing — currently the prohibition range is the full failed block; the planner doesn't try to find the exact protected register. Operator-visible diagnostic + prohibition stays correct. - Periodic re-probe to clear stale prohibitions. - Surface auto-prohibited ranges through GetHostStatuses or a new diagnostic so the Admin UI can show what's been auto-isolated. |