Files
ACP/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
T
2026-10-26 10:46:44 +08:00

729 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Prism.Commands;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32;
using TSMasterCAN;
using UIShare.ViewModelBase;
using XCPCore;
namespace TSMasterXCPModule.ViewModels
{
/// <summary>
/// TSMaster XCP 测试视图模型:总线连接(TSMaster)、XCP 会话(CONNECT/GET_STATUS)、
/// 内存读写(SET_MTA/UPLOAD/DOWNLOAD)与 DAQ 采集(FREE_DAQ/ALLOC_DAQ/WRITE_DAQ/START_STOP)。
/// 协议核心来自 XCPCore(<see cref="XcpMaster"/> + <see cref="XcpCanTransport"/>)。
/// </summary>
public class TSMasterXCPViewModel : NavigateViewModelBase, IDisposable
{
#region 字段
/// <summary>XCP CAN 传输层(XcpConnect 成功后创建)</summary>
private XcpCanTransport? _transport;
/// <summary>XCP 主站协议核心(XcpConnect 成功后创建)</summary>
private XcpMaster? _master;
/// <summary>日志缓冲(防止无限增长)</summary>
private readonly StringBuilder _logBuffer = new();
#endregion
#region 属性
private bool _isBusy;
/// <summary>是否正在执行异步操作(驱动 Busy 指示条)</summary>
public bool IsBusy { get => _isBusy; set { SetProperty(ref _isBusy, value); } }
private string _responseLog = string.Empty;
/// <summary>操作日志(时间戳前缀,只读展示)</summary>
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
// ---------- TSMaster 总线 ----------
private string _tsMasterProjectPath = string.Empty;
/// <summary>TSMaster 工程文件路径(可空:空则初始化默认工程)</summary>
public string TSMasterProjectPath { get => _tsMasterProjectPath; set => SetProperty(ref _tsMasterProjectPath, value); }
private bool _isBusConnected;
/// <summary>TSMaster 总线是否已连接(tsapp_connect + rbs_start 成功)</summary>
public bool IsBusConnected { get => _isBusConnected; set => SetProperty(ref _isBusConnected, value); }
// ---------- A2L 描述文件 ----------
private string _a2lFilePath = string.Empty;
/// <summary>A2L 文件路径</summary>
public string A2lFilePath { get => _a2lFilePath; set => SetProperty(ref _a2lFilePath, value); }
private string _a2lSummaryText = "未加载 A2L 文件";
/// <summary>A2L 解析结果摘要</summary>
public string A2lSummaryText { get => _a2lSummaryText; set => SetProperty(ref _a2lSummaryText, value); }
/// <summary>解析出的测量量列表(选中后自动填入 DAQ 地址与长度)</summary>
public ObservableCollection<A2lMeasurement> A2lMeasurements { get; } = new ObservableCollection<A2lMeasurement>();
/// <summary>解析出的标定量列表(选中后自动填入内存读写地址与长度)</summary>
public ObservableCollection<A2lCharacteristic> A2lCharacteristics { get; } = new ObservableCollection<A2lCharacteristic>();
private A2lMeasurement? _selectedMeasurement;
/// <summary>当前选中的测量量:自动带入 DAQ 的 ODT 地址与长度</summary>
public A2lMeasurement? SelectedMeasurement
{
get => _selectedMeasurement;
set
{
if (!SetProperty(ref _selectedMeasurement, value) || value == null) return;
OdtAddressText = value.AddressText;
if (value.SizeBytes > 0) OdtLength = value.SizeBytes.ToString();
Log($"A2L 测量量 {value.Name}:地址 {value.AddressText},长度 {value.SizeText}(已填入 DAQ 配置)");
}
}
private A2lCharacteristic? _selectedCharacteristic;
/// <summary>当前选中的标定量:自动带入内存读写的地址与长度</summary>
public A2lCharacteristic? SelectedCharacteristic
{
get => _selectedCharacteristic;
set
{
if (!SetProperty(ref _selectedCharacteristic, value) || value == null) return;
MemAddressText = value.AddressText;
if (value.SizeBytes > 0 && value.SizeBytes <= 200) MemReadLength = value.SizeBytes.ToString();
Log($"A2L 标定量 {value.Name}({value.CharacteristicType}):地址 {value.AddressText},长度 {value.SizeText}(已填入内存读写)");
}
}
// ---------- XCP 连接配置 ----------
private string _canChannel = "0";
/// <summary>CAN 通道号(0 起始)</summary>
public string CanChannel { get => _canChannel; set => SetProperty(ref _canChannel, value); }
private string _croIdText = "0x7E0";
/// <summary>CRO CAN ID(命令发送,hex)</summary>
public string CroIdText { get => _croIdText; set => SetProperty(ref _croIdText, value); }
private string _dtoIdText = "0x7E8";
/// <summary>DTO/RES CAN ID(响应接收,hex)</summary>
public string DtoIdText { get => _dtoIdText; set => SetProperty(ref _dtoIdText, value); }
private bool _isExtendedFrame;
/// <summary>是否扩展帧(29 位 ID)</summary>
public bool IsExtendedFrame { get => _isExtendedFrame; set => SetProperty(ref _isExtendedFrame, value); }
private bool _isCanFd;
/// <summary>是否 CAN FD 帧</summary>
public bool IsCanFd { get => _isCanFd; set => SetProperty(ref _isCanFd, value); }
private string _commandTimeoutMs = "1000";
/// <summary>XCP 命令响应超时(毫秒)</summary>
public string CommandTimeoutMs { get => _commandTimeoutMs; set => SetProperty(ref _commandTimeoutMs, value); }
private bool _isConnected;
/// <summary>XCP 逻辑连接是否建立(CONNECT 成功)</summary>
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
private string _slaveInfoText = "未连接";
/// <summary>从站能力信息(CONNECT 响应解析)</summary>
public string SlaveInfoText { get => _slaveInfoText; set => SetProperty(ref _slaveInfoText, value); }
// ---------- 内存读写 ----------
private string _memAddressText = "0x00000000";
/// <summary>内存地址(hex)</summary>
public string MemAddressText { get => _memAddressText; set => SetProperty(ref _memAddressText, value); }
private string _memReadLength = "8";
/// <summary>读取长度(字节)</summary>
public string MemReadLength { get => _memReadLength; set => SetProperty(ref _memReadLength, value); }
private string _memWriteDataText = string.Empty;
/// <summary>写入数据(hex 字节序列)</summary>
public string MemWriteDataText { get => _memWriteDataText; set => SetProperty(ref _memWriteDataText, value); }
private string _readResultText = string.Empty;
/// <summary>最近一次读取结果(hex)</summary>
public string ReadResultText { get => _readResultText; set => SetProperty(ref _readResultText, value); }
// ---------- DAQ 采集 ----------
private string _daqListNumber = "0";
/// <summary>DAQ 列表编号</summary>
public string DaqListNumber { get => _daqListNumber; set => SetProperty(ref _daqListNumber, value); }
private string _eventChannel = "0";
/// <summary>DAQ 事件通道编号</summary>
public string EventChannel { get => _eventChannel; set => SetProperty(ref _eventChannel, value); }
private string _prescaler = "1";
/// <summary>DAQ 预分频(每 N 个事件采集一次)</summary>
public string Prescaler { get => _prescaler; set => SetProperty(ref _prescaler, value); }
private string _odtAddressText = "0x00000000";
/// <summary>ODT 条目地址(hex,信号在从站内存中的地址)</summary>
public string OdtAddressText { get => _odtAddressText; set => SetProperty(ref _odtAddressText, value); }
private string _odtLength = "4";
/// <summary>ODT 条目长度(字节)</summary>
public string OdtLength { get => _odtLength; set => SetProperty(ref _odtLength, value); }
private string _latestDtoText = string.Empty;
/// <summary>最近收到的 DTO 帧(hex)</summary>
public string LatestDtoText { get => _latestDtoText; set => SetProperty(ref _latestDtoText, value); }
private int _dtoFrameCount;
/// <summary>已接收 DTO 帧计数</summary>
public int DtoFrameCount { get => _dtoFrameCount; set => SetProperty(ref _dtoFrameCount, value); }
#endregion
#region 命令
/// <summary>浏览并加载 A2L 描述文件</summary>
public DelegateCommand BrowseA2lCommand { get; }
/// <summary>按当前路径重新解析 A2L 描述文件</summary>
public DelegateCommand ReloadA2lCommand { get; }
/// <summary>初始化 TSMaster 驱动(可选加载工程)</summary>
public DelegateCommand InitDriverCommand { get; }
/// <summary>释放 TSMaster 驱动</summary>
public DelegateCommand ReleaseDriverCommand { get; }
/// <summary>连接 TSMaster CAN 总线</summary>
public DelegateCommand ConnectBusCommand { get; }
/// <summary>断开 TSMaster CAN 总线</summary>
public DelegateCommand DisconnectBusCommand { get; }
/// <summary>建立 XCP 逻辑连接(CONNECT)</summary>
public DelegateCommand XcpConnectCommand { get; }
/// <summary>断开 XCP 逻辑连接(DISCONNECT + 传输层关闭)</summary>
public DelegateCommand XcpDisconnectCommand { get; }
/// <summary>查询从站状态(GET_STATUS)</summary>
public DelegateCommand GetStatusCommand { get; }
/// <summary>读取内存(SET_MTA + UPLOAD)</summary>
public DelegateCommand ReadMemoryCommand { get; }
/// <summary>写入内存(SET_MTA + DOWNLOAD)</summary>
public DelegateCommand WriteMemoryCommand { get; }
/// <summary>配置 DAQ 列表(FREE_DAQ → ALLOC_DAQ → SET_DAQ_PTR/WRITE_DAQ → SET_DAQ_LIST_MODE)</summary>
public DelegateCommand ConfigureDaqCommand { get; }
/// <summary>启动 DAQ 采集</summary>
public DelegateCommand StartDaqCommand { get; }
/// <summary>停止 DAQ 采集</summary>
public DelegateCommand StopDaqCommand { get; }
/// <summary>接收一帧 DTO 数据</summary>
public DelegateCommand ReceiveDtoCommand { get; }
/// <summary>清空日志</summary>
public DelegateCommand ClearLogCommand { get; }
#endregion
/// <summary>创建视图模型</summary>
public TSMasterXCPViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
BrowseA2lCommand = new DelegateCommand(BrowseA2l);
ReloadA2lCommand = new DelegateCommand(ReloadA2l);
InitDriverCommand = new DelegateCommand(() => RunGuarded(InitDriverAsync, "初始化驱动"));
ReleaseDriverCommand = new DelegateCommand(() => RunGuarded(ReleaseDriverAsync, "释放驱动"));
ConnectBusCommand = new DelegateCommand(() => RunGuarded(ConnectBusAsync, "连接总线"));
DisconnectBusCommand = new DelegateCommand(() => RunGuarded(DisconnectBusAsync, "断开总线"));
XcpConnectCommand = new DelegateCommand(() => RunGuarded(XcpConnectAsync, "XCP 连接"));
XcpDisconnectCommand = new DelegateCommand(() => RunGuarded(XcpDisconnectAsync, "XCP 断开"));
GetStatusCommand = new DelegateCommand(() => RunGuarded(GetStatusAsync, "查询状态"));
ReadMemoryCommand = new DelegateCommand(() => RunGuarded(ReadMemoryAsync, "读取内存"));
WriteMemoryCommand = new DelegateCommand(() => RunGuarded(WriteMemoryAsync, "写入内存"));
ConfigureDaqCommand = new DelegateCommand(() => RunGuarded(ConfigureDaqAsync, "配置 DAQ"));
StartDaqCommand = new DelegateCommand(() => RunGuarded(StartDaqAsync, "启动 DAQ"));
StopDaqCommand = new DelegateCommand(() => RunGuarded(StopDaqAsync, "停止 DAQ"));
ReceiveDtoCommand = new DelegateCommand(() => RunGuarded(ReceiveDtoAsync, "接收 DTO"));
ClearLogCommand = new DelegateCommand(() =>
{
_logBuffer.Clear();
ResponseLog = string.Empty;
});
Log("TSMaster XCP 测试界面就绪。使用顺序:初始化驱动 → 连接总线 → XCP 连接 → 内存读写 / DAQ。");
}
#region 命令处理 —— A2L 描述文件
/// <summary>浏览选择 A2L 文件并解析</summary>
private void BrowseA2l()
{
var dialog = new OpenFileDialog
{
Title = "选择 A2L 描述文件",
Filter = "A2L 描述文件 (*.a2l)|*.a2l|所有文件 (*.*)|*.*",
CheckFileExists = true,
Multiselect = false
};
var currentDir = Path.GetDirectoryName(A2lFilePath);
if (!string.IsNullOrEmpty(currentDir) && Directory.Exists(currentDir))
dialog.InitialDirectory = currentDir;
if (dialog.ShowDialog() != true) return;
LoadA2l(dialog.FileName);
}
/// <summary>按当前路径重新解析 A2L</summary>
private void ReloadA2l()
{
var path = A2lFilePath?.Trim() ?? string.Empty;
if (path.Length == 0)
{
Log("[提示] 请先选择 A2L 文件(点击「浏览...」)。");
return;
}
LoadA2l(path);
}
/// <summary>解析并刷新测量量 / 标定量列表</summary>
private void LoadA2l(string filePath)
{
if (!File.Exists(filePath))
{
A2lSummaryText = "A2L 文件不存在";
Log($"[错误] A2L 文件不存在: {filePath}");
return;
}
try
{
var db = A2lParser.LoadFile(filePath);
A2lFilePath = filePath;
A2lMeasurements.Clear();
foreach (var item in db.Measurements) A2lMeasurements.Add(item);
A2lCharacteristics.Clear();
foreach (var item in db.Characteristics) A2lCharacteristics.Add(item);
SelectedMeasurement = null;
SelectedCharacteristic = null;
A2lSummaryText = $"{db.Summary} · {Path.GetFileName(filePath)}";
Log($"已加载 A2L:{filePath}({db.Summary})");
if (db.Measurements.Count == 0 && db.Characteristics.Count == 0)
Log("[警告] 未解析出可用条目,请确认该文件是 A2L(ASAM MCD-2 MC)描述文件。");
}
catch (Exception ex)
{
A2lMeasurements.Clear();
A2lCharacteristics.Clear();
A2lSummaryText = "A2L 解析失败";
Log($"[错误] 解析 A2L 失败:{ex.Message}");
}
}
#endregion
#region 命令处理 —— TSMaster 总线
private async Task InitDriverAsync()
{
var project = TSMasterProjectPath?.Trim() ?? string.Empty;
int re;
if (string.IsNullOrEmpty(project))
{
re = CAN.Init("TSMasterXCP");
Log("初始化 TSMaster 驱动(默认工程)...");
}
else
{
if (!File.Exists(project))
{
Log($"[错误] TSMaster 工程文件不存在: {project}");
return;
}
re = CAN.Init("TSMasterXCP", project);
Log($"初始化 TSMaster 驱动(工程: {project})...");
}
if (re != 0)
{
Log($"[错误] 驱动初始化失败,返回码 {re}:{CAN.GetErrorDescription(re)}");
return;
}
Log("TSMaster 驱动初始化成功。");
await Task.CompletedTask;
}
private async Task ReleaseDriverAsync()
{
if (_master != null)
await XcpDisconnectAsync();
CAN.Release();
IsBusConnected = false;
Log("TSMaster 驱动已释放。");
await Task.CompletedTask;
}
private async Task ConnectBusAsync()
{
if (!CAN.ConnectFlag)
{
Log("连接 TSMaster CAN 总线...");
int re = CAN.Connect();
if (re != 0)
{
Log($"[错误] 总线连接失败,返回码 {re}:{CAN.GetErrorDescription(re)}(请检查硬件通道映射)");
return;
}
}
IsBusConnected = true;
Log("TSMaster CAN 总线已连接。");
await Task.CompletedTask;
}
private async Task DisconnectBusAsync()
{
if (_master != null)
await XcpDisconnectAsync();
int re = CAN.DisConnect();
IsBusConnected = false;
Log(re == 0 ? "TSMaster CAN 总线已断开。" : $"[警告] 总线断开返回码 {re}:{CAN.GetErrorDescription(re)}");
await Task.CompletedTask;
}
#endregion
#region 命令处理 —— XCP 会话
private async Task XcpConnectAsync()
{
if (!CAN.ConnectFlag)
{
Log("[错误] TSMaster 总线未连接,请先执行「初始化驱动 → 连接总线」。");
return;
}
if (_master != null && _master.State != XcpSessionState.Disconnected)
{
Log("XCP 已处于连接状态,如需重连请先断开。");
return;
}
if (!TryGetChannel(out byte channel)) return;
if (!TryGetHexUint(CroIdText, "CRO ID", out uint croId)) return;
if (!TryGetHexUint(DtoIdText, "DTO ID", out uint dtoId)) return;
int timeoutMs = ParseInt(CommandTimeoutMs, 1000);
// 每次连接按当前界面参数重建传输层与主站
_transport = new XcpCanTransport(channel, croId, dtoId, IsExtendedFrame, IsCanFd, timeoutMs);
_master = new XcpMaster(_transport, new XcpConfig { CommandTimeoutMs = timeoutMs });
Log($"XCP CONNECT: CH={channel}, CRO=0x{croId:X}, DTO=0x{dtoId:X}, {(IsCanFd ? "CAN FD" : "经典 CAN")}...");
bool ok = await _master.ConnectAsync(0x00);
if (!ok)
{
Log("[错误] XCP CONNECT 失败(无有效响应,请检查从站上电、ID 配置与接线)。");
_master = null;
_transport?.Close();
_transport = null;
IsConnected = false;
return;
}
IsConnected = true;
var info = _master.SlaveInfo;
SlaveInfoText = $"MaxCto={info.MaxCtoSize} MaxDto={info.MaxDtoSize} 资源=0x{info.Resource:X2}"
+ $" 协议版本={info.ProtocolVersion} 传输层版本={info.TransportLayerVersion}";
Log($"XCP CONNECT 成功。{SlaveInfoText}");
}
private async Task XcpDisconnectAsync()
{
if (_master != null)
{
await _master.FullDisconnectAsync();
Log("XCP 已断开(DISCONNECT + 传输层关闭)。");
}
_master = null;
_transport = null;
IsConnected = false;
SlaveInfoText = "未连接";
}
private async Task GetStatusAsync()
{
if (!RequireMaster()) return;
var status = await _master!.GetStatusAsync();
if (status == null)
{
Log("[错误] GET_STATUS 无响应。");
return;
}
// 正响应: [FF][SESSION_STATUS][PROTECTION_STATUS][SEQ][DAQ_COUNT(2)][EVENT_COUNT(2)]
var sb = new StringBuilder("GET_STATUS: ");
sb.Append($"会话状态=0x{status[1]:X2}");
if (status.Length > 2) sb.Append($", 保护状态=0x{status[2]:X2}");
if (status.Length > 4) sb.Append($", 配置的 DAQ 列表数={(status[4] << 8) | status[5]}");
if (status.Length > 6) sb.Append($", 事件通道数={(status[6] << 8) | status[7]}");
Log(sb.ToString());
}
#endregion
#region 命令处理 —— 内存读写
private async Task ReadMemoryAsync()
{
if (!RequireMaster()) return;
if (!TryGetHexUint(MemAddressText, "地址", out uint address)) return;
int length = ParseInt(MemReadLength, 8);
if (length <= 0 || length > 200)
{
Log("[错误] 读取长度须在 1~200 字节之间。");
return;
}
var data = await _master!.ReadMemoryAsync(address, length);
if (data == null)
{
Log($"[错误] 读取内存 0x{address:X8} 失败。");
return;
}
ReadResultText = ToHexString(data);
Log($"读取 0x{address:X8} × {data.Length} 字节: {ReadResultText}");
}
private async Task WriteMemoryAsync()
{
if (!RequireMaster()) return;
if (!TryGetHexUint(MemAddressText, "地址", out uint address)) return;
byte[] data;
try
{
data = ParseHexBytes(MemWriteDataText);
}
catch (FormatException)
{
Log("[错误] 写入数据格式无效,请输入 hex 字节序列(如 FF 00 1A)。");
return;
}
if (data.Length == 0)
{
Log("[错误] 写入数据为空。");
return;
}
bool ok = await _master!.WriteMemoryAsync(address, data);
Log(ok
? $"写入 0x{address:X8} × {data.Length} 字节成功: {ToHexString(data)}"
: $"[错误] 写入内存 0x{address:X8} 失败。");
}
#endregion
#region 命令处理 —— DAQ 采集
private async Task ConfigureDaqAsync()
{
if (!RequireMaster()) return;
ushort daqList = (ushort)Math.Clamp(ParseInt(DaqListNumber, 0), 0, 0xFFFF);
ushort eventChannel = (ushort)Math.Clamp(ParseInt(EventChannel, 0), 0, 0xFFFF);
ushort prescaler = (ushort)Math.Clamp(ParseInt(Prescaler, 1), 1, 0xFFFF);
if (!TryGetHexUint(OdtAddressText, "ODT 地址", out uint address)) return;
int odtLen = ParseInt(OdtLength, 4);
if (odtLen <= 0 || odtLen > 64)
{
Log("[错误] ODT 条目长度须在 1~64 字节之间。");
return;
}
var config = new DaqListConfig
{
DaqListNumber = daqList,
EventChannel = eventChannel,
Prescaler = prescaler,
Mode = 0x00,
Enable = true,
OdEntries = { new OdEntry { Address = address, AddressExtension = 0, Length = (byte)odtLen, Name = $"ODT0_{address:X8}" } }
};
bool ok = await _master!.ConfigureDaqListAsync(config);
Log(ok
? $"DAQ 配置成功:列表 {daqList},事件通道 {eventChannel},预分频 {prescaler},条目 0x{address:X8} × {odtLen}B。"
: "[错误] DAQ 配置失败(FREE_DAQ/ALLOC_DAQ/WRITE_DAQ/SET_DAQ_LIST_MODE 某一步被从站拒绝)。");
}
private async Task StartDaqAsync()
{
if (!RequireMaster()) return;
ushort daqList = (ushort)Math.Clamp(ParseInt(DaqListNumber, 0), 0, 0xFFFF);
bool ok = await _master!.StartStopDaqAsync(true, daqList);
Log(ok ? $"DAQ 列表 {daqList} 已启动采集。" : $"[错误] 启动 DAQ 列表 {daqList} 失败。");
}
private async Task StopDaqAsync()
{
if (!RequireMaster()) return;
ushort daqList = (ushort)Math.Clamp(ParseInt(DaqListNumber, 0), 0, 0xFFFF);
bool ok = await _master!.StartStopDaqAsync(false, daqList);
Log(ok ? $"DAQ 列表 {daqList} 已停止。" : $"[错误] 停止 DAQ 列表 {daqList} 失败。");
}
private async Task ReceiveDtoAsync()
{
if (!RequireMaster()) return;
var dto = await _master!.ReceiveDtoAsync(500);
if (dto == null)
{
Log("500ms 内未收到 DTO 帧(确认 DAQ 是否已启动、DTO ID 是否正确)。");
return;
}
DtoFrameCount++;
LatestDtoText = ToHexString(dto);
Log($"DTO #{DtoFrameCount}: {LatestDtoText}");
}
#endregion
#region 通用辅助
/// <summary>统一命令入口:Busy 指示 + 异常捕获 + 日志</summary>
private async void RunGuarded(Func<Task> action, string actionName)
{
if (IsBusy) return;
IsBusy = true;
try
{
await action();
}
catch (TimeoutException tex)
{
Log($"[错误] {actionName} 超时:{tex.Message}");
}
catch (XcpProtocolException xex)
{
Log($"[错误] {actionName} 协议错误:{xex.Message}");
}
catch (InvalidOperationException ioex)
{
Log($"[错误] {actionName}:{ioex.Message}");
}
catch (Exception ex)
{
Log($"[错误] {actionName} 异常:{ex.Message}");
}
finally
{
IsBusy = false;
}
}
/// <summary>确认 XCP 主站已创建(界面校验)</summary>
private bool RequireMaster()
{
if (_master == null || _master.State == XcpSessionState.Disconnected)
{
Log("[错误] XCP 未连接,请先执行「XCP 连接」。");
return false;
}
return true;
}
/// <summary>解析 CAN 通道号(0 起始)</summary>
private bool TryGetChannel(out byte channel)
{
channel = 0;
if (!int.TryParse(CanChannel?.Trim(), out int value) || value < 0 || value > 255)
{
Log($"[错误] CAN 通道号无效: {CanChannel}(0~255)。");
return false;
}
channel = (byte)value;
return true;
}
/// <summary>解析 hex 整数:支持 0x 前缀 / 纯 hex(如 7E0 / 0x7E0)</summary>
private bool TryGetHexUint(string text, string fieldName, out uint value)
{
value = 0;
var cleaned = (text ?? string.Empty).Trim().Replace(" ", "");
if (cleaned.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
cleaned = cleaned[2..];
if (cleaned.Length == 0 || !uint.TryParse(cleaned,
System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture, out value))
{
Log($"[错误] {fieldName} 格式无效: {text}(应为 hex,如 0x7E0)。");
return false;
}
return true;
}
/// <summary>解析 hex 字节序列:FF 00 1A / FF,00,1A / 0xFF-0x00 均可</summary>
private static byte[] ParseHexBytes(string text)
{
var tokens = (text ?? string.Empty).Split(new[] { ' ', ',', '-', ';' }, StringSplitOptions.RemoveEmptyEntries);
var result = new byte[tokens.Length];
for (int i = 0; i < tokens.Length; i++)
{
var token = tokens[i];
if (token.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
token = token[2..];
result[i] = byte.Parse(token, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture);
}
return result;
}
/// <summary>字节数组转 hex 字符串(大写、空格分隔)</summary>
private static string ToHexString(byte[] data)
{
if (data == null || data.Length == 0) return string.Empty;
var sb = new StringBuilder(data.Length * 3);
foreach (var b in data)
sb.Append(b.ToString("X2")).Append(' ');
return sb.ToString().TrimEnd();
}
/// <summary>安全解析整数(失败用默认值)</summary>
private static int ParseInt(string? text, int defaultValue)
=> int.TryParse(text?.Trim(), out int value) ? value : defaultValue;
/// <summary>追加一行带时间戳的日志(超 200 行自动截断头部)</summary>
private void Log(string message)
{
var line = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
_logBuffer.AppendLine(line);
var text = _logBuffer.ToString();
var lines = text.Split('\n');
if (lines.Length > 200)
{
_logBuffer.Clear();
_logBuffer.Append(string.Join("\n", lines[^200..]));
}
ResponseLog = _logBuffer.ToString().TrimEnd();
}
#endregion
/// <summary>释放资源:断开 XCP 连接并关闭传输层</summary>
public void Dispose()
{
try
{
if (_master != null)
_master.FullDisconnectAsync().Wait(2000);
else
_transport?.Close();
}
catch { }
_master = null;
_transport = null;
}
}
}