using System; using System.Threading; using System.Threading.Tasks; using HC_APTBS.Models; namespace HC_APTBS.Services.Impl { /// /// Implements the immobilizer unlock sequence for Ford VP44 pump ECUs. /// The CAN flood messages must start before unlocking and continue running /// after unlock completes — stopping them causes the pump to re-lock. Call /// only when the pump is deselected. /// /// Start persistent CAN senders (run until explicitly stopped) /// Begin the 600 s CAN wait with progress reporting /// In parallel, wait for K-Line to become Connected, then try the fast /// unlock (RAM timer shortcut) — if the pump verifies unlocked, cancel /// the remaining wait /// TestUnlock state-machine handshake on 0x700 /// Verify via CAN TestUnlock parameter /// /// public sealed class UnlockService : IUnlockService { private readonly ICanService _can; private readonly IKwpService _kwp; private readonly IAppLogger _log; private const string LogId = "UnlockService"; /// Total duration of the Phase 1 wait (milliseconds). private const int UnlockDurationMs = 600_500; /// CTS for the persistent CAN senders — lives beyond . private CancellationTokenSource? _senderCts; /// public event Action? StatusChanged; /// public event Action? UnlockCompleted; /// Creates the unlock service wired to the CAN and K-Line buses. public UnlockService(ICanService canService, IKwpService kwpService, IAppLogger logger) { _can = canService; _kwp = kwpService; _log = logger; } /// public async Task UnlockAsync(PumpDefinition pump, CancellationToken ct) { if (pump.UnlockType == 0) return; _log.Info(LogId, $"Starting immobilizer unlock (type {pump.UnlockType}) for {pump.Id}"); // ── Start persistent CAN senders FIRST ─────────────────────────────── // These must be active before any unlock attempt and must continue // running after the unlock completes to prevent re-locking. StartSenders(pump.UnlockType); StatusChanged?.Invoke("Unlocking..."); // ── 600 s CAN wait + parallel K-Line fast unlock attempt ───────────── // The fast unlock shortens the pump's internal 10 min timer via K-Line. // It can only be attempted once the K-Line session is Connected (the // read-all-info must finish first). If the fast unlock succeeds AND // the CAN TestUnlock parameter confirms it, we skip the remaining wait. await WaitWithFastUnlockAsync(pump, ct); ct.ThrowIfCancellationRequested(); // ── Phase 2: TestUnlock state machine ──────────────────────────────── StatusChanged?.Invoke("Testing unlock..."); RunTestUnlockSequence(pump.UnlockType); // ── Verify unlock status via CAN TestUnlock parameter ──────────────── bool success = VerifyUnlock(pump); _log.Info(LogId, $"Unlock complete — success={success}"); StatusChanged?.Invoke(success ? "Unlocked" : "Unlock failed"); UnlockCompleted?.Invoke(success); } /// public void StopSenders() { if (_senderCts == null) return; _log.Info(LogId, "Stopping persistent CAN unlock senders"); _senderCts.Cancel(); _senderCts.Dispose(); _senderCts = null; } // ── Persistent CAN senders ─────────────────────────────────────────────── /// /// Starts the two continuous CAN message senders. They run indefinitely /// until is called (on pump deselection). /// private void StartSenders(int unlockType) { // Stop any leftover senders from a previous unlock. StopSenders(); byte[] msg1Data = new byte[8]; uint msg1Id; byte[] msg2Data = new byte[8]; uint msg2Id; switch (unlockType) { case 1: msg1Id = 0x700; msg1Data[0] = 0xB2; msg2Id = 0x300; msg2Data[0] = 0x01; msg2Data[1] = 0x48; msg2Data[2] = 0x50; msg2Data[3] = 0xC3; break; case 2: msg1Id = 0x700; msg1Data[3] = 0xB2; msg2Id = 0x500; msg2Data[4] = 0x78; break; default: return; } _senderCts = new CancellationTokenSource(); var senderCt = _senderCts.Token; _ = Task.Run(async () => { try { while (!senderCt.IsCancellationRequested) { _can.SendRawMessage(msg1Id, msg1Data); await Task.Delay(500, senderCt); } } catch (OperationCanceledException) { } }, senderCt); _ = Task.Run(async () => { try { while (!senderCt.IsCancellationRequested) { _can.SendRawMessage(msg2Id, msg2Data); await Task.Delay(50, senderCt); } } catch (OperationCanceledException) { } }, senderCt); _log.Info(LogId, $"Persistent CAN senders started (type {unlockType})"); } // ── Wait with parallel fast-unlock ─────────────────────────────────────── /// /// Runs the 600 s progress wait. In parallel, monitors the K-Line session: /// once it becomes Connected, checks if the pump is still locked, sends the /// fast unlock command, and if the pump verifies unlocked, cancels the /// remaining wait time. /// private async Task WaitWithFastUnlockAsync(PumpDefinition pump, CancellationToken ct) { using var waitCts = CancellationTokenSource.CreateLinkedTokenSource(ct); waitCts.CancelAfter(UnlockDurationMs); var waitCt = waitCts.Token; // Progress reporting task. var progressTask = ReportProgressAsync(waitCt); // Parallel fast-unlock task — awaits K-Line session, then attempts shortcut. var fastTask = TryFastUnlockWhenReadyAsync(pump, waitCts, ct); // Wait for either: the full duration elapses, or the fast unlock succeeds // and cancels the wait CTS. await Task.WhenAll(progressTask, fastTask); // If the outer ct was cancelled (user stop), propagate. ct.ThrowIfCancellationRequested(); } /// Reports progress every second until the wait token is cancelled. private async Task ReportProgressAsync(CancellationToken waitCt) { var start = DateTime.UtcNow; try { while (!waitCt.IsCancellationRequested) { await Task.Delay(1000, waitCt); var elapsed = DateTime.UtcNow - start; int pct = (int)(elapsed.TotalMilliseconds * 100 / UnlockDurationMs); string time = $"{(int)elapsed.TotalMinutes:D2}:{elapsed.Seconds:D2}"; StatusChanged?.Invoke($"Unlocking... {Math.Min(pct, 100)}% ({time})"); } } catch (OperationCanceledException) { } } /// /// Waits for the K-Line session to become Connected, then attempts the /// fast unlock. If the pump verifies unlocked afterward, cancels /// to skip the remaining 600 s wait. /// private async Task TryFastUnlockWhenReadyAsync( PumpDefinition pump, CancellationTokenSource waitCts, CancellationToken ct) { try { // Wait for K-Line session to become Connected. if (_kwp.KLineState != KLineConnectionState.Connected) { _log.Info(LogId, "Waiting for K-Line session to connect..."); var connectedTcs = new TaskCompletionSource(); void OnStateChanged(KLineConnectionState state) { if (state == KLineConnectionState.Connected) connectedTcs.TrySetResult(true); } _kwp.KLineStateChanged += OnStateChanged; try { // Check again after subscribing (race guard). if (_kwp.KLineState == KLineConnectionState.Connected) connectedTcs.TrySetResult(true); // Wait for connection or cancellation (user cancel or 600 s elapsed). using var reg = ct.Register(() => connectedTcs.TrySetCanceled()); using var waitReg = waitCts.Token.Register(() => connectedTcs.TrySetCanceled()); await connectedTcs.Task; } finally { _kwp.KLineStateChanged -= OnStateChanged; } } // K-Line is now connected. Check if the pump is still locked. if (VerifyUnlock(pump)) { _log.Info(LogId, "Pump already unlocked — skipping wait"); waitCts.Cancel(); return; } // Pump is locked — attempt the fast K-Line unlock (RAM timer shortcut). _log.Info(LogId, "Attempting K-Line fast unlock (timer shortcut)..."); StatusChanged?.Invoke("Fast unlock attempt..."); bool ack = await _kwp.TryFastUnlockAsync(); if (!ack) { _log.Info(LogId, "Fast unlock NAK or failed — continuing normal wait"); StatusChanged?.Invoke("Unlocking..."); return; } _log.Info(LogId, "Fast unlock ACK — waiting briefly for pump to process"); // Give the pump a moment to process the timer shortcut, then verify. await Task.Delay(2000, ct); if (VerifyUnlock(pump)) { _log.Info(LogId, "Fast unlock verified — skipping remaining wait"); waitCts.Cancel(); } else { _log.Info(LogId, "Fast unlock ACK'd but pump still locked — continuing normal wait"); StatusChanged?.Invoke("Unlocking..."); } } catch (OperationCanceledException) { // Wait elapsed or user cancelled — fast unlock window closed, that's fine. } catch (Exception ex) { _log.Warning(LogId, $"Fast unlock attempt error: {ex.Message}"); } } // ── Phase 2: TestUnlock state machine ──────────────────────────────────── private void RunTestUnlockSequence(int unlockType) { byte[][] type1Cmds = { new byte[] { 0xB2, 0, 0, 0, 0, 0, 0, 0 }, new byte[] { 0xB6, 0, 0, 0, 0, 0, 0, 0 }, new byte[] { 0x23, 0, 0, 0, 0, 0, 0, 0 }, new byte[] { 0x24, 0, 0, 0, 0, 0, 0, 0 } }; byte[][] type2Cmds = { new byte[] { 0, 0, 0, 0xB2, 0, 0, 0, 0 }, new byte[] { 0, 0, 0, 0x24, 0, 0, 0, 0 }, new byte[] { 0, 0, 0, 0x24, 0, 0, 0, 0 }, new byte[] { 0, 0, 0, 0x24, 0, 0, 0, 0 } }; byte[][] cmds = unlockType == 1 ? type1Cmds : type2Cmds; for (int loop = 0; loop < 2; loop++) { for (int step = 0; step < cmds.Length; step++) { _can.SendRawMessage(0x700, cmds[step]); Thread.Sleep(500); } } StatusChanged?.Invoke("Sending..."); } // ── Verification ───────────────────────────────────────────────────────── private bool VerifyUnlock(PumpDefinition pump) { if (!pump.ParametersByName.TryGetValue(PumpParameterNames.TestUnlock, out var unlockParam)) return false; switch (pump.UnlockType) { case 1: // Type 1: unlocked when TestUnlock value is zero. // Old code: Lock = valor != 0 (non-zero = locked). return unlockParam.Value == 0; case 2: // Type 2: unlocked when TestUnlock value equals 0xE4 (228). return (int)unlockParam.Value == 0xE4; default: return false; } } } }