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>