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; public static bool Initialized { get; private set; } public static int BoxesCount { get; private set; } private static readonly SemaphoreSlim _lock = new(1, 1); #region PUBLIC API /// /// Инициализировать модуль витрины. /// /// Id хэндла полученное после ComNativeWrapper.Serial_Open /// Количество физических коробок витрины /// (опционально) Логгер public static void Initialize(int handleId, int countOfBoxes) { 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; //Завершение 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(true, "Выдача уже в процессе"); return; } //очистка пол сек await Task.Run(() => { byte[] buf = new byte[1024]; var sw = Stopwatch.StartNew(); while (sw.Elapsed.TotalSeconds < 0.5) { ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, buf, buf.Length); } }); 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); UnityEngine.Debug.Log($"Получен статус открытия {openStatus}"); if (openStatus == BoxStatus.OPENED) { OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта"); } else { Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}"); OpeningFinished?.Invoke(true, $"Ошибка при открытии ячейки {boxNumber}"); } } catch (Exception ex) { 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("- Модуль не инициализирован."); sb.AppendLine("Витрина не подключена"); } bool boxNumberIsValid = boxNumber > 0 && boxNumber <= BoxesCount; if (!boxNumberIsValid) { isOk = false; //sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {BoxesCount}."); sb.AppendLine($"Ошибка"); } //Не лезем в порт с запросом статуса коробки, если то, что выше дало НеОК 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} не позволяет провести открытие."); sb.AppendLine($"Ошибка"); } } else { sb.AppendLine($"Ошибка"); //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) //ошибка { Log($"Ошибка при считывании статуса ячеек={entryLength} :: {ComNativeWrapper.GetLastError()}"); continue; } string entry = Encoding.UTF8.GetString(buffer, 0, entryLength).Trim(); if (!entry.StartsWith("BOX")) { continue; } var ParseResult = ParseBoxStatus(entry); UnityEngine.Debug.Log($"{ParseResult.BoxNumber} : {ParseResult.Status}"); if(ParseResult.BoxNumber == -1) { Log($"[Модуль витрины. Ошибка получения статуса ячейки]\n{ParseResult.Message}"); } else { boxesRecieved++; if (ParseResult.BoxNumber == expectedBoxNumber) status = ParseResult.Status; } } if (boxesRecieved == expectedEntriesCount) UnityEngine.Debug.Log("Все коробки считаны"); if (sw.Elapsed.TotalSeconds >= timeout) UnityEngine.Debug.Log("Вышли по таймауту"); if (status == BoxStatus.NotFound) Log($"[Модуль витрины. Не дождался ответа по статусу ячейки]\n" + $"Ожидаемый номер: {expectedBoxNumber}\n" + $"Получено статусов ячеек: {boxesRecieved}/{expectedEntriesCount}"); return status; } public static async Task> GetStatusesAsync() { byte[] buffer = new byte[1024]; Dictionary keyValuePairs = new Dictionary(); //очистка пол сек await Task.Run(() => { byte[] buf = new byte[1024]; var sw = Stopwatch.StartNew(); while (sw.Elapsed.TotalSeconds < 0.5) { ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, buf, buf.Length); } }); SendRequestStatusCommand(); int boxesRecieved = 0; var sw = Stopwatch.StartNew(); while (sw.Elapsed.TotalSeconds < 3 && boxesRecieved < BoxesCount) { //ждать пока всё не считаем int entryLength = await Task.Run(() => ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, buffer, buffer.Length)); if (entryLength == 0) //ничего не пришло { continue; } else if (entryLength < 0) //ошибка { Log($"Ошибка при считывании статуса ячеек={entryLength} :: {ComNativeWrapper.GetLastError()}"); continue; } string entry = Encoding.UTF8.GetString(buffer, 0, entryLength).Trim(); if (!entry.StartsWith("BOX")) { continue; } var ParseResult = ParseBoxStatus(entry); UnityEngine.Debug.Log($"{ParseResult.BoxNumber} : {ParseResult.Status}"); if (ParseResult.BoxNumber == -1) { Log($"[Модуль витрины. Ошибка получения статуса ячейки]\n{ParseResult.Message}"); } else { boxesRecieved++; keyValuePairs[ParseResult.BoxNumber] = ParseResult.Status; } } if (sw.Elapsed.TotalSeconds >= 3) UnityEngine.Debug.Log("Вышли по таймауту"); return keyValuePairs; } 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.OPENED, "OPEN_ERROR" => BoxStatus.OPEN_ERROR, "ERROR" => BoxStatus.Error, _ => BoxStatus.ParsingError }; #endregion ////ТЕСТОВОК /// /// Получить номера ячеек, готовых к открытию (статус OK или OKplusCard) /// /// Массив номеров готовых к выдаче ячеек public static async Task GetReadyBoxesAsync() { try { // Получаем статусы всех ячеек Dictionary allStatuses = await GetStatusesAsync(); // Фильтруем только те, что готовы к открытию var readyBoxes = allStatuses .Where(kvp => READY_TO_OPEN_STATUSES.Contains(kvp.Value)) .Select(kvp => kvp.Key) .ToArray(); Log($"[Модуль витрины] Найдено готовых к выдаче ячеек: {readyBoxes.Length}"); return readyBoxes; } catch (Exception ex) { Log($"[Модуль витрины] Ошибка при получении списка готовых ячеек: {ex.Message}"); return Array.Empty(); } } /// /// Получить номера ячеек, готовых к открытию (синхронная обертка с таймаутом) /// /// Таймаут ожидания в миллисекундах /// Массив номеров готовых к выдаче ячеек public static int[] GetReadyBoxes(int timeoutMs = 1000) { try { var task = Task.Run(async () => await GetReadyBoxesAsync()); if (task.Wait(timeoutMs)) { return task.Result; } else { Log($"[Модуль витрины] Таймаут при получении списка готовых ячеек"); return Array.Empty(); } } catch (Exception ex) { Log($"[Модуль витрины] Ошибка при получении списка готовых ячеек: {ex.Message}"); return Array.Empty(); } } private static void Log(string message) => UnityEngine.Debug.Log(message); } public enum BoxStatus { //Статусы ДО открытия OK, //Готов к выдаче OKplusCard, //Готов к выдаче + готов выдать по карте Empty, //Пусто, уже выдали Open, //Дверка УЖЕ открыта. Приз предположительно пустой //Статусы ПОСЛЕ открытия OPENED, //Всё хорошо, открылся OPEN_ERROR, //Ошибка открытия //Служебные статусы ParsingError, //При парсинге что то пошло не так, или пришла непредусмотренная фигня NotFound, //Коробка с требуемым номером не найдена Error //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок) }