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 _requestInputCommand = "91";
private const string _controllerResetCommand = "00";
private const string _cabinetLightingOnCommand = "856001";
private const string _cabinetLightingOffCommand = "856000";
private const int _controllerRestartTimeoutMs = 20000;
private const int _authHandshakeTimeoutMs = 10000;
private const int _controllerInputBitCount = 26;
private const int _inputIdleBeforePingMs = 60_000;
private const int _keepAlivePollMs = 5000;
private const int _reconnectRetryMs = 10_000;
private static CancellationTokenSource _lifetimeCts;
private static Thread _readerThread;
private static CancellationTokenSource _readerCts;
private static int h;
private static int _lastInputTick;
private static string _controllerPort;
private static volatile bool _isAuthorized;
public static bool IsPortOpen => h > 0;
public static void Initialize(string port)
{
_controllerPort = port;
if (_lifetimeCts != null)
{
Log("Инициализация пропущена: соединение уже запущено.");
return;
}
Log($"Подключение к периферийному контроллеру на порту {port}.");
OpenPort(port);
if (h <= 0)
{
Log("Порт не открылся при инициализации");
return;
}
UnityMainThreadDispatcher.Initialize();
_lifetimeCts = new CancellationTokenSource();
MarkInputActivity();
StartReaderThread();
_ = SetupBoardAsync(_lifetimeCts.Token);
}
public static void Send(string message)
{
bool isLightingCommand = IsLightingCommand(message);
if (h <= 0)
{
if (isLightingCommand)
Log($"Световой сигнал {message} не отправлен: порт закрыт.");
return;
}
try
{
int writeResult = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1);
if (!isLightingCommand)
return;
if (writeResult < 0)
Log($"Световой сигнал {message} не отправлен на {_controllerPort}: ошибка записи в порт.");
else
Log($"Световой сигнал {message} отправлен на {_controllerPort}.");
}
catch (Exception exception)
{
string commandType = isLightingCommand ? $"светового сигнала {message}" : "команды";
Debug.LogError($"[ComWrapper] Ошибка отправки {commandType}: {exception.Message}");
}
}
private static void StartReaderThread()
{
StopReaderThread();
_readerCts = new CancellationTokenSource();
var ct = _readerCts.Token;
_readerThread = new Thread(() =>
{
byte[] buffer = new byte[1024];
bool readErrorLogged = false;
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)
{
if (!readErrorLogged)
{
Debug.LogWarning($"[ComWrapper] Ошибка чтения порта: {ex.Message}");
readErrorLogged = true;
}
Thread.Sleep(50);// пауза чтобы не уйти в спин при ошибке
continue;
}
if (ct.IsCancellationRequested) break;
readErrorLogged = false;
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)
{
if (line == "COIN")
{
if (!_isAuthorized)
{
Send("10");
return;
}
PaymentReceived?.Invoke();
Send("10");
}
else if (line.StartsWith("INPUT"))
{
if (!_isAuthorized)
return;
MarkInputActivity();
string rawInputString = line.Substring(5);
if (!TryConvertInput(rawInputString, out string inputString))
{
Debug.LogWarning($"Некорректный пакет ввода от контроллера: {line}");
return;
}
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;
}
static Task WaitForPrefixAsync(string prefix, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => s.StartsWith(prefix, StringComparison.Ordinal), 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)
{
Log("Начинаю первичную авторизацию контроллера.");
_isAuthorized = await BoardAuthAsync(ct);
if (!_isAuthorized)
Log("Авторизация на плате не пройдена.");
else
{
HandleAuthorizationSucceeded();
Log("Контроллер подключён и авторизован.");
}
_ = StartKeepAliveProcessAsync(ct);
}
private static void OpenPort(string port)
{
Log("Инициализация порта");
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 bool TryConvertInput(string decimalString, out string input)
{
input = null;
if (!uint.TryParse(decimalString, out uint decimalValue))
return false;
uint maximumValue = (1u << _controllerInputBitCount) - 1u;
if (decimalValue > maximumValue)
return false;
string binary = Convert
.ToString(decimalValue, 2)
.PadLeft(_controllerInputBitCount, '0');
char[] bits = binary.ToCharArray();
Array.Reverse(bits);
input = new string(bits);
return true;
}
private static async Task BoardAuthAsync(CancellationToken ct)
{
using CancellationTokenSource authCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
CancellationToken authToken = authCts.Token;
ControllerAuthenticator authenticator = new();
authenticator.BeginControllerReset();
try
{
Task versionTask = WaitForAsync(
BoardAuthentication.IsControllerVersionLine,
_controllerRestartTimeoutMs,
authToken);
Log("Перезагружаю контроллер перед авторизацией.");
Send(_controllerResetCommand);
string versionLine = await versionTask;
Log("Контроллер ответил после перезагрузки.");
if (!authenticator.AcceptControllerVersion(versionLine))
{
Log("Авторизация отклонена: неподходящая версия контроллера.");
return false;
}
string authenticationRequest = authenticator.StartHandshake();
if (authenticationRequest == null)
{
Log("Не удалось начать обмен авторизации.");
return false;
}
Log("Начинаю обмен авторизации с контроллером.");
Task authenticationTask = WaitForAsync(
line =>
{
string response = authenticator.Handle(line);
if (response != null)
Send(response);
return authenticator.State == ControllerAuthenticationState.Ready
|| authenticator.State == ControllerAuthenticationState.Failed;
},
_authHandshakeTimeoutMs,
authToken);
Send(authenticationRequest);
await authenticationTask;
if (authenticator.State != ControllerAuthenticationState.Ready)
{
Log("Контроллер не прошёл взаимную AES-проверку.");
return false;
}
Log("Авторизация платы полностью завершена по READY!.");
return true;
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
return false;
}
catch (Exception exception)
{
authenticator.Fail();
Log($"Ошибка авторизации платы: {exception.Message}");
return false;
}
finally
{
authCts.Cancel();
}
}
public static void Shutdown()
{
if (h > 0)
Send(_cabinetLightingOffCommand);
_lifetimeCts?.Cancel();
_lifetimeCts?.Dispose();
_lifetimeCts = null;
StopReaderThread();
CancelAllWaiters();
ClosePort();
_isAuthorized = false;
_controllerPort = null;
}
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 (!_isAuthorized)
{
Log("Нет авторизованного соединения. Запускаю переподключение.");
await ReinitializePortAsync(ct);
await Task.Delay(_reconnectRetryMs, ct);
continue;
}
if (!IsInputIdleTooLong())
{
await Task.Delay(_keepAlivePollMs, ct);
continue;
}
if (!await CheckPortConnectionAsync(ct))
{
Log("Плата отвалилась. Запускаю переподключение.");
_isAuthorized = false;
await ReinitializePortAsync(ct);
}
else
{
MarkInputActivity();
}
await Task.Delay(_keepAlivePollMs, ct);
}
}
catch (OperationCanceledException)
{
Log("Асинхронная проверка порта остановлена");
}
catch (Exception e)
{
Log($"Ошибка проверки порта: {e.Message}");
}
}
private static async Task CheckPortConnectionAsync(CancellationToken ct)
{
try
{
Task inputResponseTask = WaitForPrefixAsync("INPUT", 1000, ct);
Send(_requestInputCommand);
await inputResponseTask;
return true;
}
catch
{
return false;
}
}
private static async Task ReinitializePortAsync(CancellationToken ct)
{
Log($"Начинаю переподключение контроллера на порту {_controllerPort}.");
StopReaderThread();
CancelAllWaiters();
ClosePort();
if (string.IsNullOrWhiteSpace(_controllerPort))
{
Log("Не сохранён COM-порт контроллера.");
return false;
}
OpenPort(_controllerPort);
if (h <= 0)
{
Log("Ошибка при переподключении.");
return false;
}
StartReaderThread();
Log("Порт открыт повторно, начинаю повторную авторизацию.");
_isAuthorized = await BoardAuthAsync(ct);
if (!_isAuthorized)
{
Log("Повторная авторизация платы не пройдена.");
return false;
}
HandleAuthorizationSucceeded();
Log($"Соединение с {_controllerPort} восстановлено и авторизовано.");
return true;
}
static bool IsInputIdleTooLong()
{
int last = Volatile.Read(ref _lastInputTick);
if (last == 0) return false;
int now = Environment.TickCount;
// переполнение TickCount тут ок: сравнение интервала < 24 дней работает корректно
int elapsed = unchecked(now - last);
return elapsed >= _inputIdleBeforePingMs;
}
private static void MarkInputActivity()
=> Volatile.Write(ref _lastInputTick, Environment.TickCount);
private static void HandleAuthorizationSucceeded()
{
MarkInputActivity();
Send(_cabinetLightingOnCommand);
}
private static bool IsLightingCommand(string message)
{
if (string.IsNullOrEmpty(message))
return false;
if (message == _cabinetLightingOnCommand || message == _cabinetLightingOffCommand ||
message == "2170" || message == "2171")
return true;
bool isLightMode = message.Length == 5 && message.StartsWith("80", StringComparison.Ordinal);
bool isSpecialEffect = message.Length == 7 && message.StartsWith("8560", StringComparison.Ordinal);
if (!isLightMode && !isSpecialEffect)
return false;
foreach (char digit in message)
{
if (digit < '0' || digit > '9')
return false;
}
return true;
}
private static void Log(string message) => Debug.Log($"[ComWrapper] {message}");
}