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