411 lines
12 KiB
C#
411 lines
12 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
|
||
public class PeripheralControllerV2 : ComDevice
|
||
{
|
||
public event Action PaymentReceived;
|
||
public 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 readonly byte[] AesEncryptedKey =
|
||
{
|
||
0x69, 0x40, 0xC7, 0x0E, 0xF2, 0xC7, 0x45, 0x70,
|
||
0x72, 0xCD, 0x28, 0x58, 0x99, 0x8F, 0x73, 0xA8,
|
||
0x5F, 0x5F, 0xED, 0xB4, 0xC1, 0x62, 0xB4, 0x24,
|
||
0xAC, 0xD7, 0xA3, 0xD6, 0xD8, 0x0B, 0x50, 0x37
|
||
};
|
||
|
||
|
||
private CancellationTokenSource _lifetimeCts;
|
||
private Thread _readerThread;
|
||
private CancellationTokenSource _readerCts;
|
||
|
||
//private int h;
|
||
private ILogger logger;
|
||
|
||
private int _lastInputTick = 0;
|
||
private const int INPUT_IDLE_BEFORE_PING_MS = 60_000;//1 минута
|
||
|
||
public PeripheralControllerV2(int handleId) : base(handleId)
|
||
{
|
||
if (_lifetimeCts != null) return;
|
||
OpenPort();
|
||
if (h <= 0)
|
||
{
|
||
Log("Порт не открылся при инициализации");
|
||
return;
|
||
}
|
||
|
||
UnityMainThreadDispatcher.Initialize();
|
||
_lifetimeCts = new CancellationTokenSource();
|
||
|
||
StartReaderThread();
|
||
_ = SetupBoardAsync(_lifetimeCts.Token);
|
||
}
|
||
|
||
public void Send(string message)
|
||
{
|
||
if (h <= 0) return;
|
||
try
|
||
{
|
||
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1);
|
||
}
|
||
catch { logger?.Log($"Ошибка отправки строкового сигнала. {message}"); }
|
||
}
|
||
|
||
private 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 void StopReaderThread()
|
||
{
|
||
if (_readerCts != null)
|
||
{
|
||
_readerCts.Cancel();
|
||
_readerCts.Dispose();
|
||
_readerCts = null;
|
||
}
|
||
|
||
if (_readerThread != null)
|
||
{
|
||
_readerThread.Join(200);
|
||
_readerThread = null;
|
||
}
|
||
}
|
||
|
||
void EnqueueLine(string line)
|
||
{
|
||
UnityMainThreadDispatcher.Instance.Enqueue(() => OnLine(line));
|
||
}
|
||
|
||
private void ProcessComInput(string line)
|
||
{
|
||
Log($"COM. h={h}: {line}");
|
||
|
||
if (line == "COIN")
|
||
PaymentReceived?.Invoke();
|
||
else if (line.StartsWith("INPUT"))
|
||
{
|
||
Volatile.Write(ref _lastInputTick, Environment.TickCount);
|
||
InputRecieved?.Invoke(line.Substring(5));
|
||
}
|
||
}
|
||
|
||
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(_ =>
|
||
{
|
||
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 Task<string> WaitForContainsAsync(string prefix, int timeoutMs, CancellationToken ct)
|
||
=> WaitForAsync(s => s.IndexOf(prefix, StringComparison.Ordinal) >= 0, timeoutMs, ct);
|
||
|
||
private 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 async Task SetupBoardAsync(CancellationToken ct)
|
||
{
|
||
bool auth = await BoardAuthAsync(ct);
|
||
if (!auth)
|
||
{
|
||
Log("Авторизация на плате не пройдена.");
|
||
UnityMainThreadDispatcher.Instance.Enqueue(() => Application.Quit());
|
||
return;
|
||
}
|
||
|
||
_ = StartKeepAliveProcessAsync(ct);
|
||
}
|
||
|
||
private void OpenPort()
|
||
{
|
||
Log("Инициализация порта");
|
||
|
||
var ports = ComNativeWrapper.ListPorts();
|
||
if (ports.Length == 0) { Log("Нет портов, завершаю"); return; }
|
||
|
||
string port = ports[^1];
|
||
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 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 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 void ClosePort()
|
||
{
|
||
Log($"Отпускаю порт handle:{h}");
|
||
if (h > 0)
|
||
{
|
||
ComNativeWrapper.Serial_Close(h);
|
||
h = 0;
|
||
}
|
||
}
|
||
|
||
|
||
//Сделать ожидание простоя около минуты на проверку и переподелючение.
|
||
private 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 async Task<bool> CheckPortConnectionAsync(CancellationToken ct)
|
||
{
|
||
try
|
||
{
|
||
Send(REQUEST_INPUT_COMMAND);
|
||
await WaitForPrefixAsync("INPUT", 1000, ct);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
//реализовать
|
||
private async Task ReinitializePortAsync(CancellationToken ct)
|
||
{
|
||
StopReaderThread();
|
||
CancelAllWaiters();
|
||
ClosePort();
|
||
|
||
OpenPort();
|
||
if (h <= 0)
|
||
{
|
||
Log($"Ошибка при переподключении.");
|
||
return;
|
||
}
|
||
|
||
|
||
StartReaderThread();
|
||
var auth = await BoardAuthAsync(ct);
|
||
if (!auth)
|
||
UnityMainThreadDispatcher.Instance.Enqueue(() => (new GameObject()).AddComponent(typeof(VfxPrefab)));
|
||
}
|
||
|
||
private 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 void Log(string message) => logger?.Log($"CW{message}");
|
||
|
||
}
|