From 559890c482f5973f06aafdfec9550fee5ba3c7f2 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 09 十一月 2026 17:02:15 +0800
Subject: [PATCH] feat: CAN 模拟器 TCP 化 + 多设备并发调试 + 后台生命周期管理
---
Aging.Infrastructure/Services/CanBusService.cs | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 209 insertions(+), 0 deletions(-)
diff --git a/Aging.Infrastructure/Services/CanBusService.cs b/Aging.Infrastructure/Services/CanBusService.cs
new file mode 100644
index 0000000..735b66d
--- /dev/null
+++ b/Aging.Infrastructure/Services/CanBusService.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Contracts;
+
+namespace Aging.Infrastructure.Services
+{
+ /// <summary>
+ /// CAN 甯ф暟鎹紙璺ㄩ」鐩�氱敤绫诲瀷锛岀敤浜庣被鍨嬪寲 API 杩斿洖鍊硷級
+ /// </summary>
+ public sealed class CanFrameData
+ {
+ /// <summary>甯� ID锛堝崄杩涘埗鍊硷級</summary>
+ public uint FrameId { get; set; }
+
+ /// <summary>甯� ID锛堝崄鍏繘鍒跺瓧绗︿覆锛屽 0x100锛�</summary>
+ public string FrameIdHex => FrameId > 0x7FF ? $"0x{FrameId:X8}" : $"0x{FrameId:X3}";
+
+ /// <summary>鏁版嵁瀛楄妭</summary>
+ public byte[] Data { get; set; } = Array.Empty<byte>();
+
+ /// <summary>鏁版嵁鐨勫崄鍏繘鍒跺瓧绗︿覆琛ㄧず</summary>
+ public string DataHex => BitConverter.ToString(Data).Replace("-", " ");
+
+ /// <summary>DLC锛堟暟鎹暱搴︼級</summary>
+ public int Dlc => Data.Length;
+
+ /// <summary>鍏宠仈鐨勬椂闂存埑</summary>
+ public DateTime Timestamp { get; set; } = DateTime.Now;
+
+ public override string ToString() => $"{FrameIdHex} [{Dlc}] {DataHex}";
+ }
+
+ /// <summary>
+ /// CAN 閫氳绫诲瀷鍖栨湇鍔°��
+ /// 闅愯棌搴曞眰鍛戒护瀛楃涓诧紝涓氬姟浠g爜鍙潰瀵瑰抚 ID/鏁版嵁鏁扮粍銆�
+ ///
+ /// 鏀寔涓ょ璋冪敤妯″紡锛�
+ /// 1. 鐙珛璋冪敤锛氱洿鎺ヤ紶 deviceId锛屽唴閮ㄨ蛋瀹屾暣鐢熷懡鍛ㄦ湡锛堜竴娆$敵璇凤級
+ /// 2. 鍊熺敤绉熺害锛氬閮ㄤ紶鍏� lease锛屽悓涓� lease 鍐呭娆¤皟鐢紙Per-Step锛�
+ /// </summary>
+ public interface ICanBusService
+ {
+ // ============ 妯″紡 1锛氱嫭绔嬭皟鐢紙鑷繁鐢宠绉熺害锛� ============
+
+ /// <summary>鍙戦�� CAN 甯э紙鐙珛璋冪敤锛�</summary>
+ Task<bool> SendFrameAsync(string deviceId, uint frameId, byte[] data, CancellationToken ct = default);
+
+ /// <summary>鎺ユ敹涓�甯э紙鐙珛璋冪敤锛夛紱鏃犳暟鎹椂杩斿洖 null</summary>
+ Task<CanFrameData?> ReceiveFrameAsync(string deviceId, CancellationToken ct = default);
+
+ /// <summary>鍚敤/绂佺敤鑷姩鍝嶅簲锛堢嫭绔嬭皟鐢級</summary>
+ Task<bool> SetAutoResponseAsync(string deviceId, bool enabled, CancellationToken ct = default);
+
+ /// <summary>鑾峰彇鑺傜偣鐘舵�侊紙鐙珛璋冪敤锛�</summary>
+ Task<string> GetStatusAsync(string deviceId, CancellationToken ct = default);
+
+ // ============ 妯″紡 2锛氬�熺敤绉熺害锛堝悓涓� lease 鍐呭娆¤皟鐢級 ============
+
+ /// <summary>鍙戦�� CAN 甯э紙鍊熺敤澶栭儴绉熺害锛�</summary>
+ Task<bool> SendFrameAsync(IAutoDeviceLease lease, uint frameId, byte[] data, CancellationToken ct = default);
+
+ /// <summary>鎺ユ敹涓�甯э紙鍊熺敤澶栭儴绉熺害锛�</summary>
+ Task<CanFrameData?> ReceiveFrameAsync(IAutoDeviceLease lease, CancellationToken ct = default);
+
+ /// <summary>鍚敤/绂佺敤鑷姩鍝嶅簲锛堝�熺敤澶栭儴绉熺害锛�</summary>
+ Task<bool> SetAutoResponseAsync(IAutoDeviceLease lease, bool enabled, CancellationToken ct = default);
+
+ /// <summary>鑾峰彇鑺傜偣鐘舵�侊紙鍊熺敤澶栭儴绉熺害锛�</summary>
+ Task<string> GetStatusAsync(IAutoDeviceLease lease, CancellationToken ct = default);
+
+ /// <summary>鏌ヨ璁惧鐘舵�侊紙鏃犲壇浣滅敤锛�</summary>
+ Task<string> PeekBusLogAsync(string deviceId, int maxEntries = 20, CancellationToken ct = default);
+ }
+
+ /// <summary>
+ /// CAN 绫诲瀷鍖栨湇鍔″疄鐜般��
+ /// 閫氳繃 IAutoDeviceFacade 涓嬪彂瀹為檯鍛戒护锛涘彧鍋氬弬鏁版牎楠屽拰瀛楃涓叉嫾瑁呫��
+ /// </summary>
+ public sealed class CanBusService : ICanBusService
+ {
+ private readonly IAutoDeviceFacade _auto;
+
+ public CanBusService(IAutoDeviceFacade autoFacade)
+ {
+ _auto = autoFacade ?? throw new ArgumentNullException(nameof(autoFacade));
+ }
+
+ // ============ 妯″紡 1锛氱嫭绔嬭皟鐢� ============
+
+ public async Task<bool> 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<CanFrameData?> ReceiveFrameAsync(string deviceId, CancellationToken ct = default)
+ {
+ var result = await _auto.QueryAsync(deviceId, "RECV", ct).ConfigureAwait(false);
+ return ParseRecv(result);
+ }
+
+ public async Task<bool> 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<string> 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<bool> 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<CanFrameData?> 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<bool> 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<string> 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<string> 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;
+ // 鏍煎紡锛歊ECV: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<byte>();
+ 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;
+ }
+ }
+}
--
Gitblit v1.9.1