524 lines
16 KiB
C#
524 lines
16 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 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;
|
||
_ = OpenPortAsync(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<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> 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<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)
|
||
{
|
||
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 OpenPortAsync(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($"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)
|
||
{
|
||
//Проверяем зашифрована или нет
|
||
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<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 (!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<bool> 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();
|
||
|
||
OpenPortAsync(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}");
|
||
}
|