fix: stop UI freeze when selecting a pump that needs immobilizer unlock

Phase 2 TestUnlock handshake was synchronous (Thread.Sleep x 8 = 4 s) and
the continuation after Phase 1 marshalled back to the WPF dispatcher via
the captured SynchronizationContext, so the eight 500 ms sleeps froze the
UI right before unlock completed.

- UnlockService.RunTestUnlockSequence -> async RunTestUnlockSequenceAsync
  with Task.Delay(500, ct) and ConfigureAwait(false)
- Add ConfigureAwait(false) on every internal await in UnlockService so
  continuations no longer hop to the UI thread (Task.WhenAll, Task.Delay,
  connectedTcs, TryFastUnlockAsync, fast-unlock settle delay)
- CancellationToken now propagates through Phase 2, so the snackbar Cancel
  button can interrupt the handshake within 500 ms instead of waiting out
  all eight Thread.Sleeps

Includes the companion observer in IUnlockService / UnlockService
(PumpUnlocked event, IsPumpUnlocked, StartObserver/StopObserver) that
the Phase 1 wait now races against — lets any source of unlock (fast
unlock, external manual, CAN flood finally working) short-circuit the
600 s timer as soon as the CAN TestUnlock parameter confirms it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 14:36:45 +02:00
parent d9775b48be
commit 9a593e4ff2
2 changed files with 177 additions and 15 deletions

View File

@@ -17,6 +17,37 @@ namespace HC_APTBS.Services
/// <summary>Raised when the unlock sequence completes. Argument is true if successful.</summary>
event Action<bool>? UnlockCompleted;
/// <summary>
/// Raised by the background observer on each lock→unlock transition. Unlike
/// <see cref="UnlockCompleted"/>, this fires as soon as the CAN TestUnlock
/// parameter confirms unlocked — regardless of whether the transition was
/// caused by this service, an external manual unlock, or a fast-unlock
/// completing early. Subscribers must marshal to the UI thread themselves.
/// </summary>
event Action? PumpUnlocked;
/// <summary>
/// Latched state from the background observer. True when the observer has
/// verified the pump is unlocked; false when the observer is not running
/// or the pump is currently locked. Use alongside <see cref="PumpUnlocked"/>
/// to race-guard "was the event already fired?".
/// </summary>
bool IsPumpUnlocked { get; }
/// <summary>
/// Starts a 1-second polling observer that watches the CAN TestUnlock
/// parameter and raises <see cref="PumpUnlocked"/> on each lock→unlock
/// transition. Idempotent: stops any prior observer before starting the
/// new one. No-op when <see cref="PumpDefinition.UnlockType"/> is 0.
/// </summary>
void StartObserver(PumpDefinition pump);
/// <summary>
/// Stops the polling observer. Safe to call when no observer is active.
/// Also resets <see cref="IsPumpUnlocked"/> to false.
/// </summary>
void StopObserver();
/// <summary>
/// Runs the immobilizer unlock sequence for the given pump.
/// Returns immediately if <see cref="PumpDefinition.UnlockType"/> is 0 (no unlock needed).