Files
HC_APTBS/Services/Impl/UnlockService.cs
2026-04-11 12:45:18 +02:00

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