From 559890c482f5973f06aafdfec9550fee5ba3c7f2 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 09 十一月 2026 17:02:15 +0800
Subject: [PATCH] feat: CAN 模拟器 TCP 化 + 多设备并发调试 + 后台生命周期管理

---
 Plugin.DeviceDebug/Panels/CanChannelView.xaml.cs |  485 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 485 insertions(+), 0 deletions(-)

diff --git a/Plugin.DeviceDebug/Panels/CanChannelView.xaml.cs b/Plugin.DeviceDebug/Panels/CanChannelView.xaml.cs
new file mode 100644
index 0000000..949a7a2
--- /dev/null
+++ b/Plugin.DeviceDebug/Panels/CanChannelView.xaml.cs
@@ -0,0 +1,485 @@
+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"/>锛孡ocal/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 鍏变韩璁惧涓轰唬鐞嗛┍鍔級锛�
+        /// 鏃犲垯鑷缓缁熶竴椹卞姩锛圠ocal TCP NodeChannel 杩炴ā鎷熷櫒锛�
+        /// </summary>
+        private Aging.Infrastructure.Contracts.IDeviceDriver? _driver;
+
+        /// <summary>椹卞姩鎵�鏈夋潈锛氬閮ㄦ敞鍏ョ殑浠g悊椹卞姩褰� DeviceManager锛屾柇寮�鏃朵笉寰� Dispose</summary>
+        private bool _ownsDriver;
+
+        /// <summary>澶栭儴宸叉敞鍐岄┍鍔紙鍏变韩璁惧鐢� DeviceDebug 浼犲叆缁熶竴 CanDeviceDriver锛岀姝㈣嚜寤虹墿鐞嗕細璇濓級</summary>
+        public Aging.Infrastructure.Contracts.IDeviceDriver? ExistingDriver { get; set; }
+
+        /// <summary>杞瀹氭椂鍣紙浠g悊椹卞姩鍏滃簳琛ユ樉鎺ユ敹甯э級</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 瑙f瀽鍑虹殑鎬荤嚎鍚嶇О</summary>
+        public string InitialBusName { get; set; }
+
+        /// <summary>浠庤澶� Endpoint 瑙f瀽鍑虹殑鑺傜偣缂栧彿</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 鍒锋柊锛汼TATUS 鏌ヨ鍚屾椂瑙﹀彂 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)
+            {
+                // 鍏变韩/浠g悊璁惧锛氬鐢ㄥ涓婚�氳矾锛屼笉鏂板缓鏈湴鐗╃悊浼氳瘽
+                _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锛�
+                // 浼氫笌鈥淯I 绾跨▼闃诲绛夊緟璇ヨ寰幆鎶曢�� 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);
+            }
+        }
+    }
+}

--
Gitblit v1.9.1