chip-game/Assets/Scripts/Inputs/ComSystem/PeripheralControllerV2.cs

411 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class PeripheralControllerV2 : ComDevice
{
public event Action PaymentReceived;
public event Action<string> InputRecieved;
private const string REQUEST_INPUT_COMMAND = "91";
private const string REQUEST_ENCRYPTED_COMMAND = "666";
private const string REQUEST_CONFIRMATION_COMMAND = "777";
private static readonly byte[] AesEncryptedKey =
{
0x69, 0x40, 0xC7, 0x0E, 0xF2, 0xC7, 0x45, 0x70,
0x72, 0xCD, 0x28, 0x58, 0x99, 0x8F, 0x73, 0xA8,
0x5F, 0x5F, 0xED, 0xB4, 0xC1, 0x62, 0xB4, 0x24,
0xAC, 0xD7, 0xA3, 0xD6, 0xD8, 0x0B, 0x50, 0x37
};
private CancellationTokenSource _lifetimeCts;
private Thread _readerThread;
private CancellationTokenSource _readerCts;
//private int h;
private ILogger logger;
private int _lastInputTick = 0;
private const int INPUT_IDLE_BEFORE_PING_MS = 60_000;//1 минута
public PeripheralControllerV2(int handleId) : base(handleId)
{
if (_lifetimeCts != null) return;
OpenPort();
if (h <= 0)
{
Log("Порт не открылся при инициализации");
return;
}
UnityMainThreadDispatcher.Initialize();
_lifetimeCts = new CancellationTokenSource();
StartReaderThread();
_ = SetupBoardAsync(_lifetimeCts.Token);
}
public void Send(string message)
{
if (h <= 0) return;
try
{
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1);
}
catch { logger?.Log($"Ошибка отправки строкового сигнала. {message}"); }
}
private void StartReaderThread()
{
StopReaderThread();
_readerCts = new CancellationTokenSource();
var ct = _readerCts.Token;
_readerThread = new Thread(() =>
{
byte[] buffer = new byte[1024];
while (!ct.IsCancellationRequested)
{
if (h <= 0) { Thread.Sleep(20); continue; }
int read = 0;
try
{
//блок до таймаута чтения
read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buffer, buffer.Length);
}
catch (Exception ex)
{
Log("Com reader exception: " + ex.Message);
Thread.Sleep(50);// пауза чтобы не уйти в спин при ошибке
continue;
}
if (ct.IsCancellationRequested) break;
if (read > 0)
{
var line = Encoding.UTF8.GetString(buffer, 0, read).Trim();
EnqueueLine(line);
}
}
});
_readerThread.IsBackground = true;
_readerThread.Start();
}
private void StopReaderThread()
{
if (_readerCts != null)
{
_readerCts.Cancel();
_readerCts.Dispose();
_readerCts = null;
}
if (_readerThread != null)
{
_readerThread.Join(200);
_readerThread = null;
}
}
void EnqueueLine(string line)
{
UnityMainThreadDispatcher.Instance.Enqueue(() => OnLine(line));
}
private void ProcessComInput(string line)
{
Log($"COM. h={h}: {line}");
if (line == "COIN")
PaymentReceived?.Invoke();
else if (line.StartsWith("INPUT"))
{
Volatile.Write(ref _lastInputTick, Environment.TickCount);
InputRecieved?.Invoke(line.Substring(5));
}
}
static readonly object _waitLock = new();
static readonly List<Waiter> _waiters = new();
sealed class Waiter
{
public Func<string, bool> Match;
public TaskCompletionSource<string> Tcs;
}
static Task<string> WaitForAsync(Func<string, bool> match, int timeoutMs, CancellationToken ct)
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var w = new Waiter { Match = match, Tcs = tcs };
lock (_waitLock) _waiters.Add(w);
//отмена
if (ct.CanBeCanceled)
{
ct.Register(() =>
{
lock (_waitLock) _waiters.Remove(w);
tcs.TrySetCanceled(ct);
});
}
_ = Task.Delay(timeoutMs, CancellationToken.None).ContinueWith(_ =>
{
lock (_waitLock) _waiters.Remove(w);
tcs.TrySetException(new TimeoutException($"Timeout waiting for line"));
});
return tcs.Task;
}
static Task<string> WaitForPrefixAsync(string prefix, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => s.StartsWith(prefix, StringComparison.Ordinal), timeoutMs, ct);
static Task<string> WaitForContainsAsync(string prefix, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => s.IndexOf(prefix, StringComparison.Ordinal) >= 0, timeoutMs, ct);
private void OnLine(string line)
{
if (string.IsNullOrEmpty(line))
return;
//1. обычная обработка
ProcessComInput(line);
//2. Разубудить ожидающих
List<Waiter> toFire = null;
lock (_waitLock)
{
for (int i = _waiters.Count - 1; i >= 0; i--)
{
var waiter = _waiters[i];
if (waiter.Match(line))
{
_waiters.RemoveAt(i);
(toFire ??= new List<Waiter>()).Add(waiter);
}
}
}
if (toFire != null)
foreach (var w in toFire)
w.Tcs.TrySetResult(line);
}
private async Task SetupBoardAsync(CancellationToken ct)
{
bool auth = await BoardAuthAsync(ct);
if (!auth)
{
Log("Авторизация на плате не пройдена.");
UnityMainThreadDispatcher.Instance.Enqueue(() => Application.Quit());
return;
}
_ = StartKeepAliveProcessAsync(ct);
}
private void OpenPort()
{
Log("Инициализация порта");
var ports = ComNativeWrapper.ListPorts();
if (ports.Length == 0) { Log("Нет портов, завершаю"); return; }
string port = ports[^1];
Log("Открываю порт: " + port);
h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 4, 10);
Log($"handle: {h}");
if (h <= 0)
{
Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
else
{
Log($"Открыл порт: {port}");
}
}
private async Task<bool> BoardAuthAsync(CancellationToken ct)
{
//Проверяем зашифрована или нет
try
{
Send(REQUEST_INPUT_COMMAND);
var inputLine = await WaitForPrefixAsync("INPUT", 1000, ct);
return true;
}
catch (TimeoutException)
{
//Не пришел инпут
Log("Шифрованная плата");
}
//Запрашиваем шифрованную строку
string encLine;
try
{
Send(REQUEST_ENCRYPTED_COMMAND);
encLine = await WaitForPrefixAsync("ENC:", 1000, ct);
}
catch
{
Log($"Зашифрованная строка не пришла");
return false;
}
//Расшифровка
var encrypted = encLine.Substring(4);
var decrypted = BoardUtility.DecryptSerial(encrypted);
//Отправляем расшифрованное на подтверждение
try
{
Send(REQUEST_CONFIRMATION_COMMAND + decrypted);
await WaitForContainsAsync("OK", 1000, ct);
return true;
}
catch
{
Log("Подтверждение не пришло");
return false;
}
}
public void Shutdown()
{
_lifetimeCts?.Cancel();
_lifetimeCts?.Dispose();
_lifetimeCts = null;
StopReaderThread();
CancelAllWaiters();
ClosePort();
}
private static void CancelAllWaiters()
{
List<Waiter> snapshot;
lock (_waitLock)
{
snapshot = new List<Waiter>(_waiters);
_waiters.Clear();
}
foreach (Waiter waiter in snapshot)
{
waiter.Tcs.TrySetCanceled();
}
}
private void ClosePort()
{
Log($"Отпускаю порт handle:{h}");
if (h > 0)
{
ComNativeWrapper.Serial_Close(h);
h = 0;
}
}
//Сделать ожидание простоя около минуты на проверку и переподелючение.
private async Task StartKeepAliveProcessAsync(CancellationToken ct)
{
try
{
Log("Запуск асинхронной проверки порта");
while (!ct.IsCancellationRequested)
{
if (!IsInputIdleTooLong())
{
await Task.Delay(5000, ct);
continue;
}
//Пингуем плату
if (!await CheckPortConnectionAsync(ct))
{
Log("Плата отвалилась. Запускаю переподключение.");
await ReinitializePortAsync(ct);
}
await Task.Delay(10000, ct);
}
}
catch (OperationCanceledException)
{
Log("Асинхронная проверка порта остановлена");
}
catch (Exception e)
{
Log($"Ошибка проверки порта: {e.Message}");
}
}
private async Task<bool> CheckPortConnectionAsync(CancellationToken ct)
{
try
{
Send(REQUEST_INPUT_COMMAND);
await WaitForPrefixAsync("INPUT", 1000, ct);
return true;
}
catch
{
return false;
}
}
//реализовать
private async Task ReinitializePortAsync(CancellationToken ct)
{
StopReaderThread();
CancelAllWaiters();
ClosePort();
OpenPort();
if (h <= 0)
{
Log($"Ошибка при переподключении.");
return;
}
StartReaderThread();
var auth = await BoardAuthAsync(ct);
if (!auth)
UnityMainThreadDispatcher.Instance.Enqueue(() => (new GameObject()).AddComponent(typeof(VfxPrefab)));
}
private bool IsInputIdleTooLong()
{
int last = Volatile.Read(ref _lastInputTick);
if (last == 0) return true;
int now = Environment.TickCount;
// переполнение TickCount тут ок: сравнение интервала < 24 дней работает корректно
int elapsed = unchecked(now - last);
return elapsed >= INPUT_IDLE_BEFORE_PING_MS;
}
private void Log(string message) => logger?.Log($"CW{message}");
}