feat: add CAN board test tool and timing safeguards
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# SDC-ACQ_DAC 板卡测试上位机
|
||||
|
||||
发布程序位于 `output/pc-test/SDC-ACQ_DAC-TestTool.exe`,目标为 Windows x86 和 ZLG USBCAN-II。
|
||||
|
||||
## 使用前准备
|
||||
|
||||
1. 将匹配的 ZLG x86 运行时复制到程序目录:`ControlCAN.dll`,以及 `kerneldlls/usbcan.dll` 和 `kerneldlls/kerneldll.ini`。
|
||||
2. 将 USB-CAN 与板卡 CAN1 或 CAN2 接在同一条 500 kbit/s 总线上,确认 CANH、CANL 和公共地正确连接。
|
||||
3. 关闭其他占用同一 USB-CAN 的软件后,启动程序,选择设备索引与 CAN 通道并点击“连接”。
|
||||
|
||||
## 测试能力
|
||||
|
||||
- 显示并记录 `0x6A0` 至 `0x6A6` 的实时报文;解码心跳、电压、轮速、GPIO 和 DAC 应答。
|
||||
- DAC1/DAC2 一次发送,或每 50 ms 持续发送。持续发送用于满足板卡每路 100 ms 的 DAC CAN 超时要求;停止后板卡将自行归零。
|
||||
- 一键发送 5 种 ASSI RGB 状态,以及标准帧手工发送。
|
||||
|
||||
CAN 适配器以正常模式打开,因此会为板卡的 CAN 上报帧提供 ACK。
|
||||
@@ -0,0 +1,6 @@
|
||||
<Application x:Class="SDCAcqDacTester.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources />
|
||||
</Application>
|
||||
@@ -0,0 +1,5 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace SDCAcqDacTester;
|
||||
|
||||
public partial class App : Application { }
|
||||
@@ -0,0 +1,130 @@
|
||||
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<CanFrame>? FrameReceived;
|
||||
public event EventHandler<string>? 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<byte> 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();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<Window x:Class="SDCAcqDacTester.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="SDC-ACQ_DAC 板卡测试工具" Height="820" Width="1230" MinHeight="680" MinWidth="1000"
|
||||
Closing="Window_Closing" Background="#F5F7FA">
|
||||
<Window.Resources>
|
||||
<Style TargetType="GroupBox"><Setter Property="Margin" Value="8"/><Setter Property="Padding" Value="10"/><Setter Property="BorderBrush" Value="#C8D2DC"/></Style>
|
||||
<Style TargetType="Button"><Setter Property="Margin" Value="3"/><Setter Property="Padding" Value="10,5"/></Style>
|
||||
<Style TargetType="TextBox"><Setter Property="Margin" Value="3"/><Setter Property="Padding" Value="5"/></Style>
|
||||
</Window.Resources>
|
||||
<DockPanel Margin="12">
|
||||
<Border DockPanel.Dock="Top" Background="#12324A" Padding="14" CornerRadius="5">
|
||||
<Grid><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<StackPanel><TextBlock Foreground="White" FontSize="22" FontWeight="SemiBold" Text="SDC-ACQ_DAC 板卡测试工具"/>
|
||||
<TextBlock Foreground="#C9D9E8" Margin="0,4,0,0" Text="CAN 500 kbit/s · 标准帧 · 协议 0x6A0 - 0x6A6"/></StackPanel>
|
||||
<TextBlock x:Name="ConnectionText" Grid.Column="1" VerticalAlignment="Center" Foreground="#FFCC66" FontSize="15" Text="未连接"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid Margin="0,8,0,0"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||||
<GroupBox Header="连接 - ZLG USBCAN-II(x86 ControlCAN.dll)">
|
||||
<StackPanel Orientation="Horizontal"><TextBlock VerticalAlignment="Center" Text="设备索引"/><TextBox x:Name="DeviceIndexBox" Width="45" Text="0"/>
|
||||
<TextBlock VerticalAlignment="Center" Margin="10,0,0,0" Text="通道"/><ComboBox x:Name="ChannelBox" Width="80" SelectedIndex="0"><ComboBoxItem Content="CAN1"/><ComboBoxItem Content="CAN2"/></ComboBox>
|
||||
<Button x:Name="ConnectButton" Click="ConnectButton_Click" Content="连接" Background="#2E7D32" Foreground="White"/><Button x:Name="DisconnectButton" Click="DisconnectButton_Click" Content="断开" IsEnabled="False"/>
|
||||
<TextBlock Margin="16,0,0,0" VerticalAlignment="Center" Foreground="#556575" Text="使用正常模式,适配器会 ACK 板卡上报帧。"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<Grid Grid.Row="1"><Grid.ColumnDefinitions><ColumnDefinition Width="2*"/><ColumnDefinition Width="1.45*"/><ColumnDefinition Width="1.2*"/></Grid.ColumnDefinitions>
|
||||
<GroupBox Header="DAC 控制 - 0x6A3"><Grid><Grid.RowDefinitions><RowDefinition/><RowDefinition/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0"><Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="85"/><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" Text="DAC1 (V)"/><TextBox x:Name="Dac1Box" Grid.Column="1" Text="0"/><Button Grid.Column="2" Click="SendDac1Once_Click" Content="发送一次"/><Button x:Name="HoldDac1Button" Grid.Column="3" Click="HoldDac1Button_Click" Content="开始保持"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1"><Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="85"/><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" Text="DAC2 (V)"/><TextBox x:Name="Dac2Box" Grid.Column="1" Text="0"/><Button Grid.Column="2" Click="SendDac2Once_Click" Content="发送一次"/><Button x:Name="HoldDac2Button" Grid.Column="3" Click="HoldDac2Button_Click" Content="开始保持"/>
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="2" Margin="0,8,0,0" Foreground="#8A5200" TextWrapping="Wrap" Text="“保持”每 50 ms 发送一次,满足板卡 DAC 100 ms 超时要求;停止后,板卡将在最多 100 ms 内自动归零。"/>
|
||||
</Grid></GroupBox>
|
||||
<GroupBox Grid.Column="1" Header="ASSI RGB 状态 - 0x6A5"><WrapPanel>
|
||||
<Button Click="Assi_Click" Tag="0" Content="关灯"/><Button Click="Assi_Click" Tag="1" Content="黄常亮" Background="#D8A500"/>
|
||||
<Button Click="Assi_Click" Tag="2" Content="黄闪 2 Hz" Background="#D8A500"/><Button Click="Assi_Click" Tag="3" Content="蓝闪 2 Hz" Background="#2E6DB4" Foreground="White"/>
|
||||
<Button Click="Assi_Click" Tag="4" Content="蓝常亮" Background="#2E6DB4" Foreground="White"/>
|
||||
</WrapPanel></GroupBox>
|
||||
<GroupBox Grid.Column="2" Header="手工帧发送"><StackPanel Orientation="Horizontal"><TextBox x:Name="RawIdBox" Width="72" Text="6A5" ToolTip="标准帧 ID,十六进制"/>
|
||||
<TextBox x:Name="RawDataBox" Width="115" Text="00" ToolTip="字节,十六进制,以空格分隔"/><Button Click="SendRaw_Click" Content="发送"/>
|
||||
</StackPanel></GroupBox>
|
||||
</Grid>
|
||||
<Grid Grid.Row="2"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||||
<UniformGrid Rows="1" Columns="5" Margin="8"><Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="心跳 0x6A0" Foreground="#52606D"/><TextBlock x:Name="HeartbeatValue" FontSize="17" Text="--"/></StackPanel></Border>
|
||||
<Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="输入电压 0x6A1" Foreground="#52606D"/><TextBlock x:Name="AdcValue" FontSize="17" Text="--"/></StackPanel></Border>
|
||||
<Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="轮速 0x6A2" Foreground="#52606D"/><TextBlock x:Name="PwmValue" FontSize="17" Text="--" TextWrapping="Wrap"/></StackPanel></Border>
|
||||
<Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="GPIO 0x6A6" Foreground="#52606D"/><TextBlock x:Name="GpioValue" FontSize="17" Text="--"/></StackPanel></Border>
|
||||
<Border Background="White" Padding="9" BorderBrush="#D5DDE5" BorderThickness="1"><StackPanel><TextBlock Text="DAC 应答 0x6A4" Foreground="#52606D"/><TextBlock x:Name="DacAckValue" FontSize="17" Text="--"/></StackPanel></Border>
|
||||
</UniformGrid>
|
||||
<GroupBox Grid.Row="1" Header="CAN 收发记录"><DataGrid x:Name="LogGrid" IsReadOnly="True" AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column"><DataGrid.Columns>
|
||||
<DataGridTextColumn Header="时间" Binding="{Binding Time}" Width="135"/><DataGridTextColumn Header="方向" Binding="{Binding Direction}" Width="65"/><DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="70"/>
|
||||
<DataGridTextColumn Header="DLC" Binding="{Binding Dlc}" Width="50"/><DataGridTextColumn Header="数据" Binding="{Binding Data}" Width="180"/><DataGridTextColumn Header="解释" Binding="{Binding Description}" Width="*"/>
|
||||
</DataGrid.Columns></DataGrid></GroupBox>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,194 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Threading;
|
||||
using SDCAcqDacTester.Can;
|
||||
|
||||
namespace SDCAcqDacTester;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private const uint HeartbeatId = 0x6A0;
|
||||
private const uint AdcId = 0x6A1;
|
||||
private const uint PwmId = 0x6A2;
|
||||
private const uint DacCommandId = 0x6A3;
|
||||
private const uint DacAckId = 0x6A4;
|
||||
private const uint AssiCommandId = 0x6A5;
|
||||
private const uint GpioId = 0x6A6;
|
||||
|
||||
private readonly ObservableCollection<LogRow> _rows = [];
|
||||
private readonly DispatcherTimer _holdTimer;
|
||||
private ControlCanTransport? _can;
|
||||
private bool _holdDac1;
|
||||
private bool _holdDac2;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
LogGrid.ItemsSource = _rows;
|
||||
_holdTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
|
||||
_holdTimer.Tick += HoldTimer_Tick;
|
||||
}
|
||||
|
||||
private void ConnectButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!uint.TryParse(DeviceIndexBox.Text, out uint index)) throw new InvalidOperationException("设备索引必须为非负整数。");
|
||||
_can = new ControlCanTransport();
|
||||
_can.FrameReceived += Can_FrameReceived;
|
||||
_can.ReceiveError += Can_ReceiveError;
|
||||
_can.Open(index, (uint)ChannelBox.SelectedIndex);
|
||||
ConnectionText.Text = $"已连接:USBCAN-II / CAN{ChannelBox.SelectedIndex + 1}";
|
||||
ConnectionText.Foreground = System.Windows.Media.Brushes.LightGreen;
|
||||
ConnectButton.IsEnabled = false;
|
||||
DisconnectButton.IsEnabled = true;
|
||||
AddLog("SYS", "-", [], "已连接,500 kbit/s 正常模式。");
|
||||
}
|
||||
catch (Exception ex) { ShowError(ex.Message); }
|
||||
}
|
||||
|
||||
private void DisconnectButton_Click(object sender, RoutedEventArgs e) => Disconnect();
|
||||
|
||||
private void SendDac1Once_Click(object sender, RoutedEventArgs e) => SendDac(1, ReadVolts(Dac1Box));
|
||||
private void SendDac2Once_Click(object sender, RoutedEventArgs e) => SendDac(2, ReadVolts(Dac2Box));
|
||||
|
||||
private void HoldDac1Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_holdDac1 = !_holdDac1;
|
||||
HoldDac1Button.Content = _holdDac1 ? "停止保持" : "开始保持";
|
||||
UpdateHoldTimer();
|
||||
}
|
||||
|
||||
private void HoldDac2Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_holdDac2 = !_holdDac2;
|
||||
HoldDac2Button.Content = _holdDac2 ? "停止保持" : "开始保持";
|
||||
UpdateHoldTimer();
|
||||
}
|
||||
|
||||
private void HoldTimer_Tick(object? sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_holdDac1) SendDac(1, ReadVolts(Dac1Box));
|
||||
if (_holdDac2) SendDac(2, ReadVolts(Dac2Box));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_holdDac1 = _holdDac2 = false;
|
||||
HoldDac1Button.Content = HoldDac2Button.Content = "开始保持";
|
||||
UpdateHoldTimer();
|
||||
ShowError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void Assi_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button { Tag: string value } || !byte.TryParse(value, out byte state)) return;
|
||||
Send(AssiCommandId, [state], $"ASSI 状态={state}");
|
||||
}
|
||||
|
||||
private void SendRaw_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
uint id = uint.Parse(RawIdBox.Text.Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||
byte[] data = string.IsNullOrWhiteSpace(RawDataBox.Text) ? [] : RawDataBox.Text.Split([' ', ',', ';'], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => byte.Parse(x.Replace("0x", "", StringComparison.OrdinalIgnoreCase), NumberStyles.HexNumber, CultureInfo.InvariantCulture)).ToArray();
|
||||
Send(id, data, "手工帧");
|
||||
}
|
||||
catch (Exception ex) { ShowError($"手工帧格式错误:{ex.Message}"); }
|
||||
}
|
||||
|
||||
private void SendDac(byte dac, ushort millivolts)
|
||||
{
|
||||
Send(DacCommandId, [dac, (byte)(millivolts & 0xFF), (byte)(millivolts >> 8)], $"DAC{dac}={millivolts} mV");
|
||||
}
|
||||
|
||||
private void Send(uint id, byte[] data, string description)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_can is not { IsOpen: true }) throw new InvalidOperationException("请先连接 CAN。 ");
|
||||
_can.SendStandard(id, data);
|
||||
AddLog("TX", $"0x{id:X3}", data, description);
|
||||
}
|
||||
catch (Exception ex) { ShowError(ex.Message); }
|
||||
}
|
||||
|
||||
private static ushort ReadVolts(TextBox box)
|
||||
{
|
||||
if (!decimal.TryParse(box.Text, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal volts) &&
|
||||
!decimal.TryParse(box.Text, NumberStyles.Number, CultureInfo.CurrentCulture, out volts))
|
||||
throw new InvalidOperationException("DAC 电压必须是 0 - 5 V 的数值。");
|
||||
if (volts is < 0 or > 5) throw new InvalidOperationException("DAC 电压范围为 0 - 5 V。");
|
||||
return (ushort)Math.Round(volts * 1000m, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private void Can_FrameReceived(object? sender, CanFrame frame) => Dispatcher.Invoke(() =>
|
||||
{
|
||||
string description = Decode(frame);
|
||||
AddLog("RX", frame.IsExtended ? $"0x{frame.Id:X8}" : $"0x{frame.Id:X3}", frame.Data, description, frame.Timestamp);
|
||||
});
|
||||
|
||||
private void Can_ReceiveError(object? sender, string error) => Dispatcher.Invoke(() => ShowError($"接收异常:{error}"));
|
||||
|
||||
private string Decode(CanFrame frame)
|
||||
{
|
||||
var d = frame.Data;
|
||||
switch (frame.Id)
|
||||
{
|
||||
case HeartbeatId when d.Length >= 8:
|
||||
string heartbeat = $"板号=0x{d[0]:X2} 协议={d[1]} 序号={U16(d, 2)} 运行={U32(d, 4)} ms";
|
||||
HeartbeatValue.Text = heartbeat; return heartbeat;
|
||||
case AdcId when d.Length >= 2:
|
||||
string adc = $"{U16(d, 0)} mV ({U16(d, 0) / 1000.0:F3} V)";
|
||||
AdcValue.Text = adc; return adc;
|
||||
case PwmId when d.Length >= 8:
|
||||
string pwm = string.Join(" / ", Enumerable.Range(0, 4).Select(i => $"{U16(d, i * 2)} Hz"));
|
||||
PwmValue.Text = pwm; return pwm;
|
||||
case DacAckId when d.Length >= 4:
|
||||
string ack = $"DAC{d[1]} {(d[0] == 1 ? "OK" : "ERR")} = {U16(d, 2)} mV";
|
||||
DacAckValue.Text = ack; return ack;
|
||||
case GpioId when d.Length >= 8:
|
||||
string gpio = $"状态=0x{U16(d, 0):X4} 变化=0x{U16(d, 2):X4} 时间={U32(d, 4)} ms";
|
||||
GpioValue.Text = gpio; return gpio;
|
||||
default: return "未定义帧";
|
||||
}
|
||||
}
|
||||
|
||||
private static ushort U16(byte[] data, int offset) => (ushort)(data[offset] | (data[offset + 1] << 8));
|
||||
private static uint U32(byte[] data, int offset) => (uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24));
|
||||
|
||||
private void AddLog(string direction, string id, byte[] data, string description, DateTime? timestamp = null)
|
||||
{
|
||||
_rows.Insert(0, new LogRow((timestamp ?? DateTime.Now).ToString("HH:mm:ss.fff"), direction, id, data.Length.ToString(), BitConverter.ToString(data).Replace('-', ' '), description));
|
||||
while (_rows.Count > 500) _rows.RemoveAt(_rows.Count - 1);
|
||||
}
|
||||
|
||||
private void UpdateHoldTimer()
|
||||
{
|
||||
if (_holdDac1 || _holdDac2) _holdTimer.Start(); else _holdTimer.Stop();
|
||||
}
|
||||
|
||||
private void Disconnect()
|
||||
{
|
||||
_holdDac1 = _holdDac2 = false;
|
||||
_holdTimer.Stop();
|
||||
_can?.Dispose(); _can = null;
|
||||
ConnectionText.Text = "未连接"; ConnectionText.Foreground = System.Windows.Media.Brushes.Orange;
|
||||
ConnectButton.IsEnabled = true; DisconnectButton.IsEnabled = false;
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
AddLog("ERR", "-", [], message);
|
||||
MessageBox.Show(message, "SDC-ACQ_DAC 测试工具", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
|
||||
private void Window_Closing(object? sender, System.ComponentModel.CancelEventArgs e) => Disconnect();
|
||||
}
|
||||
|
||||
public sealed record LogRow(string Time, string Direction, string Id, string Dlc, string Data, string Description);
|
||||
@@ -0,0 +1,9 @@
|
||||
# ZLG USBCAN-II driver files
|
||||
|
||||
Copy the complete matching x86 vendor runtime into this directory before using hardware CAN:
|
||||
|
||||
- `ControlCAN.dll` beside `SDC-ACQ_DAC-TestTool.exe`
|
||||
- `kerneldlls\\usbcan.dll`
|
||||
- `kerneldlls\\kerneldll.ini`
|
||||
|
||||
Do not run another CAN application against the same adapter at the same time.
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Platforms>x86</Platforms>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>SDC-ACQ_DAC-TestTool</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Native\\x86\\README.md" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user