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