# CanLink **Repository Path**: ChangGengCN/CanLink ## Basic Information - **Project Name**: CanLink - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-06-12 - **Last Updated**: 2026-09-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # CanLink [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![.NET](https://img.shields.io/badge/.NET-8.0%2B-blue.svg)](https://dotnet.microsoft.com/) CanLink 是一个用于 .NET 8 的高性能 CAN(Controller Area Network)通信库,采用现代化设计,支持多种 CAN 硬件适配器和 CAN FD 协议。 ## 功能特性 ### 现代化架构 - **精简接口**:`ICanDevice` + `ICanChannel` 双接口,职责清晰 - **真实异步**:`ValueTask` + `CancellationToken`,非 `Task.Run` 伪异步 - **每通道独立收发线程**:专用接收线程(优先级可配)尽快排空硬件 FIFO,专用发送线程串行批量提交;收发路径物理隔离,高频发送不会饿死接收 - **智能批量**:原生接口支持时,单次事务提交多帧(图莫斯/创芯);支持阻塞式读的设备由操作系统休眠/唤醒,空闲几乎不占 CPU - **接收永不阻塞**:接收线程写入一律不阻塞,消费者处理再慢也不会拖住硬件排空(这正是高频丢帧的根因) - **丢帧可观测**:`ReceivedFrames` / `DroppedFrames` / `PendingSendCount` 实时暴露收发与丢弃情况 - **可反复启停**:`StopAsync` 后仍可 `StartAsync`;`CloseAsync` 为永久关闭,断开后向旧通道发送会立即失败而非打到已释放的句柄 - **异常驱动**:摒弃 `CanOperateResult`,失败直接抛异常,全部派生自 `CanLinkException` ### 异步流消费 - **流模式**:`await foreach (var frame in channel.ReadAllAsync())`,无需注册回调 - **消费端不会反压硬件**:缓冲满时按 `FullMode` 丢弃并计入 `DroppedFrames`,接收线程始终全速排空硬件(避免适配器 FIFO 溢出丢帧);消费端过慢应自行扩容缓冲或提速 - 调用端可基于流自行实现事件分发或多路广播 ### 波特率枚举与查询 - **`CanBaudRate` 枚举**:类型安全的波特率配置,避免裸 `uint` 魔法数字 - **`GetSupportedBaudRates()`**:运行前查询设备实际支持的波特率列表 ### 多设备与 CAN FD 支持 | 包名 | 厂商 | 连接方式 | CAN FD | |------|------|----------|--------| | `CanLink` | 广成科技 / 图莫斯 / 创芯 / 智嵌 | USB | 否 | | `CanLink.PCan` | PEAK-System | USB/PCI/LAN | 是 | | `CanLink.SerialPort` | 智嵌(WL 系列) | 串口 | 否 | | `CanLink.Ethernet` | 亿佰特(ECAN-E02) | 以太网 TCP/UDP/MQTT | 否 | > 串口 CAN(智嵌 WL 系列)与以太网 CAN(亿佰特 ECAN-E02)已实现,均通过独立的传输层与 CAN 帧协议解耦设计。 ## 安装 ```bash dotnet add package CanLink # 广成/图莫斯/创芯/智嵌 dotnet add package CanLink.PCan # PEAK-System(可选) dotnet add package CanLink.SerialPort # 智嵌 WL 系列(可选) dotnet add package CanLink.Ethernet # 亿佰特 ECAN-E02(可选) ``` ## 快速开始 ### 1. 查询设备支持的波特率 ```csharp using CanLink.Core; using CanLink.Devices.GuangCheng; var device = new GuangChengCanDevice(0, new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud250K }); // 连接前查询设备支持哪些波特率 var rates = device.GetSupportedBaudRates(); Console.WriteLine($"支持的波特率: {string.Join(", ", rates)}"); // 输出: 支持的波特率: Baud5K, Baud10K, Baud20K, Baud40K, Baud50K ... ``` ### 2. 连接设备并发送消息 ```csharp using CanLink.Core; using CanLink.Devices.GuangCheng; // 创建设备,配置通道 await using var device = new GuangChengCanDevice(0, new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud250K }, new CanChannelConfig { Index = 1, BaudRate = CanBaudRate.Baud500K }); // 连接 await device.ConnectAsync(); Console.WriteLine($"已连接: {device.DeviceName}, SN={device.SerialNumber}"); // 启动通道接收 var ch0 = device.Channels[0]; await ch0.StartAsync(); // 发送一帧 var frame = CanFrame.Create(0x123, new byte[] { 0x01, 0x02, 0x03, 0x04 }, isExtended: true); await ch0.SendAsync(frame); ``` ### 3. 异步流模式接收数据 ```csharp var cts = new CancellationTokenSource(); var channel = device.Channels[0]; await channel.StartAsync(); await foreach (var frame in channel.ReadAllAsync(cts.Token)) { Console.WriteLine($"[流式] {frame}"); } ``` ### 4. 并发压力测试 ```csharp var frame = CanFrame.Create(0x100, new byte[] { 0xAA }); var stopwatch = Stopwatch.StartNew(); var tasks = device.Channels.Select(ch => Task.Run(async () => { for (int i = 0; i < 1000; i++) await ch.SendAsync(frame); }) ).ToArray(); await Task.WhenAll(tasks); stopwatch.Stop(); Console.WriteLine($"{tasks.Length * 1000} 帧 / {stopwatch.ElapsedMilliseconds}ms"); ``` ### 5. CAN FD 发送(PCAN) ```csharp using CanLink.PCan.Devices.PCan; await using var pcan = new PeakCanDevice(0, new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud500K, IsCanFd = true, FdDataBaudRate = CanBaudRate.Baud1000K }); await pcan.ConnectAsync(); var fdFrame = CanFrame.CreateCanFd(0x123, new byte[64], isExtended: false); await pcan.Channels[0].SendAsync(fdFrame); ``` > **PCAN CAN FD 已知限制**:FD 初始化当前使用固定的位时序参数,尚未依据 `BaudRate` / `FdDataBaudRate` 构造,配置这两个字段不会改变 FD 的实际波特率(待补齐 PCAN FD 位时序表)。 ### 6. 动态波特率切换 ```csharp var channel = device.Channels[0]; // 部分设备(如 PCAN)支持运行时切换波特率 await channel.UpdateBaudRateAsync(CanBaudRate.Baud500K); Console.WriteLine($"当前波特率已切换为: {channel.Config.BaudRate}"); // 切换后可立即恢复收发,无需断开重连 ``` ### 7. 丢帧诊断 ```csharp var channel = device.Channels[0]; await channel.StartAsync(); var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); while (await timer.WaitForNextTickAsync()) { Console.WriteLine( $"发布={channel.ReceivedFrames} 丢弃={channel.DroppedFrames} 待发送={channel.PendingSendCount}"); } ``` - `DroppedFrames` 持续增长 → 消费端处理速度跟不上总线速率。可加大 `ReceiveBufferCapacity`,或提升消费端吞吐。 - `DroppedFrames` 不增长、但收到的帧数少于发送端 → 丢帧发生在适配器 FIFO / 驱动层(读取节奏跟不上)。可调小 `ReadWaitTimeMs`、提高 `ReceiveThreadPriority`,并核对 `ReceiveBatchSize`。 - 通道被设备`DisconnectAsync` 关闭后再调用 `SendAsync`,会立即抛 `CanSendException`(`IsClosed` 为 `true`),不会打到已释放的设备句柄。 ## API 参考 ### 核心模型 ```csharp // 统一消息帧(值类型) public readonly record struct CanFrame { public uint Id { get; init; } public ReadOnlyMemory Data { get; init; } public bool IsExtended { get; init; } public bool IsRemote { get; init; } public bool IsCanFd { get; init; } public DateTimeOffset Timestamp { get; init; } } // 通道配置 public sealed class CanChannelConfig { public required uint Index { get; init; } public required CanBaudRate BaudRate { get; init; } // 如 CanBaudRate.Baud250K public bool IsCanFd { get; init; } public CanBaudRate FdDataBaudRate { get; init; } public bool AutoStart { get; init; } = true; // 接收 public int ReceiveBufferCapacity { get; init; } = 1000; // 必须 ≥ 1(不支持无限缓冲) public BufferFullMode FullMode { get; init; } = BufferFullMode.DropOldest; public int ReceiveBatchSize { get; init; } = 256; // 单次硬件事务最大帧数 public int ReadWaitTimeMs { get; init; } = 3; // 阻塞读等待超时(负值规整为 0) public int ReceiveIdleWaitMs { get; init; } = 1; // 非阻塞读空闲休眠(0 = 满速轮询) public ThreadPriority ReceiveThreadPriority { get; init; } = ThreadPriority.AboveNormal; // 发送 public int SendQueueCapacity { get; init; } = 65_536; // 发送队列上限,0 = 不限制 public int SendBatchSize { get; init; } = 64; // 单次提交硬件的最大帧数 } ``` > `FullMode` 的三种策略都表现为「丢弃 + 计入 `DroppedFrames`」:接收线程**永不**因缓冲满而阻塞(否则会拖住硬件排空、导致适配器 FIFO 溢出)。其中 `Wait` 与 `DropNewest` 等效(丢弃新帧),`DropOldest` 丢弃最旧帧。 ### ICanDevice ```csharp public interface ICanDevice : IAsyncDisposable { string DeviceName { get; } string? SerialNumber { get; } bool IsConnected { get; } IReadOnlyList Channels { get; } ValueTask ConnectAsync(CancellationToken cancellationToken = default); ValueTask DisconnectAsync(CancellationToken cancellationToken = default); ValueTask GetDeviceInfoAsync(CancellationToken cancellationToken = default); /// /// 获取设备支持的所有波特率列表 /// IReadOnlyList GetSupportedBaudRates(); } ``` ### ICanChannel ```csharp public interface ICanChannel { uint Index { get; } CanChannelConfig Config { get; } bool IsRunning { get; } bool IsClosed { get; } // 设备断开后永久关闭 long ReceivedFrames { get; } // 累计成功发布的帧数 long DroppedFrames { get; } // 累计因缓冲满被丢弃的帧数 long PendingSendCount { get; } // 发送队列中尚未提交到硬件的帧数 IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default); // 失败或通道已关闭 → CanChannelException ValueTask StartAsync(CancellationToken cancellationToken = default); ValueTask StopAsync(CancellationToken cancellationToken = default); // 发送队列已满或通道已关闭 → CanSendException ValueTask SendAsync(CanFrame frame, CancellationToken cancellationToken = default); ValueTask ClearBufferAsync(CancellationToken cancellationToken = default); ValueTask UpdateBaudRateAsync(CanBaudRate baudRate, CancellationToken cancellationToken = default); } ``` > **生命周期**:`StartAsync` / `StopAsync` 可反复启停(每次启动会启用新的接收管道,旧枚举随之结束);`CloseAsync`(在 `CanChannelBase` 上)为**永久**关闭,由设备的 `DisconnectAsync` 调用。 ### 异常体系 | 异常 | 说明 | |------|------| | `CanLinkException` | **所有领域异常的基类**,可一次兜住全部失败 | | `CanDeviceNotFoundException` | 设备未找到 | | `CanConnectionException` | 连接失败 | | `CanChannelException` | 通道操作失败(含 `StartAsync` 遇到已关闭的通道) | | `CanSendException` | 发送失败(含发送队列已满、通道已关闭) | ## 设备初始化 > **通道索引必须唯一**:所有设备构造函数会校验通道配置的 `Index` 不重复。重复索引会导致多个通道对象操作同一硬件通道,引发线程竞争,构造时将抛出 `ArgumentException`(如 `通道索引重复: 0`)。 ### 广成科技 (GCAN) > 走 ZLG / ECanVci 兼容协议(`ECanVci.dll`)。`DeviceName` 沿用历史前缀 `ZLG-CAN-{index}` 以保持向后兼容。 ```csharp using CanLink.Devices.GuangCheng; var device = new GuangChengCanDevice( deviceType: 4, // 设备类型 deviceIndex: 0, // 设备索引 new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud250K }, new CanChannelConfig { Index = 1, BaudRate = CanBaudRate.Baud250K } ); ``` ### 图莫斯 (Toomoss) ```csharp using CanLink.Devices.Toomoss; var device = new ToomossCanDevice(0, new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud250K }); ``` ### 创芯 (ChuangXin) ```csharp using CanLink.Devices.ChuangXin; var device = new ChuangXinCanDevice(0, new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud250K }); ``` ### PEAK-System (PCAN) ```csharp using CanLink.PCan.Devices.PCan; var device = new PeakCanDevice(0, new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud250K }); ``` ### 亿佰特 ECAN-E02(以太网) ```csharp using CanLink.Ethernet; using CanLink.Ethernet.Transports; // 方式一:TCP Client(默认,主动连接设备) await using var device = new EbyteE02CanDevice("192.168.3.7", 8881, new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud500K }); // 方式二:TCP Server(设备主动连接工作站) await using var device2 = new EbyteE02CanDevice( EthernetTransportFactory.CreateTcpServer("0.0.0.0", 8881), new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud500K }); // 方式三:UDP Client await using var device3 = new EbyteE02CanDevice( EthernetTransportFactory.CreateUdpClient("192.168.3.7", 8881), new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud500K }); // 方式四:MQTT Client(通过 MQTT 代理中转) await using var device4 = new EbyteE02CanDevice( EthernetTransportFactory.CreateMqttClient("broker.example.com", 1883, subscribeTopic: "ecan/rx", publishTopic: "ecan/tx"), new CanChannelConfig { Index = 0, BaudRate = CanBaudRate.Baud500K }); await device.ConnectAsync(); await device.Channels[0].StartAsync(); // 发送 await device.Channels[0].SendAsync(CanFrame.Create(0x123, new byte[] { 0x11, 0x22 })); // 接收 await foreach (var frame in device.Channels[0].ReadAllAsync()) { Console.WriteLine(frame); } ``` > **传输层解耦**:以太网项目将网络连接(TCP/UDP/MQTT)抽象为 `IEthernetTransport` 接口,与 CAN 帧协议完全解耦,可灵活切换传输模式而无需修改业务代码。 > **单通道限制**:ECAN-E02 只有 1 路 CAN,且 13 字节协议帧不含通道号。构造函数强制要求**仅一个**通道配置,传入多个会抛 `ArgumentException`。 > > 传输层接收循环若因网络异常终止,会通过 `ReadAllAsync` 抛出 `CanChannelException`(而不是静默地「没有数据」)。 ## 打包与发布 各库项目已启用 `GeneratePackageOnBuild`,构建即生成 .nupkg。命令行可用 `-p:` 参数临时覆盖版本等元数据: ```bash # 打包并指定版本 dotnet pack CanLink/CanLink.csproj -c Release -p:Version=6.3.3 # 推送(CanLink.Core 是依赖包,需先推) dotnet nuget push artifacts/CanLink.Core.2.2.1.nupkg -k -s https://api.nuget.org/v3/index.json dotnet nuget push artifacts/CanLink.6.3.3.nupkg -k -s https://api.nuget.org/v3/index.json ``` > **不要** `dotnet pack CanLink.sln`——会把示例程序 NetConsoleDemo 也打成包。 ### 原生 DLL 部署机制 `CanLink` 引用的厂商原生 DLL(ControlCAN、ECanVci、USB2XXX 等)通过 `runtimes` 结构打包进 NuGet 包: ``` runtimes/win-x64/native/*.dll # 64 位原生库 runtimes/win-x86/native/*.dll # 32 位原生库 ``` - **NuGet 包消费方**:无需任何配置。构建时 SDK 按 RID 解析,运行时宿主根据进程位数自动加载对应目录的 DLL(由 `.deps.json` 条目驱动)。 - **解决方案内项目引用**(如 NetConsoleDemo 本地调试):不产生 `.deps.json` native 条目,CanLink.csproj 会按 `$(Platform)` 自动将正确位数的 DLL 平铺复制到输出根目录。 - `DllImport` 全部使用纯文件名(如 `"ControlCAN.dll"`),这是自动探测的前提,请勿改为带路径的形式。 ## 项目结构 ``` CanLink/ ├── CanLink.Core/ # 核心抽象(net8.0) ├── CanLink/ # 广成/图莫斯/创芯/智嵌(net8.0) ├── CanLink.PCan/ # PCAN(net8.0) ├── CanLink.SerialPort/ # 智嵌 WL 系列 串口 CAN(net8.0) ├── CanLink.Ethernet/ # 亿佰特 ECAN-E02 以太网 CAN(net8.0) │ └── Transports/ # 传输层(TCP/UDP/MQTT,基于 TouchSocket) └── NetConsoleDemo/ # 控制台示例 ``` ## 系统要求 - .NET 8.0 或更高版本 - Windows 操作系统 - 对应厂商的 CAN 设备驱动程序 ## 许可证 MIT License