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 REQUEST_INPUT_COMMAND = "91"; private const string REQUEST_ENCRYPTED_COMMAND = "666"; private const string REQUEST_CONFIRMATION_COMMAND = "777"; private static CancellationTokenSource _lifetimeCts; private static Thread _readerThread; private static CancellationTokenSource _readerCts; private static int h; private static ILogger logger; private static int _lastInputTick = 0; private const int INPUT_IDLE_BEFORE_PING_MS = 60_000;//1 минута public static void Initialize()//Добавить логгер в конструктор { if (_lifetimeCts != null) return; OpenPort(); if (h <= 0) { Log("Порт не открылся при инициализации"); return; } UnityMainThreadDispatcher.Initialize(); _lifetimeCts = new CancellationTokenSource(); StartReaderThread(); _ = SetupBoardAsync(_lifetimeCts.Token); } public static void Send(string message) { if (h <= 0) return; try { int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать } 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") 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(_ => { //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 Task WaitForContainsAsync(string prefix, int timeoutMs, CancellationToken ct) => WaitForAsync(s => s.IndexOf(prefix, StringComparison.Ordinal) >= 0, 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) { bool auth = await BoardAuthAsync(ct); if (!auth) { Log("Авторизация на плате не пройдена."); UnityMainThreadDispatcher.Instance.Enqueue(() => Application.Quit()); return; } _ = StartKeepAliveProcessAsync(ct); } private static 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 static 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 static 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 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 (!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 static async Task CheckPortConnectionAsync(CancellationToken ct) { try { Send(REQUEST_INPUT_COMMAND); await WaitForPrefixAsync("INPUT", 1000, ct); return true; } catch { return false; } } //реализовать private static 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))); } static 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 static void Log(string message) => logger?.Log($"CW{message}"); }