From 71d45078b24b0b4a6dd148be5e27def48778fdbd Mon Sep 17 00:00:00 2001 From: _sega Date: Wed, 18 Feb 2026 14:28:36 +0300 Subject: [PATCH] =?UTF-8?q?FEAT:=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20?= =?UTF-8?q?=D1=81=20=D0=BF=D1=80=D0=B8=D0=B7=D0=BE=D0=B2=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Fonts/NEW/NexaRegular COOL.asset | 2 +- Assets/Scenes/GameScene3D.unity | 5 +- Assets/Scripts/PrizeStand.meta | 8 + .../PrizeStand/PrizeStandController.cs | 314 ++++++++++++++++++ .../PrizeStand/PrizeStandController.cs.meta | 11 + 5 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 Assets/Scripts/PrizeStand.meta create mode 100644 Assets/Scripts/PrizeStand/PrizeStandController.cs create mode 100644 Assets/Scripts/PrizeStand/PrizeStandController.cs.meta diff --git a/Assets/Fonts/NEW/NexaRegular COOL.asset b/Assets/Fonts/NEW/NexaRegular COOL.asset index f15a6a2..df31775 100644 --- a/Assets/Fonts/NEW/NexaRegular COOL.asset +++ b/Assets/Fonts/NEW/NexaRegular COOL.asset @@ -11349,7 +11349,7 @@ Material: - _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767} - _EnvMatrixRotation: {r: 0, g: 0, b: 0, a: 0} - _FaceColor: {r: 1, g: 1, b: 1, a: 1} - - _GlowColor: {r: 1, g: 0.8658613, b: 0, a: 1} + - _GlowColor: {r: 1, g: 0, b: 0.175066, a: 1} - _MaskCoord: {r: 0, g: 0, b: 32767, a: 32767} - _OutlineColor: {r: 0.554717, g: 0.554717, b: 0.554717, a: 1} - _ReflectFaceColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/Assets/Scenes/GameScene3D.unity b/Assets/Scenes/GameScene3D.unity index e7b624d..ba75d2e 100644 --- a/Assets/Scenes/GameScene3D.unity +++ b/Assets/Scenes/GameScene3D.unity @@ -14015,7 +14015,6 @@ MonoBehaviour: adminPasswordFallback: 1234 passwordFileName: admin_password.txt settingActions: [] - wsPushIntervalMs: 500 --- !u!4 &592995719 Transform: m_ObjectHideFlags: 0 @@ -35730,9 +35729,9 @@ MonoBehaviour: switchLetterSound: {fileID: 0} acceptSound: {fileID: 0} openSound: {fileID: 0} - MaxSpeedLetterPerSecond: 50 + MaxSpeedLetterPerSecond: 100 MinSpeedLetterPerSecond: 3 - MaxSpeedTime: 5 + MaxSpeedTime: 3 MinSpeedTime: 0.5 minValue: 1 maxValue: 10000 diff --git a/Assets/Scripts/PrizeStand.meta b/Assets/Scripts/PrizeStand.meta new file mode 100644 index 0000000..f4bedf8 --- /dev/null +++ b/Assets/Scripts/PrizeStand.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3aa03864eb84d2042a0517ae1f43aad7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/PrizeStand/PrizeStandController.cs b/Assets/Scripts/PrizeStand/PrizeStandController.cs new file mode 100644 index 0000000..a1309ad --- /dev/null +++ b/Assets/Scripts/PrizeStand/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/PrizeStand/PrizeStandController.cs.meta b/Assets/Scripts/PrizeStand/PrizeStandController.cs.meta new file mode 100644 index 0000000..c5d929c --- /dev/null +++ b/Assets/Scripts/PrizeStand/PrizeStandController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88aba386759d8af45a9b81d259d438b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: