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(false, "Выдача уже в процессе");
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(false, $"Ошибка при открытии ячейки {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 //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
}