Danil Iarosh 2963e342c2
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
refactor: improve COM flow and light timing
Архитектура 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-шуму и безопасным при работе со сторонними устройствами.
- Циклы подсветки стабилизированы для длительных сессий и больше не зависят от масштабирования игрового времени.
2026-02-24 11:34:25 +03:00

362 lines
11 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.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;
}
}
}