fix: gate Ford VP44 unlock on CAN liveness to prevent false-unlocked reads

Before this fix, StartUnlockIfRequired was called immediately after
registering the pump's CAN parameters, before any frames had been
decoded. The TestUnlock parameter's zero-initialised Value was
interpreted as "unlocked" for Type 1 pumps, causing Phase 1 to be
skipped and UnlockCompleted(true) to fire falsely.

Changes:
- ICanService: add IsPumpAlive property (volatile-backed in PcanAdapter)
- PcanAdapter: implement IsPumpAlive; mark _pumpAlive/_benchAlive volatile
  for safe cross-thread reads
- MainViewModel: replace direct StartUnlockIfRequired call with a
  fire-and-forget WaitForPumpCanThenUnlockAsync that waits for
  PumpLivenessChanged(true) + 250 ms grace, then invokes unlock on the
  UI thread; cancellation on pump change or CAN disconnect via
  _pumpLivenessCts
- UnlockService.UnlockAsync: skip Phase 2 state-machine when observer
  seed already reports unlocked (senders still run to prevent re-lock)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 16:52:16 +02:00
parent 9a593e4ff2
commit da0581967b
4 changed files with 388 additions and 42 deletions

View File

@@ -64,8 +64,10 @@ namespace HC_APTBS.Infrastructure.Pcan
private HashSet<uint> _pumpMessageIds = new();
private DateTime _lastBenchFrameUtc = DateTime.MinValue;
private DateTime _lastPumpFrameUtc = DateTime.MinValue;
private bool _benchAlive;
private bool _pumpAlive;
// volatile so IsPumpAlive/IsBenchAlive getters on other threads see transitions
// without relying on the memory-model guarantees of the event handler path.
private volatile bool _benchAlive;
private volatile bool _pumpAlive;
// ── ICanService ──────────────────────────────────────────────────────────
@@ -84,6 +86,9 @@ namespace HC_APTBS.Infrastructure.Pcan
/// <inheritdoc/>
public bool IsConnected => !_stopRead;
/// <inheritdoc/>
public bool IsPumpAlive => _pumpAlive;
/// <inheritdoc/>
public TPCANHandle SelectedChannel
{
@@ -246,11 +251,11 @@ namespace HC_APTBS.Infrastructure.Pcan
{
lock (_mapLock)
{
// Replace-on-conflict: callers may re-register on pump switch; the
// new pump's parameter objects must take precedence over any stale
// objects from the previous pump that shared CAN IDs.
foreach (var kv in parameters)
{
if (!_parameterMap.ContainsKey(kv.Key))
_parameterMap.Add(kv.Key, kv.Value);
}
_parameterMap[kv.Key] = kv.Value;
ResolveBenchRpmParam();
}
}
@@ -563,6 +568,11 @@ namespace HC_APTBS.Infrastructure.Pcan
// result = prev + alpha * (new - prev)
param.Value = PassFilterUpdate(previousValue, param.Value, param.Alpha);
param.NeedsUpdate = true;
// Notify observers (e.g. UnlockService) that the decoded value changed.
// The filter rounds to 4 decimals so this does not fire on float noise.
if (param.Value != previousValue)
param.RaiseValueChanged();
}
}