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 的交互逻辑
|
/// </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));
|
|
public bool IsMaintenanceMode
|
{
|
get => (bool)GetValue(IsMaintenanceModeProperty);
|
set => SetValue(IsMaintenanceModeProperty, value);
|
}
|
|
/// <summary>
|
/// 命令执行事件
|
/// </summary>
|
public event EventHandler<DebugCommand>? OnCommandExecute;
|
|
/// <summary>
|
/// 接收帧列表
|
/// </summary>
|
public ObservableCollection<CanFrameDisplay> ReceivedFrames { get; } = new();
|
|
/// <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; }
|
|
public CanDebugPanel()
|
{
|
InitializeComponent();
|
receiveList.ItemsSource = ReceivedFrames;
|
InitializePollTimer();
|
Loaded += CanDebugPanel_Loaded;
|
}
|
|
private void CanDebugPanel_Loaded(object sender, RoutedEventArgs e)
|
{
|
Loaded -= CanDebugPanel_Loaded;
|
|
// 从设备信息初始化总线/节点输入
|
if (!string.IsNullOrEmpty(InitialBusName))
|
{
|
busNameInput.Text = InitialBusName;
|
}
|
if (InitialNodeIndex.HasValue)
|
{
|
nodeIndexInput.Text = InitialNodeIndex.Value.ToString();
|
}
|
|
// 自动连接
|
if (AutoConnectOnLoad)
|
{
|
Dispatcher.BeginInvoke(new Action(() =>
|
{
|
try { DoConnect(); }
|
catch (Exception ex) { log.Warn($"自动连接失败: {ex.Message}"); }
|
}), DispatcherPriority.Background);
|
}
|
}
|
|
private void UpdateDeviceIdDisplay()
|
{
|
if (!string.IsNullOrEmpty(_deviceId) && busNameInput != null)
|
{
|
// 保留默认总线名,仅记录 deviceId 以供日志使用
|
log.Info($"[CanDebugPanel] 关联设备: {_deviceId}");
|
}
|
}
|
|
/// <summary>
|
/// 初始化轮询定时器
|
/// </summary>
|
private void InitializePollTimer()
|
{
|
_pollTimer = new DispatcherTimer
|
{
|
Interval = TimeSpan.FromMilliseconds(100) // 100ms 轮询一次
|
};
|
_pollTimer.Tick += PollTimer_Tick;
|
}
|
|
/// <summary>
|
/// 轮询定时器回调 - 仅更新统计信息(帧显示由事件驱动)
|
/// </summary>
|
private void PollTimer_Tick(object? sender, EventArgs e)
|
{
|
if (!_isConnected || _driver == null) return;
|
|
try
|
{
|
// 更新统计
|
UpdateStats();
|
}
|
catch (Exception ex)
|
{
|
log.Error($"轮询统计更新失败: {ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 连接按钮点击
|
/// </summary>
|
private void ConnectBtn_Click(object sender, RoutedEventArgs e)
|
{
|
try
|
{
|
DoConnect();
|
}
|
catch (Exception ex)
|
{
|
log.Error($"CAN 连接失败: {ex.Message}", ex);
|
MessageBox.Show($"连接失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
UpdateConnectionStatus(false);
|
}
|
}
|
|
/// <summary>
|
/// 实际连接逻辑(从 UI 或自动连接调用)
|
/// </summary>
|
private void DoConnect()
|
{
|
if (_isConnected) return;
|
|
string busName = busNameInput.Text.Trim();
|
if (!int.TryParse(nodeIndexInput.Text.Trim(), out int nodeIndex))
|
{
|
throw new ArgumentException("节点编号格式错误");
|
}
|
|
// 优先使用 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)
|
{
|
maintenanceToggleBtn.IsChecked = true;
|
maintenanceToggleBtn.IsEnabled = true;
|
}
|
|
// 更新 UI
|
UpdateConnectionStatus(true);
|
_pollTimer?.Start();
|
|
log.Info($"CAN 驱动已连接: Bus={busName}, Node={nodeIndex}, DeviceId={deviceId}");
|
}
|
|
/// <summary>
|
/// 驱动帧接收事件处理 - 即时显示接收到的帧
|
/// </summary>
|
private void OnDriverFrameReceived(object? sender, CanSimulator.CanFrame frame)
|
{
|
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);
|
}
|
}
|
|
/// <summary>
|
/// 断开按钮点击
|
/// </summary>
|
private void DisconnectBtn_Click(object sender, RoutedEventArgs e)
|
{
|
try
|
{
|
_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);
|
return;
|
}
|
|
if (!_isConnected || _driver == null)
|
{
|
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);
|
}
|
}
|
}
|
}
|