diff --git a/Assets/Scripts/GameFSM/CheckLeadersFromMenuState.cs b/Assets/Scripts/GameFSM/CheckLeadersFromMenuState.cs index 0730fb8..91bfcd2 100644 --- a/Assets/Scripts/GameFSM/CheckLeadersFromMenuState.cs +++ b/Assets/Scripts/GameFSM/CheckLeadersFromMenuState.cs @@ -9,8 +9,7 @@ public class CheckLeadersFromMenuState : IState public void enter() { - ComWrapper.InputRecieved.AddListener(ResetTimer); - + ComWrapper.InputRecieved +=ResetTimer; lb = GameManager.instance.leaderboard; //вкл @@ -24,7 +23,7 @@ public class CheckLeadersFromMenuState : IState public void exit() { - ComWrapper.InputRecieved.RemoveListener(ResetTimer); + ComWrapper.InputRecieved-=ResetTimer; lb.ClearRowsText(); lb.gameObject.SetActive(false); SoundManager.instance.playSwitch(); diff --git a/Assets/Scripts/GameManager.cs b/Assets/Scripts/GameManager.cs index 0866a14..13536b1 100644 --- a/Assets/Scripts/GameManager.cs +++ b/Assets/Scripts/GameManager.cs @@ -456,7 +456,7 @@ public class GameManager : MonoBehaviour SingleWallet.Initialize(); - ComWrapper.PaymentReceived.AddListener(ProcessDifficultyCounter); + ComWrapper.PaymentReceived += ProcessDifficultyCounter; SettingsManager.OnSettingsChanged.AddListener(ProcessDifficultyCounter); soundManager.Initialize(); diff --git a/Assets/Scripts/Inputs/COMinput.cs b/Assets/Scripts/Inputs/COMinput.cs index 5068db4..d576874 100644 --- a/Assets/Scripts/Inputs/COMinput.cs +++ b/Assets/Scripts/Inputs/COMinput.cs @@ -38,7 +38,7 @@ public class COMinput : InputSource LoadPinMappingConfiguration(); //ОБЯЗАТЕЛЬНЫЙ БЛОК - ComWrapper.InputRecieved.AddListener(UpdateInputState); + ComWrapper.InputRecieved += UpdateInputState; } catch (Exception e) @@ -82,7 +82,7 @@ public class COMinput : InputSource Debug.Log($"Используется версия 1 распиновки (значение по умолчанию). Полученная версия: {elektroVersion}"); - + } else { diff --git a/Assets/Scripts/Inputs/ComSystem/ComWrapper.cs b/Assets/Scripts/Inputs/ComSystem/ComWrapper.cs index b532fe6..29ca0e8 100644 --- a/Assets/Scripts/Inputs/ComSystem/ComWrapper.cs +++ b/Assets/Scripts/Inputs/ComSystem/ComWrapper.cs @@ -1,71 +1,227 @@ -//Рефакторинг и перевод на асинхронку ComWrapper'a -//Надо потом еще логирование нормальное прикрутить и логгер сделать -using System; +using System; using System.Text; using UnityEngine; -using UnityEngine.Events; -using System.Threading.Tasks; using System.Threading; -using System.Diagnostics; +using System.Threading.Tasks; +using System.Collections.Generic; +/// +/// Зависимости:
+/// - UnityMainThreadDispatcher
+/// - ComNativeWrapper
+///
public static class ComWrapper { - //Настраиваем из инспектора - //То, что нужно вынести в конфиг - //[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true - //[SerializeField] bool keepAlive = true; - static bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true. Да и что то не вижу реализации. Возможно оно уже где-то в ComInput - static bool keepAlive = true; - - public static UnityEvent PaymentReceived; + public static event Action PaymentReceived; public static event Action InputRecieved; private const string REQUEST_INPUT_COMMAND = "91"; - private static int h;//идентификатор хэндла - private static byte[] buf = new byte[1024]; //Байт-буфер куда будут приходить данные с порта + 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 CancellationTokenSource poll_cts, keepAlive_cts; + private static int _lastInputTick = 0; + private const int INPUT_IDLE_BEFORE_PING_MS = 60_000;//1 минута + + public static void Initialize()//Добавить логгер в конструктор { - - //Тут надо с конфига подтянуть настройки! - - PaymentReceived = new UnityEvent(); + if (_lifetimeCts != null) return; OpenPort(); - SetupBoardAsync(); + 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("Ошибка отправки строкового сигнала."); } + catch { logger?.Log($"Ошибка отправки строкового сигнала. {message}"); } } - private static async Task SetupBoardAsync() + private static void StartReaderThread() { - bool auth = await BoardAuthAsync(); - if(!auth) (new GameObject()).AddComponent(typeof(VfxPrefab));//Запускаем отвал + StopReaderThread(); - poll_cts = new CancellationTokenSource(); - keepAlive_cts = new CancellationTokenSource(); + _readerCts = new CancellationTokenSource(); + var ct = _readerCts.Token; - StartPollingAsync(); - StartKeepAliveProcessAsync(keepAlive_cts.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("Инициализация порта"); - //ComNativeWrapper.Serial_CloseAll();//или все таки h?, Пока не удаляем, посмотрим как сработает - var ports = ComNativeWrapper.ListPorts(); if (ports.Length == 0) { Log("Нет портов, завершаю"); return; } @@ -88,148 +244,77 @@ public static class ComWrapper } } - //прописать нормальные логи - private static async Task BoardAuthAsync() + private static async Task BoardAuthAsync(CancellationToken ct) { - bool boardEncrypted = true; //предполагаем что зашифрована - float timeout = 1; - var sw = new Stopwatch(); - //Проверяем зашифрована или нет - sw.Start(); - RequestInputs(); - while (sw.Elapsed.TotalSeconds < timeout) + try { - string entry = await ReadBufferLineAsync(); - if (entry.StartsWith("INPUT")) - { - boardEncrypted = false; - - break; - } - } - sw.Stop(); - - //Не шифрованная -> На выход, пользуемся. - if (!boardEncrypted) + Send(REQUEST_INPUT_COMMAND); + var inputLine = await WaitForPrefixAsync("INPUT", 1000, ct); return true; - - - //А тут возможно шифрованная - - Log("Шифрованная плата"); - + } + catch (TimeoutException) + { + //Не пришел инпут + Log("Шифрованная плата"); + } //Запрашиваем шифрованную строку - string encryptedString = string.Empty; - Send("666"); - sw.Start(); - while (sw.Elapsed.Seconds < timeout) + string encLine; + try { - string entry = await ReadBufferLineAsync(); - if (entry.StartsWith("ENC:")) - { - //пишем сразу без суффикса - encryptedString = entry.Substring(4); - break; - } + Send(REQUEST_ENCRYPTED_COMMAND); + encLine = await WaitForPrefixAsync("ENC:", 1000, ct); } - sw.Stop(); - - //Шифрованная строка не пришла -> идем нафиг - if (string.IsNullOrEmpty(encryptedString)) + catch { Log($"Зашифрованная строка не пришла"); - - return false; + return false; } - //Раз пришла, надо расшифровывать - Log($"Расшифровываю {encryptedString}"); - string decryptedString = BoardUtility.DecryptSerial(encryptedString); + //Расшифровка + var encrypted = encLine.Substring(4); + var decrypted = BoardUtility.DecryptSerial(encrypted); - //ждем подтверждение от платы - string confirmationString = string.Empty; - sw.Start(); - Send("777" + decryptedString); - while (sw.Elapsed.Seconds < timeout) + //Отправляем расшифрованное на подтверждение + try { - string entry = await ReadBufferLineAsync(); - if (entry.IndexOf("OK") != -1) - { - //Успех, плату активировали - sw.Stop(); - return true; - } + Send(REQUEST_CONFIRMATION_COMMAND + decrypted); + await WaitForContainsAsync("OK", 1000, ct); + return true; + } + catch + { + Log("Подтверждение не пришло"); + return false; } - - //А вот тут мы не дождались подтверждения. Плохо, будем ломать игру. - Application.Quit(); - return false; } - private static void OnApplicationQuit() + public static void Shutdown() { - StopAsyncPolling(); - StopKeepAlive(); + _lifetimeCts?.Cancel(); + _lifetimeCts?.Dispose(); + _lifetimeCts = null; + + StopReaderThread(); + CancelAllWaiters(); ClosePort(); } - private static void StopAsyncPolling() + private static void CancelAllWaiters() { - poll_cts?.Cancel(); - poll_cts?.Dispose(); - poll_cts = null; - } - - private static void StopKeepAlive() - { - keepAlive_cts?.Cancel(); - keepAlive_cts?.Dispose(); - keepAlive_cts = null; - } - - //прописать норм логи - private static async void StartPollingAsync() - { - try + List snapshot; + lock (_waitLock) { - Log("Запуск асинхронного опроса порта"); - - while (poll_cts != null && !poll_cts.Token.IsCancellationRequested) - { - await Task.Delay(10, poll_cts.Token); - - string comInput = await ReadBufferLineAsync(poll_cts.Token); - if (!string.IsNullOrEmpty(comInput)) - { - // Прямой вызов - мы уже в главном потоке после await - ProcessComInputImmediate(comInput); - } - } + snapshot = new List(_waiters); + _waiters.Clear(); } - catch (OperationCanceledException) + foreach (Waiter waiter in snapshot) { - Log("Асинхронный опрос порта остановлен"); - } - catch (Exception e) - { - Log($"Ошибка асинхронного опроса: {e.Message}"); + waiter.Tcs.TrySetCanceled(); } } - //Логи - private static void ProcessComInputImmediate(string comInput) - { - Log($"COM. h={h}: {comInput}"); - - if (comInput == "COIN") - PaymentReceived?.Invoke(); - else if (comInput.StartsWith("INPUT")) - InputRecieved?.Invoke(comInput.Substring(5)); - } - - #region Private Methods private static void ClosePort() { Log($"Отпускаю порт handle:{h}"); @@ -239,56 +324,30 @@ public static class ComWrapper h = 0; } } - private static async Task ReadBufferLineAsync(CancellationToken cancellationToken = default) - { - try - { - return await Task.Run(() => - { - // Проверяем отмену внутри Task.Run - if (cancellationToken.IsCancellationRequested) - return string.Empty; - - int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length); - if (read > 0) - { - return Encoding.UTF8.GetString(buf, 0, read); - } - return string.Empty; - }, cancellationToken); - } - catch (OperationCanceledException) - { - Log("Чтение порта отменено"); - return string.Empty; - } - catch (Exception e) - { - Log($"Ошибка асинхронного чтения: {e.Message}"); - return string.Empty; - } - } - - private static void RequestInputs() - => Send(REQUEST_INPUT_COMMAND); //Сделать ожидание простоя около минуты на проверку и переподелючение. - private static async Task StartKeepAliveProcessAsync(CancellationToken cancellationToken) + private static async Task StartKeepAliveProcessAsync(CancellationToken ct) { try { Log("Запуск асинхронной проверки порта"); - - while (!cancellationToken.IsCancellationRequested) + while (!ct.IsCancellationRequested) { - if (!await CheckPortConnectionAsync()) + if (!IsInputIdleTooLong()) { - Log("Плата отвалилась. Запускаю переподключение."); - await ReinitializePortAsync(cancellationToken); + await Task.Delay(5000, ct); + continue; } - await Task.Delay(10000, cancellationToken); //Задержка + //Пингуем плату + if (!await CheckPortConnectionAsync(ct)) + { + Log("Плата отвалилась. Запускаю переподключение."); + await ReinitializePortAsync(ct); + } + + await Task.Delay(10000, ct); } } catch (OperationCanceledException) @@ -300,32 +359,53 @@ public static class ComWrapper Log($"Ошибка проверки порта: {e.Message}"); } } - - private static async Task CheckPortConnectionAsync() + + private static async Task CheckPortConnectionAsync(CancellationToken ct) { - var sw = Stopwatch.StartNew(); - float timeout = 1; - RequestInputs(); - while (sw.Elapsed.Seconds < timeout) + try { - string entry = await ReadBufferLineAsync(); - if(!string.IsNullOrEmpty(entry) && entry.StartsWith("INPUT")) - { - return true; - } + Send(REQUEST_INPUT_COMMAND); + await WaitForPrefixAsync("INPUT", 1000, ct); + return true; + } + catch + { + return false; } - sw.Stop(); - return false; } //реализовать - private static async Task ReinitializePortAsync(CancellationToken cancellationToken) + private static async Task ReinitializePortAsync(CancellationToken ct) { + StopReaderThread(); + CancelAllWaiters(); ClosePort(); - await SetupBoardAsync(); - } + + 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}"); - #endregion } diff --git a/Assets/Scripts/Inputs/ComSystem/UnityMainThreadDispatcher.cs b/Assets/Scripts/Inputs/ComSystem/UnityMainThreadDispatcher.cs index 9d8ce45..9dd8b69 100644 --- a/Assets/Scripts/Inputs/ComSystem/UnityMainThreadDispatcher.cs +++ b/Assets/Scripts/Inputs/ComSystem/UnityMainThreadDispatcher.cs @@ -13,7 +13,7 @@ public class UnityMainThreadDispatcher : MonoBehaviour if (instance == null) { var go = new GameObject("UnityMainThreadDispatcher"); - //DontDestroyOnLoad(go); + DontDestroyOnLoad(go); instance = go.AddComponent(); } } @@ -24,7 +24,7 @@ public class UnityMainThreadDispatcher : MonoBehaviour if (instance == null) { var go = new GameObject("UnityMainThreadDispatcher"); - //DontDestroyOnLoad(go); + DontDestroyOnLoad(go); instance = go.AddComponent(); } return instance; diff --git a/Assets/Scripts/PaymentSystem/SingleWallet.cs b/Assets/Scripts/PaymentSystem/SingleWallet.cs index 553e7b2..e6a9f31 100644 --- a/Assets/Scripts/PaymentSystem/SingleWallet.cs +++ b/Assets/Scripts/PaymentSystem/SingleWallet.cs @@ -24,7 +24,7 @@ public static class SingleWallet onCoinAdded = new UnityEvent(); onCoinRemoved = new UnityEvent(); - ComWrapper.PaymentReceived.AddListener(AddCredit); + ComWrapper.PaymentReceived += AddCredit; }