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; public static bool IsPortOpen => h > 0; public static void Initialize(string port) { _controllerPort = port; if (_lifetimeCts != null) { Log("Инициализация пропущена: соединение уже запущено."); return; } Log($"Подключение к периферийному контроллеру на порту {port}."); OpenPort(port); if (h <= 0) { Log("Порт не открылся при инициализации"); return; } UnityMainThreadDispatcher.Initialize(); _lifetimeCts = new CancellationTokenSource(); MarkInputActivity(); StartReaderThread(); _ = SetupBoardAsync(_lifetimeCts.Token); } public static void Send(string message) { bool isLightingCommand = IsLightingCommand(message); if (h <= 0) { if (isLightingCommand) Log($"Световой сигнал {message} не отправлен: порт закрыт."); return; } try { int writeResult = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); if (!isLightingCommand) return; if (writeResult < 0) Log($"Световой сигнал {message} не отправлен на {_controllerPort}: ошибка записи в порт."); else Log($"Световой сигнал {message} отправлен на {_controllerPort}."); } catch (Exception exception) { string commandType = isLightingCommand ? $"светового сигнала {message}" : "команды"; Debug.LogError($"[ComWrapper] Ошибка отправки {commandType}: {exception.Message}"); } } private static void StartReaderThread() { StopReaderThread(); _readerCts = new CancellationTokenSource(); var ct = _readerCts.Token; _readerThread = new Thread(() => { byte[] buffer = new byte[1024]; bool readErrorLogged = false; 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) { if (!readErrorLogged) { Debug.LogWarning($"[ComWrapper] Ошибка чтения порта: {ex.Message}"); readErrorLogged = true; } Thread.Sleep(50);// пауза чтобы не уйти в спин при ошибке continue; } if (ct.IsCancellationRequested) break; readErrorLogged = false; 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) { 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) { Log("Начинаю первичную авторизацию контроллера."); _isAuthorized = await BoardAuthAsync(ct); if (!_isAuthorized) Log("Авторизация на плате не пройдена."); else { HandleAuthorizationSucceeded(); Log("Контроллер подключён и авторизован."); } _ = 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("Контроллер ответил после перезагрузки."); if (!authenticator.AcceptControllerVersion(versionLine)) { Log("Авторизация отклонена: неподходящая версия контроллера."); return false; } string authenticationRequest = authenticator.StartHandshake(); if (authenticationRequest == null) { Log("Не удалось начать обмен авторизации."); return false; } Log("Начинаю обмен авторизации с контроллером."); 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) { Log($"Начинаю переподключение контроллера на порту {_controllerPort}."); StopReaderThread(); CancelAllWaiters(); ClosePort(); if (string.IsNullOrWhiteSpace(_controllerPort)) { Log("Не сохранён COM-порт контроллера."); return false; } OpenPort(_controllerPort); if (h <= 0) { Log("Ошибка при переподключении."); return false; } StartReaderThread(); Log("Порт открыт повторно, начинаю повторную авторизацию."); _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 bool IsLightingCommand(string message) { if (string.IsNullOrEmpty(message)) return false; if (message == _cabinetLightingOnCommand || message == _cabinetLightingOffCommand || message == "2170" || message == "2171") return true; bool isLightMode = message.Length == 5 && message.StartsWith("80", StringComparison.Ordinal); bool isSpecialEffect = message.Length == 7 && message.StartsWith("8560", StringComparison.Ordinal); if (!isLightMode && !isSpecialEffect) return false; foreach (char digit in message) { if (digit < '0' || digit > '9') return false; } return true; } private static void Log(string message) => Debug.Log($"[ComWrapper] {message}"); }