最初阶段
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user