Рефакторинг ComWrapper(Готов к тестированию)

This commit is contained in:
_sega 2026-02-20 17:54:28 +03:00
parent 0e9607f728
commit 6296f9e4e1
6 changed files with 290 additions and 211 deletions

View File

@ -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();

View File

@ -456,7 +456,7 @@ public class GameManager : MonoBehaviour
SingleWallet.Initialize();
ComWrapper.PaymentReceived.AddListener(ProcessDifficultyCounter);
ComWrapper.PaymentReceived += ProcessDifficultyCounter;
SettingsManager.OnSettingsChanged.AddListener(ProcessDifficultyCounter);
soundManager.Initialize();

View File

@ -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
{

View File

@ -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;
/// <summary>
/// Зависимости:<br/>
/// - UnityMainThreadDispatcher<br/>
/// - ComNativeWrapper<br/>
/// </summary>
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<string> 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<Waiter> _waiters = new();
sealed class Waiter
{
public Func<string, bool> Match;
public TaskCompletionSource<string> Tcs;
}
static Task<string> WaitForAsync(Func<string, bool> match, int timeoutMs, CancellationToken ct)
{
var tcs = new TaskCompletionSource<string>(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<string> WaitForPrefixAsync(string prefix, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => s.StartsWith(prefix, StringComparison.Ordinal), timeoutMs, ct);
static Task<string> 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<Waiter> 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<Waiter>()).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<bool> BoardAuthAsync()
private static async Task<bool> 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<Waiter> 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<Waiter>(_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<string> 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<bool> CheckPortConnectionAsync()
private static async Task<bool> 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
}

View File

@ -13,7 +13,7 @@ public class UnityMainThreadDispatcher : MonoBehaviour
if (instance == null)
{
var go = new GameObject("UnityMainThreadDispatcher");
//DontDestroyOnLoad(go);
DontDestroyOnLoad(go);
instance = go.AddComponent<UnityMainThreadDispatcher>();
}
}
@ -24,7 +24,7 @@ public class UnityMainThreadDispatcher : MonoBehaviour
if (instance == null)
{
var go = new GameObject("UnityMainThreadDispatcher");
//DontDestroyOnLoad(go);
DontDestroyOnLoad(go);
instance = go.AddComponent<UnityMainThreadDispatcher>();
}
return instance;

View File

@ -24,7 +24,7 @@ public static class SingleWallet
onCoinAdded = new UnityEvent<int>();
onCoinRemoved = new UnityEvent<int>();
ComWrapper.PaymentReceived.AddListener(AddCredit);
ComWrapper.PaymentReceived += AddCredit;
}