729 lines
30 KiB
C#
729 lines
30 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|