Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Архитектура COM и роли - ComMultiManager переименован в ComHub и переведен на фабричный паттерн обработчиков по ROLE. - Добавлен единый контракт обработчиков COM-контроллеров: - IComControllerHandler - DefaultComControllerHandler - Ответственность за оркестрацию перенесена в ComSystemManager, чтобы инициализация COM и жизненный цикл контроллеров управлялись централизованно. - ComWrapper сохранен как слой совместимости на время миграции логики в новую схему. - PrizeStandController отрефакторен под новый role-handler pipeline вместо старой прямой привязки. - Добавлен удобный доступ к PrizeStandController из менеджера системы там, где это требуется игровому коду. Handshake/авторизация платы (ComDeviceClient) - Авторизация платы переработана в строгую поэтапную последовательность: 1) ПК отправляет 07 2) ожидает CHAL 3) отправляет 08 в ответ на CHAL 4) ожидает RAES - Парсер стадий теперь принимает только ожидаемые токены на каждой стадии: - WaitingChal -> только CHAL - WaitingRaes -> только RAES - Любой посторонний вывод из serial во время handshake игнорируется (без немедленного падения), что устраняет ложные ошибки на нерелевантных COM-устройствах. - Если ожидаемый ответ после 07 не приходит, порт быстро пропускается как нерелевантный, без эскалации в критическую ошибку. - Для случаев с неподходящими/сторонними устройствами уровень логирования снижен с ошибок до предупреждений там, где это уместно. - Добавлена подробная диагностика авторизации (переходы стадий, принятые значимые кадры, контекст таймаутов) для упрощения отладки. Тестовая утилита handshake - Добавлен Assets/Temp/Handshake_Test.cs как изолированный диагностический скрипт. - Включены очень подробные логи по каждому шагу handshake и по каждому решению парсера. - Позволяет отдельно гонять тестовый сценарий и сравнивать поведение платы, не ломая production-поток. Стабильность COM и поведение reconnect - Подправлена логика keep-alive, чтобы исключить агрессивные реконнекты. - Reconnect теперь срабатывает только после нескольких последовательных пропусков INPUT-ответа, что снижает случайные обрывы COM и прерывания подсветки. Поведение подсветки во время выполнения - Циклы подсветки сделаны независимыми от Time.timeScale: - LightController: WaitForSeconds -> WaitForSecondsRealtime - LightSequence: WaitForSeconds -> WaitForSecondsRealtime - Это устраняет случайные паузы/замедления световых циклов при изменении игрового времени (пауза, slow-mo, временные изменения scale). Итоговый эффект - COM-стек стал расширяемым по ролям и проще в сопровождении. - Handshake стал детерминированным, устойчивым к serial-шуму и безопасным при работе со сторонними устройствами. - Циклы подсветки стабилизированы для длительных сессий и больше не зависят от масштабирования игрового времени.
362 lines
11 KiB
C#
362 lines
11 KiB
C#
using System;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using RR.Game2D.Core.Managers;
|
||
|
||
namespace RR.Common.Controls
|
||
{
|
||
internal sealed class ComHub
|
||
{
|
||
private readonly Dictionary<string, ComDeviceClient> _devices = new();
|
||
private readonly Dictionary<string, IComControllerHandler> _handlers = new();
|
||
private readonly ConcurrentQueue<string> _coinQueue = new();
|
||
private readonly ConcurrentQueue<DeviceInputMessage> _inputQueue = new();
|
||
private readonly ConcurrentQueue<string> _authorizedQueue = new();
|
||
|
||
private readonly bool _useAllDetectedPorts;
|
||
private readonly string[] _explicitPorts;
|
||
private readonly string _defaultDevicePort;
|
||
private readonly bool _enableEncryptedHandshake;
|
||
private readonly int _handshakeTimeoutMs;
|
||
private readonly bool _keepAlive;
|
||
private readonly string _roleRequestCommand;
|
||
private readonly string _roleResponsePrefix;
|
||
private readonly int _roleRequestTimeoutMs;
|
||
private readonly string _mainControllerRole;
|
||
private readonly Func<string, string, IComControllerHandler> _handlerFactory;
|
||
|
||
private string _defaultDeviceId = string.Empty;
|
||
|
||
public ComHub(
|
||
bool useAllDetectedPorts,
|
||
string[] explicitPorts,
|
||
string defaultDevicePort,
|
||
bool enableEncryptedHandshake,
|
||
int handshakeTimeoutMs,
|
||
bool keepAlive,
|
||
string roleRequestCommand,
|
||
string roleResponsePrefix,
|
||
int roleRequestTimeoutMs,
|
||
string mainControllerRole,
|
||
Func<string, string, IComControllerHandler> handlerFactory)
|
||
{
|
||
_useAllDetectedPorts = useAllDetectedPorts;
|
||
_explicitPorts = explicitPorts ?? Array.Empty<string>();
|
||
_defaultDevicePort = defaultDevicePort ?? string.Empty;
|
||
_enableEncryptedHandshake = enableEncryptedHandshake;
|
||
_handshakeTimeoutMs = handshakeTimeoutMs;
|
||
_keepAlive = keepAlive;
|
||
_roleRequestCommand = roleRequestCommand ?? string.Empty;
|
||
_roleResponsePrefix = roleResponsePrefix ?? "ROLE";
|
||
_roleRequestTimeoutMs = roleRequestTimeoutMs;
|
||
_mainControllerRole = string.IsNullOrWhiteSpace(mainControllerRole) ? "main_controller" : mainControllerRole;
|
||
_handlerFactory = handlerFactory;
|
||
}
|
||
|
||
public string DefaultDeviceId => _defaultDeviceId;
|
||
|
||
public string[] ConnectedDeviceIds => _devices.Keys.ToArray();
|
||
|
||
public void InitializeDevices()
|
||
{
|
||
ComNativeWrapper.Serial_CloseAll();
|
||
|
||
string[] allPorts = ComNativeWrapper.ListPorts();
|
||
if (allPorts.Length == 0)
|
||
{
|
||
GameManager.Logger?.LogWarning("ComHub", "Не найдено COM-портов");
|
||
return;
|
||
}
|
||
|
||
string[] selectedPorts = SelectPorts(allPorts);
|
||
foreach (string port in selectedPorts)
|
||
{
|
||
TryCreateDevice(port);
|
||
}
|
||
|
||
ResolveDefaultDeviceId();
|
||
}
|
||
|
||
public void ShutdownAll(bool sendShutdownCommands)
|
||
{
|
||
foreach (ComDeviceClient client in _devices.Values)
|
||
{
|
||
UnsubscribeDevice(client);
|
||
client.Stop(sendShutdownCommands);
|
||
}
|
||
|
||
foreach (IComControllerHandler handler in _handlers.Values)
|
||
{
|
||
handler.Dispose();
|
||
}
|
||
|
||
_devices.Clear();
|
||
_handlers.Clear();
|
||
_defaultDeviceId = string.Empty;
|
||
|
||
while (_coinQueue.TryDequeue(out _))
|
||
{
|
||
}
|
||
|
||
while (_inputQueue.TryDequeue(out _))
|
||
{
|
||
}
|
||
|
||
while (_authorizedQueue.TryDequeue(out _))
|
||
{
|
||
}
|
||
}
|
||
|
||
public bool SendToDefault(string message)
|
||
{
|
||
if (string.IsNullOrEmpty(_defaultDeviceId))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return SendToDevice(_defaultDeviceId, message);
|
||
}
|
||
|
||
public bool SendToDevice(string deviceId, string message)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(deviceId))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
client.Send(message);
|
||
return true;
|
||
}
|
||
|
||
public void SendToAll(string message)
|
||
{
|
||
foreach (ComDeviceClient client in _devices.Values)
|
||
{
|
||
client.Send(message);
|
||
}
|
||
}
|
||
|
||
public string GetLastInputState(string deviceId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(deviceId))
|
||
{
|
||
return "11111111111111111111111111";
|
||
}
|
||
|
||
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
|
||
? client.LastInputBits
|
||
: "11111111111111111111111111";
|
||
}
|
||
|
||
public bool TryDequeueCoin(out string deviceId) => _coinQueue.TryDequeue(out deviceId);
|
||
|
||
public bool TryDequeueInput(out DeviceInputMessage message) => _inputQueue.TryDequeue(out message);
|
||
|
||
public bool TryDequeueAuthorized(out string deviceId) => _authorizedQueue.TryDequeue(out deviceId);
|
||
|
||
public string GetDeviceRole(string deviceId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(deviceId))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
|
||
? client.ControllerRole
|
||
: string.Empty;
|
||
}
|
||
|
||
public PrizeStandController GetPrizeStandController(string deviceId = null)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||
{
|
||
return _handlers.TryGetValue(deviceId, out IComControllerHandler handler)
|
||
? handler as PrizeStandController
|
||
: null;
|
||
}
|
||
|
||
foreach (IComControllerHandler handler in _handlers.Values)
|
||
{
|
||
if (handler is PrizeStandController standController)
|
||
{
|
||
return standController;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private string[] SelectPorts(string[] allPorts)
|
||
{
|
||
if (_useAllDetectedPorts)
|
||
{
|
||
return allPorts;
|
||
}
|
||
|
||
if (_explicitPorts.Length == 0)
|
||
{
|
||
return Array.Empty<string>();
|
||
}
|
||
|
||
HashSet<string> explicitSet = new(_explicitPorts.Where(p => !string.IsNullOrWhiteSpace(p)));
|
||
return allPorts.Where(explicitSet.Contains).ToArray();
|
||
}
|
||
|
||
private void ResolveDefaultDeviceId()
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(_defaultDevicePort) && _devices.ContainsKey(_defaultDevicePort))
|
||
{
|
||
_defaultDeviceId = _defaultDevicePort;
|
||
return;
|
||
}
|
||
|
||
_defaultDeviceId = _devices.Keys.FirstOrDefault() ?? string.Empty;
|
||
}
|
||
|
||
private void TryCreateDevice(string port)
|
||
{
|
||
ComDeviceClient client = new(
|
||
port,
|
||
_enableEncryptedHandshake,
|
||
_handshakeTimeoutMs,
|
||
_keepAlive,
|
||
_roleRequestCommand,
|
||
_roleResponsePrefix,
|
||
_roleRequestTimeoutMs);
|
||
|
||
SubscribeDevice(client);
|
||
|
||
if (!client.Start())
|
||
{
|
||
UnsubscribeDevice(client);
|
||
return;
|
||
}
|
||
|
||
_devices[client.DeviceId] = client;
|
||
}
|
||
|
||
private void SubscribeDevice(ComDeviceClient client)
|
||
{
|
||
client.CoinAccepted += OnDeviceCoinAccepted;
|
||
client.InputReceived += OnDeviceInputReceived;
|
||
client.RawLineReceived += OnDeviceRawLineReceived;
|
||
client.Authorized += OnDeviceAuthorized;
|
||
client.Disconnected += OnDeviceDisconnected;
|
||
}
|
||
|
||
private void UnsubscribeDevice(ComDeviceClient client)
|
||
{
|
||
client.CoinAccepted -= OnDeviceCoinAccepted;
|
||
client.InputReceived -= OnDeviceInputReceived;
|
||
client.RawLineReceived -= OnDeviceRawLineReceived;
|
||
client.Authorized -= OnDeviceAuthorized;
|
||
client.Disconnected -= OnDeviceDisconnected;
|
||
}
|
||
|
||
private void OnDeviceCoinAccepted(string deviceId)
|
||
{
|
||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
|
||
{
|
||
handler.HandleCoinAccepted();
|
||
return;
|
||
}
|
||
|
||
_coinQueue.Enqueue(deviceId);
|
||
}
|
||
|
||
private void OnDeviceInputReceived(string deviceId, string inputBits)
|
||
{
|
||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
|
||
{
|
||
handler.HandleInputReceived(inputBits);
|
||
return;
|
||
}
|
||
|
||
_inputQueue.Enqueue(new DeviceInputMessage(deviceId, inputBits));
|
||
}
|
||
|
||
private void OnDeviceRawLineReceived(string deviceId, string line)
|
||
{
|
||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
|
||
{
|
||
handler.HandleRawLine(line);
|
||
}
|
||
}
|
||
|
||
private void OnDeviceAuthorized(string deviceId)
|
||
{
|
||
if (_devices.TryGetValue(deviceId, out ComDeviceClient client) &&
|
||
string.Equals(client.ControllerRole, _mainControllerRole, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_defaultDeviceId = deviceId;
|
||
}
|
||
else if (string.IsNullOrWhiteSpace(_defaultDeviceId))
|
||
{
|
||
_defaultDeviceId = deviceId;
|
||
}
|
||
|
||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler existing))
|
||
{
|
||
existing.Dispose();
|
||
}
|
||
|
||
_handlers[deviceId] = CreateHandlerForDevice(deviceId);
|
||
_authorizedQueue.Enqueue(deviceId);
|
||
}
|
||
|
||
private void OnDeviceDisconnected(string deviceId)
|
||
{
|
||
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client))
|
||
{
|
||
return;
|
||
}
|
||
|
||
UnsubscribeDevice(client);
|
||
_devices.Remove(deviceId);
|
||
|
||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
|
||
{
|
||
handler.Dispose();
|
||
_handlers.Remove(deviceId);
|
||
}
|
||
|
||
if (_defaultDeviceId == deviceId)
|
||
{
|
||
_defaultDeviceId = _devices.Keys.FirstOrDefault() ?? string.Empty;
|
||
}
|
||
}
|
||
|
||
private IComControllerHandler CreateHandlerForDevice(string deviceId)
|
||
{
|
||
string role = GetDeviceRole(deviceId);
|
||
IComControllerHandler customHandler = _handlerFactory?.Invoke(deviceId, role);
|
||
if (customHandler != null)
|
||
{
|
||
return customHandler;
|
||
}
|
||
|
||
return new DefaultComControllerHandler(deviceId, _coinQueue, _inputQueue);
|
||
}
|
||
}
|
||
|
||
internal readonly struct DeviceInputMessage
|
||
{
|
||
public readonly string DeviceId;
|
||
public readonly string InputBits;
|
||
|
||
public DeviceInputMessage(string deviceId, string inputBits)
|
||
{
|
||
DeviceId = deviceId;
|
||
InputBits = inputBits;
|
||
}
|
||
}
|
||
}
|
||
|
||
|