chip-gameV3/Assets/Scripts/Vitrina/Hardware/PrizeStandController.cs

458 lines
18 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
}