Danil Iarosh 79b66b2d17
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: add controller disconnect/reconnect handling, MAC identification, lighting pause/resume system
- Рефакторинг системы подсветки: вместо длительности в секундах теперь
  используется количество циклов (float). Добавлена таблица длительности
  циклов эффектов `LightCycleDurations` для пересчёта циклов в секунды.
  Префикс команд подсветки изменён с `85` на `86`.
- `LightSequence`: добавлены `Pause()` / `Resume()` с запоминанием текущего
  шага последовательности; возобновление продолжает с сохранённой позиции.
- `LightController`: добавлены `PauseAll()` / `ResumeAll()` для
  приостановки и восстановления подсветки и мигания шляпы при
  отключении/переподключении платы. Свойства `IsSequencePaused`,
  `IsBlinkPaused`.
- `ComDeviceClient`: после успешной авторизации отправляются команды
  `890003` и `8600`. Добавлен запрос MAC-адреса через команду `06`
  (ответ — 12 hex-символов без префикса). Новое событие `ConnectionLost`
  при начале KeepAlive-реконнекта.
- `ComHub`: очередь `_disconnectedQueue` с типом `DeviceDisconnectMessage`
  (DeviceId + Role + Mac) — сохраняет идентификатор устройства до его
  удаления из словаря. Подписка на `ConnectionLost`.
  Добавлен `GetDeviceMac()`.
- `ComSystemManager`: при инициализации, если плата не обнаружена,
  подсветка сразу ставится на паузу. При отвале дефолтного устройства —
  `PauseAll()`, при переподключении — сравнение по MAC и роли через
  `IsSameDevice()`: та же плата → `ResumeAll()`, другая → `Blink()` с нуля.
  Добавлены событие `OnDeviceDisconnected` и метод `GetDeviceMac()`.
- `COMInput`: при событии `OnDeviceDisconnected` сбрасываются все состояния
  кнопок через `ResetAllButtons()`, генерируя `buttonUp` для зажатых кнопок.
  Устраняет баг залипания управления после переподключения платы.
- `GameManager`: расширена последовательность световых эффектов в меню
  (добавлены `DiagonalWave3`, `WaveUp`, `OrangeWave`); вызовы `SetLight`
  для game over/won переведены на float-циклы.
- Обновлены `ARCHITECTURE.md` и `README.md`: переписана документация
  на актуальную структуру проекта и текущие возможности игры.
2026-03-04 13:55:08 +03:00

404 lines
13 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 ConcurrentQueue<DeviceDisconnectMessage> _disconnectedQueue = 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 _))
{
}
while (_disconnectedQueue.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 bool TryDequeueDisconnected(out DeviceDisconnectMessage message) => _disconnectedQueue.TryDequeue(out message);
public string GetDeviceRole(string deviceId)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
return string.Empty;
}
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
? client.ControllerRole
: string.Empty;
}
public string GetDeviceMac(string deviceId)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
return string.Empty;
}
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
? client.MacAddress
: 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;
client.ConnectionLost += OnDeviceConnectionLost;
}
private void UnsubscribeDevice(ComDeviceClient client)
{
client.CoinAccepted -= OnDeviceCoinAccepted;
client.InputReceived -= OnDeviceInputReceived;
client.RawLineReceived -= OnDeviceRawLineReceived;
client.Authorized -= OnDeviceAuthorized;
client.Disconnected -= OnDeviceDisconnected;
client.ConnectionLost -= OnDeviceConnectionLost;
}
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;
}
_disconnectedQueue.Enqueue(deviceId);
}
private void OnDeviceConnectionLost(string deviceId)
{
_disconnectedQueue.Enqueue(deviceId);
}
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;
}
}
internal readonly struct DeviceDisconnectMessage
{
public readonly string DeviceId;
public readonly string Role;
public readonly string Mac;
public DeviceDisconnectMessage(string deviceId, string role, string mac)
{
DeviceId = deviceId;
Role = role ?? string.Empty;
Mac = mac ?? string.Empty;
}
}
}