| | |
| | | using Aging.Infrastructure.Drivers; |
| | | using log4net; |
| | | using Plugin.DeviceDebug.Entity; |
| | | using System; |
| | | using System.Collections.ObjectModel; |
| | | using System.Globalization; |
| | | using System.Linq; |
| | | using System.Threading; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Media; |
| | | using System.Windows.Threading; |
| | | |
| | | namespace Plugin.DeviceDebug.Panels |
| | | { |
| | | /// <summary> |
| | | /// CAN 帧显示模型 |
| | | /// </summary> |
| | | public class CanFrameDisplay |
| | | { |
| | | public DateTime Timestamp { get; set; } |
| | | public string FrameId { get; set; } = string.Empty; |
| | | public int Dlc { get; set; } |
| | | public string Data { get; set; } = string.Empty; |
| | | public string Direction { get; set; } = string.Empty; // TX / RX |
| | | public string Source { get; set; } = string.Empty; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// CanDebugPanel.xaml 的交互逻辑 |
| | | /// CAN 调试面板多通道壳:以 TabControl 承载若干 <see cref="CanChannelView"/>, |
| | | /// 支持同屏并列多个 CAN 通道独立收发(验收场景 4)。 |
| | | /// 对外保持与旧单面板一致的属性契约(DeviceId/InitialBusName/InitialNodeIndex/ |
| | | /// AutoConnectOnLoad/ExistingDriver/IsMaintenanceMode/OnCommandExecute), |
| | | /// DeviceDebug 选设备时这些配置被转发到"设备通道"(首个 Tab)。 |
| | | /// </summary> |
| | | public partial class CanDebugPanel : UserControl |
| | | { |
| | | private static readonly ILog log = LogManager.GetLogger(typeof(CanDebugPanel)); |
| | | |
| | | /// <summary> |
| | | /// 是否为维护模式(控制输入可用性) |
| | | /// </summary> |
| | | public static readonly DependencyProperty IsMaintenanceModeProperty = |
| | | DependencyProperty.Register(nameof(IsMaintenanceMode), typeof(bool), typeof(CanDebugPanel), |
| | | new PropertyMetadata(false)); |
| | | new PropertyMetadata(false, OnMaintenanceModeChanged)); |
| | | |
| | | public bool IsMaintenanceMode |
| | | { |
| | |
| | | set => SetValue(IsMaintenanceModeProperty, value); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 命令执行事件 |
| | | /// </summary> |
| | | private static void OnMaintenanceModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) |
| | | { |
| | | var panel = (CanDebugPanel)d; |
| | | if (panel._deviceChannel != null) |
| | | panel._deviceChannel.IsMaintenanceMode = (bool)e.NewValue; |
| | | } |
| | | |
| | | /// <summary>命令执行事件(任一通道触发均上抛,供 DeviceDebug 统一处理)</summary> |
| | | public event EventHandler<DebugCommand>? OnCommandExecute; |
| | | |
| | | /// <summary> |
| | | /// 接收帧列表 |
| | | /// </summary> |
| | | public ObservableCollection<CanFrameDisplay> ReceivedFrames { get; } = new(); |
| | | private CanChannelView? _deviceChannel; |
| | | private int _manualSeq; |
| | | |
| | | /// <summary> |
| | | /// CAN 驱动适配器 |
| | | /// </summary> |
| | | private CanDriverAdapter? _driver; |
| | | |
| | | /// <summary> |
| | | /// 轮询定时器(用于检查接收邮箱) |
| | | /// </summary> |
| | | private DispatcherTimer? _pollTimer; |
| | | |
| | | /// <summary> |
| | | /// 是否已连接 |
| | | /// </summary> |
| | | private bool _isConnected; |
| | | |
| | | /// <summary> |
| | | /// 设备ID(从 DeviceDebug 传入,面板自动拼接 deviceId) |
| | | /// </summary> |
| | | public string DeviceId |
| | | { |
| | | get => _deviceId; |
| | | set |
| | | { |
| | | _deviceId = value; |
| | | if (!string.IsNullOrEmpty(value)) |
| | | { |
| | | // 将设备ID前缀拼接到默认设备标识中,避免与模拟器面板重名 |
| | | UpdateDeviceIdDisplay(); |
| | | } |
| | | } |
| | | } |
| | | // 待转发到设备通道的配置(可能早于通道创建) |
| | | private string _deviceId = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 从设备 Endpoint 解析出的总线名称 |
| | | /// </summary> |
| | | public string InitialBusName { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 从设备 Endpoint 解析出的节点编号 |
| | | /// </summary> |
| | | public int? InitialNodeIndex { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 是否自动连接(从 DeviceDebug 打开时=true) |
| | | /// </summary> |
| | | public bool AutoConnectOnLoad { get; set; } |
| | | private string _initialBusName; |
| | | private int? _initialNodeIndex; |
| | | private bool _autoConnectOnLoad; |
| | | private Aging.Infrastructure.Contracts.IDeviceDriver? _existingDriver; |
| | | |
| | | public CanDebugPanel() |
| | | { |
| | | InitializeComponent(); |
| | | receiveList.ItemsSource = ReceivedFrames; |
| | | InitializePollTimer(); |
| | | Loaded += CanDebugPanel_Loaded; |
| | | } |
| | | |
| | | private void CanDebugPanel_Loaded(object sender, RoutedEventArgs e) |
| | | /// <summary>设备ID(转发到设备通道)</summary> |
| | | public string DeviceId |
| | | { |
| | | Loaded -= CanDebugPanel_Loaded; |
| | | |
| | | // 从设备信息初始化总线/节点输入 |
| | | if (!string.IsNullOrEmpty(InitialBusName)) |
| | | { |
| | | busNameInput.Text = InitialBusName; |
| | | } |
| | | if (InitialNodeIndex.HasValue) |
| | | { |
| | | nodeIndexInput.Text = InitialNodeIndex.Value.ToString(); |
| | | get => _deviceId; |
| | | set { _deviceId = value; EnsureDeviceChannel().DeviceId = value; } |
| | | } |
| | | |
| | | // 自动连接 |
| | | if (AutoConnectOnLoad) |
| | | /// <summary>总线名称(转发到设备通道)</summary> |
| | | public string InitialBusName |
| | | { |
| | | Dispatcher.BeginInvoke(new Action(() => |
| | | { |
| | | try { DoConnect(); } |
| | | catch (Exception ex) { log.Warn($"自动连接失败: {ex.Message}"); } |
| | | }), DispatcherPriority.Background); |
| | | } |
| | | get => _initialBusName; |
| | | set { _initialBusName = value; EnsureDeviceChannel().InitialBusName = value; } |
| | | } |
| | | |
| | | private void UpdateDeviceIdDisplay() |
| | | /// <summary>节点编号(转发到设备通道)</summary> |
| | | public int? InitialNodeIndex |
| | | { |
| | | if (!string.IsNullOrEmpty(_deviceId) && busNameInput != null) |
| | | { |
| | | // 保留默认总线名,仅记录 deviceId 以供日志使用 |
| | | log.Info($"[CanDebugPanel] 关联设备: {_deviceId}"); |
| | | } |
| | | get => _initialNodeIndex; |
| | | set { _initialNodeIndex = value; EnsureDeviceChannel().InitialNodeIndex = value; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 初始化轮询定时器 |
| | | /// </summary> |
| | | private void InitializePollTimer() |
| | | /// <summary>加载即自动连接(转发到设备通道)</summary> |
| | | public bool AutoConnectOnLoad |
| | | { |
| | | _pollTimer = new DispatcherTimer |
| | | { |
| | | Interval = TimeSpan.FromMilliseconds(100) // 100ms 轮询一次 |
| | | }; |
| | | _pollTimer.Tick += PollTimer_Tick; |
| | | get => _autoConnectOnLoad; |
| | | set { _autoConnectOnLoad = value; EnsureDeviceChannel().AutoConnectOnLoad = value; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 轮询定时器回调 - 仅更新统计信息(帧显示由事件驱动) |
| | | /// </summary> |
| | | private void PollTimer_Tick(object? sender, EventArgs e) |
| | | /// <summary>外部已注册驱动(转发到设备通道,共享设备复用宿主通路)</summary> |
| | | public Aging.Infrastructure.Contracts.IDeviceDriver? ExistingDriver |
| | | { |
| | | if (!_isConnected || _driver == null) return; |
| | | |
| | | try |
| | | { |
| | | // 更新统计 |
| | | UpdateStats(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"轮询统计更新失败: {ex.Message}"); |
| | | } |
| | | get => _existingDriver; |
| | | set { _existingDriver = value; EnsureDeviceChannel().ExistingDriver = value; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 连接按钮点击 |
| | | /// </summary> |
| | | private void ConnectBtn_Click(object sender, RoutedEventArgs e) |
| | | /// <summary>惰性创建"设备通道"(首个 Tab),并挂接命令事件与当前维护态。</summary> |
| | | private CanChannelView EnsureDeviceChannel() |
| | | { |
| | | try |
| | | { |
| | | DoConnect(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"CAN 连接失败: {ex.Message}", ex); |
| | | MessageBox.Show($"连接失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | UpdateConnectionStatus(false); |
| | | } |
| | | if (_deviceChannel != null) return _deviceChannel; |
| | | |
| | | _deviceChannel = new CanChannelView(); |
| | | WireChannel(_deviceChannel, MakeHeader(_deviceId, _initialBusName, _initialNodeIndex)); |
| | | // 应用已到达的维护态 |
| | | _deviceChannel.IsMaintenanceMode = IsMaintenanceMode; |
| | | var devTab = MakeTab(_deviceChannel); |
| | | devTab.Header = MakeHeader(_deviceId, _initialBusName, _initialNodeIndex); |
| | | channelTabs.Items.Add(devTab); |
| | | return _deviceChannel; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 实际连接逻辑(从 UI 或自动连接调用) |
| | | /// </summary> |
| | | private void DoConnect() |
| | | private void WireChannel(CanChannelView view, string header) |
| | | { |
| | | if (_isConnected) return; |
| | | |
| | | string busName = busNameInput.Text.Trim(); |
| | | if (!int.TryParse(nodeIndexInput.Text.Trim(), out int nodeIndex)) |
| | | { |
| | | throw new ArgumentException("节点编号格式错误"); |
| | | view.OnCommandExecute += (s, e) => OnCommandExecute?.Invoke(this, e); |
| | | } |
| | | |
| | | // 优先使用 DeviceDebug 传入的设备 ID,否则自动生成一个 |
| | | string deviceId = string.IsNullOrEmpty(_deviceId) |
| | | ? $"CAN_Test_{nodeIndex}_{DateTime.Now:HHmmss}" |
| | | : _deviceId; |
| | | |
| | | _driver = new CanDriverAdapter(deviceId, busName, nodeIndex); |
| | | |
| | | // 订阅驱动的帧接收事件(即时显示接收到的帧) |
| | | _driver.DriverFrameReceived += OnDriverFrameReceived; |
| | | |
| | | // 连接 |
| | | _driver.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult(); |
| | | _isConnected = true; |
| | | |
| | | // 模拟器模式下,连接后自动启用维护模式(允许发送) |
| | | IsMaintenanceMode = true; |
| | | if (maintenanceToggleBtn != null) |
| | | private TabItem MakeTab(CanChannelView view) |
| | | { |
| | | maintenanceToggleBtn.IsChecked = true; |
| | | maintenanceToggleBtn.IsEnabled = true; |
| | | // 复用视图自身的标题区不显示设备名,这里用 Tab 头标注通道身份 |
| | | return new TabItem { Content = view }; |
| | | } |
| | | |
| | | // 更新 UI |
| | | UpdateConnectionStatus(true); |
| | | _pollTimer?.Start(); |
| | | |
| | | log.Info($"CAN 驱动已连接: Bus={busName}, Node={nodeIndex}, DeviceId={deviceId}"); |
| | | private static string MakeHeader(string deviceId, string bus, int? node) |
| | | { |
| | | string id = string.IsNullOrEmpty(deviceId) ? "通道" : deviceId; |
| | | if (!string.IsNullOrEmpty(bus)) |
| | | return $"{id} @{bus}:{node}"; |
| | | return id; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 驱动帧接收事件处理 - 即时显示接收到的帧 |
| | | /// </summary> |
| | | private void OnDriverFrameReceived(object? sender, CanSimulator.CanFrame frame) |
| | | /// <summary>手动添加一个可编辑的空白通道(Local TCP 直连,用户自填总线/节点后连接)。</summary> |
| | | private void AddChannelBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | // 判断方向:如果帧的来源是当前节点,则为自动响应(显示为 RX) |
| | | string direction = "RX"; |
| | | string source = frame.SourceNodeId; |
| | | |
| | | AddReceivedFrame( |
| | | frame.FrameIdHex, |
| | | frame.DataHex, |
| | | direction, |
| | | source |
| | | ); |
| | | |
| | | log.Debug($"[即时显示] 接收帧: {frame}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"即时显示帧失败: {ex.Message}", ex); |
| | | } |
| | | var view = new CanChannelView { AutoConnectOnLoad = false }; |
| | | _manualSeq++; |
| | | string header = $"CAN 通道 #{_manualSeq}"; // 手动通道具体总线/节点由用户页内填写,标签不预设 |
| | | WireChannel(view, header); |
| | | var tab = MakeTab(view); |
| | | tab.Header = header; |
| | | channelTabs.Items.Add(tab); |
| | | channelTabs.SelectedItem = tab; |
| | | log.Info($"[CanDebugPanel] 手动添加通道 {header}"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 断开按钮点击 |
| | | /// </summary> |
| | | private void DisconnectBtn_Click(object sender, RoutedEventArgs e) |
| | | /// <summary>关闭当前选中的通道(设备通道不可关闭,避免误退出 DeviceDebug 会话视图)。</summary> |
| | | private void CloseChannelBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | if (channelTabs.SelectedItem is not TabItem tab) return; |
| | | if (tab.Content == _deviceChannel) |
| | | { |
| | | _pollTimer?.Stop(); |
| | | if (_driver != null) |
| | | { |
| | | _driver.DriverFrameReceived -= OnDriverFrameReceived; |
| | | _driver.DisconnectAsync().GetAwaiter().GetResult(); |
| | | _driver.Dispose(); |
| | | } |
| | | _driver = null; |
| | | _isConnected = false; |
| | | IsMaintenanceMode = false; |
| | | |
| | | UpdateConnectionStatus(false); |
| | | log.Info("CAN 驱动已断开"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"CAN 断开失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 混杂模式按钮点击 |
| | | /// </summary> |
| | | private void PromiscuousBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | if (!_isConnected || _driver == null) return; |
| | | |
| | | try |
| | | { |
| | | _driver.QueryAsync("PROMISCUOUS", CancellationToken.None).GetAwaiter().GetResult(); |
| | | log.Info("已设置混杂模式(接收所有帧)"); |
| | | MessageBox.Show("已设置混杂模式,将接收所有 CAN 帧", "提示", MessageBoxButton.OK, MessageBoxImage.Information); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"设置混杂模式失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 维护模式切换按钮点击 |
| | | /// </summary> |
| | | private void MaintenanceToggleBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | bool isOn = maintenanceToggleBtn.IsChecked == true; |
| | | IsMaintenanceMode = isOn; |
| | | |
| | | if (isOn) |
| | | { |
| | | maintenanceToggleBtn.Content = "维护模式: 开"; |
| | | maintenanceToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")); |
| | | log.Info("维护模式已开启(可发送 CAN 帧)"); |
| | | } |
| | | else |
| | | { |
| | | maintenanceToggleBtn.Content = "维护模式: 关"; |
| | | maintenanceToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E")); |
| | | log.Info("维护模式已关闭(只读监控)"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 自动响应模式切换按钮点击 |
| | | /// </summary> |
| | | private void AutoResponseToggleBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | if (!_isConnected || _driver == null) return; |
| | | |
| | | try |
| | | { |
| | | bool isOn = autoResponseToggleBtn.IsChecked == true; |
| | | |
| | | // 通过驱动发送命令启用/禁用自动响应 |
| | | string cmd = isOn ? "AUTO_RESPONSE:ON" : "AUTO_RESPONSE:OFF"; |
| | | _driver.QueryAsync(cmd, CancellationToken.None).GetAwaiter().GetResult(); |
| | | |
| | | if (isOn) |
| | | { |
| | | autoResponseToggleBtn.Content = "自动响应: 开"; |
| | | autoResponseToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF9800")); |
| | | log.Info("自动响应模式已开启(收到帧将自动回复)"); |
| | | } |
| | | else |
| | | { |
| | | autoResponseToggleBtn.Content = "自动响应: 关"; |
| | | autoResponseToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E")); |
| | | log.Info("自动响应模式已关闭"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"切换自动响应模式失败: {ex.Message}"); |
| | | MessageBox.Show($"切换失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新连接状态显示 |
| | | /// </summary> |
| | | private void UpdateConnectionStatus(bool connected) |
| | | { |
| | | _isConnected = connected; |
| | | if (connected) |
| | | { |
| | | statusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")); |
| | | statusText.Text = "已连接"; |
| | | connectBtn.IsEnabled = false; |
| | | disconnectBtn.IsEnabled = true; |
| | | promiscuousBtn.IsEnabled = true; |
| | | maintenanceToggleBtn.IsEnabled = true; |
| | | autoResponseToggleBtn.IsEnabled = true; |
| | | busNameInput.IsEnabled = false; |
| | | nodeIndexInput.IsEnabled = false; |
| | | } |
| | | else |
| | | { |
| | | statusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E")); |
| | | statusText.Text = "未连接"; |
| | | connectBtn.IsEnabled = true; |
| | | disconnectBtn.IsEnabled = false; |
| | | promiscuousBtn.IsEnabled = false; |
| | | maintenanceToggleBtn.IsEnabled = false; |
| | | autoResponseToggleBtn.IsEnabled = false; |
| | | busNameInput.IsEnabled = true; |
| | | nodeIndexInput.IsEnabled = true; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新统计信息 |
| | | /// </summary> |
| | | private void UpdateStats() |
| | | { |
| | | if (_driver == null) return; |
| | | |
| | | try |
| | | { |
| | | var status = _driver.GetStatus(); |
| | | var response = _driver.QueryAsync("STATUS", CancellationToken.None).GetAwaiter().GetResult(); |
| | | // 解析状态: "STATUS:Node=..., Active=True, Rx=0, Tx=0, Err=0, Mailbox=0|Bus=..." |
| | | statsText.Text = $"TX: {GetStatValue(response, "Tx")} RX: {GetStatValue(response, "Rx")} Err: {GetStatValue(response, "Err")}"; |
| | | } |
| | | catch |
| | | { |
| | | // 忽略统计更新失败 |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 从状态字符串中提取数值值 |
| | | /// </summary> |
| | | private string GetStatValue(string statusStr, string key) |
| | | { |
| | | try |
| | | { |
| | | var parts = statusStr.Split(','); |
| | | foreach (var part in parts) |
| | | { |
| | | var kv = part.Trim().Split('='); |
| | | if (kv.Length == 2 && kv[0].Trim() == key) |
| | | { |
| | | return kv[1].Trim(); |
| | | } |
| | | } |
| | | } |
| | | catch { } |
| | | return "0"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 添加接收帧 |
| | | /// </summary> |
| | | public void AddReceivedFrame(string frameId, string data, string direction = "RX", string source = "") |
| | | { |
| | | try |
| | | { |
| | | var frame = new CanFrameDisplay |
| | | { |
| | | Timestamp = DateTime.Now, |
| | | FrameId = frameId, |
| | | Dlc = data.Split(new[] { ' ', '-' }, StringSplitOptions.RemoveEmptyEntries).Length, |
| | | Data = data, |
| | | Direction = direction, |
| | | Source = source |
| | | }; |
| | | |
| | | Application.Current.Dispatcher.Invoke(() => |
| | | { |
| | | ReceivedFrames.Insert(0, frame); |
| | | |
| | | // 限制显示数量 |
| | | if (ReceivedFrames.Count > 500) |
| | | { |
| | | var toRemove = ReceivedFrames.Skip(500).ToList(); |
| | | foreach (var item in toRemove) |
| | | { |
| | | ReceivedFrames.Remove(item); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"添加接收帧失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 发送按钮点击 |
| | | /// </summary> |
| | | private void SendBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (!IsMaintenanceMode) |
| | | { |
| | | MessageBox.Show("当前为只读模式,无法发送数据", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); |
| | | MessageBox.Show("设备通道不可在此关闭,请从设备选择切换。", "提示", |
| | | MessageBoxButton.OK, MessageBoxImage.Information); |
| | | return; |
| | | } |
| | | if (tab.Content is CanChannelView view) |
| | | view.DisconnectInternalSafe(); |
| | | channelTabs.Items.Remove(tab); |
| | | } |
| | | |
| | | if (!_isConnected || _driver == null) |
| | | /// <summary>更新设备通道 Tab 头(在配置齐备后调用一次即可)。</summary> |
| | | public void RefreshDeviceHeader() |
| | | { |
| | | MessageBox.Show("请先连接 CAN 总线", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); |
| | | return; |
| | | } |
| | | |
| | | string frameIdStr = frameIdInput.Text.Trim(); |
| | | string dataStr = dataInput.Text.Trim(); |
| | | |
| | | // 验证帧 ID |
| | | uint frameId = CanSimulator.CanFrame.ParseFrameId(frameIdStr); |
| | | |
| | | // 验证数据 |
| | | byte[] dataBytes = CanSimulator.CanFrame.ParseData(dataStr); |
| | | if (dataBytes.Length > 8) |
| | | { |
| | | MessageBox.Show("数据长度不能超过 8 字节", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | return; |
| | | } |
| | | |
| | | // 通过驱动发送 |
| | | string command = $"SEND:{frameIdStr}:{dataStr}"; |
| | | var response = _driver.SendCommandAsync(command, CancellationToken.None).GetAwaiter().GetResult(); |
| | | |
| | | // 添加到显示列表(TX) |
| | | AddReceivedFrame($"0x{frameId:X3}", BitConverter.ToString(dataBytes).Replace("-", " "), "TX", "LOCAL"); |
| | | |
| | | log.Info($"发送 CAN 帧: ID=0x{frameId:X3}, Data={BitConverter.ToString(dataBytes)}, Response={response}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"发送 CAN 帧失败: {ex.Message}", ex); |
| | | MessageBox.Show($"发送失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 清空按钮点击 |
| | | /// </summary> |
| | | private void ClearBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | ReceivedFrames.Clear(); |
| | | log.Info("清空 CAN 接收记录"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"清空接收记录失败: {ex.Message}", ex); |
| | | } |
| | | if (_deviceChannel == null || channelTabs.Items.Count == 0) return; |
| | | if (channelTabs.Items[0] is TabItem t) |
| | | t.Header = MakeHeader(_deviceId, _initialBusName, _initialNodeIndex); |
| | | } |
| | | } |
| | | } |