572 lines
18 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Text;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
/// <summary>
/// Зависимости:<br/>
/// - UnityMainThreadDispatcher<br/>
/// - ComNativeWrapper<br/>
/// </summary>
public static class ComWrapper
{
public static event Action PaymentReceived;
public static event Action<string> 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<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 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)
{
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<bool> BoardAuthAsync(CancellationToken ct)
{
using CancellationTokenSource authCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
CancellationToken authToken = authCts.Token;
ControllerAuthenticator authenticator = new();
authenticator.BeginControllerReset();
try
{
Task<string> 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<string> 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<Waiter> snapshot;
lock (_waitLock)
{
snapshot = new List<Waiter>(_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<bool> CheckPortConnectionAsync(CancellationToken ct)
{
try
{
Task<string> inputResponseTask = WaitForPrefixAsync("INPUT", 1000, ct);
Send(_requestInputCommand);
await inputResponseTask;
return true;
}
catch
{
return false;
}
}
private static async Task<bool> 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}");
}