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: