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.
This commit is contained in:
@@ -156,6 +156,38 @@ public sealed class ModbusCoalescingAutoRecoveryTests
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAutoProhibitedRanges_Surfaces_Operator_Visible_Snapshot()
|
||||
{
|
||||
// #152 — diagnostic accessor returns the live prohibition map as a snapshot of public
|
||||
// ModbusAutoProhibition records. Consumers (Admin UI, dashboards) project this list
|
||||
// into whatever shape they need.
|
||||
var fake = new ProtectedHoleTransport { ProtectedAddress = 102 };
|
||||
var t100 = new ModbusTagDefinition("T100", ModbusRegion.HoldingRegisters, 100, ModbusDataType.Int16);
|
||||
var t102 = new ModbusTagDefinition("T102", ModbusRegion.HoldingRegisters, 102, ModbusDataType.Int16);
|
||||
var t104 = new ModbusTagDefinition("T104", ModbusRegion.HoldingRegisters, 104, ModbusDataType.Int16);
|
||||
var opts = new ModbusDriverOptions { Host = "f", UnitId = 7, Tags = [t100, t102, t104], MaxReadGap = 5,
|
||||
Probe = new ModbusProbeOptions { Enabled = false } };
|
||||
var drv = new ModbusDriver(opts, "m1", _ => fake);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
// Pre-failure: nothing prohibited.
|
||||
drv.GetAutoProhibitedRanges().ShouldBeEmpty();
|
||||
|
||||
await drv.ReadAsync(["T100", "T102", "T104"], CancellationToken.None);
|
||||
|
||||
var snapshot = drv.GetAutoProhibitedRanges();
|
||||
snapshot.Count.ShouldBe(1);
|
||||
snapshot[0].UnitId.ShouldBe((byte)7);
|
||||
snapshot[0].Region.ShouldBe(ModbusRegion.HoldingRegisters);
|
||||
snapshot[0].StartAddress.ShouldBe((ushort)100);
|
||||
snapshot[0].EndAddress.ShouldBe((ushort)104);
|
||||
snapshot[0].BisectionPending.ShouldBeTrue("multi-register prohibition starts split-pending");
|
||||
snapshot[0].LastProbedUtc.ShouldBeGreaterThan(DateTime.UtcNow.AddMinutes(-1));
|
||||
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tags_Outside_Prohibited_Range_Still_Coalesce()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user