using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RR.Common.Controls
{
// Зависит от ComHub и обрабатывает контроллеры витрины по ROLE.
public sealed class PrizeStandController : IComControllerHandler
{
public static readonly string[] SupportedRoles = { "prize_stand", "prize_stand_controller", "vitrina" };
///
/// По завершении операции открытия.
/// bool - успех/неуспех.
/// string - сообщение в случае неуспеха
///
public event Action OpeningFinished;
private const string RequestStandStatus = "25";
private const string OpenBoxPrefix = "2501";
private const float StatusRequestTimeout = 3f;
private const float WaitOpenStatusTimeout = 5f;
private static readonly IReadOnlyList ReadyToOpenStatuses =
new[] { BoxStatus.OK, BoxStatus.OKplusCard };
private readonly string _deviceId;
private readonly Func _sendToDevice;
private readonly Action _log;
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly ConcurrentQueue _statusQueue = new();
private int _boxesCount;
public PrizeStandController(
string deviceId,
Func sendToDevice,
Action log,
int boxesCount = 0)
{
_deviceId = deviceId;
_sendToDevice = sendToDevice;
_log = log;
_boxesCount = Math.Max(0, boxesCount);
}
public void SetBoxesCount(int boxesCount)
{
_boxesCount = Math.Max(0, boxesCount);
}
public int GetBoxesCount()
{
return _boxesCount;
}
public async Task> GetStatusesAsync(float timeoutSeconds = StatusRequestTimeout)
{
if (_boxesCount <= 0)
{
return new Dictionary();
}
ClearStatusQueue();
SendCommand(RequestStandStatus);
Dictionary statuses = new();
Stopwatch sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < timeoutSeconds && statuses.Count < _boxesCount)
{
while (_statusQueue.TryDequeue(out BoxStatusEntry entry))
{
statuses[entry.BoxNumber] = entry.Status;
if (statuses.Count >= _boxesCount)
{
break;
}
}
if (statuses.Count >= _boxesCount)
{
break;
}
await Task.Delay(10);
}
if (statuses.Count < _boxesCount)
{
_log?.Invoke("[Модуль витрины. Не дождался всех статусов ячеек]\n" +
$"Получено статусов: {statuses.Count}/{_boxesCount}");
}
return statuses;
}
public async Task GetReadyBoxesAsync(float timeoutSeconds = StatusRequestTimeout)
{
Dictionary allStatuses = await GetStatusesAsync(timeoutSeconds);
return allStatuses
.Where(kvp => ReadyToOpenStatuses.Contains(kvp.Value))
.Select(kvp => kvp.Key)
.OrderBy(boxNumber => boxNumber)
.ToArray();
}
public int[] GetReadyBoxes(int timeoutMs = 1000)
{
try
{
Task task = Task.Run(() => GetReadyBoxesAsync(timeoutMs / 1000f));
return task.Wait(timeoutMs) ? task.Result : Array.Empty();
}
catch (Exception ex)
{
_log?.Invoke($"[Модуль витрины] Ошибка при получении готовых ячеек: {ex.Message}");
return Array.Empty();
}
}
public 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;
}
ClearStatusQueue();
SendCommand($"{OpenBoxPrefix}{boxNumber:D2}");
BoxStatus openStatus = await ReadBoxStatusAsync(boxNumber, 1, WaitOpenStatusTimeout);
if (openStatus == BoxStatus.OPENED)
{
OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта");
}
else
{
_log?.Invoke($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}");
OpeningFinished?.Invoke(false, $"Ошибка при открытии ячейки номер {boxNumber}");
}
}
catch (Exception ex)
{
_log?.Invoke($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}: {ex.Message}");
OpeningFinished?.Invoke(false, ex.Message);
}
finally
{
_lock.Release();
}
}
public void HandleCoinAccepted()
{
// Для витрины монеты не обрабатываются.
}
public void HandleInputReceived(string inputBits)
{
// Для витрины INPUT-пакеты не используются.
}
public void HandleRawLine(string line)
{
var parseResult = ParseBoxStatus(line);
if (parseResult.BoxNumber < 0)
{
return;
}
_statusQueue.Enqueue(new BoxStatusEntry(parseResult.BoxNumber, parseResult.Status));
}
public void Dispose()
{
_lock.Dispose();
ClearStatusQueue();
}
public static bool IsSupportedRole(string role)
{
if (string.IsNullOrWhiteSpace(role))
{
return false;
}
return SupportedRoles.Any(r => string.Equals(r, role, StringComparison.OrdinalIgnoreCase));
}
private async Task<(bool OK, string Message)> CanExecuteOpenAsync(int boxNumber)
{
bool isOk = true;
StringBuilder sb = new();
sb.AppendLine("[Модуль витрины. Проверка перед открытием]");
if (boxNumber <= 0)
{
isOk = false;
sb.AppendLine("- Номер коробки должен быть больше 0.");
}
if (_boxesCount > 0 && boxNumber > _boxesCount)
{
isOk = false;
sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {_boxesCount}.");
}
if (isOk)
{
ClearStatusQueue();
SendCommand(RequestStandStatus);
int expectedEntriesCount = _boxesCount > 0 ? _boxesCount : 1;
BoxStatus boxStatus = await ReadBoxStatusAsync(boxNumber, expectedEntriesCount, StatusRequestTimeout);
if (!ReadyToOpenStatuses.Contains(boxStatus))
{
isOk = false;
sb.AppendLine($"- Статус коробки {boxNumber}: {boxStatus} не позволяет провести открытие.");
}
}
else
{
sb.AppendLine($"- Невозможно проверить статус коробки {boxNumber} из-за ошибок выше.");
}
return (isOk, sb.ToString());
}
private async Task ReadBoxStatusAsync(int expectedBoxNumber, int expectedEntriesCount, float timeout)
{
BoxStatus status = BoxStatus.NotFound;
int boxesReceived = 0;
Stopwatch sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < timeout && boxesReceived < expectedEntriesCount)
{
while (_statusQueue.TryDequeue(out BoxStatusEntry entry))
{
boxesReceived++;
if (entry.BoxNumber == expectedBoxNumber)
{
status = entry.Status;
}
if (boxesReceived >= expectedEntriesCount)
{
break;
}
}
if (boxesReceived >= expectedEntriesCount)
{
break;
}
await Task.Delay(10);
}
if (status == BoxStatus.NotFound)
{
_log?.Invoke("[Модуль витрины. Не дождался ответа по статусу ячейки]\n" +
$"Ожидаемый номер: {expectedBoxNumber}\n" +
$"Получено статусов ячеек: {boxesReceived}/{expectedEntriesCount}");
}
return status;
}
private void SendCommand(string command)
{
if (!_sendToDevice(_deviceId, command))
{
throw new InvalidOperationException($"Не удалось отправить команду '{command}' в устройство {_deviceId}");
}
}
private void ClearStatusQueue()
{
while (_statusQueue.TryDequeue(out _))
{
}
}
private static (int BoxNumber, BoxStatus Status) ParseBoxStatus(string entryString)
{
if (string.IsNullOrWhiteSpace(entryString) || !entryString.StartsWith("BOX", StringComparison.OrdinalIgnoreCase))
{
return (-1, BoxStatus.ParsingError);
}
string noPrefixEntry = entryString.Substring(3);
string[] parts = noPrefixEntry.Split('_');
if (parts.Length != 2)
{
return (-1, BoxStatus.ParsingError);
}
if (!int.TryParse(parts[0], out int parsedNumber))
{
return (-1, BoxStatus.ParsingError);
}
return (parsedNumber, MapStatusFromString(parts[1]));
}
private static BoxStatus MapStatusFromString(string statusString)
{
return 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
};
}
private readonly struct BoxStatusEntry
{
public readonly int BoxNumber;
public readonly BoxStatus Status;
public BoxStatusEntry(int boxNumber, BoxStatus status)
{
BoxNumber = boxNumber;
Status = status;
}
}
}
public enum BoxStatus
{
// Статусы ДО открытия
OK,
OKplusCard,
Empty,
Open,
// Статусы ПОСЛЕ открытия
OPENED,
OPEN_ERROR,
// Служебные статусы
ParsingError,
NotFound,
Error
}
}