添加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 -1
View File
@@ -33,7 +33,7 @@
<ProjectReference Include="..\SettingModule\SettingModule.csproj" /> <ProjectReference Include="..\SettingModule\SettingModule.csproj" />
<ProjectReference Include="..\TestingModule\TestingModule.csproj" /> <ProjectReference Include="..\TestingModule\TestingModule.csproj" />
<ProjectReference Include="..\TSMasterCAN\TSMasterCAN.csproj" /> <ProjectReference Include="..\TSMasterCAN\TSMasterCAN.csproj" />
<ProjectReference Include="..\TSMatserXCPModule\TSMasterXCPModule.csproj" /> <ProjectReference Include="..\TSMasterXCPModule\TSMasterXCPModule.csproj" />
<ProjectReference Include="..\UIShare\UIShare.csproj" /> <ProjectReference Include="..\UIShare\UIShare.csproj" />
<ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" /> <ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" />
</ItemGroup> </ItemGroup>
+4
View File
@@ -906,6 +906,10 @@ namespace ACP.ViewModels
if (_globalInfo.CurrentScope != "default") return; if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "PowerAdaptabilityView"); _regionManager.RequestNavigate("ShellViewManager", "PowerAdaptabilityView");
break; break;
case "TSMaster XCP 测试":
if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "TSMasterXCPView");
break;
} }
} }
+5
View File
@@ -103,6 +103,11 @@
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}" Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" /> Margin="8" />
<Button Content="TSMaster XCP 测试"
Command="{Binding NavigateCommand}"
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" />
</StackPanel> </StackPanel>
</materialDesign:DrawerHost.LeftDrawerContent> </materialDesign:DrawerHost.LeftDrawerContent>
<Grid> <Grid>
+32
View File
@@ -146,7 +146,22 @@ namespace TSMasterCAN
return re; return re;
} }
/// <summary>
/// 加载DBC至单一通道
/// </summary>
/// <param name="filePath"></param>
/// <param name="channel"></param>
/// <returns></returns>
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;
}
/// <summary> /// <summary>
/// 卸载DBC /// 卸载DBC
/// </summary> /// </summary>
@@ -321,6 +336,23 @@ namespace TSMasterCAN
} }
} }
/// <summary>
/// 按帧 ID 构造并发送 CANFD 报文(不依赖 DBC 数据库与报文名),可选择单次发送或周期发送。
/// </summary>
/// <param name="channel">通道号</param>
/// <param name="ID">报文帧 ID</param>
/// <param name="bytes">报文数据数组(最多 64 字节,超出部分截断)</param>
/// <param name="sendPeriod">发送周期(毫秒):0 = 只发送一次;&gt;0 = 按该周期循环发送</param>
/// <param name="isExt">是否扩展帧(29 位 ID),默认标准帧</param>
/// <returns>返回代码,0 表示成功,非 0 表示失败(可用 <see cref="GetErrorDescription"/> 查询错误说明)</returns>
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);
}
/// <summary> /// <summary>
/// 构造并发送自定义 CAN/CANFD 报文,可选择单次发送或周期发送,并可同步更新 DBC 数据库中同 ID 报文的数据。 /// 构造并发送自定义 CAN/CANFD 报文,可选择单次发送或周期发送,并可同步更新 DBC 数据库中同 ID 报文的数据。
/// </summary> /// </summary>
+1 -1
View File
@@ -14,7 +14,7 @@
<ItemGroup> <ItemGroup>
<Reference Include="Interop.TSMasterAPI"> <Reference Include="Interop.TSMasterAPI">
<HintPath>..\CanDriver\bin\Debug\net8.0\Interop.TSMasterAPI.dll</HintPath> <HintPath>Interop.TSMasterAPI.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
+9 -5
View File
@@ -1,14 +1,18 @@
using System.Reflection; using Prism;
using UpdateInfoModule.Views; using Prism.Ioc;
using TSMasterXCPModule.Views;
namespace UpdateInfoModule namespace TSMasterXCPModule
{ {
/// <summary>
/// TSMaster XCP 测试模块:基于同星 CAN(TSMaster)+ XCPCore 的 XCP 主站调试界面,
/// 支持 XCP 连接、内存读写与 DAQ 采集。
/// 通过左侧菜单"TSMaster XCP 测试"导航进入(仅 default 作用域可用)。
/// </summary>
public class TSMasterXCPModule : IModule public class TSMasterXCPModule : IModule
{ {
public void OnInitialized(IContainerProvider containerProvider) public void OnInitialized(IContainerProvider containerProvider)
{ {
IRegionManager regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("ShellViewManager", typeof(TSMasterXCPView));
} }
public void RegisterTypes(IContainerRegistry containerRegistry) public void RegisterTypes(IContainerRegistry containerRegistry)
@@ -8,8 +8,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\TSMasterCAN\TSMasterCAN.csproj" />
<ProjectReference Include="..\TestingModule\TestingModule.csproj" /> <ProjectReference Include="..\TestingModule\TestingModule.csproj" />
<ProjectReference Include="..\UIShare\UIShare.csproj" /> <ProjectReference Include="..\UIShare\UIShare.csproj" />
<ProjectReference Include="..\XCPCore\XCPCore\XCPCore.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -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 属性 #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 #endregion
#region 命令 #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 #endregion
/// <summary>创建视图模型</summary>
public TSMasterXCPViewModel(IContainerProvider containerProvider) : base(containerProvider) 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 #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;
}
} }
} }
+268 -4
View File
@@ -1,4 +1,4 @@
<UserControl x:Class="UpdateInfoModule.Views.TSMasterXCPView" <UserControl x:Class="TSMasterXCPModule.Views.TSMasterXCPView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -6,10 +6,274 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="1080" d:DesignHeight="1080"
d:DesignWidth="1920"> d:DesignWidth="1920">
<Grid Background="green"> <UserControl.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
</Grid> </UserControl.Resources>
<Border Background="#F5F7FA">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Margin="0,0,8,0">
<StackPanel>
<!-- ============ 设备状态卡片 ============ -->
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<materialDesign:PackIcon Kind="Connection"
Width="22" Height="22"
Foreground="#1565C0"
Margin="0,0,8,0"
VerticalAlignment="Center" />
<TextBlock Text="TSMaster XCP 测试 · XcpMaster"
FontSize="15" FontWeight="Bold"
VerticalAlignment="Center" />
<TextBlock FontSize="12" VerticalAlignment="Center" Margin="12,0,0,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="总线: 未连接" />
<Setter Property="Foreground" Value="#F44336" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsBusConnected}" Value="True">
<Setter Property="Text" Value="总线: 已连接" />
<Setter Property="Foreground" Value="#4CAF50" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F44336" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Background" Value="#4CAF50" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock VerticalAlignment="Center" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="XCP 未连接" />
<Setter Property="Foreground" Value="#F44336" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Text" Value="XCP 已连接" />
<Setter Property="Foreground" Value="#4CAF50" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ProgressBar IsIndeterminate="True" Width="60" Height="4"
Margin="12,0,0,0"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}" />
</StackPanel>
</Grid>
</materialDesign:Card>
<!-- ============ TSMaster 总线连接 ============ -->
<GroupBox Header="TSMaster 总线连接" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="工程文件" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="360"
ToolTip="TSMaster 工程文件(.tscproj / .xcp 等全路径),留空则使用默认工程初始化"
Text="{Binding TSMasterProjectPath, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="初始化驱动" Command="{Binding InitDriverCommand}"
Style="{StaticResource CmdBtn}"
Background="#388E3C" Foreground="White" />
<Button Content="释放驱动" Command="{Binding ReleaseDriverCommand}"
Style="{StaticResource WarnBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="连接总线" Command="{Binding ConnectBusCommand}"
Style="{StaticResource CmdBtn}"
Background="#388E3C" Foreground="White"
ToolTip="tsapp_connect + 启动 RBS 仿真,XCP 收发前必须先连接总线" />
<Button Content="断开总线" Command="{Binding DisconnectBusCommand}"
Style="{StaticResource WarnBtn}" />
<TextBlock Text="{Binding IsBusConnected, StringFormat='总线状态: {0}'}"
VerticalAlignment="Center" Margin="12,0,0,0"
FontSize="12" Foreground="#616161" />
</StackPanel>
</StackPanel>
</GroupBox>
<!-- ============ XCP 连接配置 ============ -->
<GroupBox Header="XCP 连接配置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="CAN通道" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="60"
ToolTip="TSMaster 通道号,0 起始"
Text="{Binding CanChannel, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="CRO ID" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="90"
ToolTip="命令发送 CAN ID(hex),如 0x7E0"
Text="{Binding CroIdText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="DTO ID" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="90"
ToolTip="响应/DAQ 接收 CAN ID(hex),如 0x7E8"
Text="{Binding DtoIdText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="超时(ms)" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="70"
ToolTip="XCP 命令响应超时时间"
Text="{Binding CommandTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="帧类型" Style="{StaticResource ParamLabel}" />
<CheckBox Content="扩展帧 (29位ID)" IsChecked="{Binding IsExtendedFrame}"
VerticalAlignment="Center" Margin="4,0" />
<CheckBox Content="CAN FD" IsChecked="{Binding IsCanFd}"
VerticalAlignment="Center" Margin="4,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="XCP 连接" Command="{Binding XcpConnectCommand}"
Style="{StaticResource CmdBtn}"
Background="#388E3C" Foreground="White"
ToolTip="CONNECT 0x00:建立 XCP 逻辑连接(先连总线)" />
<Button Content="XCP 断开" Command="{Binding XcpDisconnectCommand}"
Style="{StaticResource WarnBtn}" />
<Button Content="查询状态" Command="{Binding GetStatusCommand}"
Style="{StaticResource CmdBtn}"
ToolTip="GET_STATUS:会话状态 / 已配置 DAQ 列表数 / 事件通道数" />
</StackPanel>
<TextBlock Text="{Binding SlaveInfoText}"
FontSize="12" Foreground="#1565C0"
Margin="4,4,4,0" TextWrapping="Wrap" />
</StackPanel>
</GroupBox>
<!-- ============ 内存读写 ============ -->
<GroupBox Header="内存读写(SET_MTA / UPLOAD / DOWNLOAD)" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="地址(hex)" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="110"
ToolTip="从站内存地址,如 0x20000000"
Text="{Binding MemAddressText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="读取长度" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="60"
ToolTip="字节数,1~200"
Text="{Binding MemReadLength, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="读取内存" Command="{Binding ReadMemoryCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,2">
<TextBlock Text="读取结果" Style="{StaticResource ParamLabel}" />
<TextBox IsReadOnly="True" Width="420" Height="30" FontSize="12"
FontFamily="Consolas" Background="#FAFAFA"
VerticalContentAlignment="Center" Margin="4,0"
Text="{Binding ReadResultText, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="写入数据" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="300"
ToolTip="hex 字节序列(空格/逗号分隔),如 01 A2 FF 00"
Text="{Binding MemWriteDataText, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="写入内存" Command="{Binding WriteMemoryCommand}"
Style="{StaticResource CmdBtn}"
Background="#388E3C" Foreground="White" />
</StackPanel>
</StackPanel>
</GroupBox>
<!-- ============ DAQ 采集 ============ -->
<GroupBox Header="DAQ 采集(FREE_DAQ / ALLOC_DAQ / WRITE_DAQ / START_STOP)" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="DAQ列表号" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="60"
ToolTip="DAQ 列表编号(十进制,0 起始)"
Text="{Binding DaqListNumber, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="事件通道" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="60"
ToolTip="事件通道编号(十进制),决定采集触发源"
Text="{Binding EventChannel, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="预分频" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="60"
ToolTip="每 N 个事件采集一次(1=每个事件都采)"
Text="{Binding Prescaler, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="ODT地址" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="110"
ToolTip="要采集的信号在从站内存中的地址(hex)"
Text="{Binding OdtAddressText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="长度" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="50"
ToolTip="信号长度(字节,1~64)"
Text="{Binding OdtLength, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="配置 DAQ" Command="{Binding ConfigureDaqCommand}"
Style="{StaticResource CmdBtn}"
Background="#388E3C" Foreground="White"
ToolTip="释放并重新分配 DAQ 资源后写入 ODT 条目(会清空从站已有 DAQ 配置)" />
<Button Content="启动 DAQ" Command="{Binding StartDaqCommand}"
Style="{StaticResource CmdBtn}" />
<Button Content="停止 DAQ" Command="{Binding StopDaqCommand}"
Style="{StaticResource WarnBtn}" />
</StackPanel>
<Separator Margin="0,4" />
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="接收一帧 DTO" Command="{Binding ReceiveDtoCommand}"
Style="{StaticResource CmdBtn}"
ToolTip="从接收队列取一帧 DAQ 数据(500ms 超时);注意 DAQ 运行期间命令响应与 DTO 共用队列" />
<TextBlock Text="{Binding DtoFrameCount, StringFormat='已收帧数: {0}'}"
VerticalAlignment="Center" Margin="12,0,8,0"
FontSize="12" Foreground="#616161" />
<TextBox IsReadOnly="True" Width="380" Height="30" FontSize="12"
FontFamily="Consolas" Background="#FAFAFA"
VerticalContentAlignment="Center"
ToolTip="最近收到的 DTO 帧原始数据(hex)"
Text="{Binding LatestDtoText, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</GroupBox>
<!-- ============ 日志 ============ -->
<GroupBox Header="日志" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,4,2">
<Button Content="清空日志" Command="{Binding ClearLogCommand}"
Style="{StaticResource CmdBtn}" Height="26" />
</StackPanel>
<ScrollViewer Height="170" VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
IsReadOnly="True" TextWrapping="Wrap"
FontSize="11" FontFamily="Consolas"
Background="#FAFAFA" BorderThickness="0"
VerticalAlignment="Top" />
</ScrollViewer>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl> </UserControl>
@@ -13,7 +13,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
namespace UpdateInfoModule.Views namespace TSMasterXCPModule.Views
{ {
/// <summary> /// <summary>
/// TSMasterXCPView.xaml 的交互逻辑 /// TSMasterXCPView.xaml 的交互逻辑
+5 -1
View File
@@ -10,9 +10,13 @@
<ItemGroup>
<ProjectReference Include="..\..\TSMasterCAN\TSMasterCAN.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Interop.TSMasterAPI"> <Reference Include="Interop.TSMasterAPI">
<HintPath>..\TOSUNCAN\Interop.TSMasterAPI.dll</HintPath> <HintPath>..\..\TSMasterCAN\Interop.TSMasterAPI.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
+79 -205
View File
@@ -1,34 +1,21 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using TSMaster; using TSMaster;
using TSMasterCAN; using TSMasterCAN;
using ZLGCANAPI;
using ZLG_USB_CAN;
namespace XCPCore namespace XCPCore
{ {
/// <summary> /// <summary>
/// CAN 传输层后端类型 /// XCP over CAN 传输层实现(同星 TSMaster 后端)。
/// </summary> /// 命令发送复用 <see cref="CAN.SendMsg"/>(TSMaster 通道报文 API),
public enum CanBackendType /// 接收走 TSMaster 接收 FIFO(tsfifo_receive_canfd_msgs_list / tsfifo_receive_can_msgs_list),
{ /// 由后台轮询任务按 DTO ID 过滤后推入队列。
/// <summary>同星 TSCAN</summary>
TSCAN,
/// <summary>周立功 ZLGCAN</summary>
ZLGCAN
}
/// <summary>
/// XCP over CAN 传输层实现。
/// 支持同星 CAN(TSMaster)和周立功 CAN(ZLGCAN)两种后端。
/// </summary> /// </summary>
public class XcpCanTransport : IXcpTransport public class XcpCanTransport : IXcpTransport
{ {
private readonly CanBackendType _backend;
private readonly byte _channel; private readonly byte _channel;
private readonly uint _croId; private readonly uint _croId;
private readonly uint _dtoId; private readonly uint _dtoId;
@@ -51,23 +38,18 @@ namespace XCPCore
/// <summary>DTO ID(接收响应的 CAN ID)</summary> /// <summary>DTO ID(接收响应的 CAN ID)</summary>
public uint DtoId => _dtoId; public uint DtoId => _dtoId;
/// <summary>CAN 后端类型</summary>
public CanBackendType Backend => _backend;
/// <summary> /// <summary>
/// 创建 CAN XCP 传输层实例 /// 创建 CAN XCP 传输层实例(同星 TSMaster 后端)
/// </summary> /// </summary>
/// <param name="backend">CAN 后端(TSCAN / ZLGCAN)</param>
/// <param name="channel">CAN 通道号</param> /// <param name="channel">CAN 通道号</param>
/// <param name="croId">CRO CAN ID(发送命令)</param> /// <param name="croId">CRO CAN ID(发送命令)</param>
/// <param name="dtoId">DTO CAN ID(接收响应)</param> /// <param name="dtoId">DTO CAN ID(接收响应)</param>
/// <param name="isExtended">是否使用扩展帧(29位 ID)</param> /// <param name="isExtended">是否使用扩展帧(29位 ID)</param>
/// <param name="isCanFd">是否使用 CAN FD 帧</param> /// <param name="isCanFd">是否使用 CAN FD 帧</param>
/// <param name="timeoutMs">默认超时时间(毫秒)</param> /// <param name="timeoutMs">默认超时时间(毫秒)</param>
public XcpCanTransport(CanBackendType backend, byte channel, public XcpCanTransport(byte channel,
uint croId, uint dtoId, bool isExtended = false, bool isCanFd = false, int timeoutMs = 3000) uint croId, uint dtoId, bool isExtended = false, bool isCanFd = false, int timeoutMs = 3000)
{ {
_backend = backend;
_channel = channel; _channel = channel;
_croId = croId; _croId = croId;
_dtoId = dtoId; _dtoId = dtoId;
@@ -77,21 +59,25 @@ namespace XCPCore
} }
/// <summary> /// <summary>
/// 打开传输层(启动 DTO 接收轮询任务) /// 打开传输层(启用 TSMaster 接收 FIFO 并启动 DTO 接收轮询任务)
/// </summary> /// </summary>
public Task<bool> OpenAsync(CancellationToken ct = default) public Task<bool> OpenAsync(CancellationToken ct = default)
{ {
if (IsOpen) return Task.FromResult(true); if (IsOpen) return Task.FromResult(true);
// 启用 TSMaster 接收 FIFO(全局开关,幂等)并清空本通道残留帧
TsMasterApi.tsfifo_enable_receive_fifo();
ClearFifoBuffers();
_receiveCts = new CancellationTokenSource(); _receiveCts = new CancellationTokenSource();
_receiveTask = Task.Run(() => ReceiveLoop(_receiveCts.Token), _receiveCts.Token); _receiveTask = Task.Run(() => ReceiveLoop(_receiveCts.Token), _receiveCts.Token);
IsOpen = true; 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); return Task.FromResult(true);
} }
/// <summary> /// <summary>
/// 发送 CTO 帧(构造 CAN 帧并发送) /// 发送 CTO 帧(构造 CAN 帧并经 CAN.SendMsg 发送)
/// </summary> /// </summary>
public Task SendCtoAsync(byte[] data, CancellationToken ct = default) public Task SendCtoAsync(byte[] data, CancellationToken ct = default)
{ {
@@ -103,30 +89,22 @@ namespace XCPCore
byte[] sendData = new byte[maxLen]; byte[] sendData = new byte[maxLen];
Array.Copy(data, sendData, sendLen); Array.Copy(data, sendData, sendLen);
if (_backend == CanBackendType.TSCAN) int result = CAN.SendMsg(
{ (APP_CHANNEL)_channel,
// 同星 CAN: 使用 CAN.SendMsg 发送 (int)_croId,
int result = CAN.SendMsg( true, // AIsTx
(APP_CHANNEL)_channel, _isExtended, // AIsExt
(int)_croId, false, // AIsRemote
true, // AIsTx (byte)sendLen, // ADLC
_isExtended, // AIsExt sendData,
false, // AIsRemote _isCanFd, // AIsFD
(byte)sendLen, // ADLC false, // AIsBRS
sendData, false, // isUpdateDBCDatabase
_isCanFd, // AIsFD 0 // sendPeriod (单次发送)
false, // AIsBRS );
false, // isUpdateDBCDatabase
0 // sendPeriod (单次发送)
);
if (result != 0) if (result != 0)
throw new InvalidOperationException($"XCP CAN 发送失败: TSCAN 返回值={result}"); throw new InvalidOperationException($"XCP CAN 发送失败: TSCAN 返回值={result} ({CAN.GetErrorDescription(result)})");
}
else // ZLGCAN
{
SendViaZlgCan(sendData, sendLen);
}
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -176,6 +154,7 @@ namespace XCPCore
// 清空队列 // 清空队列
while (_responseQueue.TryDequeue(out _)) { } while (_responseQueue.TryDequeue(out _)) { }
ClearFifoBuffers();
IsOpen = false; IsOpen = false;
Debug.WriteLine("XCP CAN 传输层已关闭"); Debug.WriteLine("XCP CAN 传输层已关闭");
@@ -186,7 +165,18 @@ namespace XCPCore
// ============================================================ // ============================================================
/// <summary> /// <summary>
/// 后台接收轮询任务:持续从 CAN 总线读取报文,按 DTO ID 过滤后推入队列 /// 清空 TSMaster 本通道接收 FIFO 残留帧
/// </summary>
private void ClearFifoBuffers()
{
if (_isCanFd)
TsMasterApi.tsfifo_clear_canfd_receive_buffers(_channel);
else
TsMasterApi.tsfifo_clear_can_receive_buffers(_channel);
}
/// <summary>
/// 后台接收轮询任务:从 TSMaster 接收 FIFO 读取报文,按 DTO ID 过滤后推入队列
/// </summary> /// </summary>
private async Task ReceiveLoop(CancellationToken ct) private async Task ReceiveLoop(CancellationToken ct)
{ {
@@ -194,14 +184,10 @@ namespace XCPCore
{ {
try try
{ {
if (_backend == CanBackendType.TSCAN) if (_isCanFd)
{ ReceiveFdFromFifo();
ReceiveFromTsCan();
}
else else
{ ReceiveClassicFromFifo();
ReceiveFromZlgCan();
}
// 轮询间隔:1ms 足以满足大多数 XCP 场景 // 轮询间隔:1ms 足以满足大多数 XCP 场景
await Task.Delay(1, ct).ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled); await Task.Delay(1, ct).ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
@@ -219,171 +205,59 @@ namespace XCPCore
} }
/// <summary> /// <summary>
/// 从同星 CAN 接收队列中读取 DTO 响应帧 /// 从 TSMaster 接收 FIFO 读取 CAN FD 帧并按 DTO ID 过滤
/// </summary> /// </summary>
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 过滤 EnqueueIfDto(buffers[i].FIdxChn, (uint)buffers[i].FIdentifier, buffers[i].FDLC, buffers[i].FData);
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();
}
} }
} }
/// <summary> /// <summary>
/// 从周立功 CAN 接收缓冲区中读取 DTO 响应帧 /// 从 TSMaster 接收 FIFO 读取经典 CAN 帧并按 DTO ID 过滤
/// </summary> /// </summary>
private void ReceiveFromZlgCan() private void ReceiveClassicFromFifo()
{ {
IntPtr curChn = ZLG_USBCANFD.chnHandle[ZLG_USBCANFD.channel_index]; int count = 0;
if (curChn == IntPtr.Zero) return; 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); EnqueueIfDto(buffers[i].FIdxChn, (uint)buffers[i].FIdentifier, buffers[i].FDLC, buffers[i].FData);
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);
}
} }
} }
/// <summary> /// <summary>
/// 通过周立功 CAN 发送帧 /// 校验帧(通道 + DTO ID 匹配)后截取有效数据入队并释放等待信号
/// </summary> /// </summary>
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 (frameChannel != _channel || frameId != _dtoId)
if (curChn == IntPtr.Zero) return;
throw new InvalidOperationException("ZLGCAN 通道句柄无效");
if (_isCanFd) int len = Math.Min((int)dlc, data?.Length ?? 0);
{ var payload = new byte[len];
var frame = new ZLGCAN.canfd_frame if (len > 0)
{ Array.Copy(data, payload, len);
can_id = _croId, _responseQueue.Enqueue(payload);
len = (byte)dataLen, _responseSignal.Release();
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);
}
}
} }
} }
} }