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 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 _waiters = new(); sealed class Waiter { public Func Match; public TaskCompletionSource Tcs; } static Task WaitForAsync(Func match, int timeoutMs, CancellationToken ct) { var tcs = new TaskCompletionSource(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 WaitForPrefixAsync(string prefix, int timeoutMs, CancellationToken ct) => WaitForAsync(s => s.StartsWith(prefix, StringComparison.Ordinal), timeoutMs, ct); static Task 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 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()).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 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 snapshot; lock (_waitLock) { snapshot = new List(_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 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) { Debug.Log("Не пройдена авторизация на плате. Выход."); Application.Quit(); } } 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}"); }