450 lines
14 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;
public static class ComPortController
{
public static event Action PaymentReceived;
public static event Action<string> InputRecieved;
private const string REQUEST_INPUT_COMMAND = "91";
private static CancellationTokenSource _lifetimeCts;
private static Thread _readerThread;
private static CancellationTokenSource _readerCts;
private static int h;
private static string _port;
private static ILogger logger;
private static ControllerConfig cachedConfig;
private static int _lastInputTick = 0;
private const int INPUT_IDLE_BEFORE_PING_MS = 60_000;//1 минута
private static InputType _inputType;
public static void Initialize(ControllerConfig config, InputType inputType)//Добавить логгер в конструктор
{
Debug.Log("Инициализация контроллера");
_inputType = inputType;
cachedConfig = config;
_port = cachedConfig.PortName;
if (_lifetimeCts != null) return;
OpenPort(cachedConfig);
if (h <= 0)
{
Debug.Log("Порт не открылся при инициализации");
return;
}
_lifetimeCts = new CancellationTokenSource();
StartReaderThread();
_ = StartKeepAliveProcessAsync(_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();
Debug.Log("Поток чтения запущен");
_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)
{
if (line == "COIN")
{
OnCoinRecieved();
}
else if (line.StartsWith("INPUT"))
{
Volatile.Write(ref _lastInputTick, Environment.TickCount);
string rawInputString = line.Substring(5);
string inputString = _inputType == InputType.Decimal ? DecimalToBinary(rawInputString) : rawInputString;
Debug.Log($"Отправляю инпут {inputString}");
InputRecieved?.Invoke(inputString);
}
}
public static void OnCoinRecieved()
{
PaymentReceived?.Invoke();
Send("10");
}
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 void OpenPort(ControllerConfig config)
{
Log("Инициализация порта");
ComNativeWrapper.Serial_CloseAll();
Log("Открываю порт: " + config.PortName);
h = ComNativeWrapper.Serial_Open(config.PortName,
config.BaudRate,
config.DataBits, config.Parity,
config.StopBits,
config.ReadTimeoutMs,
config.WriteTimeoutMs);
Log($"handle: {h}");
if (h <= 0)
{
Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
else
{
Log($"Открыл порт: {config.PortName}");
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);
string reversed = new string(chars);
return reversed.PadRight(26, '0');
}
Debug.Log($"Не удалось распарсить значение INPUT: {decimalString}");
return new string('1', 26);
}
catch (Exception e)
{
Debug.Log($"Ошибка преобразования в двоичную систему: {e.Message}");
return new string('1', 26);
}
}
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("Плата отвалилась. Запускаю переподключение.");
ReinitializePort(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 void ReinitializePort(CancellationToken ct)
{
StopReaderThread();
CancelAllWaiters();
ClosePort();
OpenPort(cachedConfig);
if (h <= 0)
{
Log($"Ошибка при переподключении.");
return;
}
StartReaderThread();
}
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(message);
public enum InputType { Binary, Decimal}
}