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

306 lines
10 KiB
C#

using System;
using RR.Game2D.Core.Managers;
using UnityEngine;
using UnityEngine.Events;
namespace RR.Common.Controls
{
/// <summary>
/// Публичный компонент COM-системы.
/// </summary>
[AddComponentMenu("RR/Controls/Com System Manager")]
public class ComSystemManager : MonoBehaviour
{
private enum ControllerHandlerType
{
Default = 0,
PrizeStand = 1
}
[Serializable]
private sealed class RoleHandlerBinding
{
public string role;
public ControllerHandlerType handlerType = ControllerHandlerType.Default;
[Min(0)] public int prizeStandBoxesCount;
}
[Header("Основные настройки")]
[SerializeField] private bool saveMoneyBetweenSessions;
[SerializeField] private bool enablePrizeChecking;
[SerializeField] private bool keepAlive = true;
[Header("Авторизация")]
[SerializeField] private bool enableEncryptedHandshake = true;
[SerializeField] private int handshakeTimeoutMs = 5000;
[Header("Роль контроллера (после handshake)")]
[SerializeField] private string roleRequestCommand = "97";
[SerializeField] private string roleResponsePrefix = "ROLE";
[SerializeField] private int roleRequestTimeoutMs = 1500;
[SerializeField] private string mainControllerRole = "main_controller";
[Header("Мульти-устройства")]
[SerializeField] private bool useAllDetectedPorts = true;
[SerializeField] private string[] explicitPorts = Array.Empty<string>();
[SerializeField] private string defaultDevicePort = "";
[Header("Фабрика обработчиков по ROLE")]
[SerializeField] private RoleHandlerBinding[] roleHandlerBindings =
{
new RoleHandlerBinding { role = "prize_stand", handlerType = ControllerHandlerType.PrizeStand, prizeStandBoxesCount = 24 },
new RoleHandlerBinding { role = "prize_stand_controller", handlerType = ControllerHandlerType.PrizeStand, prizeStandBoxesCount = 24 }
};
public static UnityEvent<int> OnCreditAdded;
public static UnityEvent<int> OnCreditRemoved;
public static UnityEvent<string> OnInputReceived;
public static Action OnInitialize;
public static UnityEvent<string, string> OnDeviceInputReceived;
public static UnityEvent<string> OnDeviceAuthorized;
private static ComSystemManager _instance;
private static int _creditsCount;
private ComHub _manager;
private LightController _light;
private bool _initializeInvoked;
public LightController Light => _light;
public void Initialize()
{
_instance = this;
OnCreditAdded = new UnityEvent<int>();
OnCreditRemoved = new UnityEvent<int>();
OnInputReceived = new UnityEvent<string>();
OnDeviceInputReceived = new UnityEvent<string, string>();
OnDeviceAuthorized = new UnityEvent<string>();
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
_creditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
_light = GetComponent<LightController>() ?? gameObject.AddComponent<LightController>();
_manager = new ComHub(
useAllDetectedPorts,
explicitPorts,
defaultDevicePort,
enableEncryptedHandshake,
handshakeTimeoutMs,
keepAlive || enablePrizeChecking,
roleRequestCommand,
roleResponsePrefix,
roleRequestTimeoutMs,
mainControllerRole,
CreateRoleHandler);
_manager.InitializeDevices();
}
public static void AddCredit()
{
_creditsCount++;
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
OnCreditAdded?.Invoke(_creditsCount);
}
public static bool Pay(int amount = 1)
{
if (_creditsCount < amount)
{
return false;
}
_creditsCount -= amount;
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
OnCreditRemoved?.Invoke(_creditsCount);
return true;
}
public static int GetCredits() => _creditsCount;
public static void SetCredits(int amount)
{
int oldCredits = _creditsCount;
_creditsCount = Mathf.Max(0, amount);
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
if (_creditsCount > oldCredits)
{
OnCreditAdded?.Invoke(_creditsCount);
}
else if (_creditsCount < oldCredits)
{
OnCreditRemoved?.Invoke(_creditsCount);
}
}
public static void Send(string message)
{
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
_instance._manager?.SendToDefault(message);
}
public static void SendToDevice(string deviceId, string message)
{
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
_instance._manager?.SendToDevice(deviceId, message);
}
public static void SendToAll(string message)
{
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
_instance._manager?.SendToAll(message);
}
public static string GetLastInputState()
{
if (_instance?._manager == null) return "11111111111111111111111111";
string defaultId = _instance._manager.DefaultDeviceId;
return string.IsNullOrWhiteSpace(defaultId)
? "11111111111111111111111111"
: _instance._manager.GetLastInputState(defaultId);
}
public static string GetLastInputState(string deviceId)
{
return _instance?._manager?.GetLastInputState(deviceId) ?? "11111111111111111111111111";
}
public static string[] GetConnectedDeviceIds()
{
return _instance?._manager?.ConnectedDeviceIds ?? Array.Empty<string>();
}
public static string GetDefaultDeviceId()
{
return _instance?._manager?.DefaultDeviceId ?? string.Empty;
}
public static string GetDeviceRole(string deviceId)
{
return _instance?._manager?.GetDeviceRole(deviceId) ?? string.Empty;
}
public static PrizeStandController GetPrizeStandController(string deviceId = null)
{
return _instance?._manager?.GetPrizeStandController(deviceId);
}
private IComControllerHandler CreateRoleHandler(string deviceId, string role)
{
if (string.IsNullOrWhiteSpace(role) || roleHandlerBindings == null || roleHandlerBindings.Length == 0)
{
return null;
}
RoleHandlerBinding binding = FindRoleBinding(role);
if (binding == null)
{
return null;
}
if (binding.handlerType == ControllerHandlerType.PrizeStand)
{
PrizeStandController controller = new PrizeStandController(
deviceId,
(id, message) => _manager != null && _manager.SendToDevice(id, message),
message => GameManager.Logger?.Log("PrizeStandController", $"[{deviceId}] {message}"));
controller.SetBoxesCount(binding.prizeStandBoxesCount);
return controller;
}
return null;
}
private RoleHandlerBinding FindRoleBinding(string role)
{
for (int i = 0; i < roleHandlerBindings.Length; i++)
{
RoleHandlerBinding binding = roleHandlerBindings[i];
if (binding == null || string.IsNullOrWhiteSpace(binding.role))
{
continue;
}
if (string.Equals(binding.role.Trim(), role.Trim(), StringComparison.OrdinalIgnoreCase))
{
return binding;
}
}
return null;
}
public void Blink() => _light.Blink();
public void StopBlink(bool enableHat = true) => _light.StopBlink(enableHat);
private void Update()
{
if (_manager == null) return;
int coinsProcessed = 0;
while (_manager.TryDequeueCoin(out _))
{
AddCredit();
coinsProcessed++;
}
while (_manager.TryDequeueInput(out DeviceInputMessage msg))
{
OnDeviceInputReceived?.Invoke(msg.DeviceId, msg.InputBits);
if (msg.DeviceId == _manager.DefaultDeviceId)
{
OnInputReceived?.Invoke(msg.InputBits);
}
}
while (_manager.TryDequeueAuthorized(out string deviceId))
{
OnDeviceAuthorized?.Invoke(deviceId);
if (!_initializeInvoked)
{
_initializeInvoked = true;
OnInitialize?.Invoke();
}
if (deviceId == _manager.DefaultDeviceId)
{
_light.Blink();
}
}
if (coinsProcessed > 0)
{
GameManager.Logger?.Log("ComSystemManager", $"Обработано монет: {coinsProcessed}");
}
}
private void OnDisable()
{
_manager?.ShutdownAll(false);
_manager = null;
_initializeInvoked = false;
}
private void OnApplicationQuit()
{
_manager?.ShutdownAll(true);
_manager = null;
_initializeInvoked = false;
}
}
}