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" }; /// /// По завершении операции открытия. /// 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) { _deviceId = deviceId; _sendToDevice = sendToDevice; _log = log; } public void SetBoxesCount(int boxesCount) { _boxesCount = Math.Max(0, boxesCount); } 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 } }