using System; using System.Text; using UnityEngine; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; /// /// Зависимости:
/// - UnityMainThreadDispatcher
/// - ComNativeWrapper
///
public static class ComWrapper { public static event Action PaymentReceived; public static event Action InputRecieved; private const string _requestInputCommand = "91"; private const string _controllerResetCommand = "00"; private const string _cabinetLightingOnCommand = "856001"; private const string _cabinetLightingOffCommand = "856000"; private const int _controllerRestartTimeoutMs = 20000; private const int _authHandshakeTimeoutMs = 10000; private const int _controllerInputBitCount = 26; private const int _inputIdleBeforePingMs = 60_000; private const int _keepAlivePollMs = 5000; private const int _reconnectRetryMs = 10_000; private static CancellationTokenSource _lifetimeCts; private static Thread _readerThread; private static CancellationTokenSource _readerCts; private static int h; private static int _lastInputTick; private static string _controllerPort; private static volatile bool _isAuthorized; private static ILogger _logger; public static bool IsPortOpen => h > 0; public static void Initialize(string port) { _controllerPort = port; if (_lifetimeCts != null) return; OpenPort(port); if (h <= 0) { Log("Порт не открылся при инициализации"); return; } UnityMainThreadDispatcher.Initialize(); _lifetimeCts = new CancellationTokenSource(); MarkInputActivity(); StartReaderThread(); _ = SetupBoardAsync(_lifetimeCts.Token); } public static void Send(string message) { if (h <= 0) return; try { int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); } catch { _logger?.Log($"Ошибка отправки строкового сигнала. {message}"); } } private static 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 static void StopReaderThread() { if (_readerCts != null) { _readerCts.Cancel(); _readerCts.Dispose(); _readerCts = null; } if (_readerThread != null) { _readerThread.Join(200); _readerThread = null; } } static void EnqueueLine(string line) { UnityMainThreadDispatcher.Instance.Enqueue(() => OnLine(line)); } private static void ProcessComInput(string line) { Log($"COM. h={h}: {line}"); if (line == "COIN") { if (!_isAuthorized) { Send("10"); return; } PaymentReceived?.Invoke(); Send("10"); } else if (line.StartsWith("INPUT")) { if (!_isAuthorized) return; MarkInputActivity(); string rawInputString = line.Substring(5); if (!TryConvertInput(rawInputString, out string inputString)) { Debug.LogWarning($"Некорректный пакет ввода от контроллера: {line}"); return; } InputRecieved?.Invoke(inputString); } } 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(_ => { //if (tcs.Task.IsCompleted) return; 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 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 static async Task SetupBoardAsync(CancellationToken ct) { _isAuthorized = await BoardAuthAsync(ct); if (!_isAuthorized) Log("Авторизация на плате не пройдена."); else HandleAuthorizationSucceeded(); _ = StartKeepAliveProcessAsync(ct); } private static void OpenPort(string port) { Log("Инициализация порта"); 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 static bool TryConvertInput(string decimalString, out string input) { input = null; if (!uint.TryParse(decimalString, out uint decimalValue)) return false; uint maximumValue = (1u << _controllerInputBitCount) - 1u; if (decimalValue > maximumValue) return false; string binary = Convert .ToString(decimalValue, 2) .PadLeft(_controllerInputBitCount, '0'); char[] bits = binary.ToCharArray(); Array.Reverse(bits); input = new string(bits); return true; } private static async Task BoardAuthAsync(CancellationToken ct) { using CancellationTokenSource authCts = CancellationTokenSource.CreateLinkedTokenSource(ct); CancellationToken authToken = authCts.Token; ControllerAuthenticator authenticator = new(); authenticator.BeginControllerReset(); try { Task versionTask = WaitForAsync( BoardAuthentication.IsControllerVersionLine, _controllerRestartTimeoutMs, authToken); Log("Перезагружаю контроллер перед авторизацией."); Send(_controllerResetCommand); string versionLine = await versionTask; Log($"Контроллер готов: {versionLine}"); if (!authenticator.AcceptControllerVersion(versionLine)) return false; string authenticationRequest = authenticator.StartHandshake(); if (authenticationRequest == null) return false; Task authenticationTask = WaitForAsync( line => { string response = authenticator.Handle(line); if (response != null) Send(response); return authenticator.State == ControllerAuthenticationState.Ready || authenticator.State == ControllerAuthenticationState.Failed; }, _authHandshakeTimeoutMs, authToken); Send(authenticationRequest); await authenticationTask; if (authenticator.State != ControllerAuthenticationState.Ready) { Log("Контроллер не прошёл взаимную AES-проверку."); return false; } Log("Авторизация платы полностью завершена по READY!."); return true; } catch (OperationCanceledException) when (ct.IsCancellationRequested) { return false; } catch (Exception exception) { authenticator.Fail(); Log($"Ошибка авторизации платы: {exception.Message}"); return false; } finally { authCts.Cancel(); } } public static void Shutdown() { if (h > 0) Send(_cabinetLightingOffCommand); _lifetimeCts?.Cancel(); _lifetimeCts?.Dispose(); _lifetimeCts = null; StopReaderThread(); CancelAllWaiters(); ClosePort(); _isAuthorized = false; _controllerPort = null; } private static void CancelAllWaiters() { List snapshot; lock (_waitLock) { snapshot = new List(_waiters); _waiters.Clear(); } foreach (Waiter waiter in snapshot) { waiter.Tcs.TrySetCanceled(); } } private static void ClosePort() { Log($"Отпускаю порт handle:{h}"); if (h > 0) { ComNativeWrapper.Serial_Close(h); h = 0; } } private static async Task StartKeepAliveProcessAsync(CancellationToken ct) { try { Log("Запуск асинхронной проверки порта"); while (!ct.IsCancellationRequested) { if (!_isAuthorized) { Log("Нет авторизованного соединения. Запускаю переподключение."); await ReinitializePortAsync(ct); await Task.Delay(_reconnectRetryMs, ct); continue; } if (!IsInputIdleTooLong()) { await Task.Delay(_keepAlivePollMs, ct); continue; } if (!await CheckPortConnectionAsync(ct)) { Log("Плата отвалилась. Запускаю переподключение."); _isAuthorized = false; await ReinitializePortAsync(ct); } else { MarkInputActivity(); } await Task.Delay(_keepAlivePollMs, ct); } } catch (OperationCanceledException) { Log("Асинхронная проверка порта остановлена"); } catch (Exception e) { Log($"Ошибка проверки порта: {e.Message}"); } } private static async Task CheckPortConnectionAsync(CancellationToken ct) { try { Task inputResponseTask = WaitForPrefixAsync("INPUT", 1000, ct); Send(_requestInputCommand); await inputResponseTask; return true; } catch { return false; } } private static async Task ReinitializePortAsync(CancellationToken ct) { StopReaderThread(); CancelAllWaiters(); ClosePort(); if (string.IsNullOrWhiteSpace(_controllerPort)) { Log("Не сохранён COM-порт контроллера."); return false; } OpenPort(_controllerPort); if (h <= 0) { Log("Ошибка при переподключении."); return false; } StartReaderThread(); _isAuthorized = await BoardAuthAsync(ct); if (!_isAuthorized) { Log("Повторная авторизация платы не пройдена."); return false; } HandleAuthorizationSucceeded(); Log($"Соединение с {_controllerPort} восстановлено и авторизовано."); return true; } static bool IsInputIdleTooLong() { int last = Volatile.Read(ref _lastInputTick); if (last == 0) return false; int now = Environment.TickCount; // переполнение TickCount тут ок: сравнение интервала < 24 дней работает корректно int elapsed = unchecked(now - last); return elapsed >= _inputIdleBeforePingMs; } private static void MarkInputActivity() => Volatile.Write(ref _lastInputTick, Environment.TickCount); private static void HandleAuthorizationSucceeded() { MarkInputActivity(); Send(_cabinetLightingOnCommand); } private static void Log(string message) => _logger?.Log($"CW{message}"); }