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>
|
/// 单通道 CAN 调试视图(承载连接控制/发送/接收记录/状态统计全部逻辑)。
|
/// 由 <see cref="CanDebugPanel"/> 多通道壳按 Tab 托管;也可独立使用(旧单面板语义)。
|
/// 驱动统一为 <see cref="CanDeviceDriver"/>,Local/Broker 差异对视图完全透明。
|
/// </summary>
|
public partial class CanChannelView : UserControl
|
{
|
private static readonly ILog log = LogManager.GetLogger(typeof(CanChannelView));
|
|
/// <summary>是否为维护模式(控制输入可用性)</summary>
|
public static readonly DependencyProperty IsMaintenanceModeProperty =
|
DependencyProperty.Register(nameof(IsMaintenanceMode), typeof(bool), typeof(CanChannelView),
|
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 驱动:优先复用 DeviceManager 已注册驱动(Broker 共享设备为代理驱动),
|
/// 无则自建统一驱动(Local TCP NodeChannel 连模拟器)
|
/// </summary>
|
private Aging.Infrastructure.Contracts.IDeviceDriver? _driver;
|
|
/// <summary>驱动所有权:外部注入的代理驱动归 DeviceManager,断开时不得 Dispose</summary>
|
private bool _ownsDriver;
|
|
/// <summary>外部已注册驱动(共享设备由 DeviceDebug 传入统一 CanDeviceDriver,禁止自建物理会话)</summary>
|
public Aging.Infrastructure.Contracts.IDeviceDriver? ExistingDriver { get; set; }
|
|
/// <summary>轮询定时器(代理驱动兜底补显接收帧)</summary>
|
private DispatcherTimer? _pollTimer;
|
|
private bool _isConnected;
|
|
/// <summary>设备ID(从 DeviceDebug 传入)</summary>
|
public string DeviceId
|
{
|
get => _deviceId;
|
set
|
{
|
_deviceId = value;
|
if (!string.IsNullOrEmpty(value))
|
{
|
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 CanChannelView()
|
{
|
InitializeComponent();
|
receiveList.ItemsSource = ReceivedFrames;
|
InitializePollTimer();
|
Loaded += CanChannelView_Loaded;
|
}
|
|
private void CanChannelView_Loaded(object sender, RoutedEventArgs e)
|
{
|
Loaded -= CanChannelView_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)
|
log.Info($"[CanChannelView] 关联设备: {_deviceId}");
|
}
|
|
private void InitializePollTimer()
|
{
|
_pollTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
|
_pollTimer.Tick += PollTimer_Tick;
|
}
|
|
private void PollTimer_Tick(object? sender, EventArgs e)
|
{
|
if (!_isConnected || _driver == null) return;
|
try
|
{
|
// 统计每 tick 刷新;STATUS 查询同时触发 Broker 宿主捎带回推(事件链路)
|
UpdateStats();
|
}
|
catch (Exception ex)
|
{
|
log.Error($"轮询更新失败: {ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 兜底补显(仅当事件链路未覆盖时手动调用):统一驱动已对 Local/Broker 均提供
|
/// DriverFrameReceived 事件,面板默认只走事件显示,避免与轮询双报重复。
|
/// </summary>
|
private void DrainBrokerMailbox()
|
{
|
string resp;
|
try
|
{
|
resp = _driver!.QueryAsync("RECV_ALL", CancellationToken.None).GetAwaiter().GetResult();
|
}
|
catch (Exception ex)
|
{
|
log.Debug($"接收兜底查询失败(不影响事件链路,下轮重试): {ex.Message}");
|
return;
|
}
|
|
if (string.IsNullOrEmpty(resp) || !resp.StartsWith("RECV_ALL:")) return;
|
var head = resp.Substring("RECV_ALL:".Length);
|
var split = head.IndexOf(':');
|
string body = split >= 0 ? head.Substring(split + 1) : "";
|
if (!int.TryParse(split >= 0 ? head.Substring(0, split) : head, out int count) || count <= 0) return;
|
|
foreach (var entry in body.Split('|'))
|
{
|
var f = entry.Split(':');
|
if (f.Length < 2 || string.IsNullOrEmpty(f[0])) continue;
|
AddReceivedFrame(f[0], f[1], "RX", "BUS");
|
}
|
}
|
|
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);
|
}
|
}
|
|
private void DoConnect()
|
{
|
if (_isConnected) return;
|
|
string busName = busNameInput.Text.Trim();
|
if (!int.TryParse(nodeIndexInput.Text.Trim(), out int nodeIndex))
|
throw new ArgumentException("节点编号格式错误");
|
|
string deviceId = string.IsNullOrEmpty(_deviceId)
|
? $"CAN_Test_{nodeIndex}_{DateTime.Now:HHmmss}"
|
: _deviceId;
|
|
if (ExistingDriver != null)
|
{
|
// 共享/代理设备:复用宿主通路,不新建本地物理会话
|
_driver = ExistingDriver;
|
_ownsDriver = false;
|
if (!_driver.IsConnected)
|
_driver.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult();
|
// 统一驱动对 Broker 也提供接收事件(经宿主捎带回流),与本地一致地订阅
|
if (_driver is CanDeviceDriver shared)
|
shared.DriverFrameReceived += OnDriverFrameReceived;
|
log.Info($"[CanChannelView] 复用已注册驱动: {deviceId} ({_driver.GetType().Name})");
|
}
|
else
|
{
|
var adapter = new CanDeviceDriver(deviceId, $"{busName}:{nodeIndex}");
|
adapter.DriverFrameReceived += OnDriverFrameReceived;
|
_driver = adapter;
|
_ownsDriver = true;
|
adapter.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult();
|
}
|
_isConnected = true;
|
|
// 模拟器模式下连接后自动启用维护模式(允许发送)
|
IsMaintenanceMode = true;
|
if (maintenanceToggleBtn != null)
|
{
|
maintenanceToggleBtn.IsChecked = true;
|
maintenanceToggleBtn.IsEnabled = true;
|
}
|
|
UpdateConnectionStatus(true);
|
_pollTimer?.Start();
|
log.Info($"CAN 驱动已连接: Bus={busName}, Node={nodeIndex}, DeviceId={deviceId}");
|
}
|
|
private void OnDriverFrameReceived(object? sender, CanSimulator.CanFrame frame)
|
{
|
try
|
{
|
AddReceivedFrame(frame.FrameIdHex, frame.DataHex, "RX", frame.SourceNodeId);
|
log.Debug($"[即时显示] 接收帧: {frame}");
|
}
|
catch (Exception ex)
|
{
|
log.Error($"即时显示帧失败: {ex.Message}", ex);
|
}
|
}
|
|
private void DisconnectBtn_Click(object sender, RoutedEventArgs e)
|
{
|
try
|
{
|
_pollTimer?.Stop();
|
if (_driver != null)
|
{
|
if (_driver is CanDeviceDriver adapter)
|
adapter.DriverFrameReceived -= OnDriverFrameReceived;
|
_driver.DisconnectAsync().GetAwaiter().GetResult();
|
if (_ownsDriver)
|
_driver.Dispose();
|
}
|
_driver = null;
|
_ownsDriver = false;
|
_isConnected = false;
|
IsMaintenanceMode = false;
|
UpdateConnectionStatus(false);
|
log.Info("CAN 驱动已断开");
|
}
|
catch (Exception ex)
|
{
|
log.Error($"CAN 断开失败: {ex.Message}", ex);
|
}
|
}
|
|
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}");
|
}
|
}
|
|
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("维护模式已关闭(只读监控)");
|
}
|
}
|
|
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);
|
}
|
}
|
|
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;
|
}
|
}
|
|
private void UpdateStats()
|
{
|
if (_driver == null) return;
|
try
|
{
|
var response = _driver.QueryAsync("STATUS", CancellationToken.None).GetAwaiter().GetResult();
|
statsText.Text = $"TX: {GetStatValue(response, "Tx")} RX: {GetStatValue(response, "Rx")} Err: {GetStatValue(response, "Err")}";
|
}
|
catch { /* 忽略统计更新失败 */ }
|
}
|
|
private string GetStatValue(string statusStr, string key)
|
{
|
try
|
{
|
foreach (var part in statusStr.Split(','))
|
{
|
var kv = part.Trim().Split('=');
|
if (kv.Length == 2 && kv[0].Trim() == key)
|
return kv[1].Trim();
|
}
|
}
|
catch { }
|
return "0";
|
}
|
|
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
|
};
|
|
// 必须用异步 BeginInvoke:推送常在驱动后台读循环线程触发,若用同步 Invoke,
|
// 会与“UI 线程阻塞等待该读循环投递 RPC 响应”形成环形等待 → 开启应答器后推送骤增即冻结。
|
if (Application.Current == null) return;
|
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
{
|
ReceivedFrames.Insert(0, frame);
|
while (ReceivedFrames.Count > 500)
|
ReceivedFrames.RemoveAt(ReceivedFrames.Count - 1);
|
}));
|
}
|
catch (Exception ex)
|
{
|
log.Error($"添加接收帧失败: {ex.Message}", ex);
|
}
|
}
|
|
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();
|
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();
|
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>供多通道壳在关闭标签时安全断开本通道(不弹 UI 错误)。</summary>
|
public void DisconnectInternalSafe()
|
{
|
try
|
{
|
if (_isConnected) DisconnectBtn_Click(this, new RoutedEventArgs());
|
_pollTimer?.Stop();
|
}
|
catch (Exception ex) { log.Warn($"通道安全断开异常: {ex.Message}"); }
|
}
|
|
private void ClearBtn_Click(object sender, RoutedEventArgs e)
|
{
|
try
|
{
|
ReceivedFrames.Clear();
|
log.Info("清空 CAN 接收记录");
|
}
|
catch (Exception ex)
|
{
|
log.Error($"清空接收记录失败: {ex.Message}", ex);
|
}
|
}
|
}
|
}
|