From a2a62a03272a146eedf3abac75f56ceeb28a5e9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9Chsc=E2=80=9D?= <“huangsucan@kaiyili-lab.com”>
Date: Wed, 14 Oct 2026 16:36:12 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E5=88=9D=E9=98=B6=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ACP.sln | 14 +-
ACP/ACP.csproj | 1 +
TSMasterXCPModule/TSMasterXCPModule.cs | 19 +
TSMasterXCPModule/TSMasterXCPModule.csproj | 15 +
.../ViewModels/TSMasterXCPViewModel.cs | 30 ++
TSMasterXCPModule/Views/TSMasterXCPView.xaml | 15 +
.../Views/TSMasterXCPView.xaml.cs | 28 +
XCPCore/XCPCore/DaqListConfig.cs | 85 +++
XCPCore/XCPCore/IXcpTransport.cs | 62 +++
XCPCore/XCPCore/XCPCore.csproj | 19 +
XCPCore/XCPCore/XcpCanTransport.cs | 389 ++++++++++++++
XCPCore/XCPCore/XcpMaster.cs | 491 ++++++++++++++++++
XCPCore/XCPCore/XcpProtocolDefs.cs | 139 +++++
XCPCore/XCPCore/XcpUdpTransport.cs | 214 ++++++++
14 files changed, 1520 insertions(+), 1 deletion(-)
create mode 100644 TSMasterXCPModule/TSMasterXCPModule.cs
create mode 100644 TSMasterXCPModule/TSMasterXCPModule.csproj
create mode 100644 TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
create mode 100644 TSMasterXCPModule/Views/TSMasterXCPView.xaml
create mode 100644 TSMasterXCPModule/Views/TSMasterXCPView.xaml.cs
create mode 100644 XCPCore/XCPCore/DaqListConfig.cs
create mode 100644 XCPCore/XCPCore/IXcpTransport.cs
create mode 100644 XCPCore/XCPCore/XCPCore.csproj
create mode 100644 XCPCore/XCPCore/XcpCanTransport.cs
create mode 100644 XCPCore/XCPCore/XcpMaster.cs
create mode 100644 XCPCore/XCPCore/XcpProtocolDefs.cs
create mode 100644 XCPCore/XCPCore/XcpUdpTransport.cs
diff --git a/ACP.sln b/ACP.sln
index b55c9d9..01256a6 100644
--- a/ACP.sln
+++ b/ACP.sln
@@ -47,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TSMasterCAN", "TSMasterCAN\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerAdaptabilityModule", "PowerAdaptabilityModule\PowerAdaptabilityModule.csproj", "{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XCPCore", "XCPCore\XCPCore\XCPCore.csproj", "{9D2D7AC1-60D3-44A5-ADB9-46E8FAE3A1D6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TSMasterXCPModule", "TSMasterXCPModule\TSMasterXCPModule.csproj", "{1296318F-BD2F-4650-B16E-656BA04E80C0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -137,6 +141,14 @@ Global
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D2D7AC1-60D3-44A5-ADB9-46E8FAE3A1D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D2D7AC1-60D3-44A5-ADB9-46E8FAE3A1D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D2D7AC1-60D3-44A5-ADB9-46E8FAE3A1D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D2D7AC1-60D3-44A5-ADB9-46E8FAE3A1D6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1296318F-BD2F-4650-B16E-656BA04E80C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1296318F-BD2F-4650-B16E-656BA04E80C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1296318F-BD2F-4650-B16E-656BA04E80C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1296318F-BD2F-4650-B16E-656BA04E80C0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -150,8 +162,8 @@ Global
{850A5BE8-0BC1-4D6A-8351-DD812C9C4427} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{170AD4C1-189D-4FBE-B10D-2A4304527834} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{AF2533DD-599D-49D7-942B-B0C26534D15F} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
- {84006890-9288-44C4-9D17-FA88F3FDECFA} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{8923122F-F7D3-4AE7-9E86-9E78F1296A11} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
+ {1296318F-BD2F-4650-B16E-656BA04E80C0} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
EndGlobalSection
EndGlobal
diff --git a/ACP/ACP.csproj b/ACP/ACP.csproj
index b8151fe..8b9356a 100644
--- a/ACP/ACP.csproj
+++ b/ACP/ACP.csproj
@@ -33,6 +33,7 @@
+
diff --git a/TSMasterXCPModule/TSMasterXCPModule.cs b/TSMasterXCPModule/TSMasterXCPModule.cs
new file mode 100644
index 0000000..d0b398f
--- /dev/null
+++ b/TSMasterXCPModule/TSMasterXCPModule.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using UpdateInfoModule.Views;
+
+namespace UpdateInfoModule
+{
+ public class TSMasterXCPModule : IModule
+ {
+ public void OnInitialized(IContainerProvider containerProvider)
+ {
+ IRegionManager regionManager = containerProvider.Resolve();
+ regionManager.RegisterViewWithRegion("ShellViewManager", typeof(TSMasterXCPView));
+ }
+
+ public void RegisterTypes(IContainerRegistry containerRegistry)
+ {
+ containerRegistry.RegisterForNavigation("TSMasterXCPView");
+ }
+ }
+}
diff --git a/TSMasterXCPModule/TSMasterXCPModule.csproj b/TSMasterXCPModule/TSMasterXCPModule.csproj
new file mode 100644
index 0000000..8b229f9
--- /dev/null
+++ b/TSMasterXCPModule/TSMasterXCPModule.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0-windows
+ enable
+ true
+ enable
+
+
+
+
+
+
+
+
diff --git a/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs b/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
new file mode 100644
index 0000000..43784cb
--- /dev/null
+++ b/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
@@ -0,0 +1,30 @@
+using UIShare.ViewModelBase;
+
+namespace UpdateInfoModule.ViewModels
+{
+ public class TSMasterXCPViewModel : NavigateViewModelBase
+ {
+ #region 属性
+
+
+ #endregion
+ #region 命令
+
+
+
+ #endregion
+
+ public TSMasterXCPViewModel(IContainerProvider containerProvider) : base(containerProvider)
+ {
+
+
+ }
+ #region 命令处理
+
+
+
+ #endregion
+
+
+ }
+}
diff --git a/TSMasterXCPModule/Views/TSMasterXCPView.xaml b/TSMasterXCPModule/Views/TSMasterXCPView.xaml
new file mode 100644
index 0000000..0f634ba
--- /dev/null
+++ b/TSMasterXCPModule/Views/TSMasterXCPView.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/TSMasterXCPModule/Views/TSMasterXCPView.xaml.cs b/TSMasterXCPModule/Views/TSMasterXCPView.xaml.cs
new file mode 100644
index 0000000..9681bdd
--- /dev/null
+++ b/TSMasterXCPModule/Views/TSMasterXCPView.xaml.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace UpdateInfoModule.Views
+{
+ ///
+ /// TSMasterXCPView.xaml 的交互逻辑
+ ///
+ public partial class TSMasterXCPView : UserControl
+ {
+ public TSMasterXCPView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/XCPCore/XCPCore/DaqListConfig.cs b/XCPCore/XCPCore/DaqListConfig.cs
new file mode 100644
index 0000000..bc0bf71
--- /dev/null
+++ b/XCPCore/XCPCore/DaqListConfig.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+
+namespace XCPCore
+{
+ ///
+ /// DAQ 列表配置,描述一个完整的 DAQ 采集方案
+ ///
+ public class DaqListConfig
+ {
+ ///
+ /// DAQ 列表编号
+ ///
+ public ushort DaqListNumber { get; set; }
+
+ ///
+ /// DAQ 列表模式:0x00=默认,0x01=时间戳附加,0x02=优先级,0x04=预设 DAQ 列表
+ ///
+ public byte Mode { get; set; } = 0x00;
+
+ ///
+ /// 事件通道编号(决定采集触发源)
+ ///
+ public ushort EventChannel { get; set; }
+
+ ///
+ /// 预分频值(每 N 个事件触发一次采集,0 表示每个事件都采集)
+ ///
+ public ushort Prescaler { get; set; } = 1;
+
+ ///
+ /// DAQ 列表是否启动(true=启动,false=停止)
+ ///
+ public bool Enable { get; set; } = true;
+
+ ///
+ /// ODT(Object Descriptor Table)条目列表
+ /// 每个 ODT 描述一组要采集的信号(地址 + 长度)
+ ///
+ public List OdEntries { get; set; } = new List();
+ }
+
+ ///
+ /// OD(Object Descriptor)条目,描述单个要采集的信号
+ ///
+ public class OdEntry
+ {
+ ///
+ /// 信号地址(从站内存地址)
+ ///
+ public uint Address { get; set; }
+
+ ///
+ /// 地址扩展(用于 > 32 位地址的从站)
+ ///
+ public byte AddressExtension { get; set; } = 0;
+
+ ///
+ /// 信号长度(字节)
+ ///
+ public byte Length { get; set; }
+
+ ///
+ /// 信号名称(便于上层引用,非协议字段)
+ ///
+ public string Name { get; set; } = "";
+ }
+
+ ///
+ /// XCP 协议配置模型(由 ATS 传入 XcpMaster 使用)
+ ///
+ public class XcpConfig
+ {
+ /// 连接超时时间(毫秒)
+ public int ConnectTimeoutMs { get; set; } = 3000;
+
+ /// 命令响应超时时间(毫秒)
+ public int CommandTimeoutMs { get; set; } = 1000;
+
+ ///
+ /// 地址粒度:1=BYTE,2=WORD,4=DWORD
+ /// 影响 SET_MTA 和 UPLOAD 等命令中地址和长度的单位
+ ///
+ public byte AddressGranularity { get; set; } = 1;
+ }
+}
diff --git a/XCPCore/XCPCore/IXcpTransport.cs b/XCPCore/XCPCore/IXcpTransport.cs
new file mode 100644
index 0000000..ae4b9a2
--- /dev/null
+++ b/XCPCore/XCPCore/IXcpTransport.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace XCPCore
+{
+ ///
+ /// XCP 传输层抽象接口,隔离协议核心与底层通信。
+ /// 实现此接口的类负责 CTO(命令/响应)和 DTO(DAQ 数据)帧的收发。
+ ///
+ public interface IXcpTransport
+ {
+ ///
+ /// 发送 CTO 帧(命令请求)
+ ///
+ /// CTO 帧数据(含 PID + 参数)
+ /// 取消令牌
+ Task SendCtoAsync(byte[] data, CancellationToken ct = default);
+
+ ///
+ /// 接收 CTO 帧(命令响应,带超时控制)
+ ///
+ /// 超时时间(毫秒)
+ /// 取消令牌
+ /// 响应帧数据(首字节为 PID)
+ /// 接收超时
+ Task ReceiveCtoAsync(int timeoutMs, CancellationToken ct = default);
+
+ ///
+ /// 发送 DTO 帧(DAQ 数据传输)
+ ///
+ /// DTO 帧数据
+ /// 取消令牌
+ Task SendDtoAsync(byte[] data, CancellationToken ct = default);
+
+ ///
+ /// 接收 DTO 帧(DAQ 数据接收,带超时控制)
+ ///
+ /// 超时时间(毫秒)
+ /// 取消令牌
+ /// DTO 帧数据
+ /// 接收超时
+ Task ReceiveDtoAsync(int timeoutMs, CancellationToken ct = default);
+
+ ///
+ /// 打开传输层连接(初始化底层通信资源)
+ ///
+ /// 取消令牌
+ /// 是否成功打开
+ Task OpenAsync(CancellationToken ct = default);
+
+ ///
+ /// 关闭传输层连接(释放底层通信资源)
+ ///
+ void Close();
+
+ ///
+ /// 传输层是否已打开
+ ///
+ bool IsOpen { get; }
+ }
+}
diff --git a/XCPCore/XCPCore/XCPCore.csproj b/XCPCore/XCPCore/XCPCore.csproj
new file mode 100644
index 0000000..9eac25d
--- /dev/null
+++ b/XCPCore/XCPCore/XCPCore.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ True
+
+
+
+
+
+
+ ..\TOSUNCAN\Interop.TSMasterAPI.dll
+
+
+
+
diff --git a/XCPCore/XCPCore/XcpCanTransport.cs b/XCPCore/XCPCore/XcpCanTransport.cs
new file mode 100644
index 0000000..73c0759
--- /dev/null
+++ b/XCPCore/XCPCore/XcpCanTransport.cs
@@ -0,0 +1,389 @@
+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)两种后端。
+ ///
+ public class XcpCanTransport : IXcpTransport
+ {
+ private readonly CanBackendType _backend;
+ private readonly byte _channel;
+ private readonly uint _croId;
+ private readonly uint _dtoId;
+ private readonly bool _isExtended;
+ private readonly bool _isCanFd;
+ private readonly int _timeoutMs;
+
+ // DTO 接收队列(由 CAN 轮询任务填充)
+ private readonly ConcurrentQueue _responseQueue = new ConcurrentQueue();
+ private readonly SemaphoreSlim _responseSignal = new SemaphoreSlim(0);
+ private CancellationTokenSource? _receiveCts;
+ private Task? _receiveTask;
+
+ /// 传输层是否已打开
+ public bool IsOpen { get; private set; }
+
+ /// CRO ID(发送命令的 CAN ID)
+ public uint CroId => _croId;
+
+ /// DTO ID(接收响应的 CAN ID)
+ public uint DtoId => _dtoId;
+
+ /// CAN 后端类型
+ public CanBackendType Backend => _backend;
+
+ ///
+ /// 创建 CAN XCP 传输层实例
+ ///
+ /// CAN 后端(TSCAN / ZLGCAN)
+ /// CAN 通道号
+ /// CRO CAN ID(发送命令)
+ /// DTO CAN ID(接收响应)
+ /// 是否使用扩展帧(29位 ID)
+ /// 是否使用 CAN FD 帧
+ /// 默认超时时间(毫秒)
+ public XcpCanTransport(CanBackendType backend, byte channel,
+ uint croId, uint dtoId, bool isExtended = false, bool isCanFd = false, int timeoutMs = 3000)
+ {
+ _backend = backend;
+ _channel = channel;
+ _croId = croId;
+ _dtoId = dtoId;
+ _isExtended = isExtended;
+ _isCanFd = isCanFd;
+ _timeoutMs = timeoutMs;
+ }
+
+ ///
+ /// 打开传输层(启动 DTO 接收轮询任务)
+ ///
+ public Task OpenAsync(CancellationToken ct = default)
+ {
+ if (IsOpen) return Task.FromResult(true);
+
+ _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}");
+ return Task.FromResult(true);
+ }
+
+ ///
+ /// 发送 CTO 帧(构造 CAN 帧并发送)
+ ///
+ public Task SendCtoAsync(byte[] data, CancellationToken ct = default)
+ {
+ if (!IsOpen) throw new InvalidOperationException("XCP CAN 传输层未打开");
+
+ // 限制 CAN 帧最大长度
+ int maxLen = _isCanFd ? 64 : 8;
+ int sendLen = Math.Min(data.Length, maxLen);
+ 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 (单次发送)
+ );
+
+ if (result != 0)
+ throw new InvalidOperationException($"XCP CAN 发送失败: TSCAN 返回值={result}");
+ }
+ else // ZLGCAN
+ {
+ SendViaZlgCan(sendData, sendLen);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// 接收 CTO 帧(等待 DTO 响应,带超时)
+ ///
+ public async Task ReceiveCtoAsync(int timeoutMs, CancellationToken ct = default)
+ {
+ if (!IsOpen) throw new InvalidOperationException("XCP CAN 传输层未打开");
+
+ var timeoutTask = Task.Delay(timeoutMs, ct);
+ var waitTask = _responseSignal.WaitAsync(ct);
+
+ var completed = await Task.WhenAny(waitTask, timeoutTask);
+ if (completed == timeoutTask)
+ throw new TimeoutException($"XCP CAN 接收超时 ({timeoutMs}ms), 等待 DTO ID=0x{_dtoId:X}");
+
+ if (_responseQueue.TryDequeue(out var response))
+ return response;
+
+ throw new TimeoutException("XCP CAN 接收队列为空");
+ }
+
+ ///
+ /// 发送 DTO 帧(CAN XCP 中 DTO 与 CTO 共用发送通道)
+ ///
+ public Task SendDtoAsync(byte[] data, CancellationToken ct = default) => SendCtoAsync(data, ct);
+
+ ///
+ /// 接收 DTO 帧(DAQ 数据接收)
+ ///
+ public Task ReceiveDtoAsync(int timeoutMs, CancellationToken ct = default) => ReceiveCtoAsync(timeoutMs, ct);
+
+ ///
+ /// 关闭传输层
+ ///
+ public void Close()
+ {
+ if (!IsOpen) return;
+
+ _receiveCts?.Cancel();
+ try { _receiveTask?.Wait(1000); } catch { }
+ _receiveTask = null;
+ _receiveCts?.Dispose();
+ _receiveCts = null;
+
+ // 清空队列
+ while (_responseQueue.TryDequeue(out _)) { }
+
+ IsOpen = false;
+ Debug.WriteLine("XCP CAN 传输层已关闭");
+ }
+
+ // ============================================================
+ // 内部方法
+ // ============================================================
+
+ ///
+ /// 后台接收轮询任务:持续从 CAN 总线读取报文,按 DTO ID 过滤后推入队列
+ ///
+ private async Task ReceiveLoop(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ if (_backend == CanBackendType.TSCAN)
+ {
+ ReceiveFromTsCan();
+ }
+ else
+ {
+ ReceiveFromZlgCan();
+ }
+
+ // 轮询间隔:1ms 足以满足大多数 XCP 场景
+ await Task.Delay(1, ct).ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"XCP CAN 接收循环异常: {ex.Message}");
+ await Task.Delay(10, ct).ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
+ }
+ }
+ }
+
+ ///
+ /// 从同星 CAN 接收队列中读取 DTO 响应帧
+ ///
+ private void ReceiveFromTsCan()
+ {
+ while (CAN.TryGetReceivedMessage(out var msg))
+ {
+ // 按 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();
+ }
+ }
+ }
+
+ ///
+ /// 从周立功 CAN 接收缓冲区中读取 DTO 响应帧
+ ///
+ private void ReceiveFromZlgCan()
+ {
+ IntPtr curChn = ZLG_USBCANFD.chnHandle[ZLG_USBCANFD.channel_index];
+ if (curChn == IntPtr.Zero) return;
+
+ if (_isCanFd)
+ {
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// 通过周立功 CAN 发送帧
+ ///
+ private void SendViaZlgCan(byte[] data, int dataLen)
+ {
+ IntPtr curChn = ZLG_USBCANFD.chnHandle[ZLG_USBCANFD.channel_index];
+ if (curChn == IntPtr.Zero)
+ throw new InvalidOperationException("ZLGCAN 通道句柄无效");
+
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/XCPCore/XCPCore/XcpMaster.cs b/XCPCore/XCPCore/XcpMaster.cs
new file mode 100644
index 0000000..b968cad
--- /dev/null
+++ b/XCPCore/XCPCore/XcpMaster.cs
@@ -0,0 +1,491 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace XCPCore
+{
+ ///
+ /// XCP 主站协议核心,处理 CTO/DTO 帧、状态机和 DAQ 列表管理。
+ /// 协议核心逻辑与传输层完全解耦,通过 接口注入。
+ ///
+ public class XcpMaster
+ {
+ private readonly IXcpTransport _transport;
+ private readonly XcpConfig _config;
+ private XcpSessionState _state = XcpSessionState.Disconnected;
+ private readonly object _cmdLock = new object();
+
+ /// CONNECT 成功后缓存的从站能力信息
+ public XcpSlaveInfo SlaveInfo { get; private set; } = new XcpSlaveInfo();
+
+ /// 当前会话状态
+ public XcpSessionState State => _state;
+
+ /// 传输层实例(供外部查询传输层类型)
+ public IXcpTransport Transport => _transport;
+
+ ///
+ /// 创建 XCP 主站实例
+ ///
+ /// 传输层实现(CAN 或 UDP)
+ /// 协议配置
+ public XcpMaster(IXcpTransport transport, XcpConfig config)
+ {
+ _transport = transport ?? throw new ArgumentNullException(nameof(transport));
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ }
+
+ // ============================================================
+ // 标准命令
+ // ============================================================
+
+ ///
+ /// CONNECT 命令 - 建立与从站的逻辑连接
+ ///
+ /// 连接模式:0x00=正常,0x01=用户自定义
+ /// 取消令牌
+ /// 连接是否成功
+ public async Task ConnectAsync(byte mode = 0x00, CancellationToken ct = default)
+ {
+ try
+ {
+ if (!_transport.IsOpen)
+ await _transport.OpenAsync(ct);
+
+ // CONNECT 命令: PID=0xFF, 参数=MODE(1byte)
+ var response = await SendCommandAsync(XcpCommand.CONNECT, new byte[] { mode }, ct);
+ if (response != null && response[0] == XcpResponseCode.POSITIVE)
+ {
+ SlaveInfo = ParseConnectResponse(response);
+ _state = XcpSessionState.Connected;
+ Debug.WriteLine($"XCP CONNECT 成功: MaxCto={SlaveInfo.MaxCtoSize}, MaxDto={SlaveInfo.MaxDtoSize}");
+ return true;
+ }
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"XCP CONNECT 失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// DISCONNECT 命令 - 断开与从站的逻辑连接
+ ///
+ public async Task DisconnectAsync(CancellationToken ct = default)
+ {
+ try
+ {
+ if (_state == XcpSessionState.Disconnected)
+ return true;
+
+ var response = await SendCommandAsync(XcpCommand.DISCONNECT, Array.Empty(), ct);
+ _state = XcpSessionState.Disconnected;
+ return response != null && response[0] == XcpResponseCode.POSITIVE;
+ }
+ finally
+ {
+ _state = XcpSessionState.Disconnected;
+ }
+ }
+
+ ///
+ /// GET_STATUS 命令 - 获取从站当前状态
+ ///
+ /// 状态字节,失败返回 null
+ public async Task GetStatusAsync(CancellationToken ct = default)
+ {
+ var response = await SendCommandAsync(XcpCommand.GET_STATUS, Array.Empty(), ct);
+ if (response != null && response[0] == XcpResponseCode.POSITIVE)
+ return response;
+ return null;
+ }
+
+ // ============================================================
+ // 数据传输命令
+ // ============================================================
+
+ ///
+ /// SET_MTA 命令 - 设置内存传输地址
+ ///
+ /// 目标地址
+ /// 地址扩展(通常 0)
+ public async Task SetMtaAsync(uint address, byte addressExtension = 0, CancellationToken ct = default)
+ {
+ // SET_MTA 帧格式: [PID][0x00][0x00][ADDRESS_EXTENSION][ADDRESS(4bytes)]
+ var param = new byte[6];
+ param[0] = 0x00; // 保留
+ param[1] = 0x00; // 保留
+ param[2] = addressExtension;
+ param[3] = (byte)(address >> 24);
+ param[4] = (byte)(address >> 16);
+ param[5] = (byte)(address >> 8);
+ // 注意: 此处简化,完整实现需按 AddressGranularity 处理
+ var paramFull = new byte[8];
+ paramFull[0] = 0x00;
+ paramFull[1] = 0x00;
+ paramFull[2] = 0x00;
+ paramFull[3] = addressExtension;
+ paramFull[4] = (byte)((address >> 24) & 0xFF);
+ paramFull[5] = (byte)((address >> 16) & 0xFF);
+ paramFull[6] = (byte)((address >> 8) & 0xFF);
+ paramFull[7] = (byte)(address & 0xFF);
+
+ var response = await SendCommandAsync(XcpCommand.SET_MTA, paramFull, ct);
+ return response != null && response[0] == XcpResponseCode.POSITIVE;
+ }
+
+ ///
+ /// UPLOAD 命令 - 从从站读取数据(需先调用 SetMtaAsync 设置地址)
+ ///
+ /// 读取长度(字节数,按 AddressGranularity 为单位)
+ /// 读取到的数据,失败返回 null
+ public async Task UploadAsync(int length, CancellationToken ct = default)
+ {
+ // UPLOAD 帧格式: [PID][DATA_TRANSFER_LENGTH(1byte)]
+ var param = new byte[] { (byte)length };
+ var response = await SendCommandAsync(XcpCommand.UPLOAD, param, ct);
+ if (response != null && response[0] == XcpResponseCode.POSITIVE)
+ {
+ // 正响应后紧跟数据: [PID][DATA...]
+ var data = new byte[response.Length - 1];
+ Array.Copy(response, 1, data, 0, data.Length);
+ return data;
+ }
+ return null;
+ }
+
+ ///
+ /// SHORT_UPLOAD 命令 - 短上传(地址和数据在同一帧中)
+ ///
+ /// 目标地址
+ /// 读取长度(字节数)
+ /// 地址扩展
+ /// 读取到的数据
+ public async Task ShortUploadAsync(uint address, byte length, byte addressExtension = 0, CancellationToken ct = default)
+ {
+ // SHORT_UPLOAD 帧: [PID][LENGTH][0x00][ADDR_EXT][ADDR(4bytes)]
+ var param = new byte[7];
+ param[0] = length;
+ param[1] = 0x00;
+ param[2] = addressExtension;
+ param[3] = (byte)((address >> 24) & 0xFF);
+ param[4] = (byte)((address >> 16) & 0xFF);
+ param[5] = (byte)((address >> 8) & 0xFF);
+ param[6] = (byte)(address & 0xFF);
+
+ var response = await SendCommandAsync(XcpCommand.SHORT_UPLOAD, param, ct);
+ if (response != null && response[0] == XcpResponseCode.POSITIVE)
+ {
+ var data = new byte[response.Length - 1];
+ Array.Copy(response, 1, data, 0, data.Length);
+ return data;
+ }
+ return null;
+ }
+
+ ///
+ /// DOWNLOAD 命令 - 向从站写入数据(需先调用 SetMtaAsync 设置地址)
+ ///
+ /// 要写入的数据
+ /// 是否成功
+ public async Task DownloadAsync(byte[] data, CancellationToken ct = default)
+ {
+ // DOWNLOAD 帧: [PID][DATA_TRANSFER_LENGTH(1byte)][DATA...]
+ var param = new byte[1 + data.Length];
+ param[0] = (byte)data.Length;
+ Array.Copy(data, 0, param, 1, data.Length);
+
+ var response = await SendCommandAsync(XcpCommand.DOWNLOAD, param, ct);
+ if (response != null && response[0] == XcpResponseCode.POSITIVE)
+ {
+ // 正响应: [PID][MAX_BS(1byte)]
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// 便捷方法:读取指定地址的数据
+ ///
+ /// 目标地址
+ /// 读取长度
+ /// 取消令牌
+ /// 读取到的数据
+ public async Task ReadMemoryAsync(uint address, int length, CancellationToken ct = default)
+ {
+ if (!await SetMtaAsync(address, 0, ct))
+ return null;
+ return await UploadAsync(length, ct);
+ }
+
+ ///
+ /// 便捷方法:向指定地址写入数据
+ ///
+ /// 目标地址
+ /// 要写入的数据
+ /// 取消令牌
+ /// 是否成功
+ public async Task WriteMemoryAsync(uint address, byte[] data, CancellationToken ct = default)
+ {
+ if (!await SetMtaAsync(address, 0, ct))
+ return false;
+ return await DownloadAsync(data, ct);
+ }
+
+ // ============================================================
+ // DAQ 命令
+ // ============================================================
+
+ ///
+ /// 配置 DAQ 列表(FREE_DAQ -> ALLOC_DAQ -> SET_DAQ_PTR -> WRITE_DAQ -> SET_DAQ_LIST_MODE)
+ ///
+ public async Task ConfigureDaqListAsync(DaqListConfig config, CancellationToken ct = default)
+ {
+ try
+ {
+ // 1. FREE_DAQ - 释放现有 DAQ 资源
+ var resp = await SendCommandAsync(XcpCommand.FREE_DAQ, Array.Empty(), ct);
+ if (resp == null || resp[0] != XcpResponseCode.POSITIVE)
+ return false;
+
+ // 2. ALLOC_DAQ - 分配 DAQ 列表
+ var allocParam = new byte[2];
+ allocParam[0] = (byte)(config.DaqListNumber >> 8);
+ allocParam[1] = (byte)(config.DaqListNumber & 0xFF);
+ resp = await SendCommandAsync(XcpCommand.ALLOC_DAQ, allocParam, ct);
+ if (resp == null || resp[0] != XcpResponseCode.POSITIVE)
+ return false;
+
+ // 3. SET_DAQ_PTR + WRITE_DAQ - 写入 ODT 条目
+ for (int i = 0; i < config.OdEntries.Count; i++)
+ {
+ var entry = config.OdEntries[i];
+
+ // SET_DAQ_PTR: [PID][0x00][DAQ_LIST_NUMBER(2)][ODT_NUMBER(1)][ODT_ENTRY_NUMBER(1)]
+ var ptrParam = new byte[5];
+ ptrParam[0] = 0x00;
+ ptrParam[1] = (byte)(config.DaqListNumber >> 8);
+ ptrParam[2] = (byte)(config.DaqListNumber & 0xFF);
+ ptrParam[3] = 0x00; // ODT 编号(简化:每个信号一个 ODT)
+ ptrParam[4] = (byte)i;
+
+ resp = await SendCommandAsync(XcpCommand.SET_DAQ_PTR, ptrParam, ct);
+ if (resp == null || resp[0] != XcpResponseCode.POSITIVE)
+ return false;
+
+ // WRITE_DAQ: [PID][BIT_OFFSET][SIZE][ADDR_EXT][ADDRESS(4bytes)]
+ var writeParam = new byte[7];
+ writeParam[0] = 0x00; // BIT_OFFSET
+ writeParam[1] = entry.Length;
+ writeParam[2] = entry.AddressExtension;
+ writeParam[3] = (byte)((entry.Address >> 24) & 0xFF);
+ writeParam[4] = (byte)((entry.Address >> 16) & 0xFF);
+ writeParam[5] = (byte)((entry.Address >> 8) & 0xFF);
+ writeParam[6] = (byte)(entry.Address & 0xFF);
+
+ resp = await SendCommandAsync(XcpCommand.WRITE_DAQ, writeParam, ct);
+ if (resp == null || resp[0] != XcpResponseCode.POSITIVE)
+ return false;
+ }
+
+ // 4. SET_DAQ_LIST_MODE: [PID][MODE][DAQ_LIST_NUMBER(2)][EVENT_CHANNEL(2)][TRANS_RATE_PRESCALER(2)][0x00][PRIORITY]
+ var modeParam = new byte[8];
+ modeParam[0] = config.Mode;
+ modeParam[1] = (byte)(config.DaqListNumber >> 8);
+ modeParam[2] = (byte)(config.DaqListNumber & 0xFF);
+ modeParam[3] = (byte)(config.EventChannel >> 8);
+ modeParam[4] = (byte)(config.EventChannel & 0xFF);
+ modeParam[5] = (byte)(config.Prescaler >> 8);
+ modeParam[6] = (byte)(config.Prescaler & 0xFF);
+ modeParam[7] = 0x00;
+
+ resp = await SendCommandAsync(XcpCommand.SET_DAQ_LIST_MODE, modeParam, ct);
+ return resp != null && resp[0] == XcpResponseCode.POSITIVE;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"XCP 配置 DAQ 列表失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// START_STOP_DAQ 命令 - 启动或停止 DAQ 列表采集
+ ///
+ /// true=启动,false=停止
+ /// DAQ 列表编号(0xFFFF 表示所有列表)
+ public async Task StartStopDaqAsync(bool start, ushort daqListNumber = 0xFFFF, CancellationToken ct = default)
+ {
+ // START_STOP_DAQ: [PID][MODE][DAQ_LIST_NUMBER(2)]
+ var param = new byte[3];
+ param[0] = start ? (byte)0x01 : (byte)0x00;
+ param[1] = (byte)(daqListNumber >> 8);
+ param[2] = (byte)(daqListNumber & 0xFF);
+
+ var response = await SendCommandAsync(XcpCommand.START_STOP_DAQ, param, ct);
+ if (response != null && response[0] == XcpResponseCode.POSITIVE)
+ {
+ _state = start ? XcpSessionState.DaqRunning : XcpSessionState.Connected;
+ return true;
+ }
+ return false;
+ }
+
+ // ============================================================
+ // DTO 数据接收
+ // ============================================================
+
+ ///
+ /// 接收一帧 DTO 数据(DAQ 采集数据)
+ ///
+ /// 超时时间
+ /// 取消令牌
+ /// DTO 帧数据
+ public async Task ReceiveDtoAsync(int timeoutMs = 1000, CancellationToken ct = default)
+ {
+ try
+ {
+ return await _transport.ReceiveDtoAsync(timeoutMs, ct);
+ }
+ catch (TimeoutException)
+ {
+ return null;
+ }
+ }
+
+ // ============================================================
+ // 连接管理
+ // ============================================================
+
+ ///
+ /// 完全断开(协议层 DISCONNECT + 传输层 Close)
+ ///
+ public async Task FullDisconnectAsync(CancellationToken ct = default)
+ {
+ try
+ {
+ if (_state != XcpSessionState.Disconnected)
+ await DisconnectAsync(ct);
+ }
+ finally
+ {
+ _transport.Close();
+ _state = XcpSessionState.Disconnected;
+ }
+ }
+
+ // ============================================================
+ // 内部核心方法
+ // ============================================================
+
+ ///
+ /// 发送 CTO 命令并等待响应(线程安全)
+ ///
+ private async Task SendCommandAsync(byte commandCode, byte[] parameters, CancellationToken ct)
+ {
+ lock (_cmdLock)
+ {
+ // 串行化命令发送,防止并发命令互相干扰
+ }
+
+ var frame = BuildCtoFrame(commandCode, parameters);
+ await _transport.SendCtoAsync(frame, ct);
+ var response = await _transport.ReceiveCtoAsync(_config.CommandTimeoutMs, ct);
+ return ValidateResponse(response, commandCode);
+ }
+
+ ///
+ /// 构造 CTO 帧: [PID(1byte)][参数...]
+ /// 如果从站 CTO 大小大于帧长度,需填充到 MaxCtoSize
+ ///
+ private byte[] BuildCtoFrame(byte pid, byte[] parameters)
+ {
+ int frameSize = 1 + parameters.Length;
+ int maxCto = SlaveInfo.MaxCtoSize > 0 ? SlaveInfo.MaxCtoSize : frameSize;
+ int actualSize = Math.Max(frameSize, maxCto);
+
+ var frame = new byte[actualSize];
+ frame[0] = pid;
+ if (parameters.Length > 0)
+ Array.Copy(parameters, 0, frame, 1, parameters.Length);
+ return frame;
+ }
+
+ ///
+ /// 校验响应帧,处理负响应
+ ///
+ private byte[] ValidateResponse(byte[] response, byte expectedCommand)
+ {
+ if (response == null || response.Length < 1)
+ throw new TimeoutException("XCP 响应为空");
+
+ if (response[0] == XcpResponseCode.POSITIVE)
+ return response;
+
+ if (response[0] == XcpResponseCode.NEGATIVE)
+ {
+ byte errorCode = response.Length > 1 ? response[1] : (byte)0xFF;
+ string errorDesc = GetErrorDescription(errorCode);
+ throw new XcpProtocolException(
+ $"XCP 命令 0x{expectedCommand:X2} 返回负响应: ERR=0x{errorCode:X2} ({errorDesc})",
+ errorCode);
+ }
+
+ // 非预期的 PID,返回原始响应让上层处理
+ return response;
+ }
+
+ ///
+ /// 解析 CONNECT 正响应
+ /// 响应格式: [PID][RESOURCE][COMM_MODE_BASIC][MAX_CTO][MAX_DTO(2)][TRANSPORT_VERSION][PROTOCOL_VERSION]
+ ///
+ private XcpSlaveInfo ParseConnectResponse(byte[] response)
+ {
+ var info = new XcpSlaveInfo();
+ if (response.Length >= 2) info.Resource = response[1];
+ if (response.Length >= 3) info.CommModeBasic = response[2];
+ if (response.Length >= 4) info.MaxCtoSize = response[3];
+ if (response.Length >= 6) info.MaxDtoSize = (ushort)((response[4] << 8) | response[5]);
+ if (response.Length >= 7) info.TransportLayerVersion = response[6];
+ if (response.Length >= 8) info.ProtocolVersion = response[7];
+ return info;
+ }
+
+ ///
+ /// 获取错误码的文字描述
+ ///
+ private static string GetErrorDescription(byte errorCode)
+ {
+ return errorCode switch
+ {
+ XcpErrorCode.ERR_CMD_UNKNOWN => "未知命令或命令被禁用",
+ XcpErrorCode.ERR_CMD_BUSY => "命令繁忙",
+ XcpErrorCode.ERR_DAQ_CONFIG => "DAQ 配置错误",
+ XcpErrorCode.ERR_OUT_OF_RANGE => "参数超出范围",
+ XcpErrorCode.ERR_MODE_NOT_VALID => "当前模式无效",
+ XcpErrorCode.ERR_SEGMENT_NOT_VALID => "段无效",
+ XcpErrorCode.ERR_SEQUENCE => "序列号错误",
+ XcpErrorCode.ERR_ACCESS_DENIED => "内存访问被拒绝",
+ XcpErrorCode.ERR_PAGE_NOT_VALID => "页无效",
+ _ => $"未知错误(0x{errorCode:X2})"
+ };
+ }
+ }
+
+ ///
+ /// XCP 协议异常
+ ///
+ public class XcpProtocolException : Exception
+ {
+ /// XCP 错误码
+ public byte ErrorCode { get; }
+
+ public XcpProtocolException(string message, byte errorCode) : base(message)
+ {
+ ErrorCode = errorCode;
+ }
+ }
+}
diff --git a/XCPCore/XCPCore/XcpProtocolDefs.cs b/XCPCore/XCPCore/XcpProtocolDefs.cs
new file mode 100644
index 0000000..159431f
--- /dev/null
+++ b/XCPCore/XCPCore/XcpProtocolDefs.cs
@@ -0,0 +1,139 @@
+namespace XCPCore
+{
+ ///
+ /// XCP 命令码定义(参照 ASAM XCP 规范)
+ ///
+ public static class XcpCommand
+ {
+ // ===== 标准命令 =====
+ /// 建立与从站的逻辑连接
+ public const byte CONNECT = 0xFF;
+ /// 断开与从站的逻辑连接
+ public const byte DISCONNECT = 0xFE;
+ /// 获取从站状态
+ public const byte GET_STATUS = 0xFD;
+ /// 同步命令(用于恢复通信)
+ public const byte SYNCH = 0xFC;
+ /// 获取从站 COMM_MODE 支持的详细信息
+ public const byte GET_COMM_MODE_INFO = 0xFB;
+ /// 获取从站 ID(用于识别设备)
+ public const byte GET_ID = 0xFA;
+ /// 设置从站 MTU(最大传输单元)
+ public const byte SET_MTA = 0xF6;
+ /// 从从站上传数据(需要先 SET_MTA 设置地址)
+ public const byte UPLOAD = 0xF5;
+ /// 短上传(地址 + 数据在同一帧中)
+ public const byte SHORT_UPLOAD = 0xF4;
+ /// 向从站下载数据
+ public const byte DOWNLOAD = 0xF0;
+ /// 下载数据(下一块,用于大数据块分页传输)
+ public const byte DOWNLOAD_NEXT = 0xEF;
+ /// 下载数据(最大模式)
+ public const byte DOWNLOAD_MAX = 0xEE;
+ /// 修改源地址(用于复制操作)
+ public const byte MODIFY_BITS = 0xED;
+ /// 设置校准页
+ public const byte SET_CAL_PAGE = 0xEB;
+ /// 获取校准页
+ public const byte GET_CAL_PAGE = 0xEA;
+ /// 获取处理器信息
+ public const byte GET_PROCESSOR_INFO = 0xE9;
+
+ // ===== DAQ 命令 =====
+ /// 设置 DAQ 列表指针(选择 DAQ/O DT/ODT 条目)
+ public const byte SET_DAQ_PTR = 0xE2;
+ /// 写入 DAQ 列表条目
+ public const byte WRITE_DAQ = 0xE1;
+ /// 设置 DAQ 列表条目数量
+ public const byte SET_DAQ_LIST_MODE = 0xE0;
+ /// 启动/停止 DAQ 列表采集
+ public const byte START_STOP_DAQ = 0xDE;
+ /// 释放 DAQ 列表资源
+ public const byte FREE_DAQ = 0xD9;
+ /// 分配 DAQ 列表资源
+ public const byte ALLOC_DAQ = 0xD8;
+ /// 获取 DAQ 列表信息
+ public const byte GET_DAQ_LIST_INFO = 0xE3;
+ /// 获取 DAQ 事件信息
+ public const byte GET_DAQ_EVENT_INFO = 0xD7;
+ /// 清除 DAQ 列表
+ public const byte CLEAR_DAQ_LIST = 0xDB;
+ }
+
+ ///
+ /// XCP 响应 PID 码
+ ///
+ public static class XcpResponseCode
+ {
+ /// 正响应(命令执行成功)
+ public const byte POSITIVE = 0xFF;
+ /// 负响应(命令执行失败,紧跟错误码)
+ public const byte NEGATIVE = 0xFE;
+ /// 事件帧(DAQ 数据传输)
+ public const byte EVENT = 0xFD;
+ }
+
+ ///
+ /// XCP 错误码
+ ///
+ public static class XcpErrorCode
+ {
+ /// 未知命令或命令被禁用
+ public const byte ERR_CMD_UNKNOWN = 0x10;
+ /// 命令繁忙(上一个命令尚未完成)
+ public const byte ERR_CMD_BUSY = 0x12;
+ /// DAQ 配置错误
+ public const byte ERR_DAQ_CONFIG = 0x26;
+ /// 参数超出范围
+ public const byte ERR_OUT_OF_RANGE = 0x22;
+ /// 当前模式无效
+ public const byte ERR_MODE_NOT_VALID = 0x24;
+ /// 段无效
+ public const byte ERR_SEGMENT_NOT_VALID = 0x25;
+ /// 序列号错误(用于 DOWNLOAD_NEXT)
+ public const byte ERR_SEQUENCE = 0x20;
+ /// 内存访问被拒绝
+ public const byte ERR_ACCESS_DENIED = 0x23;
+ /// 页无效
+ public const byte ERR_PAGE_NOT_VALID = 0x21;
+ }
+
+ ///
+ /// XCP 从站信息(CONNECT 响应解析结果)
+ ///
+ public class XcpSlaveInfo
+ {
+ /// 基础通信模式
+ public byte CommModeBasic { get; set; }
+ /// 最大 CTO 帧大小(字节)
+ public byte MaxCtoSize { get; set; }
+ /// 最大 DTO 帧大小(字节)
+ public ushort MaxDtoSize { get; set; }
+ /// 传输层版本号
+ public byte TransportLayerVersion { get; set; }
+ /// 协议版本号
+ public byte ProtocolVersion { get; set; }
+ /// 从站支持的功能(RESOURCE 字段)
+ public byte Resource { get; set; }
+
+ /// 是否支持 CAL(标定)
+ public bool SupportsCal => (Resource & 0x01) != 0;
+ /// 是否支持 DAQ(数据采集)
+ public bool SupportsDaq => (Resource & 0x04) != 0;
+ /// 是否支持 STIM(刺激)
+ public bool SupportsStim => (Resource & 0x08) != 0;
+ }
+
+ ///
+ /// XCP 会话状态枚举
+ ///
+ public enum XcpSessionState
+ {
+ /// 未连接
+ Disconnected,
+ /// 已连接(CONNECT 成功)
+ Connected,
+ /// DAQ 正在运行
+ DaqRunning
+ }
+}
diff --git a/XCPCore/XCPCore/XcpUdpTransport.cs b/XCPCore/XCPCore/XcpUdpTransport.cs
new file mode 100644
index 0000000..6b4248f
--- /dev/null
+++ b/XCPCore/XCPCore/XcpUdpTransport.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace XCPCore
+{
+ ///
+ /// XCP over UDP 传输层实现。
+ /// 使用两个独立的 UdpClient:一个用于 CTO(命令/响应),一个用于 DTO(DAQ 数据)。
+ /// 复用现有 Udp.cs 中的超时模式(Task.WhenAny + Task.Delay)。
+ ///
+ public class XcpUdpTransport : IXcpTransport
+ {
+ private UdpClient? _ctoClient;
+ private UdpClient? _dtoClient;
+ private IPEndPoint? _ctoEndpoint;
+ private IPEndPoint? _dtoEndpoint;
+ private ushort _ctr; // 标准头包计数器
+
+ /// 远端 IP 地址
+ public string RemoteIp { get; set; } = "127.0.0.1";
+
+ /// CTO 端口(命令/响应通道,默认 5555)
+ public int CtoPort { get; set; } = 5555;
+
+ /// DTO 端口(DAQ 数据通道,默认 5556)
+ public int DtoPort { get; set; } = 5556;
+
+ /// 本地绑定端口(0=系统自动分配)
+ public int LocalPort { get; set; } = 0;
+
+ /// 发送/接收超时时间(毫秒)
+ public int TimeoutMs { get; set; } = 3000;
+
+ ///
+ /// 是否使用 ASAM XCP on UDP 标准头(LEN + CTR,共 4 字节,大端序)。
+ /// 标准头格式: [LEN(2字节,大端)][CTR(2字节,大端)][XCP包...]
+ /// 部分从站(如 Vector CANoe/CANape、HostPRO)要求此头,部分从站不需要。
+ ///
+ public bool UseHeader { get; set; } = false;
+
+ /// 传输层是否已打开
+ public bool IsOpen { get; private set; }
+
+ ///
+ /// 打开 UDP 传输层(创建并配置 UdpClient)
+ ///
+ public Task OpenAsync(CancellationToken ct = default)
+ {
+ if (IsOpen) return Task.FromResult(true);
+
+ try
+ {
+ // CTO 通道
+ _ctoClient = LocalPort > 0
+ ? new UdpClient(LocalPort)
+ : new UdpClient();
+ _ctoClient.Client.SendTimeout = TimeoutMs;
+ _ctoClient.Client.ReceiveTimeout = TimeoutMs;
+
+ // DTO 通道
+ _dtoClient = new UdpClient();
+ _dtoClient.Client.SendTimeout = TimeoutMs;
+ _dtoClient.Client.ReceiveTimeout = TimeoutMs;
+
+ _ctoEndpoint = new IPEndPoint(IPAddress.Parse(RemoteIp), CtoPort);
+ _dtoEndpoint = new IPEndPoint(IPAddress.Parse(RemoteIp), DtoPort);
+
+ IsOpen = true;
+ Debug.WriteLine($"XCP UDP 传输层已打开: {RemoteIp}, CTO={CtoPort}, DTO={DtoPort}");
+ return Task.FromResult(true);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"XCP UDP 传输层打开失败: {ex.Message}");
+ Close();
+ return Task.FromResult(false);
+ }
+ }
+
+ ///
+ /// 发送 CTO 帧
+ ///
+ public async Task SendCtoAsync(byte[] data, CancellationToken ct = default)
+ {
+ if (_ctoClient == null || _ctoEndpoint == null)
+ throw new InvalidOperationException("XCP UDP CTO 通道未初始化");
+
+ var packet = UseHeader ? WrapWithHeader(data) : data;
+ var timeoutTask = Task.Delay(TimeoutMs, ct);
+ var sendTask = _ctoClient.SendAsync(packet, packet.Length, _ctoEndpoint);
+
+ var completed = await Task.WhenAny(sendTask, timeoutTask);
+ if (completed == timeoutTask)
+ throw new TimeoutException($"XCP UDP CTO 发送超时 ({TimeoutMs}ms)");
+
+ await sendTask;
+ }
+
+ ///
+ /// 接收 CTO 帧(带超时控制)
+ ///
+ public async Task ReceiveCtoAsync(int timeoutMs, CancellationToken ct = default)
+ {
+ if (_ctoClient == null)
+ throw new InvalidOperationException("XCP UDP CTO 通道未初始化");
+
+ var receiveTask = _ctoClient.ReceiveAsync();
+ var timeoutTask = Task.Delay(timeoutMs, ct);
+
+ var completed = await Task.WhenAny(receiveTask, timeoutTask);
+ if (completed == timeoutTask)
+ throw new TimeoutException($"XCP UDP CTO 接收超时 ({timeoutMs}ms)");
+
+ var result = await receiveTask;
+ return UseHeader ? StripHeader(result.Buffer) : result.Buffer;
+ }
+
+ ///
+ /// 发送 DTO 帧
+ ///
+ public async Task SendDtoAsync(byte[] data, CancellationToken ct = default)
+ {
+ if (_dtoClient == null || _dtoEndpoint == null)
+ throw new InvalidOperationException("XCP UDP DTO 通道未初始化");
+
+ var packet = UseHeader ? WrapWithHeader(data) : data;
+ var timeoutTask = Task.Delay(TimeoutMs, ct);
+ var sendTask = _dtoClient.SendAsync(packet, packet.Length, _dtoEndpoint);
+
+ var completed = await Task.WhenAny(sendTask, timeoutTask);
+ if (completed == timeoutTask)
+ throw new TimeoutException($"XCP UDP DTO 发送超时 ({TimeoutMs}ms)");
+
+ await sendTask;
+ }
+
+ ///
+ /// 接收 DTO 帧(带超时控制)
+ ///
+ public async Task ReceiveDtoAsync(int timeoutMs, CancellationToken ct = default)
+ {
+ if (_dtoClient == null)
+ throw new InvalidOperationException("XCP UDP DTO 通道未初始化");
+
+ var receiveTask = _dtoClient.ReceiveAsync();
+ var timeoutTask = Task.Delay(timeoutMs, ct);
+
+ var completed = await Task.WhenAny(receiveTask, timeoutTask);
+ if (completed == timeoutTask)
+ throw new TimeoutException($"XCP UDP DTO 接收超时 ({timeoutMs}ms)");
+
+ var result = await receiveTask;
+ return UseHeader ? StripHeader(result.Buffer) : result.Buffer;
+ }
+
+ ///
+ /// 关闭传输层(释放 UdpClient 资源)
+ ///
+ public void Close()
+ {
+ _ctoClient?.Close();
+ _dtoClient?.Close();
+ _ctoClient?.Dispose();
+ _dtoClient?.Dispose();
+ _ctoClient = null;
+ _dtoClient = null;
+ _ctoEndpoint = null;
+ _dtoEndpoint = null;
+ IsOpen = false;
+ Debug.WriteLine("XCP UDP 传输层已关闭");
+ }
+
+ // ============================================================
+ // 标准头处理(ASAM XCP on UDP: LEN + CTR)
+ // ============================================================
+
+ ///
+ /// 为 XCP 包添加标准头: [LEN(2字节,大端)][CTR(2字节,大端)][XCP包...]
+ /// LEN = XCP 包长度(不含头本身),CTR = 递增包计数器。
+ /// 字节序采用大端(网络字节序),与 ASAM XCP on UDP 及主流从站实现一致。
+ ///
+ private byte[] WrapWithHeader(byte[] xcpPacket)
+ {
+ var packet = new byte[4 + xcpPacket.Length];
+ // LEN: XCP 包长度(大端序 / 网络字节序)
+ packet[0] = (byte)((xcpPacket.Length >> 8) & 0xFF);
+ packet[1] = (byte)(xcpPacket.Length & 0xFF);
+ // CTR: 包计数器(大端序,每次发送递增,ushort 自动回绕)
+ packet[2] = (byte)((_ctr >> 8) & 0xFF);
+ packet[3] = (byte)(_ctr & 0xFF);
+ _ctr++;
+ // XCP 包本体
+ Array.Copy(xcpPacket, 0, packet, 4, xcpPacket.Length);
+ return packet;
+ }
+
+ ///
+ /// 剥离标准头(前 4 字节),返回 XCP 包本体。
+ /// 若数据不足 4 字节则原样返回(容错)。
+ ///
+ private static byte[] StripHeader(byte[] data)
+ {
+ if (data.Length <= 4)
+ return data;
+ var xcpPacket = new byte[data.Length - 4];
+ Array.Copy(data, 4, xcpPacket, 0, xcpPacket.Length);
+ return xcpPacket;
+ }
+ }
+}