using System; using System.Threading; using System.Threading.Tasks; using Aging.Infrastructure.Contracts; namespace Aging.Infrastructure.Services { /// /// CAN 帧数据(跨项目通用类型,用于类型化 API 返回值) /// public sealed class CanFrameData { /// 帧 ID(十进制值) public uint FrameId { get; set; } /// 帧 ID(十六进制字符串,如 0x100) public string FrameIdHex => FrameId > 0x7FF ? $"0x{FrameId:X8}" : $"0x{FrameId:X3}"; /// 数据字节 public byte[] Data { get; set; } = Array.Empty(); /// 数据的十六进制字符串表示 public string DataHex => BitConverter.ToString(Data).Replace("-", " "); /// DLC(数据长度) public int Dlc => Data.Length; /// 关联的时间戳 public DateTime Timestamp { get; set; } = DateTime.Now; public override string ToString() => $"{FrameIdHex} [{Dlc}] {DataHex}"; } /// /// CAN 通讯类型化服务。 /// 隐藏底层命令字符串,业务代码只面对帧 ID/数据数组。 /// /// 支持两种调用模式: /// 1. 独立调用:直接传 deviceId,内部走完整生命周期(一次申请) /// 2. 借用租约:外部传入 lease,同一 lease 内多次调用(Per-Step) /// public interface ICanBusService { // ============ 模式 1:独立调用(自己申请租约) ============ /// 发送 CAN 帧(独立调用) Task SendFrameAsync(string deviceId, uint frameId, byte[] data, CancellationToken ct = default); /// 接收一帧(独立调用);无数据时返回 null Task ReceiveFrameAsync(string deviceId, CancellationToken ct = default); /// 启用/禁用自动响应(独立调用) Task SetAutoResponseAsync(string deviceId, bool enabled, CancellationToken ct = default); /// 获取节点状态(独立调用) Task GetStatusAsync(string deviceId, CancellationToken ct = default); // ============ 模式 2:借用租约(同一 lease 内多次调用) ============ /// 发送 CAN 帧(借用外部租约) Task SendFrameAsync(IAutoDeviceLease lease, uint frameId, byte[] data, CancellationToken ct = default); /// 接收一帧(借用外部租约) Task ReceiveFrameAsync(IAutoDeviceLease lease, CancellationToken ct = default); /// 启用/禁用自动响应(借用外部租约) Task SetAutoResponseAsync(IAutoDeviceLease lease, bool enabled, CancellationToken ct = default); /// 获取节点状态(借用外部租约) Task GetStatusAsync(IAutoDeviceLease lease, CancellationToken ct = default); /// 查询设备状态(无副作用) Task PeekBusLogAsync(string deviceId, int maxEntries = 20, CancellationToken ct = default); } /// /// CAN 类型化服务实现。 /// 通过 IAutoDeviceFacade 下发实际命令;只做参数校验和字符串拼装。 /// public sealed class CanBusService : ICanBusService { private readonly IAutoDeviceFacade _auto; public CanBusService(IAutoDeviceFacade autoFacade) { _auto = autoFacade ?? throw new ArgumentNullException(nameof(autoFacade)); } // ============ 模式 1:独立调用 ============ public async Task SendFrameAsync(string deviceId, uint frameId, byte[] data, CancellationToken ct = default) { ValidateFrame(frameId, data); var result = await _auto.ExecuteAsync(deviceId, BuildSendCmd(frameId, data), ct).ConfigureAwait(false); return result.Success; } public async Task ReceiveFrameAsync(string deviceId, CancellationToken ct = default) { var result = await _auto.QueryAsync(deviceId, "RECV", ct).ConfigureAwait(false); return ParseRecv(result); } public async Task SetAutoResponseAsync(string deviceId, bool enabled, CancellationToken ct = default) { var cmd = enabled ? "AUTO_RESPONSE:ON" : "AUTO_RESPONSE:OFF"; var result = await _auto.QueryAsync(deviceId, cmd, ct).ConfigureAwait(false); return result.Success; } public Task GetStatusAsync(string deviceId, CancellationToken ct = default) => _auto.QueryAsync(deviceId, "STATUS", ct).ContinueWith( t => t.Result.Readback ?? t.Result.RawResponse ?? "N/A", ct); // ============ 模式 2:借用租约 ============ public async Task SendFrameAsync(IAutoDeviceLease lease, uint frameId, byte[] data, CancellationToken ct = default) { if (lease == null) throw new ArgumentNullException(nameof(lease)); ValidateFrame(frameId, data); var result = await lease.ExecuteAsync(BuildSendCmd(frameId, data), ct).ConfigureAwait(false); return result.Success; } public async Task ReceiveFrameAsync(IAutoDeviceLease lease, CancellationToken ct = default) { if (lease == null) throw new ArgumentNullException(nameof(lease)); var result = await lease.QueryAsync("RECV", ct).ConfigureAwait(false); return ParseRecv(result); } public async Task SetAutoResponseAsync(IAutoDeviceLease lease, bool enabled, CancellationToken ct = default) { if (lease == null) throw new ArgumentNullException(nameof(lease)); var cmd = enabled ? "AUTO_RESPONSE:ON" : "AUTO_RESPONSE:OFF"; var result = await lease.QueryAsync(cmd, ct).ConfigureAwait(false); return result.Success; } public async Task GetStatusAsync(IAutoDeviceLease lease, CancellationToken ct = default) { if (lease == null) throw new ArgumentNullException(nameof(lease)); var result = await lease.QueryAsync("STATUS", ct).ConfigureAwait(false); return result.Readback ?? result.RawResponse ?? "N/A"; } public async Task PeekBusLogAsync(string deviceId, int maxEntries = 20, CancellationToken ct = default) { var result = await _auto.QueryAsync(deviceId, "LOG", ct).ConfigureAwait(false); return result.Readback ?? result.RawResponse ?? "EMPTY"; } // ============ 内部辅助 ============ private static string BuildSendCmd(uint frameId, byte[] data) { var hex = BitConverter.ToString(data).Replace("-", " "); return $"SEND:0x{frameId:X3}:{hex}"; } private static void ValidateFrame(uint frameId, byte[] data) { if (frameId > 0x1FFFFFFF) throw new ArgumentException($"CAN 帧 ID 超出范围(最大 0x1FFFFFFF): 0x{frameId:X8}"); if (data == null) throw new ArgumentNullException(nameof(data)); if (data.Length > 8) throw new ArgumentException($"CAN 数据长度超限(最大 8 字节): {data.Length}"); } private static CanFrameData? ParseRecv(DeviceCommandResult result) { var raw = result.Readback ?? result.RawResponse; if (string.IsNullOrWhiteSpace(raw)) return null; // 格式:RECV:frameIdHex:dataHex:HH:mm:ss.fff var parts = raw.Split(':'); if (parts.Length < 3) return null; if (parts[1].Equals("EMPTY", StringComparison.OrdinalIgnoreCase)) return null; try { var frameHex = parts[1].StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? parts[1].Substring(2) : parts[1]; var frameId = Convert.ToUInt32(frameHex, 16); var dataHex = parts[2]; var data = ParseHexBytes(dataHex); DateTime.TryParse(parts.Length >= 4 ? parts[3] : null, out var ts); return new CanFrameData { FrameId = frameId, Data = data, Timestamp = ts == default ? DateTime.Now : ts }; } catch { return null; } } private static byte[] ParseHexBytes(string hex) { if (string.IsNullOrWhiteSpace(hex)) return Array.Empty(); var tokens = hex.Split(new[] { ' ', '-' }, StringSplitOptions.RemoveEmptyEntries); var bytes = new byte[tokens.Length]; for (int i = 0; i < tokens.Length; i++) { bytes[i] = Convert.ToByte(tokens[i], 16); } return bytes; } } }