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;
}
}
}
}