using System.Runtime.InteropServices; namespace SDCAcqDacTester.Can; public sealed record CanFrame(uint Id, bool IsExtended, byte[] Data, DateTime Timestamp); public sealed class ControlCanTransport : IDisposable { private const uint DeviceTypeUsbCan2 = 4; private const uint StatusOk = 1; private CancellationTokenSource? _receiveCancel; private Task? _receiveTask; private uint _deviceIndex; private uint _channelIndex; public bool IsOpen { get; private set; } public event EventHandler? FrameReceived; public event EventHandler? ReceiveError; [StructLayout(LayoutKind.Sequential)] private struct VciInitConfig { public uint AccCode; public uint AccMask; public uint Reserved; public byte Filter; public byte Timing0; public byte Timing1; public byte Mode; } [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct VciCanObj { public uint Id; public uint TimeStamp; public byte TimeFlag; public byte SendType; public byte RemoteFlag; public byte ExternFlag; public byte DataLen; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public byte[] Data; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] public byte[] Reserved; } [DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)] private static extern uint VCI_OpenDevice(uint deviceType, uint deviceIndex, uint reserved); [DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)] private static extern uint VCI_CloseDevice(uint deviceType, uint deviceIndex); [DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)] private static extern uint VCI_InitCAN(uint deviceType, uint deviceIndex, uint canIndex, ref VciInitConfig config); [DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)] private static extern uint VCI_StartCAN(uint deviceType, uint deviceIndex, uint canIndex); [DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)] private static extern uint VCI_ResetCAN(uint deviceType, uint deviceIndex, uint canIndex); [DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)] private static extern uint VCI_Transmit(uint deviceType, uint deviceIndex, uint canIndex, [In] VciCanObj[] send, uint length); [DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)] private static extern uint VCI_Receive(uint deviceType, uint deviceIndex, uint canIndex, [Out] VciCanObj[] receive, uint length, int waitTime); public void Open(uint deviceIndex, uint channelIndex) { if (IsOpen) return; _deviceIndex = deviceIndex; _channelIndex = channelIndex; if (VCI_OpenDevice(DeviceTypeUsbCan2, deviceIndex, 0) != StatusOk) throw new InvalidOperationException("打开 USBCAN-II 失败。请确认设备未被其他程序占用,并检查 ControlCAN.dll。 "); // SJA1000 timing for 500 kbit/s, normal mode so the adapter ACKs board frames. var config = new VciInitConfig { AccCode = 0, AccMask = 0xFFFFFFFF, Filter = 1, Timing0 = 0x00, Timing1 = 0x1C, Mode = 0 }; if (VCI_InitCAN(DeviceTypeUsbCan2, deviceIndex, channelIndex, ref config) != StatusOk) { VCI_CloseDevice(DeviceTypeUsbCan2, deviceIndex); throw new InvalidOperationException("初始化 CAN 失败(500 kbit/s)。"); } if (VCI_StartCAN(DeviceTypeUsbCan2, deviceIndex, channelIndex) != StatusOk) { VCI_ResetCAN(DeviceTypeUsbCan2, deviceIndex, channelIndex); VCI_CloseDevice(DeviceTypeUsbCan2, deviceIndex); throw new InvalidOperationException("启动 CAN 通道失败。"); } IsOpen = true; _receiveCancel = new CancellationTokenSource(); _receiveTask = Task.Run(() => ReceiveLoop(_receiveCancel.Token)); } public void SendStandard(uint id, ReadOnlySpan data) { if (!IsOpen) throw new InvalidOperationException("CAN 未连接。"); if (id > 0x7FF || data.Length > 8) throw new ArgumentOutOfRangeException(nameof(id)); var payload = new byte[8]; data.CopyTo(payload); var frame = new VciCanObj { Id = id, SendType = 0, RemoteFlag = 0, ExternFlag = 0, DataLen = (byte)data.Length, Data = payload, Reserved = new byte[3] }; if (VCI_Transmit(DeviceTypeUsbCan2, _deviceIndex, _channelIndex, [frame], 1) != StatusOk) throw new InvalidOperationException("CAN 发送失败。"); } private void ReceiveLoop(CancellationToken token) { var buffer = Enumerable.Range(0, 100).Select(_ => new VciCanObj { Data = new byte[8], Reserved = new byte[3] }).ToArray(); try { while (!token.IsCancellationRequested) { uint count = VCI_Receive(DeviceTypeUsbCan2, _deviceIndex, _channelIndex, buffer, (uint)buffer.Length, 50); if (count == 0 || count == uint.MaxValue) continue; for (var i = 0; i < Math.Min(count, (uint)buffer.Length); i++) { var item = buffer[i]; if (item.RemoteFlag != 0) continue; FrameReceived?.Invoke(this, new CanFrame(item.Id, item.ExternFlag != 0, item.Data.Take(Math.Min(item.DataLen, (byte)8)).ToArray(), DateTime.Now)); } } } catch (Exception ex) when (!token.IsCancellationRequested) { ReceiveError?.Invoke(this, ex.Message); } } public void Close() { if (!IsOpen) return; _receiveCancel?.Cancel(); _receiveTask?.Wait(300); VCI_ResetCAN(DeviceTypeUsbCan2, _deviceIndex, _channelIndex); VCI_CloseDevice(DeviceTypeUsbCan2, _deviceIndex); _receiveCancel?.Dispose(); _receiveCancel = null; IsOpen = false; } public void Dispose() => Close(); }