2026-06-10 15:54:30 +08:00

348 lines
13 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.IO.Ports;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
using YD_TrainingCompetition_Api.Options;
namespace YD_TrainingCompetition_Api.Serial;
// SerialGateway 封装了 2.4G 网关串口的最底层读写能力。
// 这个类只负责“打开串口 / 关闭串口 / 收字节 / 发字节”,
// 不关心具体协议语义,协议解析由上层 worker 处理。
public sealed class SerialGateway : ISerialGateway
{
private static readonly HttpClient DebugClient = new();
private readonly object _serialLock = new();
private readonly SerialPort _serialPort;
// 构造时按配置初始化串口参数,并注册 DataReceived 事件。
// 事件里只读取原始字节并向上抛出,不在这里做业务判断。
public SerialGateway(IOptions<Serial24GOptions> options)
{
var cfg = options.Value;
_serialPort = new SerialPort
{
PortName = cfg.PortName,
BaudRate = cfg.BaudRate,
DataBits = cfg.DataBits,
StopBits = StopBits.One,
Parity = Parity.None,
ReadBufferSize = cfg.ReadBufferSize,
WriteBufferSize = cfg.WriteBufferSize
};
_serialPort.DataReceived += (_, _) =>
{
try
{
// 一次事件可能读到半帧、整帧或多帧拼在一起的数据,
// 所以这里直接把当前缓冲区可读字节全部取出,交给上层做组帧。
byte[]? buf = null;
var len = 0;
lock (_serialLock)
{
len = _serialPort.BytesToRead;
if (len <= 0)
{
return;
}
buf = new byte[len];
_serialPort.Read(buf, 0, len);
}
#region debug-point A:serial-receive
_ = DebugReportAsync("A", "SerialGateway.DataReceived", "[DEBUG] serial bytes received", new
{
port = _serialPort.PortName,
length = len,
hex = Convert.ToHexString(buf)
});
#endregion
DataReceived?.Invoke(this, buf);
}
catch
{
}
};
}
public event EventHandler<byte[]>? DataReceived;
public bool IsOpen => _serialPort.IsOpen;
public string? CurrentPort => _serialPort.IsOpen ? _serialPort.PortName : null;
// 返回系统当前可见的串口列表,按名字排序后给前端展示。
public string[] GetAvailablePorts() => SerialPort.GetPortNames().OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
public Task OpenAsync(string portName, CancellationToken cancellationToken = default)
{
if (_serialPort.IsOpen)
{
// 切换串口前先关闭旧连接,避免 SerialPort 对象状态残留。
_serialPort.Close();
}
_serialPort.PortName = portName;
_serialPort.Open();
// 对齐旧版 Form1:尽量按整帧触发接收,减少高频碎片事件对控制命令时序的干扰。
_serialPort.ReceivedBytesThreshold = 12;
#region debug-point A:serial-open
_ = DebugReportAsync("A", "SerialGateway.OpenAsync", "[DEBUG] serial opened", new
{
port = _serialPort.PortName,
baudRate = _serialPort.BaudRate,
dataBits = _serialPort.DataBits,
readBufferSize = _serialPort.ReadBufferSize,
writeBufferSize = _serialPort.WriteBufferSize
});
#endregion
return Task.CompletedTask;
}
public Task CloseAsync(CancellationToken cancellationToken = default)
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}
return Task.CompletedTask;
}
// 下载本场前会清空输入缓存,避免把旧应答或残留半帧误当成当前操作结果。
public void ClearInputBuffer()
{
lock (_serialLock)
{
if (_serialPort.IsOpen)
{
_serialPort.DiscardInBuffer();
}
}
}
public Task SendAsync(byte[] data, CancellationToken cancellationToken = default)
{
if (!_serialPort.IsOpen)
{
throw new InvalidOperationException("Serial port is not open");
}
// 这里发送的是已经由上层构造好的完整协议帧。
#region debug-point B:serial-send
_ = DebugReportAsync("B", "SerialGateway.SendAsync", "[DEBUG] serial bytes sent", new
{
port = _serialPort.PortName,
length = data.Length,
hex = Convert.ToHexString(data)
});
#endregion
if (data.Length >= 2 && data[1] == 0x30)
{
#region debug-point A:stop-frame-sent
_ = DebugTimeupProjectionReportAsync("A", "SerialGateway.SendAsync", "[DEBUG] stop frame sent to serial", new
{
port = _serialPort.PortName,
length = data.Length,
hex = Convert.ToHexString(data)
});
#endregion
}
if (data.Length >= 2 && (data[1] == 0x06 || data[1] == 0x10 || data[1] == 0x20 || data[1] == 0x30 || data[1] == 0x0A))
{
#region debug-point E:countdown-control-frame
_ = DebugCountdownStopCounterReportAsync("E", "SerialGateway.SendAsync", "[DEBUG] countdown control frame sent", new
{
port = _serialPort.PortName,
command = data[1],
subCommand = data.Length >= 3 ? data[2] : (byte)0,
data3 = data.Length >= 4 ? data[3] : (byte)0,
data4 = data.Length >= 5 ? data[4] : (byte)0,
length = data.Length,
hex = Convert.ToHexString(data)
});
#endregion
#region debug-point S4:stop-reset-control-frame
_ = DebugStopResetControlReportAsync("C", "SerialGateway.SendAsync", "[DEBUG] stop/reset control frame sent", new
{
port = _serialPort.PortName,
command = data[1],
subCommand = data.Length >= 3 ? data[2] : (byte)0,
data3 = data.Length >= 4 ? data[3] : (byte)0,
data4 = data.Length >= 5 ? data[4] : (byte)0,
length = data.Length,
hex = Convert.ToHexString(data)
});
#endregion
}
lock (_serialLock)
{
_serialPort.Write(data, 0, data.Length);
}
return Task.CompletedTask;
}
// 调试阶段把关键串口事件上报到独立调试服务器,便于对照旧版行为。
// 如果调试服务器不存在,这里会静默失败,不影响正常业务收发。
private static async Task DebugReportAsync(string hypothesisId, string location, string msg, object data)
{
try
{
var url = "http://127.0.0.1:7777/event";
var sessionId = "serial-gateway-download";
var envPath = Path.Combine(Directory.GetCurrentDirectory(), ".dbg", "serial-gateway-download.env");
if (File.Exists(envPath))
{
foreach (var line in await File.ReadAllLinesAsync(envPath))
{
if (line.StartsWith("DEBUG_SERVER_URL=", StringComparison.Ordinal))
{
url = line["DEBUG_SERVER_URL=".Length..].Trim();
}
else if (line.StartsWith("DEBUG_SESSION_ID=", StringComparison.Ordinal))
{
sessionId = line["DEBUG_SESSION_ID=".Length..].Trim();
}
}
}
var payload = JsonSerializer.Serialize(new
{
sessionId,
runId = "pre-fix",
hypothesisId,
location,
msg,
data,
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
await DebugClient.PostAsync(url, content);
}
catch
{
}
}
private static async Task DebugTimeupProjectionReportAsync(string hypothesisId, string location, string msg, object data)
{
try
{
var url = "http://127.0.0.1:7777/event";
var sessionId = "timeup-stop-projection";
var envPath = Path.Combine(Directory.GetCurrentDirectory(), ".dbg", "timeup-stop-projection.env");
if (File.Exists(envPath))
{
foreach (var line in await File.ReadAllLinesAsync(envPath))
{
if (line.StartsWith("DEBUG_SERVER_URL=", StringComparison.Ordinal))
{
url = line["DEBUG_SERVER_URL=".Length..].Trim();
}
else if (line.StartsWith("DEBUG_SESSION_ID=", StringComparison.Ordinal))
{
sessionId = line["DEBUG_SESSION_ID=".Length..].Trim();
}
}
}
var payload = JsonSerializer.Serialize(new
{
sessionId,
runId = "pre-fix",
hypothesisId,
location,
msg,
data,
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
await DebugClient.PostAsync(url, content);
}
catch
{
}
}
private static async Task DebugCountdownStopCounterReportAsync(string hypothesisId, string location, string msg, object data)
{
try
{
var url = "http://127.0.0.1:7777/event";
var sessionId = "countdown-stop-counter";
var envPath = Path.Combine(Directory.GetCurrentDirectory(), ".dbg", "countdown-stop-counter.env");
if (File.Exists(envPath))
{
foreach (var line in await File.ReadAllLinesAsync(envPath))
{
if (line.StartsWith("DEBUG_SERVER_URL=", StringComparison.Ordinal))
{
url = line["DEBUG_SERVER_URL=".Length..].Trim();
}
else if (line.StartsWith("DEBUG_SESSION_ID=", StringComparison.Ordinal))
{
sessionId = line["DEBUG_SESSION_ID=".Length..].Trim();
}
}
}
var payload = JsonSerializer.Serialize(new
{
sessionId,
runId = "pre-fix",
hypothesisId,
location,
msg,
data,
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
await DebugClient.PostAsync(url, content);
}
catch
{
}
}
private static async Task DebugStopResetControlReportAsync(string hypothesisId, string location, string msg, object data)
{
try
{
var url = "http://127.0.0.1:7777/event";
var sessionId = "stop-reset-control";
var envPath = Path.Combine(Directory.GetCurrentDirectory(), ".dbg", "stop-reset-control.env");
if (File.Exists(envPath))
{
foreach (var line in await File.ReadAllLinesAsync(envPath))
{
if (line.StartsWith("DEBUG_SERVER_URL=", StringComparison.Ordinal))
{
url = line["DEBUG_SERVER_URL=".Length..].Trim();
}
else if (line.StartsWith("DEBUG_SESSION_ID=", StringComparison.Ordinal))
{
sessionId = line["DEBUG_SESSION_ID=".Length..].Trim();
}
}
}
var payload = JsonSerializer.Serialize(new
{
sessionId,
runId = "pre-fix",
hypothesisId,
location,
msg,
data,
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
await DebugClient.PostAsync(url, content);
}
catch
{
}
}
}