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;
}