using System;
using System.Text;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditorInternal.VR;
///
/// Зависимости:
/// - UnityMainThreadDispatcher
/// - ComNativeWrapper
///
public static class ComWrapper
{
public static event Action PaymentReceived;
public static event Action InputRecieved;
private const string REQUEST_INPUT_COMMAND = "91";
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;
public static int BoardVersion { get; private set; }
private static ILogger logger;
private static string[] AvailablePorts;
private static int _lastInputTick = 0;
private const int INPUT_IDLE_BEFORE_PING_MS = 60_000;//1 минута
public static void Initialize(string[] availablePorts, int boardVersion)//Добавить логгер в конструктор
{
BoardVersion = boardVersion;
AvailablePorts = availablePorts;
if (_lifetimeCts != null) return;
OpenPort(AvailablePorts);
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($"Ошибка отправки строкового сигнала. {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")
{
PaymentReceived?.Invoke();
Send("10");
}
else if (line.StartsWith("INPUT"))
{
Volatile.Write(ref _lastInputTick, Environment.TickCount);
string rawInputString = line.Substring(5);
string inputString = BoardVersion == 2 ? DecimalToBinary(rawInputString) : rawInputString;
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;
}
///
/// Очищает буфер порта от накопившихся старых данных
///
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 WaitForPrefixAsync(string prefix, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => s.StartsWith(prefix, StringComparison.Ordinal), timeoutMs, ct);
static Task 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 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)
{
bool auth;
if (BoardVersion == 1)
auth = await BoardAuthAsync(ct);
else
auth = true;
if (!auth)
{
Log("Авторизация на плате не пройдена.");
UnityMainThreadDispatcher.Instance.Enqueue(() => Application.Quit());
return;
}
_ = StartKeepAliveProcessAsync(ct);
}
private static async Task OpenPort(string[] availablePorts)
{
Log("Инициализация порта");
var ports = availablePorts;
if (ports.Length == 0) { Log("Нет портов, завершаю"); return; }
string port = ports[^1];
ComNativeWrapper.Serial_CloseAll();
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("count coins " + count);
}
///
/// Преобразует строку с десятичным числом в двоичное представление.
/// Младшие биты слева, старшие справа (перевёрнутый порядок для соответствия индексам пинов).
///
/// Строка с десятичным числом
/// Строка с двоичным представлением
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 BoardAuthAsync(CancellationToken ct)
{
//Проверяем зашифрована или нет
try
{
Send(REQUEST_INPUT_COMMAND);
var inputLine = await WaitForPrefixAsync("INPUT", 1000, ct);
return true;
}
catch (TimeoutException)
{
//Не пришел инпут
Log("Шифрованная плата");
}
//Запрашиваем шифрованную строку
string encLine;
try
{
Send(REQUEST_ENCRYPTED_COMMAND);
encLine = await WaitForPrefixAsync("ENC:", 1000, ct);
}
catch
{
Log($"Зашифрованная строка не пришла");
return false;
}
//Расшифровка
var encrypted = encLine.Substring(4);
var decrypted = BoardUtility.DecryptSerial(encrypted);
//Отправляем расшифрованное на подтверждение
try
{
Send(REQUEST_CONFIRMATION_COMMAND + decrypted);
await WaitForContainsAsync("OK", 1000, ct);
return true;
}
catch
{
Log("Подтверждение не пришло");
return false;
}
}
public static void Shutdown()
{
_lifetimeCts?.Cancel();
_lifetimeCts?.Dispose();
_lifetimeCts = null;
StopReaderThread();
CancelAllWaiters();
ClosePort();
}
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 (!IsInputIdleTooLong())
{
await Task.Delay(5000, ct);
continue;
}
//Пингуем плату
if (!await CheckPortConnectionAsync(ct))
{
Log("Плата отвалилась. Запускаю переподключение.");
await ReinitializePortAsync(ct);
}
await Task.Delay(10000, ct);
}
}
catch (OperationCanceledException)
{
Log("Асинхронная проверка порта остановлена");
}
catch (Exception e)
{
Log($"Ошибка проверки порта: {e.Message}");
}
}
private static async Task CheckPortConnectionAsync(CancellationToken ct)
{
try
{
Send(REQUEST_INPUT_COMMAND);
await WaitForPrefixAsync("INPUT", 1000, ct);
return true;
}
catch
{
return false;
}
}
//реализовать
private static async Task ReinitializePortAsync(CancellationToken ct)
{
StopReaderThread();
CancelAllWaiters();
ClosePort();
OpenPort(AvailablePorts);
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}");
}