202 lines
7.6 KiB
C#
202 lines
7.6 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using HC_APTBS.Models;
|
|
|
|
namespace HC_APTBS.Services.Impl
|
|
{
|
|
/// <summary>
|
|
/// Implements the immobilizer unlock sequence for Ford VP44 pump ECUs.
|
|
/// The unlock has two phases:
|
|
/// <list type="number">
|
|
/// <item>Continuous CAN message sends for ~10 minutes (600.5 s)</item>
|
|
/// <item>A state-machine handshake that cycles through command bytes on 0x700</item>
|
|
/// </list>
|
|
/// </summary>
|
|
public sealed class UnlockService : IUnlockService
|
|
{
|
|
private readonly ICanService _can;
|
|
private readonly IAppLogger _log;
|
|
private const string LogId = "UnlockService";
|
|
|
|
/// <summary>Total duration of the Phase 1 continuous send (milliseconds).</summary>
|
|
private const int UnlockDurationMs = 600_500;
|
|
|
|
/// <inheritdoc/>
|
|
public event Action<string>? StatusChanged;
|
|
|
|
/// <inheritdoc/>
|
|
public event Action<bool>? UnlockCompleted;
|
|
|
|
/// <summary>Creates the unlock service wired to the CAN bus.</summary>
|
|
public UnlockService(ICanService canService, IAppLogger logger)
|
|
{
|
|
_can = canService;
|
|
_log = logger;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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}");
|
|
StatusChanged?.Invoke("Unlocking...");
|
|
|
|
// ── Phase 1: Continuous sends for ~10 minutes ─────────────────────────
|
|
await RunPhase1Async(pump.UnlockType, ct);
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
// ── Phase 2: TestUnlock state machine ─────────────────────────────────
|
|
StatusChanged?.Invoke("Testing unlock...");
|
|
RunTestUnlockSequence(pump.UnlockType);
|
|
|
|
// ── Verify unlock status ──────────────────────────────────────────────
|
|
bool success = VerifyUnlock(pump);
|
|
|
|
_log.Info(LogId, $"Unlock complete — success={success}");
|
|
StatusChanged?.Invoke(success ? "Unlocked" : "Unlock failed");
|
|
UnlockCompleted?.Invoke(success);
|
|
}
|
|
|
|
// ── Phase 1 ──────────────────────────────────────────────────────────────
|
|
|
|
private async Task RunPhase1Async(int unlockType, CancellationToken ct)
|
|
{
|
|
// Build message payloads based on unlock type.
|
|
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;
|
|
}
|
|
|
|
// Run two parallel senders for the full unlock duration.
|
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
cts.CancelAfter(UnlockDurationMs);
|
|
var linkedCt = cts.Token;
|
|
|
|
var sender1 = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (!linkedCt.IsCancellationRequested)
|
|
{
|
|
_can.SendRawMessage(msg1Id, msg1Data);
|
|
await Task.Delay(500, linkedCt);
|
|
}
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
}, linkedCt);
|
|
|
|
var sender2 = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (!linkedCt.IsCancellationRequested)
|
|
{
|
|
_can.SendRawMessage(msg2Id, msg2Data);
|
|
await Task.Delay(50, linkedCt);
|
|
}
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
}, linkedCt);
|
|
|
|
// Report progress periodically.
|
|
var progressTask = Task.Run(async () =>
|
|
{
|
|
var start = DateTime.UtcNow;
|
|
try
|
|
{
|
|
while (!linkedCt.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(1000, linkedCt);
|
|
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) { }
|
|
}, linkedCt);
|
|
|
|
await Task.WhenAll(sender1, sender2, progressTask);
|
|
|
|
// If the outer ct was cancelled (user stop), propagate.
|
|
ct.ThrowIfCancellationRequested();
|
|
}
|
|
|
|
// ── Phase 2: TestUnlock state machine ────────────────────────────────────
|
|
|
|
private void RunTestUnlockSequence(int unlockType)
|
|
{
|
|
// The state machine cycles through 4 command bytes, twice.
|
|
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 non-zero.
|
|
return unlockParam.Value != 0;
|
|
case 2:
|
|
// Type 2: unlocked when TestUnlock value equals 0xE4 (228).
|
|
return (int)unlockParam.Value == 0xE4;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|