- Добавлен 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
316 lines
13 KiB
C#
316 lines
13 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using RR.Common.Controls;
|
||
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 //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
|
||
}
|