diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs b/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs new file mode 100644 index 0000000..a1309ad --- /dev/null +++ b/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs @@ -0,0 +1,314 @@ +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 +{ + /// + /// По завершении инициализации. + /// bool - успех/неуспех. + /// string - сообщение в случае неуспеха + /// + public static event Action InitializationComplete; + /// + /// По завершении операции открытия. + /// bool - успех/неуспех. + /// string - сообщение в случае неуспеха + /// + public static event Action 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 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 + /// + /// Инициализировать модуль витрины. + /// + /// Id хэндла полученное после ComNativeWrapper.Serial_Open + /// Количество физических коробок витрины + /// (опционально) Логгер + 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()); + } + } + + + /// + /// Открыть коробку по номеру + /// + /// Номер коробки (считаем с 1) + 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); + + /// + /// Проверка может ли модуль произвести выдачу + /// + 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()); + } + + + /// + /// Читать статус заданной ячейки витрины. Выход когда прочитали заданное количество вхождений, или по таймауту. + /// + /// Номер ячейки которую мы ждем + /// Сколько ячеек мы ждем + /// Таймаут операции(сек.) + /// + private static async Task 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 //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок) +} \ No newline at end of file diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs.meta b/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs.meta new file mode 100644 index 0000000..ac67df9 --- /dev/null +++ b/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ce33b35c8b4042a5b85c0263727d588b +timeCreated: 1771575375 \ No newline at end of file