YD_TrainingCompetition_Api/Services/SerialIngestWorker.cs

119 lines
3.8 KiB
C#
Raw Normal View History

2026-06-02 10:30:05 +08:00
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<SerialIngestWorker> _logger;
private readonly Serial24GOptions _options;
private readonly List<byte> _buffer = [];
private readonly Channel<byte[]> _chunks = Channel.CreateUnbounded<byte[]>();
public SerialIngestWorker(
Serial.ISerialGateway serialGateway,
IJumpProtocolParser parser,
IDeviceStateStore store,
IRealtimeNotifier notifier,
IParticipantDirectory directory,
IOptions<Serial24GOptions> options,
ILogger<SerialIngestWorker> 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);
}
}
}
}