最初阶段

This commit is contained in:
“hsc”
2026-10-14 16:45:58 +08:00
parent 59d0c24deb
commit a2a62a0327
14 changed files with 1520 additions and 1 deletions
+13 -1
View File
@@ -47,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TSMasterCAN", "TSMasterCAN\
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerAdaptabilityModule", "PowerAdaptabilityModule\PowerAdaptabilityModule.csproj", "{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerAdaptabilityModule", "PowerAdaptabilityModule\PowerAdaptabilityModule.csproj", "{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -150,8 +162,8 @@ Global
{850A5BE8-0BC1-4D6A-8351-DD812C9C4427} = {1E92D601-68FC-45F6-8251-BD0F39226E5B} {850A5BE8-0BC1-4D6A-8351-DD812C9C4427} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{170AD4C1-189D-4FBE-B10D-2A4304527834} = {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} {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} {8923122F-F7D3-4AE7-9E86-9E78F1296A11} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89} = {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 EndGlobalSection
EndGlobal EndGlobal
+1
View File
@@ -33,6 +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="..\UIShare\UIShare.csproj" /> <ProjectReference Include="..\UIShare\UIShare.csproj" />
<ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" /> <ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" />
</ItemGroup> </ItemGroup>
+19
View File
@@ -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<IRegionManager>();
regionManager.RegisterViewWithRegion("ShellViewManager", typeof(TSMasterXCPView));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<TSMasterXCPView>("TSMasterXCPView");
}
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TestingModule\TestingModule.csproj" />
<ProjectReference Include="..\UIShare\UIShare.csproj" />
</ItemGroup>
</Project>
@@ -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
}
}
@@ -0,0 +1,15 @@
<UserControl x:Class="UpdateInfoModule.Views.TSMasterXCPView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="1080"
d:DesignWidth="1920">
<Grid Background="green">
</Grid>
</UserControl>
@@ -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
{
/// <summary>
/// TSMasterXCPView.xaml 的交互逻辑
/// </summary>
public partial class TSMasterXCPView : UserControl
{
public TSMasterXCPView()
{
InitializeComponent();
}
}
}
+85
View File
@@ -0,0 +1,85 @@
using System.Collections.Generic;
namespace XCPCore
{
/// <summary>
/// DAQ 列表配置,描述一个完整的 DAQ 采集方案
/// </summary>
public class DaqListConfig
{
/// <summary>
/// DAQ 列表编号
/// </summary>
public ushort DaqListNumber { get; set; }
/// <summary>
/// DAQ 列表模式:0x00=默认,0x01=时间戳附加,0x02=优先级,0x04=预设 DAQ 列表
/// </summary>
public byte Mode { get; set; } = 0x00;
/// <summary>
/// 事件通道编号(决定采集触发源)
/// </summary>
public ushort EventChannel { get; set; }
/// <summary>
/// 预分频值(每 N 个事件触发一次采集,0 表示每个事件都采集)
/// </summary>
public ushort Prescaler { get; set; } = 1;
/// <summary>
/// DAQ 列表是否启动(true=启动,false=停止)
/// </summary>
public bool Enable { get; set; } = true;
/// <summary>
/// ODT(Object Descriptor Table)条目列表
/// 每个 ODT 描述一组要采集的信号(地址 + 长度)
/// </summary>
public List<OdEntry> OdEntries { get; set; } = new List<OdEntry>();
}
/// <summary>
/// OD(Object Descriptor)条目,描述单个要采集的信号
/// </summary>
public class OdEntry
{
/// <summary>
/// 信号地址(从站内存地址)
/// </summary>
public uint Address { get; set; }
/// <summary>
/// 地址扩展(用于 > 32 位地址的从站)
/// </summary>
public byte AddressExtension { get; set; } = 0;
/// <summary>
/// 信号长度(字节)
/// </summary>
public byte Length { get; set; }
/// <summary>
/// 信号名称(便于上层引用,非协议字段)
/// </summary>
public string Name { get; set; } = "";
}
/// <summary>
/// XCP 协议配置模型(由 ATS 传入 XcpMaster 使用)
/// </summary>
public class XcpConfig
{
/// <summary>连接超时时间(毫秒)</summary>
public int ConnectTimeoutMs { get; set; } = 3000;
/// <summary>命令响应超时时间(毫秒)</summary>
public int CommandTimeoutMs { get; set; } = 1000;
/// <summary>
/// 地址粒度:1=BYTE,2=WORD,4=DWORD
/// 影响 SET_MTA 和 UPLOAD 等命令中地址和长度的单位
/// </summary>
public byte AddressGranularity { get; set; } = 1;
}
}
+62
View File
@@ -0,0 +1,62 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace XCPCore
{
/// <summary>
/// XCP 传输层抽象接口,隔离协议核心与底层通信。
/// 实现此接口的类负责 CTO(命令/响应)和 DTO(DAQ 数据)帧的收发。
/// </summary>
public interface IXcpTransport
{
/// <summary>
/// 发送 CTO 帧(命令请求)
/// </summary>
/// <param name="data">CTO 帧数据(含 PID + 参数)</param>
/// <param name="ct">取消令牌</param>
Task SendCtoAsync(byte[] data, CancellationToken ct = default);
/// <summary>
/// 接收 CTO 帧(命令响应,带超时控制)
/// </summary>
/// <param name="timeoutMs">超时时间(毫秒)</param>
/// <param name="ct">取消令牌</param>
/// <returns>响应帧数据(首字节为 PID)</returns>
/// <exception cref="TimeoutException">接收超时</exception>
Task<byte[]> ReceiveCtoAsync(int timeoutMs, CancellationToken ct = default);
/// <summary>
/// 发送 DTO 帧(DAQ 数据传输)
/// </summary>
/// <param name="data">DTO 帧数据</param>
/// <param name="ct">取消令牌</param>
Task SendDtoAsync(byte[] data, CancellationToken ct = default);
/// <summary>
/// 接收 DTO 帧(DAQ 数据接收,带超时控制)
/// </summary>
/// <param name="timeoutMs">超时时间(毫秒)</param>
/// <param name="ct">取消令牌</param>
/// <returns>DTO 帧数据</returns>
/// <exception cref="TimeoutException">接收超时</exception>
Task<byte[]> ReceiveDtoAsync(int timeoutMs, CancellationToken ct = default);
/// <summary>
/// 打开传输层连接(初始化底层通信资源)
/// </summary>
/// <param name="ct">取消令牌</param>
/// <returns>是否成功打开</returns>
Task<bool> OpenAsync(CancellationToken ct = default);
/// <summary>
/// 关闭传输层连接(释放底层通信资源)
/// </summary>
void Close();
/// <summary>
/// 传输层是否已打开
/// </summary>
bool IsOpen { get; }
}
}
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="Interop.TSMasterAPI">
<HintPath>..\TOSUNCAN\Interop.TSMasterAPI.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
+389
View File
@@ -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
{
/// <summary>
/// CAN 传输层后端类型
/// </summary>
public enum CanBackendType
{
/// <summary>同星 TSCAN</summary>
TSCAN,
/// <summary>周立功 ZLGCAN</summary>
ZLGCAN
}
/// <summary>
/// XCP over CAN 传输层实现。
/// 支持同星 CAN(TSMaster)和周立功 CAN(ZLGCAN)两种后端。
/// </summary>
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<byte[]> _responseQueue = new ConcurrentQueue<byte[]>();
private readonly SemaphoreSlim _responseSignal = new SemaphoreSlim(0);
private CancellationTokenSource? _receiveCts;
private Task? _receiveTask;
/// <summary>传输层是否已打开</summary>
public bool IsOpen { get; private set; }
/// <summary>CRO ID(发送命令的 CAN ID)</summary>
public uint CroId => _croId;
/// <summary>DTO ID(接收响应的 CAN ID)</summary>
public uint DtoId => _dtoId;
/// <summary>CAN 后端类型</summary>
public CanBackendType Backend => _backend;
/// <summary>
/// 创建 CAN XCP 传输层实例
/// </summary>
/// <param name="backend">CAN 后端(TSCAN / ZLGCAN)</param>
/// <param name="channel">CAN 通道号</param>
/// <param name="croId">CRO CAN ID(发送命令)</param>
/// <param name="dtoId">DTO CAN ID(接收响应)</param>
/// <param name="isExtended">是否使用扩展帧(29位 ID)</param>
/// <param name="isCanFd">是否使用 CAN FD 帧</param>
/// <param name="timeoutMs">默认超时时间(毫秒)</param>
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;
}
/// <summary>
/// 打开传输层(启动 DTO 接收轮询任务)
/// </summary>
public Task<bool> 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);
}
/// <summary>
/// 发送 CTO 帧(构造 CAN 帧并发送)
/// </summary>
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;
}
/// <summary>
/// 接收 CTO 帧(等待 DTO 响应,带超时)
/// </summary>
public async Task<byte[]> 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 接收队列为空");
}
/// <summary>
/// 发送 DTO 帧(CAN XCP 中 DTO 与 CTO 共用发送通道)
/// </summary>
public Task SendDtoAsync(byte[] data, CancellationToken ct = default) => SendCtoAsync(data, ct);
/// <summary>
/// 接收 DTO 帧(DAQ 数据接收)
/// </summary>
public Task<byte[]> ReceiveDtoAsync(int timeoutMs, CancellationToken ct = default) => ReceiveCtoAsync(timeoutMs, ct);
/// <summary>
/// 关闭传输层
/// </summary>
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 传输层已关闭");
}
// ============================================================
// 内部方法
// ============================================================
/// <summary>
/// 后台接收轮询任务:持续从 CAN 总线读取报文,按 DTO ID 过滤后推入队列
/// </summary>
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);
}
}
}
/// <summary>
/// 从同星 CAN 接收队列中读取 DTO 响应帧
/// </summary>
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();
}
}
}
/// <summary>
/// 从周立功 CAN 接收缓冲区中读取 DTO 响应帧
/// </summary>
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);
}
}
}
/// <summary>
/// 通过周立功 CAN 发送帧
/// </summary>
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);
}
}
}
}
}
+491
View File
@@ -0,0 +1,491 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace XCPCore
{
/// <summary>
/// XCP 主站协议核心,处理 CTO/DTO 帧、状态机和 DAQ 列表管理。
/// 协议核心逻辑与传输层完全解耦,通过 <see cref="IXcpTransport"/> 接口注入。
/// </summary>
public class XcpMaster
{
private readonly IXcpTransport _transport;
private readonly XcpConfig _config;
private XcpSessionState _state = XcpSessionState.Disconnected;
private readonly object _cmdLock = new object();
/// <summary>CONNECT 成功后缓存的从站能力信息</summary>
public XcpSlaveInfo SlaveInfo { get; private set; } = new XcpSlaveInfo();
/// <summary>当前会话状态</summary>
public XcpSessionState State => _state;
/// <summary>传输层实例(供外部查询传输层类型)</summary>
public IXcpTransport Transport => _transport;
/// <summary>
/// 创建 XCP 主站实例
/// </summary>
/// <param name="transport">传输层实现(CAN 或 UDP)</param>
/// <param name="config">协议配置</param>
public XcpMaster(IXcpTransport transport, XcpConfig config)
{
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
_config = config ?? throw new ArgumentNullException(nameof(config));
}
// ============================================================
// 标准命令
// ============================================================
/// <summary>
/// CONNECT 命令 - 建立与从站的逻辑连接
/// </summary>
/// <param name="mode">连接模式:0x00=正常,0x01=用户自定义</param>
/// <param name="ct">取消令牌</param>
/// <returns>连接是否成功</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// DISCONNECT 命令 - 断开与从站的逻辑连接
/// </summary>
public async Task<bool> DisconnectAsync(CancellationToken ct = default)
{
try
{
if (_state == XcpSessionState.Disconnected)
return true;
var response = await SendCommandAsync(XcpCommand.DISCONNECT, Array.Empty<byte>(), ct);
_state = XcpSessionState.Disconnected;
return response != null && response[0] == XcpResponseCode.POSITIVE;
}
finally
{
_state = XcpSessionState.Disconnected;
}
}
/// <summary>
/// GET_STATUS 命令 - 获取从站当前状态
/// </summary>
/// <returns>状态字节,失败返回 null</returns>
public async Task<byte[]?> GetStatusAsync(CancellationToken ct = default)
{
var response = await SendCommandAsync(XcpCommand.GET_STATUS, Array.Empty<byte>(), ct);
if (response != null && response[0] == XcpResponseCode.POSITIVE)
return response;
return null;
}
// ============================================================
// 数据传输命令
// ============================================================
/// <summary>
/// SET_MTA 命令 - 设置内存传输地址
/// </summary>
/// <param name="address">目标地址</param>
/// <param name="addressExtension">地址扩展(通常 0)</param>
public async Task<bool> 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;
}
/// <summary>
/// UPLOAD 命令 - 从从站读取数据(需先调用 SetMtaAsync 设置地址)
/// </summary>
/// <param name="length">读取长度(字节数,按 AddressGranularity 为单位)</param>
/// <returns>读取到的数据,失败返回 null</returns>
public async Task<byte[]?> 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;
}
/// <summary>
/// SHORT_UPLOAD 命令 - 短上传(地址和数据在同一帧中)
/// </summary>
/// <param name="address">目标地址</param>
/// <param name="length">读取长度(字节数)</param>
/// <param name="addressExtension">地址扩展</param>
/// <returns>读取到的数据</returns>
public async Task<byte[]?> 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;
}
/// <summary>
/// DOWNLOAD 命令 - 向从站写入数据(需先调用 SetMtaAsync 设置地址)
/// </summary>
/// <param name="data">要写入的数据</param>
/// <returns>是否成功</returns>
public async Task<bool> 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;
}
/// <summary>
/// 便捷方法:读取指定地址的数据
/// </summary>
/// <param name="address">目标地址</param>
/// <param name="length">读取长度</param>
/// <param name="ct">取消令牌</param>
/// <returns>读取到的数据</returns>
public async Task<byte[]?> ReadMemoryAsync(uint address, int length, CancellationToken ct = default)
{
if (!await SetMtaAsync(address, 0, ct))
return null;
return await UploadAsync(length, ct);
}
/// <summary>
/// 便捷方法:向指定地址写入数据
/// </summary>
/// <param name="address">目标地址</param>
/// <param name="data">要写入的数据</param>
/// <param name="ct">取消令牌</param>
/// <returns>是否成功</returns>
public async Task<bool> WriteMemoryAsync(uint address, byte[] data, CancellationToken ct = default)
{
if (!await SetMtaAsync(address, 0, ct))
return false;
return await DownloadAsync(data, ct);
}
// ============================================================
// DAQ 命令
// ============================================================
/// <summary>
/// 配置 DAQ 列表(FREE_DAQ -> ALLOC_DAQ -> SET_DAQ_PTR -> WRITE_DAQ -> SET_DAQ_LIST_MODE)
/// </summary>
public async Task<bool> ConfigureDaqListAsync(DaqListConfig config, CancellationToken ct = default)
{
try
{
// 1. FREE_DAQ - 释放现有 DAQ 资源
var resp = await SendCommandAsync(XcpCommand.FREE_DAQ, Array.Empty<byte>(), 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;
}
}
/// <summary>
/// START_STOP_DAQ 命令 - 启动或停止 DAQ 列表采集
/// </summary>
/// <param name="start">true=启动,false=停止</param>
/// <param name="daqListNumber">DAQ 列表编号(0xFFFF 表示所有列表)</param>
public async Task<bool> 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 数据接收
// ============================================================
/// <summary>
/// 接收一帧 DTO 数据(DAQ 采集数据)
/// </summary>
/// <param name="timeoutMs">超时时间</param>
/// <param name="ct">取消令牌</param>
/// <returns>DTO 帧数据</returns>
public async Task<byte[]?> ReceiveDtoAsync(int timeoutMs = 1000, CancellationToken ct = default)
{
try
{
return await _transport.ReceiveDtoAsync(timeoutMs, ct);
}
catch (TimeoutException)
{
return null;
}
}
// ============================================================
// 连接管理
// ============================================================
/// <summary>
/// 完全断开(协议层 DISCONNECT + 传输层 Close)
/// </summary>
public async Task FullDisconnectAsync(CancellationToken ct = default)
{
try
{
if (_state != XcpSessionState.Disconnected)
await DisconnectAsync(ct);
}
finally
{
_transport.Close();
_state = XcpSessionState.Disconnected;
}
}
// ============================================================
// 内部核心方法
// ============================================================
/// <summary>
/// 发送 CTO 命令并等待响应(线程安全)
/// </summary>
private async Task<byte[]> 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);
}
/// <summary>
/// 构造 CTO 帧: [PID(1byte)][参数...]
/// 如果从站 CTO 大小大于帧长度,需填充到 MaxCtoSize
/// </summary>
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;
}
/// <summary>
/// 校验响应帧,处理负响应
/// </summary>
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;
}
/// <summary>
/// 解析 CONNECT 正响应
/// 响应格式: [PID][RESOURCE][COMM_MODE_BASIC][MAX_CTO][MAX_DTO(2)][TRANSPORT_VERSION][PROTOCOL_VERSION]
/// </summary>
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;
}
/// <summary>
/// 获取错误码的文字描述
/// </summary>
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})"
};
}
}
/// <summary>
/// XCP 协议异常
/// </summary>
public class XcpProtocolException : Exception
{
/// <summary>XCP 错误码</summary>
public byte ErrorCode { get; }
public XcpProtocolException(string message, byte errorCode) : base(message)
{
ErrorCode = errorCode;
}
}
}
+139
View File
@@ -0,0 +1,139 @@
namespace XCPCore
{
/// <summary>
/// XCP 命令码定义(参照 ASAM XCP 规范)
/// </summary>
public static class XcpCommand
{
// ===== 标准命令 =====
/// <summary>建立与从站的逻辑连接</summary>
public const byte CONNECT = 0xFF;
/// <summary>断开与从站的逻辑连接</summary>
public const byte DISCONNECT = 0xFE;
/// <summary>获取从站状态</summary>
public const byte GET_STATUS = 0xFD;
/// <summary>同步命令(用于恢复通信)</summary>
public const byte SYNCH = 0xFC;
/// <summary>获取从站 COMM_MODE 支持的详细信息</summary>
public const byte GET_COMM_MODE_INFO = 0xFB;
/// <summary>获取从站 ID(用于识别设备)</summary>
public const byte GET_ID = 0xFA;
/// <summary>设置从站 MTU(最大传输单元)</summary>
public const byte SET_MTA = 0xF6;
/// <summary>从从站上传数据(需要先 SET_MTA 设置地址)</summary>
public const byte UPLOAD = 0xF5;
/// <summary>短上传(地址 + 数据在同一帧中)</summary>
public const byte SHORT_UPLOAD = 0xF4;
/// <summary>向从站下载数据</summary>
public const byte DOWNLOAD = 0xF0;
/// <summary>下载数据(下一块,用于大数据块分页传输)</summary>
public const byte DOWNLOAD_NEXT = 0xEF;
/// <summary>下载数据(最大模式)</summary>
public const byte DOWNLOAD_MAX = 0xEE;
/// <summary>修改源地址(用于复制操作)</summary>
public const byte MODIFY_BITS = 0xED;
/// <summary>设置校准页</summary>
public const byte SET_CAL_PAGE = 0xEB;
/// <summary>获取校准页</summary>
public const byte GET_CAL_PAGE = 0xEA;
/// <summary>获取处理器信息</summary>
public const byte GET_PROCESSOR_INFO = 0xE9;
// ===== DAQ 命令 =====
/// <summary>设置 DAQ 列表指针(选择 DAQ/O DT/ODT 条目)</summary>
public const byte SET_DAQ_PTR = 0xE2;
/// <summary>写入 DAQ 列表条目</summary>
public const byte WRITE_DAQ = 0xE1;
/// <summary>设置 DAQ 列表条目数量</summary>
public const byte SET_DAQ_LIST_MODE = 0xE0;
/// <summary>启动/停止 DAQ 列表采集</summary>
public const byte START_STOP_DAQ = 0xDE;
/// <summary>释放 DAQ 列表资源</summary>
public const byte FREE_DAQ = 0xD9;
/// <summary>分配 DAQ 列表资源</summary>
public const byte ALLOC_DAQ = 0xD8;
/// <summary>获取 DAQ 列表信息</summary>
public const byte GET_DAQ_LIST_INFO = 0xE3;
/// <summary>获取 DAQ 事件信息</summary>
public const byte GET_DAQ_EVENT_INFO = 0xD7;
/// <summary>清除 DAQ 列表</summary>
public const byte CLEAR_DAQ_LIST = 0xDB;
}
/// <summary>
/// XCP 响应 PID 码
/// </summary>
public static class XcpResponseCode
{
/// <summary>正响应(命令执行成功)</summary>
public const byte POSITIVE = 0xFF;
/// <summary>负响应(命令执行失败,紧跟错误码)</summary>
public const byte NEGATIVE = 0xFE;
/// <summary>事件帧(DAQ 数据传输)</summary>
public const byte EVENT = 0xFD;
}
/// <summary>
/// XCP 错误码
/// </summary>
public static class XcpErrorCode
{
/// <summary>未知命令或命令被禁用</summary>
public const byte ERR_CMD_UNKNOWN = 0x10;
/// <summary>命令繁忙(上一个命令尚未完成)</summary>
public const byte ERR_CMD_BUSY = 0x12;
/// <summary>DAQ 配置错误</summary>
public const byte ERR_DAQ_CONFIG = 0x26;
/// <summary>参数超出范围</summary>
public const byte ERR_OUT_OF_RANGE = 0x22;
/// <summary>当前模式无效</summary>
public const byte ERR_MODE_NOT_VALID = 0x24;
/// <summary>段无效</summary>
public const byte ERR_SEGMENT_NOT_VALID = 0x25;
/// <summary>序列号错误(用于 DOWNLOAD_NEXT)</summary>
public const byte ERR_SEQUENCE = 0x20;
/// <summary>内存访问被拒绝</summary>
public const byte ERR_ACCESS_DENIED = 0x23;
/// <summary>页无效</summary>
public const byte ERR_PAGE_NOT_VALID = 0x21;
}
/// <summary>
/// XCP 从站信息(CONNECT 响应解析结果)
/// </summary>
public class XcpSlaveInfo
{
/// <summary>基础通信模式</summary>
public byte CommModeBasic { get; set; }
/// <summary>最大 CTO 帧大小(字节)</summary>
public byte MaxCtoSize { get; set; }
/// <summary>最大 DTO 帧大小(字节)</summary>
public ushort MaxDtoSize { get; set; }
/// <summary>传输层版本号</summary>
public byte TransportLayerVersion { get; set; }
/// <summary>协议版本号</summary>
public byte ProtocolVersion { get; set; }
/// <summary>从站支持的功能(RESOURCE 字段)</summary>
public byte Resource { get; set; }
/// <summary>是否支持 CAL(标定)</summary>
public bool SupportsCal => (Resource & 0x01) != 0;
/// <summary>是否支持 DAQ(数据采集)</summary>
public bool SupportsDaq => (Resource & 0x04) != 0;
/// <summary>是否支持 STIM(刺激)</summary>
public bool SupportsStim => (Resource & 0x08) != 0;
}
/// <summary>
/// XCP 会话状态枚举
/// </summary>
public enum XcpSessionState
{
/// <summary>未连接</summary>
Disconnected,
/// <summary>已连接(CONNECT 成功)</summary>
Connected,
/// <summary>DAQ 正在运行</summary>
DaqRunning
}
}
+214
View File
@@ -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
{
/// <summary>
/// XCP over UDP 传输层实现。
/// 使用两个独立的 UdpClient:一个用于 CTO(命令/响应),一个用于 DTO(DAQ 数据)。
/// 复用现有 <c>Udp.cs</c> 中的超时模式(Task.WhenAny + Task.Delay)。
/// </summary>
public class XcpUdpTransport : IXcpTransport
{
private UdpClient? _ctoClient;
private UdpClient? _dtoClient;
private IPEndPoint? _ctoEndpoint;
private IPEndPoint? _dtoEndpoint;
private ushort _ctr; // 标准头包计数器
/// <summary>远端 IP 地址</summary>
public string RemoteIp { get; set; } = "127.0.0.1";
/// <summary>CTO 端口(命令/响应通道,默认 5555)</summary>
public int CtoPort { get; set; } = 5555;
/// <summary>DTO 端口(DAQ 数据通道,默认 5556)</summary>
public int DtoPort { get; set; } = 5556;
/// <summary>本地绑定端口(0=系统自动分配)</summary>
public int LocalPort { get; set; } = 0;
/// <summary>发送/接收超时时间(毫秒)</summary>
public int TimeoutMs { get; set; } = 3000;
/// <summary>
/// 是否使用 ASAM XCP on UDP 标准头(LEN + CTR,共 4 字节,大端序)。
/// 标准头格式: [LEN(2字节,大端)][CTR(2字节,大端)][XCP包...]
/// 部分从站(如 Vector CANoe/CANape、HostPRO)要求此头,部分从站不需要。
/// </summary>
public bool UseHeader { get; set; } = false;
/// <summary>传输层是否已打开</summary>
public bool IsOpen { get; private set; }
/// <summary>
/// 打开 UDP 传输层(创建并配置 UdpClient)
/// </summary>
public Task<bool> 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);
}
}
/// <summary>
/// 发送 CTO 帧
/// </summary>
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;
}
/// <summary>
/// 接收 CTO 帧(带超时控制)
/// </summary>
public async Task<byte[]> 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;
}
/// <summary>
/// 发送 DTO 帧
/// </summary>
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;
}
/// <summary>
/// 接收 DTO 帧(带超时控制)
/// </summary>
public async Task<byte[]> 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;
}
/// <summary>
/// 关闭传输层(释放 UdpClient 资源)
/// </summary>
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)
// ============================================================
/// <summary>
/// 为 XCP 包添加标准头: [LEN(2字节,大端)][CTR(2字节,大端)][XCP包...]
/// LEN = XCP 包长度(不含头本身),CTR = 递增包计数器。
/// 字节序采用大端(网络字节序),与 ASAM XCP on UDP 及主流从站实现一致。
/// </summary>
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;
}
/// <summary>
/// 剥离标准头(前 4 字节),返回 XCP 包本体。
/// 若数据不足 4 字节则原样返回(容错)。
/// </summary>
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;
}
}
}