feat: add CAN board test tool and timing safeguards
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user