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;
|
}
|
}
|
}
|