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; + } + } +}