wenxiangchao
4 天以前 559890c482f5973f06aafdfec9550fee5ba3c7f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
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 通讯类型化服务。
    /// 隐藏底层命令字符串,业务代码只面对帧 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;
            // 格式: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<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;
        }
    }
}