using System.Threading.Channels; using Microsoft.Extensions.Options; using YD_TrainingCompetition_Api.Competition; using YD_TrainingCompetition_Api.Options; using YD_TrainingCompetition_Api.Protocols; namespace YD_TrainingCompetition_Api.Services; public sealed class SerialIngestWorker : BackgroundService { private readonly Serial.ISerialGateway _serialGateway; private readonly IJumpProtocolParser _parser; private readonly IDeviceStateStore _store; private readonly IRealtimeNotifier _notifier; private readonly IParticipantDirectory _directory; private readonly ILogger _logger; private readonly Serial24GOptions _options; private readonly List _buffer = []; private readonly Channel _chunks = Channel.CreateUnbounded(); public SerialIngestWorker( Serial.ISerialGateway serialGateway, IJumpProtocolParser parser, IDeviceStateStore store, IRealtimeNotifier notifier, IParticipantDirectory directory, IOptions options, ILogger logger) { _serialGateway = serialGateway; _parser = parser; _store = store; _notifier = notifier; _directory = directory; _logger = logger; _options = options.Value; _serialGateway.DataReceived += (_, chunk) => _chunks.Writer.TryWrite(chunk); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { await _serialGateway.OpenAsync(_options.PortName, stoppingToken); _logger.LogInformation("2.4G serial opened on {Port}", _options.PortName); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to open serial port {Port}. You can open it later from API.", _options.PortName); } await foreach (var chunk in _chunks.Reader.ReadAllAsync(stoppingToken)) { await ProcessChunkAsync(chunk, stoppingToken); } } private async Task ProcessChunkAsync(byte[] chunk, CancellationToken cancellationToken) { _buffer.AddRange(chunk); while (_buffer.Count >= _options.FrameLength) { var start = _buffer.IndexOf(0xAA); if (start < 0) { _buffer.Clear(); return; } if (_buffer.Count < start + _options.FrameLength) { if (start > 0) { _buffer.RemoveRange(0, start); } return; } var frame = _buffer.Skip(start).Take(_options.FrameLength).ToArray(); _buffer.RemoveRange(0, start + _options.FrameLength); if (!_parser.TryParse(frame, out var parsed) || parsed is null) { continue; } var deviceId = parsed.DeviceNo.ToString(); var roster = _directory.FindByDeviceId(deviceId); var telemetry = new Models.DeviceTelemetry { DeviceId = deviceId, BsName = $"NO-{parsed.DeviceNo}", AthleteName = roster?.Name, School = roster?.School, Counter = parsed.Counter, ErrorCount = null, Battery = parsed.Battery, Timestamp = DateTimeOffset.UtcNow, IngestedAt = DateTimeOffset.UtcNow, GatewayId = "serial-2.4g" }; if (!_store.TryUpsert(telemetry, out _)) { continue; } var snapshot = _store.GetByDeviceId(deviceId); if (snapshot is not null) { await _notifier.BroadcastTelemetryAsync(snapshot, cancellationToken); } } } }