添加xcpcore,添加加载dbc至单一通道

This commit is contained in:
“hsc”
2026-10-15 14:05:18 +08:00
parent a2a62a0327
commit 981bc1e558
12 changed files with 983 additions and 225 deletions
@@ -1,30 +1,599 @@
using UIShare.ViewModelBase;
using Prism.Commands;
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TSMasterCAN;
using UIShare.ViewModelBase;
using XCPCore;
namespace UpdateInfoModule.ViewModels
namespace TSMasterXCPModule.ViewModels
{
public class TSMasterXCPViewModel : NavigateViewModelBase
/// <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); }
// ---------- 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>初始化 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)
{
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 命令处理
#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;
}
}
}