458 lines
18 KiB
C#
458 lines
18 KiB
C#
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;
|
||
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
|
||
/// <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)
|
||
{
|
||
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());
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Открыть коробку по номеру
|
||
/// </summary>
|
||
/// <param name="boxNumber">Номер коробки (считаем с 1)</param>
|
||
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);
|
||
|
||
/// <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("- Модуль не инициализирован.");
|
||
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());
|
||
}
|
||
|
||
|
||
/// <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) //ошибка
|
||
{
|
||
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<Dictionary<int, BoxStatus>> GetStatusesAsync()
|
||
{
|
||
byte[] buffer = new byte[1024];
|
||
Dictionary<int, BoxStatus> keyValuePairs = new Dictionary<int, BoxStatus>();
|
||
|
||
//очистка пол сек
|
||
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
|
||
|
||
////ТЕСТОВОК
|
||
|
||
/// <summary>
|
||
/// Получить номера ячеек, готовых к открытию (статус OK или OKplusCard)
|
||
/// </summary>
|
||
/// <returns>Массив номеров готовых к выдаче ячеек</returns>
|
||
public static async Task<int[]> GetReadyBoxesAsync()
|
||
{
|
||
try
|
||
{
|
||
// Получаем статусы всех ячеек
|
||
Dictionary<int, BoxStatus> 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<int>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить номера ячеек, готовых к открытию (синхронная обертка с таймаутом)
|
||
/// </summary>
|
||
/// <param name="timeoutMs">Таймаут ожидания в миллисекундах</param>
|
||
/// <returns>Массив номеров готовых к выдаче ячеек</returns>
|
||
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<int>();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log($"[Модуль витрины] Ошибка при получении списка готовых ячеек: {ex.Message}");
|
||
return Array.Empty<int>();
|
||
}
|
||
}
|
||
|
||
private static void Log(string message) => UnityEngine.Debug.Log(message);
|
||
}
|
||
|
||
public enum BoxStatus
|
||
{
|
||
//Статусы ДО открытия
|
||
OK, //Готов к выдаче
|
||
OKplusCard, //Готов к выдаче + готов выдать по карте
|
||
Empty, //Пусто, уже выдали
|
||
Open, //Дверка УЖЕ открыта. Приз предположительно пустой
|
||
|
||
//Статусы ПОСЛЕ открытия
|
||
OPENED, //Всё хорошо, открылся
|
||
OPEN_ERROR, //Ошибка открытия
|
||
|
||
//Служебные статусы
|
||
ParsingError, //При парсинге что то пошло не так, или пришла непредусмотренная фигня
|
||
NotFound, //Коробка с требуемым номером не найдена
|
||
Error //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
|
||
}
|