581 lines
18 KiB
C#
Raw 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 _requestAuthCommand = "07";
private const string _responseAuthCommand = "08";
private const string _challengePrefix = "CHAL";
private const string _encryptedResponsePrefix = "RAES";
private const string _readyResponse = "READY!";
private const string _cabinetLightingOnCommand = "856001";
private const string _cabinetLightingOffCommand = "856000";
private const int _authStepTimeoutMs = 3000;
private const int _authReadyTimeoutMs = 5000;
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 int BoardVersion { get; private set; }
private static ILogger logger;
public static void Initialize(string port, int boardVersion)//Добавить логгер в конструктор
{
BoardVersion = boardVersion;
_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)
return;
PaymentReceived?.Invoke();
Send("10");
}
else if (line.StartsWith("INPUT"))
{
if (!_isAuthorized)
return;
MarkInputActivity();
string rawInputString = line.Substring(5);
string inputString = BoardVersion == 2 ? DecimalToBinary(rawInputString) : rawInputString;
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;
}
/// <summary>
/// Очищает буфер порта от накопившихся старых данных
/// </summary>
private static void FlushPortBuffer()
{
try
{
// Проверяем валидность handle
if (h <= 0)
{
return;
}
// Используем локальный буфер
byte[] localBuf = new byte[1024];
int flushedCount = 0;
// Читаем все накопившиеся данные из порта
int read;
while ((read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, localBuf, localBuf.Length)) > 0)
{
flushedCount++;
if (flushedCount > 100) break; // Защита от бесконечного цикла
}
Log($"Очищено: {flushedCount} сообщений");
}
catch (Exception e)
{
Log($"FlushPortBuffer exception: {e.Message}");
}
}
static Task<string> WaitForPrefixAsync(string prefix, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => s.StartsWith(prefix, StringComparison.Ordinal), timeoutMs, ct);
static Task<string> WaitForExactAsync(string expected, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => string.Equals(s, expected, 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}");
FlushPortBuffer();
}
WaitAllCoins();
}
static void WaitAllCoins()
{
int count = 0;
int maxCount = 100;
long timeout = 3000;
byte[] bytes = new byte[1024];
long startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long lastCoinTime = startTime;
while (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - lastCoinTime < 250)
{
int w = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, bytes, bytes.Length);
if (w <= 0) continue;
if( Encoding.UTF8.GetString(bytes, 0, w) == "COIN")
{
lastCoinTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
Send("10");
count++;
}
if (count >= maxCount) break;
if(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTime >= timeout) break;
}
Debug.Log($"Before game coins: {count}");
}
/// <summary>
/// Преобразует строку с десятичным числом в двоичное представление.
/// Младшие биты слева, старшие справа (перевёрнутый порядок для соответствия индексам пинов).
/// </summary>
/// <param name="decimalString">Строка с десятичным числом</param>
/// <returns>Строка с двоичным представлением</returns>
private static string DecimalToBinary(string decimalString)
{
try
{
if (long.TryParse(decimalString, out long decimalValue))
{
// Стандартное преобразование: старшие биты слева
string binary = Convert.ToString(decimalValue, 2);
// Переворачиваем строку: младшие биты слева, старшие справа
char[] chars = binary.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
Debug.Log($"Не удалось распарсить значение INPUT: {decimalString}");
return "11111111111111111111111111"; // 26 единиц по умолчанию
}
catch (Exception e)
{
Debug.Log($"Ошибка преобразования в двоичную систему: {e.Message}");
return "11111111111111111111111111"; // 26 единиц по умолчанию
}
}
private static async Task<bool> BoardAuthAsync(CancellationToken ct)
{
using CancellationTokenSource authCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
CancellationToken authToken = authCts.Token;
string computerChallenge = BoardAuthentication.CreateChallengeHex();
try
{
Task<string> controllerChallengeTask = WaitForAsync(
line => BoardAuthentication.TryExtractHexPayload(line, _challengePrefix, out _),
_authStepTimeoutMs,
authToken);
Send(_requestAuthCommand + computerChallenge);
string controllerChallengeLine = await controllerChallengeTask;
if (!BoardAuthentication.TryExtractHexPayload(
controllerChallengeLine,
_challengePrefix,
out string controllerChallenge))
return false;
Task<string> encryptedResponseTask = WaitForAsync(
line => BoardAuthentication.TryExtractHexPayload(line, _encryptedResponsePrefix, out _),
_authStepTimeoutMs,
authToken);
Task<string> readyTask = WaitForExactAsync(
_readyResponse,
_authReadyTimeoutMs,
authToken);
Send(_responseAuthCommand + BoardAuthentication.EncryptHexBlock(controllerChallenge));
string encryptedResponseLine = await encryptedResponseTask;
if (!BoardAuthentication.TryExtractHexPayload(
encryptedResponseLine,
_encryptedResponsePrefix,
out string encryptedResponse)
|| !BoardAuthentication.ValidateEncryptedResponse(computerChallenge, encryptedResponse))
{
Log("Контроллер не прошёл взаимную AES-проверку.");
return false;
}
await readyTask;
Log("Авторизация платы полностью завершена по READY!.");
return true;
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
return false;
}
catch (Exception exception)
{
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}");
}