Danro/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs
Danil Iarosh 4a079dd46b
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
refactor: Refactor work with COM to ComSystemManager
- Добавлен ComSystemManager как новый публичный компонент COM-системы для поэтапной замены ComWrapper
- Реализовано получение роли контроллера только после успешного handshake в ComDeviceClient
- Добавлено публичное поле ControllerRole в ComDeviceClient
- Добавлен запрос роли по команде 97 и разбор ответа формата ROLE : <value> (с пробелами)
- В ComMultiManager добавлена логика выбора DefaultDeviceId по роли main_controller
- Добавлен доступ к роли устройства через GetDeviceRole(deviceId) в менеджере и фасаде
- В ComWrapper добавлены настройки роли: roleRequestCommand, roleResponsePrefix, roleRequestTimeoutMs, mainControllerRole
- Дефолтная команда запроса роли изменена на 97
- Выполнена кодовая миграция зависимостей с ComWrapper на ComSystemManager в GameManager, GlobalsManager, PaymentManager, BalanceManager, InactivityManager, COMInput, LightController, PrizeSystem
- В GlobalsManager исправлена инициализация ссылки после AddComponent<ComSystemManager>(), чтобы исключить NullReference
2026-02-20 14:57:20 +03:00

314 lines
12 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.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
//Зависит от ComNativeWrapper и одноименного плагина
//рекомендуется соединение с таймаутом не меньше 10мс. Иначе вероятно busy loop и много алокаций.
public static class PrizeStandController
{
/// <summary>
/// По завершении инициализации.
/// bool - успех/неуспех.
/// string - сообщение в случае неуспеха
/// </summary>
public static event Action<bool, string> InitializationComplete;
/// <summary>
/// По завершении операции открытия.
/// bool - успех/неуспех.
/// string - сообщение в случае неуспеха
/// </summary>
public static event Action<bool, string> OpeningFinished;
private const string REQUEST_STAND_STATUS = "25";
private const string OPEN_BOX_PREFIX = "2501";
private const float STATUS_REQUEST_TIMEOUT = 3;
private const float WAIT_OPEN_STATUS_TIMEOUT = 5;
private static readonly IReadOnlyList<BoxStatus> READY_TO_OPEN_STATUSES = new[] { BoxStatus.OK, BoxStatus.OKplusCard };
private static int handle = -1;
private static int boxesCount;
private static readonly SemaphoreSlim _lock = new(1, 1);
private static bool initialized = false;
private static ILogger _logger;
#region PUBLIC API
/// <summary>
/// Инициализировать модуль витрины.
/// </summary>
/// <param name="handleId">Id хэндла полученное после ComNativeWrapper.Serial_Open</param>
/// <param name="countOfBoxes">Количество физических коробок витрины</param>
/// <param name="logger">(опционально) Логгер</param>
public static void Initialize(int handleId, int countOfBoxes, ILogger logger)
{
bool hasError = false;
StringBuilder sb = new StringBuilder();
//Проверка хэндла. h<=0 - ошибка открытия порта
if (handleId <= 0)
{
hasError = true;
sb.AppendLine($"[Модуль витрины] Переданный handle={handleId}. Ожидалось значение>0.");
}
else handle = handleId;
//Проверка количества коробок
if (countOfBoxes <= 0)
{
hasError = true;
sb.AppendLine($"[Модуль витрины] Попытка задать количество коробок={countOfBoxes}. Ожидалось значение>0.");
}
else boxesCount = countOfBoxes;
//Установка логгера
_logger = logger;
if (_logger == null)
sb.AppendLine($"[Модуль витрины] Получен логгер NULL. Если нужно логгирование, передайте дествующий логгер.");
//Завершение
if (hasError)
{
initialized = false;
handle = -1;
boxesCount = 0;
InitializationComplete?.Invoke(false, sb.ToString());
}
else
{
initialized = true;
InitializationComplete?.Invoke(true, sb.ToString());
}
}
/// <summary>
/// Открыть коробку по номеру
/// </summary>
/// <param name="boxNumber">Номер коробки (считаем с 1)</param>
public static async Task OpenBoxAsync(int boxNumber)
{
if(!await _lock.WaitAsync(0))
{
OpeningFinished?.Invoke(false, "Выдача уже в процессе");
return;
}
try
{
//Валидация
var validationResult = await CanExecuteOpenAsync(boxNumber);
if (!validationResult.OK)
{
OpeningFinished?.Invoke(false, validationResult.Message);
return;
}
SendOpenCommand(boxNumber);
//Обработка результатов
BoxStatus openStatus = await ReadBoxStatus(boxNumber, 1, WAIT_OPEN_STATUS_TIMEOUT);
if (openStatus == BoxStatus.OPENED)
{
OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта");
}
else
{
_logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}");
OpeningFinished?.Invoke(false, $"Ошибка при открытии ячейки номер {boxNumber}");
}
}
catch (Exception ex)
{
_logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}: {ex.Message}");
OpeningFinished?.Invoke(false, ex.Message);
}
finally
{
_lock.Release();
}
}
#endregion
#region HELPERS
private static void SendOpenCommand(int boxNumber)
=> ComNativeWrapper.Serial_WriteStringUTF8(handleId: handle,
utf8: $"{OPEN_BOX_PREFIX}{boxNumber:D2}", //D2 приводит в формат XX (2 знака, ноль впереди)
appendNewline: 1);
private static void SendRequestStatusCommand()
=> ComNativeWrapper.Serial_WriteStringUTF8(handle, REQUEST_STAND_STATUS, 1);
/// <summary>
/// Проверка может ли модуль произвести выдачу
/// </summary>
private static async Task<(bool OK, string Message)> CanExecuteOpenAsync(int boxNumber)
{
bool isOk = true;
StringBuilder sb = new StringBuilder();
sb.AppendLine($"[Модуль витрины. Проверка перед открытием]");
//Проверка инициализации
if (!initialized)
{
isOk = false;
sb.AppendLine("- Модуль не инициализирован.");
}
bool boxNumberIsValid = boxNumber > 0 && boxNumber <= boxesCount;
if (!boxNumberIsValid)
{
isOk = false;
sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {boxesCount}.");
}
//Не лезем в порт с запросом статуса коробки, если то, что выше дало НеОК
if (isOk)
{
SendRequestStatusCommand();
BoxStatus boxStatus = await ReadBoxStatus(boxNumber, boxesCount, STATUS_REQUEST_TIMEOUT);
if (!READY_TO_OPEN_STATUSES.Contains(boxStatus))
{
isOk = false;
sb.AppendLine($"- Статус коробки {boxNumber}: {boxStatus} не позволяет провести открытие.");
}
}
else
{
sb.AppendLine($"- Невозможно проверить статус коробки {boxNumber} из-за ошибок выше.");
}
return (isOk, sb.ToString());
}
/// <summary>
/// Читать статус заданной ячейки витрины. Выход когда прочитали заданное количество вхождений, или по таймауту.
/// </summary>
/// <param name="expectedBoxNumber">Номер ячейки которую мы ждем</param>
/// <param name="expectedEntriesCount">Сколько ячеек мы ждем</param>
/// <param name="timeout">Таймаут операции(сек.)</param>
/// <returns></returns>
private static async Task<BoxStatus> ReadBoxStatus(int expectedBoxNumber, int expectedEntriesCount, float timeout)
{
byte[] buffer = new byte[1024];
//Изначально предполагаем что статус нужной коробки не пришел
BoxStatus status = BoxStatus.NotFound;
int boxesRecieved = 0;
var sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < timeout && boxesRecieved < expectedEntriesCount)
{
//ждать пока всё не считаем
int entryLength = await Task.Run(() => ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, buffer, buffer.Length));
if (entryLength == 0) //ничего не пришло
{
continue;
}
else if (entryLength < 0) //ошибка
{
_logger?.Log($"Ошибка при считывании статуса ячеек={entryLength} :: {ComNativeWrapper.GetLastError()}");
continue;
}
string entry = Encoding.UTF8.GetString(buffer, 0, entryLength).Trim();
var ParseResult = ParseBoxStatus(entry);
if(ParseResult.BoxNumber == -1)
{
_logger?.Log($"[Модуль витрины. Ошибка получения статуса ячейки]\n{ParseResult.Message}");
}
else
{
boxesRecieved++;
if (ParseResult.BoxNumber == expectedBoxNumber)
status = ParseResult.Status;
}
}
if (status == BoxStatus.NotFound)
_logger?.Log($"[Модуль витрины. Не дождался ответа по статусу ячейки]\n" +
$"Ожидаемый номер: {expectedBoxNumber}\n" +
$"Получено статусов ячеек: {boxesRecieved}/{expectedEntriesCount}");
return status;
}
private static (int BoxNumber, BoxStatus Status, string Message) ParseBoxStatus(string entryString)
{
int number = -1;
BoxStatus status = BoxStatus.ParsingError;
string message = "";
if (entryString.StartsWith("BOX"))
{
var noPrefixEntry = entryString.Substring(3); //Убираем "BOX"
string[] parts = noPrefixEntry.Split('_'); //ожидаем массив [<номер_коробки>, <статус_коробки>]
if (parts.Length == 2)
{
//Парсим номер коробки
bool boxNumberParsed = (int.TryParse(parts[0], out int parsedNumber));
if (boxNumberParsed)
{
number = parsedNumber;
status = MapStatusFromString(parts[1]);
}
else message = $"Не удалось спарсить номер коробки: {entryString}";
}
else
{
message = $"Ошибка парсинга. Часть строки не пришла: {entryString}";
}
}
else message = $"Неожиданный префикс при парсинге ячеек: {entryString}";
return (number, status, message);
}
private static BoxStatus MapStatusFromString(string statusString) => statusString switch
{
"OK" => BoxStatus.OK,
"OK+CARD" => BoxStatus.OKplusCard,
"EMPTY" => BoxStatus.Empty,
"OPEN" => BoxStatus.Open,
"OPENED" => BoxStatus.OPENED,
"OPEN_ERROR" => BoxStatus.OPEN_ERROR,
"ERROR" => BoxStatus.Error,
_ => BoxStatus.ParsingError
};
#endregion
}
enum BoxStatus
{
//Статусы ДО открытия
OK, //Готов к выдаче
OKplusCard, //Готов к выдаче + готов выдать по карте
Empty, //Пусто, уже выдали
Open, //Дверка УЖЕ открыта. Приз предположительно пустой
//Статусы ПОСЛЕ открытия
OPENED, //Всё хорошо, открылся
OPEN_ERROR, //Ошибка открытия
//Служебные статусы
ParsingError, //При парсинге что то пошло не так, или пришла непредусмотренная фигня
NotFound, //Коробка с требуемым номером не найдена
Error //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
}