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; public static int BoardVersion { get; private set; } private static ILogger logger; private static string[] AvailablePorts; private static int _lastInputTick = 0; private const int INPUT_IDLE_BEFORE_PING_MS = 60_000;//1 минута public static void Initialize(string[] availablePorts, int boardVersion)//Добавить логгер в конструктор { BoardVersion = boardVersion; AvailablePorts = availablePorts; if (_lifetimeCts != null) return; OpenPort(AvailablePorts); 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(); Send("10"); } else if (line.StartsWith("INPUT")) { Volatile.Write(ref _lastInputTick, Environment.TickCount); string rawInputString = line.Substring(5); string inputString = BoardVersion == 2 ? DecimalToBinary(rawInputString) : rawInputString; 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; } /// /// Очищает буфер порта от накопившихся старых данных /// private static void FlushPortBuffer() { try { // Проверяем валидность handle if (h <= 0) { return; } // Используем локальный буфер byte[] localBuf = new byte[1024]; int flushedCount = 0; // Читаем все накопившиеся данные из порта int read; while ((read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, localBuf, localBuf.Length)) > 0) { flushedCount++; if (flushedCount > 100) break; // Защита от бесконечного цикла } Log($"Очищено: {flushedCount} сообщений"); } catch (Exception e) { Log($"FlushPortBuffer exception: {e.Message}"); } } 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; if (BoardVersion == 1) auth = await BoardAuthAsync(ct); else auth = true; if (!auth) { Log("Авторизация на плате не пройдена."); UnityMainThreadDispatcher.Instance.Enqueue(() => Application.Quit()); return; } _ = StartKeepAliveProcessAsync(ct); } private static async Task OpenPort(string[] availablePorts) { Log("Инициализация порта"); var ports = availablePorts; if (ports.Length == 0) { Log("Нет портов, завершаю"); return; } string port = ports[^1]; ComNativeWrapper.Serial_CloseAll(); 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}"); FlushPortBuffer(); } WaitAllCoins(); } static void WaitAllCoins() { int count = 0; int maxCount = 100; long timeout = 3000; byte[] bytes = new byte[1024]; long startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); long lastCoinTime = startTime; while (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - lastCoinTime < 250) { int w = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, bytes, bytes.Length); if (w <= 0) continue; if( Encoding.UTF8.GetString(bytes, 0, w) == "COIN") { lastCoinTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); Send("10"); count++; } if (count >= maxCount) break; if(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTime >= timeout) break; } Debug.Log($"Before game coins: {count}"); } /// /// Преобразует строку с десятичным числом в двоичное представление. /// Младшие биты слева, старшие справа (перевёрнутый порядок для соответствия индексам пинов). /// /// Строка с десятичным числом /// Строка с двоичным представлением private static string DecimalToBinary(string decimalString) { try { if (long.TryParse(decimalString, out long decimalValue)) { // Стандартное преобразование: старшие биты слева string binary = Convert.ToString(decimalValue, 2); // Переворачиваем строку: младшие биты слева, старшие справа char[] chars = binary.ToCharArray(); Array.Reverse(chars); return new string(chars); } Debug.Log($"Не удалось распарсить значение INPUT: {decimalString}"); return "11111111111111111111111111"; // 26 единиц по умолчанию } catch (Exception e) { Debug.Log($"Ошибка преобразования в двоичную систему: {e.Message}"); return "11111111111111111111111111"; // 26 единиц по умолчанию } } 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(AvailablePorts); 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}"); }