FEAT: Реализован модуль для работы с призовой витриной

This commit is contained in:
_sega 2026-02-18 14:28:36 +03:00
parent c49f7fa850
commit 71d45078b2
5 changed files with 336 additions and 4 deletions

View File

@ -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}

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3aa03864eb84d2042a0517ae1f43aad7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
{
/// <summary>
/// По завершении инициализации.
/// bool - успех/неуспех.
/// string - сообщение в случае неуспеха
/// </summary>
public static event Action<bool, string> InitializationComplete;
/// <summary>
/// По завершении операции открытия.
/// bool - успех/неуспех.
/// string - сообщение в случае неуспеха
/// </summary>
public static event Action<bool, string> 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<BoxStatus> 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
/// <summary>
/// Инициализировать модуль витрины.
/// </summary>
/// <param name="handleId">Id хэндла полученное после ComNativeWrapper.Serial_Open</param>
/// <param name="countOfBoxes">Количество физических коробок витрины</param>
/// <param name="logger">(опционально) Логгер</param>
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());
}
}
/// <summary>
/// Открыть коробку по номеру
/// </summary>
/// <param name="boxNumber">Номер коробки (считаем с 1)</param>
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);
/// <summary>
/// Проверка может ли модуль произвести выдачу
/// </summary>
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());
}
/// <summary>
/// Читать статус заданной ячейки витрины. Выход когда прочитали заданное количество вхождений, или по таймауту.
/// </summary>
/// <param name="expectedBoxNumber">Номер ячейки которую мы ждем</param>
/// <param name="expectedEntriesCount">Сколько ячеек мы ждем</param>
/// <param name="timeout">Таймаут операции(сек.)</param>
/// <returns></returns>
private static async Task<BoxStatus> 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 //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88aba386759d8af45a9b81d259d438b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: