diff --git a/ACP/ACP.csproj b/ACP/ACP.csproj
index 8b9356a..b096934 100644
--- a/ACP/ACP.csproj
+++ b/ACP/ACP.csproj
@@ -33,7 +33,7 @@
-
+
diff --git a/ACP/ViewModels/ShellViewModel.cs b/ACP/ViewModels/ShellViewModel.cs
index 00455f3..b736d2d 100644
--- a/ACP/ViewModels/ShellViewModel.cs
+++ b/ACP/ViewModels/ShellViewModel.cs
@@ -906,6 +906,10 @@ namespace ACP.ViewModels
if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "PowerAdaptabilityView");
break;
+ case "TSMaster XCP 测试":
+ if (_globalInfo.CurrentScope != "default") return;
+ _regionManager.RequestNavigate("ShellViewManager", "TSMasterXCPView");
+ break;
}
}
diff --git a/ACP/Views/ShellView.xaml b/ACP/Views/ShellView.xaml
index 736a740..79ad2bd 100644
--- a/ACP/Views/ShellView.xaml
+++ b/ACP/Views/ShellView.xaml
@@ -103,6 +103,11 @@
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" />
+
diff --git a/TSMasterCAN/CAN.cs b/TSMasterCAN/CAN.cs
index 6964fc5..0b31c35 100644
--- a/TSMasterCAN/CAN.cs
+++ b/TSMasterCAN/CAN.cs
@@ -146,7 +146,22 @@ namespace TSMasterCAN
return re;
}
+ ///
+ /// 加载DBC至单一通道
+ ///
+ ///
+ ///
+ ///
+ public static int LoadDBCToSingleChannel(string filePath, int channel)
+ {
+ uint databaseID = 0;
+ if (!Path.Exists(filePath)) return -1;
+ var re = TsMasterApi.tsdb_load_can_db(Path.GetFullPath(filePath), string.Join(",", channel), ref databaseID);
+ DBCParse.parse(databaseID, channel);
+ DBCParse.rbs_parse(databaseID, channel);
+ return re;
+ }
///
/// 卸载DBC
///
@@ -321,6 +336,23 @@ namespace TSMasterCAN
}
}
+ ///
+ /// 按帧 ID 构造并发送 CANFD 报文(不依赖 DBC 数据库与报文名),可选择单次发送或周期发送。
+ ///
+ /// 通道号
+ /// 报文帧 ID
+ /// 报文数据数组(最多 64 字节,超出部分截断)
+ /// 发送周期(毫秒):0 = 只发送一次;>0 = 按该周期循环发送
+ /// 是否扩展帧(29 位 ID),默认标准帧
+ /// 返回代码,0 表示成功,非 0 表示失败(可用 查询错误说明)
+ public static int SendMsgById(APP_CHANNEL channel, int ID, byte[] bytes, float sendPeriod = 0, bool isExt = false)
+ {
+ var data = new byte[64];
+ Array.Copy(bytes, data, Math.Min(bytes.Length, 64));
+ return SendMsg(channel, ID, true, isExt, false, (byte)Math.Min(bytes.Length, 64),
+ data, true, false, false, sendPeriod);
+ }
+
///
/// 构造并发送自定义 CAN/CANFD 报文,可选择单次发送或周期发送,并可同步更新 DBC 数据库中同 ID 报文的数据。
///
diff --git a/TSMasterCAN/TSMasterCAN.csproj b/TSMasterCAN/TSMasterCAN.csproj
index 023256a..0e63c8e 100644
--- a/TSMasterCAN/TSMasterCAN.csproj
+++ b/TSMasterCAN/TSMasterCAN.csproj
@@ -14,7 +14,7 @@
- ..\CanDriver\bin\Debug\net8.0\Interop.TSMasterAPI.dll
+ Interop.TSMasterAPI.dll
diff --git a/TSMasterXCPModule/TSMasterXCPModule.cs b/TSMasterXCPModule/TSMasterXCPModule.cs
index d0b398f..bf449b1 100644
--- a/TSMasterXCPModule/TSMasterXCPModule.cs
+++ b/TSMasterXCPModule/TSMasterXCPModule.cs
@@ -1,14 +1,18 @@
-using System.Reflection;
-using UpdateInfoModule.Views;
+using Prism;
+using Prism.Ioc;
+using TSMasterXCPModule.Views;
-namespace UpdateInfoModule
+namespace TSMasterXCPModule
{
+ ///
+ /// TSMaster XCP 测试模块:基于同星 CAN(TSMaster)+ XCPCore 的 XCP 主站调试界面,
+ /// 支持 XCP 连接、内存读写与 DAQ 采集。
+ /// 通过左侧菜单"TSMaster XCP 测试"导航进入(仅 default 作用域可用)。
+ ///
public class TSMasterXCPModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
- IRegionManager regionManager = containerProvider.Resolve();
- regionManager.RegisterViewWithRegion("ShellViewManager", typeof(TSMasterXCPView));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
diff --git a/TSMasterXCPModule/TSMasterXCPModule.csproj b/TSMasterXCPModule/TSMasterXCPModule.csproj
index 8b229f9..e9c5de3 100644
--- a/TSMasterXCPModule/TSMasterXCPModule.csproj
+++ b/TSMasterXCPModule/TSMasterXCPModule.csproj
@@ -8,8 +8,10 @@
+
+
diff --git a/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs b/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
index 43784cb..70e9a5e 100644
--- a/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
+++ b/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
@@ -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
+ ///
+ /// TSMaster XCP 测试视图模型:总线连接(TSMaster)、XCP 会话(CONNECT/GET_STATUS)、
+ /// 内存读写(SET_MTA/UPLOAD/DOWNLOAD)与 DAQ 采集(FREE_DAQ/ALLOC_DAQ/WRITE_DAQ/START_STOP)。
+ /// 协议核心来自 XCPCore( + )。
+ ///
+ public class TSMasterXCPViewModel : NavigateViewModelBase, IDisposable
{
+ #region 字段
+
+ /// XCP CAN 传输层(XcpConnect 成功后创建)
+ private XcpCanTransport? _transport;
+
+ /// XCP 主站协议核心(XcpConnect 成功后创建)
+ private XcpMaster? _master;
+
+ /// 日志缓冲(防止无限增长)
+ private readonly StringBuilder _logBuffer = new();
+
+ #endregion
+
#region 属性
+ private bool _isBusy;
+ /// 是否正在执行异步操作(驱动 Busy 指示条)
+ public bool IsBusy { get => _isBusy; set { SetProperty(ref _isBusy, value); } }
+
+ private string _responseLog = string.Empty;
+ /// 操作日志(时间戳前缀,只读展示)
+ public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
+
+ // ---------- TSMaster 总线 ----------
+
+ private string _tsMasterProjectPath = string.Empty;
+ /// TSMaster 工程文件路径(可空:空则初始化默认工程)
+ public string TSMasterProjectPath { get => _tsMasterProjectPath; set => SetProperty(ref _tsMasterProjectPath, value); }
+
+ private bool _isBusConnected;
+ /// TSMaster 总线是否已连接(tsapp_connect + rbs_start 成功)
+ public bool IsBusConnected { get => _isBusConnected; set => SetProperty(ref _isBusConnected, value); }
+
+ // ---------- XCP 连接配置 ----------
+
+ private string _canChannel = "0";
+ /// CAN 通道号(0 起始)
+ public string CanChannel { get => _canChannel; set => SetProperty(ref _canChannel, value); }
+
+ private string _croIdText = "0x7E0";
+ /// CRO CAN ID(命令发送,hex)
+ public string CroIdText { get => _croIdText; set => SetProperty(ref _croIdText, value); }
+
+ private string _dtoIdText = "0x7E8";
+ /// DTO/RES CAN ID(响应接收,hex)
+ public string DtoIdText { get => _dtoIdText; set => SetProperty(ref _dtoIdText, value); }
+
+ private bool _isExtendedFrame;
+ /// 是否扩展帧(29 位 ID)
+ public bool IsExtendedFrame { get => _isExtendedFrame; set => SetProperty(ref _isExtendedFrame, value); }
+
+ private bool _isCanFd;
+ /// 是否 CAN FD 帧
+ public bool IsCanFd { get => _isCanFd; set => SetProperty(ref _isCanFd, value); }
+
+ private string _commandTimeoutMs = "1000";
+ /// XCP 命令响应超时(毫秒)
+ public string CommandTimeoutMs { get => _commandTimeoutMs; set => SetProperty(ref _commandTimeoutMs, value); }
+
+ private bool _isConnected;
+ /// XCP 逻辑连接是否建立(CONNECT 成功)
+ public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
+
+ private string _slaveInfoText = "未连接";
+ /// 从站能力信息(CONNECT 响应解析)
+ public string SlaveInfoText { get => _slaveInfoText; set => SetProperty(ref _slaveInfoText, value); }
+
+ // ---------- 内存读写 ----------
+
+ private string _memAddressText = "0x00000000";
+ /// 内存地址(hex)
+ public string MemAddressText { get => _memAddressText; set => SetProperty(ref _memAddressText, value); }
+
+ private string _memReadLength = "8";
+ /// 读取长度(字节)
+ public string MemReadLength { get => _memReadLength; set => SetProperty(ref _memReadLength, value); }
+
+ private string _memWriteDataText = string.Empty;
+ /// 写入数据(hex 字节序列)
+ public string MemWriteDataText { get => _memWriteDataText; set => SetProperty(ref _memWriteDataText, value); }
+
+ private string _readResultText = string.Empty;
+ /// 最近一次读取结果(hex)
+ public string ReadResultText { get => _readResultText; set => SetProperty(ref _readResultText, value); }
+
+ // ---------- DAQ 采集 ----------
+
+ private string _daqListNumber = "0";
+ /// DAQ 列表编号
+ public string DaqListNumber { get => _daqListNumber; set => SetProperty(ref _daqListNumber, value); }
+
+ private string _eventChannel = "0";
+ /// DAQ 事件通道编号
+ public string EventChannel { get => _eventChannel; set => SetProperty(ref _eventChannel, value); }
+
+ private string _prescaler = "1";
+ /// DAQ 预分频(每 N 个事件采集一次)
+ public string Prescaler { get => _prescaler; set => SetProperty(ref _prescaler, value); }
+
+ private string _odtAddressText = "0x00000000";
+ /// ODT 条目地址(hex,信号在从站内存中的地址)
+ public string OdtAddressText { get => _odtAddressText; set => SetProperty(ref _odtAddressText, value); }
+
+ private string _odtLength = "4";
+ /// ODT 条目长度(字节)
+ public string OdtLength { get => _odtLength; set => SetProperty(ref _odtLength, value); }
+
+ private string _latestDtoText = string.Empty;
+ /// 最近收到的 DTO 帧(hex)
+ public string LatestDtoText { get => _latestDtoText; set => SetProperty(ref _latestDtoText, value); }
+
+ private int _dtoFrameCount;
+ /// 已接收 DTO 帧计数
+ public int DtoFrameCount { get => _dtoFrameCount; set => SetProperty(ref _dtoFrameCount, value); }
#endregion
+
#region 命令
-
+ /// 初始化 TSMaster 驱动(可选加载工程)
+ public DelegateCommand InitDriverCommand { get; }
+ /// 释放 TSMaster 驱动
+ public DelegateCommand ReleaseDriverCommand { get; }
+ /// 连接 TSMaster CAN 总线
+ public DelegateCommand ConnectBusCommand { get; }
+ /// 断开 TSMaster CAN 总线
+ public DelegateCommand DisconnectBusCommand { get; }
+ /// 建立 XCP 逻辑连接(CONNECT)
+ public DelegateCommand XcpConnectCommand { get; }
+ /// 断开 XCP 逻辑连接(DISCONNECT + 传输层关闭)
+ public DelegateCommand XcpDisconnectCommand { get; }
+ /// 查询从站状态(GET_STATUS)
+ public DelegateCommand GetStatusCommand { get; }
+ /// 读取内存(SET_MTA + UPLOAD)
+ public DelegateCommand ReadMemoryCommand { get; }
+ /// 写入内存(SET_MTA + DOWNLOAD)
+ public DelegateCommand WriteMemoryCommand { get; }
+ /// 配置 DAQ 列表(FREE_DAQ → ALLOC_DAQ → SET_DAQ_PTR/WRITE_DAQ → SET_DAQ_LIST_MODE)
+ public DelegateCommand ConfigureDaqCommand { get; }
+ /// 启动 DAQ 采集
+ public DelegateCommand StartDaqCommand { get; }
+ /// 停止 DAQ 采集
+ public DelegateCommand StopDaqCommand { get; }
+ /// 接收一帧 DTO 数据
+ public DelegateCommand ReceiveDtoCommand { get; }
+ /// 清空日志
+ public DelegateCommand ClearLogCommand { get; }
#endregion
-
+
+ /// 创建视图模型
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 通用辅助
+
+ /// 统一命令入口:Busy 指示 + 异常捕获 + 日志
+ private async void RunGuarded(Func 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;
+ }
+ }
+
+ /// 确认 XCP 主站已创建(界面校验)
+ private bool RequireMaster()
+ {
+ if (_master == null || _master.State == XcpSessionState.Disconnected)
+ {
+ Log("[错误] XCP 未连接,请先执行「XCP 连接」。");
+ return false;
+ }
+ return true;
+ }
+
+ /// 解析 CAN 通道号(0 起始)
+ 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;
+ }
+
+ /// 解析 hex 整数:支持 0x 前缀 / 纯 hex(如 7E0 / 0x7E0)
+ 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;
+ }
+
+ /// 解析 hex 字节序列:FF 00 1A / FF,00,1A / 0xFF-0x00 均可
+ 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;
+ }
+
+ /// 字节数组转 hex 字符串(大写、空格分隔)
+ 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();
+ }
+
+ /// 安全解析整数(失败用默认值)
+ private static int ParseInt(string? text, int defaultValue)
+ => int.TryParse(text?.Trim(), out int value) ? value : defaultValue;
+
+ /// 追加一行带时间戳的日志(超 200 行自动截断头部)
+ 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
+
+ /// 释放资源:断开 XCP 连接并关闭传输层
+ public void Dispose()
+ {
+ try
+ {
+ if (_master != null)
+ _master.FullDisconnectAsync().Wait(2000);
+ else
+ _transport?.Close();
+ }
+ catch { }
+ _master = null;
+ _transport = null;
+ }
}
}
diff --git a/TSMasterXCPModule/Views/TSMasterXCPView.xaml b/TSMasterXCPModule/Views/TSMasterXCPView.xaml
index 0f634ba..5140dc8 100644
--- a/TSMasterXCPModule/Views/TSMasterXCPView.xaml
+++ b/TSMasterXCPModule/Views/TSMasterXCPView.xaml
@@ -1,4 +1,4 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TSMasterXCPModule/Views/TSMasterXCPView.xaml.cs b/TSMasterXCPModule/Views/TSMasterXCPView.xaml.cs
index 9681bdd..117a489 100644
--- a/TSMasterXCPModule/Views/TSMasterXCPView.xaml.cs
+++ b/TSMasterXCPModule/Views/TSMasterXCPView.xaml.cs
@@ -13,7 +13,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
-namespace UpdateInfoModule.Views
+namespace TSMasterXCPModule.Views
{
///
/// TSMasterXCPView.xaml 的交互逻辑
diff --git a/XCPCore/XCPCore/XCPCore.csproj b/XCPCore/XCPCore/XCPCore.csproj
index 9eac25d..40d33c2 100644
--- a/XCPCore/XCPCore/XCPCore.csproj
+++ b/XCPCore/XCPCore/XCPCore.csproj
@@ -10,9 +10,13 @@
+
+
+
+
- ..\TOSUNCAN\Interop.TSMasterAPI.dll
+ ..\..\TSMasterCAN\Interop.TSMasterAPI.dll
diff --git a/XCPCore/XCPCore/XcpCanTransport.cs b/XCPCore/XCPCore/XcpCanTransport.cs
index 73c0759..45b404f 100644
--- a/XCPCore/XCPCore/XcpCanTransport.cs
+++ b/XCPCore/XCPCore/XcpCanTransport.cs
@@ -1,34 +1,21 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using TSMaster;
using TSMasterCAN;
-using ZLGCANAPI;
-using ZLG_USB_CAN;
namespace XCPCore
{
///
- /// CAN 传输层后端类型
- ///
- public enum CanBackendType
- {
- /// 同星 TSCAN
- TSCAN,
- /// 周立功 ZLGCAN
- ZLGCAN
- }
-
- ///
- /// XCP over CAN 传输层实现。
- /// 支持同星 CAN(TSMaster)和周立功 CAN(ZLGCAN)两种后端。
+ /// XCP over CAN 传输层实现(同星 TSMaster 后端)。
+ /// 命令发送复用 (TSMaster 通道报文 API),
+ /// 接收走 TSMaster 接收 FIFO(tsfifo_receive_canfd_msgs_list / tsfifo_receive_can_msgs_list),
+ /// 由后台轮询任务按 DTO ID 过滤后推入队列。
///
public class XcpCanTransport : IXcpTransport
{
- private readonly CanBackendType _backend;
private readonly byte _channel;
private readonly uint _croId;
private readonly uint _dtoId;
@@ -51,23 +38,18 @@ namespace XCPCore
/// DTO ID(接收响应的 CAN ID)
public uint DtoId => _dtoId;
- /// CAN 后端类型
- public CanBackendType Backend => _backend;
-
///
- /// 创建 CAN XCP 传输层实例
+ /// 创建 CAN XCP 传输层实例(同星 TSMaster 后端)
///
- /// CAN 后端(TSCAN / ZLGCAN)
/// CAN 通道号
/// CRO CAN ID(发送命令)
/// DTO CAN ID(接收响应)
/// 是否使用扩展帧(29位 ID)
/// 是否使用 CAN FD 帧
/// 默认超时时间(毫秒)
- public XcpCanTransport(CanBackendType backend, byte channel,
+ public XcpCanTransport(byte channel,
uint croId, uint dtoId, bool isExtended = false, bool isCanFd = false, int timeoutMs = 3000)
{
- _backend = backend;
_channel = channel;
_croId = croId;
_dtoId = dtoId;
@@ -77,21 +59,25 @@ namespace XCPCore
}
///
- /// 打开传输层(启动 DTO 接收轮询任务)
+ /// 打开传输层(启用 TSMaster 接收 FIFO 并启动 DTO 接收轮询任务)
///
public Task OpenAsync(CancellationToken ct = default)
{
if (IsOpen) return Task.FromResult(true);
+ // 启用 TSMaster 接收 FIFO(全局开关,幂等)并清空本通道残留帧
+ TsMasterApi.tsfifo_enable_receive_fifo();
+ ClearFifoBuffers();
+
_receiveCts = new CancellationTokenSource();
_receiveTask = Task.Run(() => ReceiveLoop(_receiveCts.Token), _receiveCts.Token);
IsOpen = true;
- Debug.WriteLine($"XCP CAN 传输层已打开: Backend={_backend}, CH={_channel}, CRO=0x{_croId:X}, DTO=0x{_dtoId:X}");
+ Debug.WriteLine($"XCP CAN 传输层已打开: CH={_channel}, CRO=0x{_croId:X}, DTO=0x{_dtoId:X}, FD={_isCanFd}");
return Task.FromResult(true);
}
///
- /// 发送 CTO 帧(构造 CAN 帧并发送)
+ /// 发送 CTO 帧(构造 CAN 帧并经 CAN.SendMsg 发送)
///
public Task SendCtoAsync(byte[] data, CancellationToken ct = default)
{
@@ -103,30 +89,22 @@ namespace XCPCore
byte[] sendData = new byte[maxLen];
Array.Copy(data, sendData, sendLen);
- if (_backend == CanBackendType.TSCAN)
- {
- // 同星 CAN: 使用 CAN.SendMsg 发送
- int result = CAN.SendMsg(
- (APP_CHANNEL)_channel,
- (int)_croId,
- true, // AIsTx
- _isExtended, // AIsExt
- false, // AIsRemote
- (byte)sendLen, // ADLC
- sendData,
- _isCanFd, // AIsFD
- false, // AIsBRS
- false, // isUpdateDBCDatabase
- 0 // sendPeriod (单次发送)
- );
+ int result = CAN.SendMsg(
+ (APP_CHANNEL)_channel,
+ (int)_croId,
+ true, // AIsTx
+ _isExtended, // AIsExt
+ false, // AIsRemote
+ (byte)sendLen, // ADLC
+ sendData,
+ _isCanFd, // AIsFD
+ false, // AIsBRS
+ false, // isUpdateDBCDatabase
+ 0 // sendPeriod (单次发送)
+ );
- if (result != 0)
- throw new InvalidOperationException($"XCP CAN 发送失败: TSCAN 返回值={result}");
- }
- else // ZLGCAN
- {
- SendViaZlgCan(sendData, sendLen);
- }
+ if (result != 0)
+ throw new InvalidOperationException($"XCP CAN 发送失败: TSCAN 返回值={result} ({CAN.GetErrorDescription(result)})");
return Task.CompletedTask;
}
@@ -176,6 +154,7 @@ namespace XCPCore
// 清空队列
while (_responseQueue.TryDequeue(out _)) { }
+ ClearFifoBuffers();
IsOpen = false;
Debug.WriteLine("XCP CAN 传输层已关闭");
@@ -186,7 +165,18 @@ namespace XCPCore
// ============================================================
///
- /// 后台接收轮询任务:持续从 CAN 总线读取报文,按 DTO ID 过滤后推入队列
+ /// 清空 TSMaster 本通道接收 FIFO 残留帧
+ ///
+ private void ClearFifoBuffers()
+ {
+ if (_isCanFd)
+ TsMasterApi.tsfifo_clear_canfd_receive_buffers(_channel);
+ else
+ TsMasterApi.tsfifo_clear_can_receive_buffers(_channel);
+ }
+
+ ///
+ /// 后台接收轮询任务:从 TSMaster 接收 FIFO 读取报文,按 DTO ID 过滤后推入队列
///
private async Task ReceiveLoop(CancellationToken ct)
{
@@ -194,14 +184,10 @@ namespace XCPCore
{
try
{
- if (_backend == CanBackendType.TSCAN)
- {
- ReceiveFromTsCan();
- }
+ if (_isCanFd)
+ ReceiveFdFromFifo();
else
- {
- ReceiveFromZlgCan();
- }
+ ReceiveClassicFromFifo();
// 轮询间隔:1ms 足以满足大多数 XCP 场景
await Task.Delay(1, ct).ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
@@ -219,171 +205,59 @@ namespace XCPCore
}
///
- /// 从同星 CAN 接收队列中读取 DTO 响应帧
+ /// 从 TSMaster 接收 FIFO 读取 CAN FD 帧并按 DTO ID 过滤
///
- private void ReceiveFromTsCan()
+ private void ReceiveFdFromFifo()
{
- while (CAN.TryGetReceivedMessage(out var msg))
+ int count = 0;
+ if (TsMasterApi.tsfifo_read_canfd_rx_buffer_frame_count(_channel, ref count) != 0 || count <= 0)
+ return;
+
+ var buffers = new TLIBCANFD[count];
+ int size = count;
+ if (TsMasterApi.tsfifo_receive_canfd_msgs_list(ref buffers, ref size, _channel, false) != 0)
+ return;
+
+ for (int i = 0; i < size; i++)
{
- // 按 DTO ID 过滤
- if ((uint)msg.FIdentifier == _dtoId && msg.FIdxChn == _channel)
- {
- int len = Math.Min((int)msg.FDLC, msg.FData.Length);
- var payload = new byte[len];
- Array.Copy(msg.FData, payload, len);
- _responseQueue.Enqueue(payload);
- _responseSignal.Release();
- }
+ EnqueueIfDto(buffers[i].FIdxChn, (uint)buffers[i].FIdentifier, buffers[i].FDLC, buffers[i].FData);
}
}
///
- /// 从周立功 CAN 接收缓冲区中读取 DTO 响应帧
+ /// 从 TSMaster 接收 FIFO 读取经典 CAN 帧并按 DTO ID 过滤
///
- private void ReceiveFromZlgCan()
+ private void ReceiveClassicFromFifo()
{
- IntPtr curChn = ZLG_USBCANFD.chnHandle[ZLG_USBCANFD.channel_index];
- if (curChn == IntPtr.Zero) return;
+ int count = 0;
+ if (TsMasterApi.tsfifo_read_can_rx_buffer_frame_count(_channel, ref count) != 0 || count <= 0)
+ return;
- if (_isCanFd)
+ var buffers = new TLIBCAN[count];
+ int size = count;
+ if (TsMasterApi.tsfifo_receive_can_msgs_list(ref buffers, ref size, _channel, false) != 0)
+ return;
+
+ for (int i = 0; i < size; i++)
{
- uint recNum = ZLGCAN.ZCAN_GetReceiveNum(curChn, 1);
- if (recNum == 0) return;
-
- int fdSize = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_ReceiveFD_Data));
- IntPtr ptrData = Marshal.AllocHGlobal(fdSize * (int)Math.Min(recNum, 100));
- try
- {
- uint count = ZLGCAN.ZCAN_ReceiveFD(curChn, ptrData, Math.Min(recNum, 100), 0);
- for (int i = 0; i < count; i++)
- {
- IntPtr curPtr = IntPtr.Add(ptrData, i * fdSize);
- var recvData = (ZLGCAN.ZCAN_ReceiveFD_Data)Marshal.PtrToStructure(
- curPtr, typeof(ZLGCAN.ZCAN_ReceiveFD_Data))!;
-
- if (recvData.frame.can_id == _dtoId)
- {
- int len = Math.Min((int)recvData.frame.len, 64);
- var payload = new byte[len];
- Array.Copy(recvData.frame.data, payload, len);
- _responseQueue.Enqueue(payload);
- _responseSignal.Release();
- }
- }
- }
- finally
- {
- Marshal.FreeHGlobal(ptrData);
- }
- }
- else
- {
- uint recNum = ZLGCAN.ZCAN_GetReceiveNum(curChn, 0);
- if (recNum == 0) return;
-
- int canSize = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_Receive_Data));
- IntPtr ptrData = Marshal.AllocHGlobal(canSize * (int)Math.Min(recNum, 100));
- try
- {
- uint count = ZLGCAN.ZCAN_Receive(curChn, ptrData, Math.Min(recNum, 100), 0);
- for (int i = 0; i < count; i++)
- {
- IntPtr curPtr = IntPtr.Add(ptrData, i * canSize);
- var recvData = (ZLGCAN.ZCAN_Receive_Data)Marshal.PtrToStructure(
- curPtr, typeof(ZLGCAN.ZCAN_Receive_Data))!;
-
- if (recvData.frame.can_id == _dtoId)
- {
- int len = Math.Min((int)recvData.frame.can_dlc, 8);
- var payload = new byte[len];
- Array.Copy(recvData.frame.data, payload, len);
- _responseQueue.Enqueue(payload);
- _responseSignal.Release();
- }
- }
- }
- finally
- {
- Marshal.FreeHGlobal(ptrData);
- }
+ EnqueueIfDto(buffers[i].FIdxChn, (uint)buffers[i].FIdentifier, buffers[i].FDLC, buffers[i].FData);
}
}
///
- /// 通过周立功 CAN 发送帧
+ /// 校验帧(通道 + DTO ID 匹配)后截取有效数据入队并释放等待信号
///
- private void SendViaZlgCan(byte[] data, int dataLen)
+ private void EnqueueIfDto(byte frameChannel, uint frameId, byte dlc, byte[] data)
{
- IntPtr curChn = ZLG_USBCANFD.chnHandle[ZLG_USBCANFD.channel_index];
- if (curChn == IntPtr.Zero)
- throw new InvalidOperationException("ZLGCAN 通道句柄无效");
+ if (frameChannel != _channel || frameId != _dtoId)
+ return;
- if (_isCanFd)
- {
- var frame = new ZLGCAN.canfd_frame
- {
- can_id = _croId,
- len = (byte)dataLen,
- flags = 0,
- __res0 = 0,
- __res1 = 0,
- data = new byte[64]
- };
- Array.Copy(data, frame.data, dataLen);
-
- var txData = new ZLGCAN.ZCAN_TransmitFD_Data
- {
- frame = frame,
- transmit_type = 0 // 正常发送
- };
-
- int size = Marshal.SizeOf(txData);
- IntPtr ptr = Marshal.AllocHGlobal(size);
- try
- {
- Marshal.StructureToPtr(txData, ptr, true);
- uint ret = ZLGCAN.ZCAN_TransmitFD(curChn, ptr, 1);
- if (ret != 1)
- throw new InvalidOperationException($"XCP CAN FD 发送失败: ZLGCAN 返回值={ret}");
- }
- finally
- {
- Marshal.FreeHGlobal(ptr);
- }
- }
- else
- {
- var frame = new ZLGCAN.can_frame
- {
- can_id = _croId,
- can_dlc = (byte)dataLen,
- __pad = 0,
- __res0 = 0,
- __res1 = 0,
- data = new byte[8]
- };
- Array.Copy(data, frame.data, Math.Min(dataLen, 8));
-
- var txData = new ZLGCAN.ZCAN_Transmit_Data
- {
- frame = frame,
- transmit_type = 0
- };
-
- int size = Marshal.SizeOf(txData);
- IntPtr ptr = Marshal.AllocHGlobal(size);
- try
- {
- Marshal.StructureToPtr(txData, ptr, true);
- uint ret = ZLGCAN.ZCAN_Transmit(curChn, ptr, 1);
- if (ret != 1)
- throw new InvalidOperationException($"XCP CAN 发送失败: ZLGCAN 返回值={ret}");
- }
- finally
- {
- Marshal.FreeHGlobal(ptr);
- }
- }
+ int len = Math.Min((int)dlc, data?.Length ?? 0);
+ var payload = new byte[len];
+ if (len > 0)
+ Array.Copy(data, payload, len);
+ _responseQueue.Enqueue(payload);
+ _responseSignal.Release();
}
}
}