Pre-#150 a coalesced read failure recorded the FULL failed range as
permanently prohibited. Healthy registers around the actual protected
register stayed in per-tag mode forever (until ReinitializeAsync). The
re-probe loop shipped in #151 retried the whole range as a single block,
which would either succeed (clearing everything) or fail (changing
nothing).
Post-#150 the re-probe loop bisects multi-register prohibitions:
- _autoProhibited refactored from Dictionary<key, DateTime> to
Dictionary<key, ProhibitionState> where ProhibitionState carries
LastProbedUtc + SplitPending. Multi-register prohibitions enter with
SplitPending=true; single-register prohibitions enter with
SplitPending=false (already minimal).
- ReprobeLoopAsync delegates the per-pass work to
RunReprobeOnceForTestAsync (also exposed for synchronous test driving).
Each entry routes to BisectAndReprobeAsync (split-pending + multi-reg)
or StraightReprobeAsync (single-reg / non-split-pending).
- Bisection: split (start, end) at mid = (start+end)/2. Try (start, mid)
and (mid+1, end) as separate coalesced reads. Each FAILED half re-enters
the prohibition map with SplitPending = (its end > its start). SUCCEEDED
halves vanish, freeing the planner to coalesce across them on the next
scan.
- Convergence: log2(span) re-probe ticks pin the prohibition to the
actual single offending register(s). For a 100-register block with one
protected address that's ~7 ticks.
Tests (3 new ModbusCoalescingBisectionTests):
- Bisection_Narrows_Multi_Register_Prohibition_Per_Reprobe — 11 tags
100..110 with protected address 105. After 4 re-probe passes the
prohibition collapses from (100..110) → (100..105) → (103..105) →
(105..105).
- Bisection_Clears_When_Both_Halves_Are_Healthy — transient failure
scenario; protection lifted before re-probe; both bisection halves
succeed and the parent vanishes entirely.
- Bisection_Splits_Into_Two_When_Both_Halves_Still_Fail — TwoHoleTransport
with protected addresses 102 + 108 in the same coalesced range. After
bisection both halves still fail (each contains one of the protected
addresses); the prohibition map grows to 2 entries.
236 + 3 = 239 unit tests green. Solution build clean.