509 lines
15 KiB
C#
509 lines
15 KiB
C#
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;
|
||
private static ILogger _logger;
|
||
|
||
public static bool IsPortOpen => h > 0;
|
||
|
||
public static void Initialize(string port)
|
||
{
|
||
_controllerPort = port;
|
||
|
||
if (_lifetimeCts != null) return;
|
||
OpenPort(port);
|
||
if (h <= 0)
|
||
{
|
||
Log("Порт не открылся при инициализации");
|
||
return;
|
||
}
|
||
UnityMainThreadDispatcher.Initialize();
|
||
_lifetimeCts = new CancellationTokenSource();
|
||
MarkInputActivity();
|
||
|
||
StartReaderThread();
|
||
_ = SetupBoardAsync(_lifetimeCts.Token);
|
||
}
|
||
|
||
public static void Send(string message)
|
||
{
|
||
if (h <= 0) return;
|
||
try
|
||
{
|
||
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1);
|
||
}
|
||
catch { _logger?.Log($"Ошибка отправки строкового сигнала. {message}"); }
|
||
}
|
||
|
||
private static void StartReaderThread()
|
||
{
|
||
StopReaderThread();
|
||
|
||
_readerCts = new CancellationTokenSource();
|
||
var ct = _readerCts.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")
|
||
{
|
||
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)
|
||
{
|
||
_isAuthorized = await BoardAuthAsync(ct);
|
||
|
||
if (!_isAuthorized)
|
||
Log("Авторизация на плате не пройдена.");
|
||
else
|
||
HandleAuthorizationSucceeded();
|
||
|
||
_ = 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($"Контроллер готов: {versionLine}");
|
||
|
||
if (!authenticator.AcceptControllerVersion(versionLine))
|
||
return false;
|
||
|
||
string authenticationRequest = authenticator.StartHandshake();
|
||
if (authenticationRequest == null)
|
||
return false;
|
||
|
||
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)
|
||
{
|
||
StopReaderThread();
|
||
CancelAllWaiters();
|
||
ClosePort();
|
||
|
||
if (string.IsNullOrWhiteSpace(_controllerPort))
|
||
{
|
||
Log("Не сохранён COM-порт контроллера.");
|
||
return false;
|
||
}
|
||
|
||
OpenPort(_controllerPort);
|
||
|
||
if (h <= 0)
|
||
{
|
||
Log("Ошибка при переподключении.");
|
||
return false;
|
||
}
|
||
|
||
StartReaderThread();
|
||
_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 void Log(string message) => _logger?.Log($"CW{message}");
|
||
}
|