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 化 + 多设备并发调试 + 后台生命周期管理
---
CanSimulatorServer/CanSimulatorServer.App/MainWindow.cs | 311 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 311 insertions(+), 0 deletions(-)
diff --git a/CanSimulatorServer/CanSimulatorServer.App/MainWindow.cs b/CanSimulatorServer/CanSimulatorServer.App/MainWindow.cs
new file mode 100644
index 0000000..2e87f52
--- /dev/null
+++ b/CanSimulatorServer/CanSimulatorServer.App/MainWindow.cs
@@ -0,0 +1,311 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Configuration;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Threading;
+using CanSimulator;
+using log4net;
+
+namespace CanSimulatorServer.App
+{
+ /// <summary>
+ /// 妯℃嫙鍣ㄦ湇鍔$洃鎺х獥鍙o細鐘舵�佹爮 + 杩炴帴鑺傜偣鏍� + 瀹炴椂甯ц〃 + 鏃ュ織鍖恒��
+ /// 绾唬鐮佹瀯寤� UI锛堜笉璧� XAML 鏍囪缂栬瘧閾捐矾锛夛紱甯ц〃缁� 100ms 瀹氭椂鍣ㄦ壒閲忓埛鏂帮紝涓婇檺 2000 琛屻��
+ /// </summary>
+ public class MainWindow : Window
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(MainWindow));
+
+ private const int MaxFrameRows = 2000;
+
+ private readonly TcpSimulatorHost _host = new();
+ private readonly ConcurrentQueue<FrameRow> _pendingRows = new();
+ private readonly ObservableCollection<FrameRow> _rows = new();
+ private readonly DispatcherTimer _uiTimer = new();
+ private int _tickCount;
+
+ private TextBlock _statusText = null!;
+ private TextBlock _statsText = null!;
+ private TreeView _nodeTree = null!;
+ private ListView _frameList = null!;
+ private TextBox _logBox = null!;
+ private Button _startBtn = null!;
+ private Button _stopBtn = null!;
+ private CheckBox _echoChk = null!;
+ private bool _echoWanted;
+ private readonly int _port;
+ private readonly bool _listenAll;
+
+ private sealed class FrameRow
+ {
+ public string Time { get; set; } = "";
+ public string Bus { get; set; } = "";
+ public string Src { get; set; } = "";
+ public string FrameId { get; set; } = "";
+ public string Data { get; set; } = "";
+ public string Kind { get; set; } = "TX";
+ }
+
+ public MainWindow()
+ {
+ _port = ReadIntConfig("ServerPort", 9000);
+ _listenAll = string.Equals(ConfigurationManager.AppSettings["ListenOnAllInterfaces"],
+ "true", StringComparison.OrdinalIgnoreCase);
+
+ BuildUi();
+ HookHostEvents();
+
+ _uiTimer.Interval = TimeSpan.FromMilliseconds(100);
+ _uiTimer.Tick += (s, e) => DrainPendingRows();
+ _uiTimer.Start();
+
+ Loaded += async (s, e) => await StartHostAsync();
+ Closing += (s, e) => { _host.Dispose(); };
+ }
+
+ // ==================== 瀹夸富鐢熷懡鍛ㄦ湡 ====================
+
+ private async Task StartHostAsync()
+ {
+ try
+ {
+ await _host.StartAsync(_port, _listenAll);
+ _statusText.Text = $"鐩戝惉涓� {(_listenAll ? "0.0.0.0" : "127.0.0.1")}:{_port}";
+ _statusText.Foreground = Brushes.ForestGreen;
+ _startBtn.IsEnabled = false;
+ _stopBtn.IsEnabled = true;
+ AppendLog($"鏈嶅姟绔凡鍚姩锛岀洃鍚� {_port}锛坽(_listenAll ? "鍏ㄩ儴鎺ュ彛" : "浠呮湰鏈�")}锛�");
+ }
+ catch (Exception ex)
+ {
+ _statusText.Text = "鍚姩澶辫触锛氱鍙h鍗犵敤锛�";
+ _statusText.Foreground = Brushes.Firebrick;
+ AppendLog($"[FATAL] 鍚姩澶辫触: {ex.Message}");
+ _startBtn.IsEnabled = true;
+ Log.Error("瀹夸富鍚姩澶辫触", ex);
+ }
+ }
+
+ private void HookHostEvents()
+ {
+ _host.SessionAttached += session =>
+ {
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ AppendLog($"鑺傜偣鎺ュ叆锛歿session.ClientTag}");
+ RefreshTree();
+ }));
+ };
+ }
+
+ /// <summary>鎬荤嚎甯т簨浠舵寕鎺ワ紙瀹夸富鍚姩鍚庢�荤嚎鎵嶅瓨鍦紝鏁呭湪瀹氭椂鍣ㄩ噷鎯版�цˉ鎸傦級</summary>
+ private void EnsureBusSubscriptions()
+ {
+ foreach (var bus in _host.Manager.AllBuses())
+ {
+ if (_subscribedBuses.Add(bus.BusName))
+ {
+ bus.FrameTransmitted += (s, e) => EnqueueBusRow(bus.BusName, e.Frame, e.SourceNodeId);
+ bus.EchoResponderToSender = _echoWanted; // 鏂板缓鎬荤嚎缁ф壙褰撳墠搴旂瓟鍣ㄥ紑鍏�
+ }
+ }
+ }
+ private readonly HashSet<string> _subscribedBuses = new(StringComparer.OrdinalIgnoreCase);
+
+ // ==================== UI 鏋勫缓 ====================
+
+ private void BuildUi()
+ {
+ var root = new Grid { Margin = new Thickness(10) };
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(160) });
+
+ // 鈹�鈹� 椤堕儴鐘舵��/鎸夐挳鏍� 鈹�鈹�
+ var bar = new StackPanel { Orientation = Orientation.Horizontal };
+ _statusText = new TextBlock
+ {
+ Text = "鏈惎鍔�",
+ FontWeight = FontWeights.Bold,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, 16, 0)
+ };
+ _statsText = new TextBlock { VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 16, 0) };
+ _startBtn = new Button { Content = "鍚姩鐩戝惉", Width = 90, Height = 28, Margin = new Thickness(0, 0, 6, 0) };
+ _startBtn.Click += async (s, e) => await StartHostAsync();
+ _stopBtn = new Button { Content = "鍋滄鐩戝惉", Width = 90, Height = 28, Margin = new Thickness(0, 0, 6, 0), IsEnabled = false };
+ _stopBtn.Click += (s, e) =>
+ {
+ _host.Dispose();
+ _statusText.Text = "宸插仠姝�";
+ _statusText.Foreground = Brushes.Gray;
+ _startBtn.IsEnabled = true;
+ _stopBtn.IsEnabled = false;
+ AppendLog("鏈嶅姟绔凡鍋滄锛堟敞鎰忥細宸茶繍琛屽鎴风灏嗘柇绾块噸杩烇級");
+ };
+ var clearBtn = new Button { Content = "娓呯┖甯ц〃", Width = 90, Height = 28 };
+ clearBtn.Click += (s, e) => { _rows.Clear(); while (_pendingRows.TryDequeue(out _)) { } };
+ _echoChk = new CheckBox
+ {
+ Content = "鎬荤嚎搴旂瓟鍣�(鍥炴樉鍙戦�佽��)",
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(12, 0, 0, 0),
+ IsChecked = false
+ };
+ _echoChk.Click += (s, e) =>
+ {
+ _echoWanted = _echoChk.IsChecked == true;
+ foreach (var bus in _host.Manager.AllBuses())
+ bus.EchoResponderToSender = _echoWanted;
+ AppendLog($"鎬荤嚎搴旂瓟鍣ㄥ凡{(_echoWanted ? "寮�鍚�" : "鍏抽棴")}锛堜綔鐢ㄤ簬褰撳墠鎵�鏈夋�荤嚎锛�");
+ };
+ bar.Children.Add(_statusText);
+ bar.Children.Add(_statsText);
+ bar.Children.Add(_startBtn);
+ bar.Children.Add(_stopBtn);
+ bar.Children.Add(clearBtn);
+ bar.Children.Add(_echoChk);
+ Grid.SetRow(bar, 0);
+ root.Children.Add(bar);
+
+ // 鈹�鈹� 涓儴锛氬乏鑺傜偣鏍� + 鍙冲抚琛� 鈹�鈹�
+ var split = new Grid();
+ split.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(280) });
+ split.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ _nodeTree = new TreeView();
+ var treeBorder = new Border
+ {
+ Child = _nodeTree,
+ BorderBrush = Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ Margin = new Thickness(0, 8, 8, 0)
+ };
+ Grid.SetColumn(treeBorder, 0);
+
+ _frameList = new ListView { View = BuildGridView() };
+ _frameList.ItemsSource = _rows;
+ var listBorder = new Border
+ {
+ Child = _frameList,
+ BorderBrush = Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ Margin = new Thickness(0, 8, 0, 0)
+ };
+ Grid.SetColumn(listBorder, 1);
+
+ split.Children.Add(treeBorder);
+ split.Children.Add(listBorder);
+ Grid.SetRow(split, 1);
+ root.Children.Add(split);
+
+ // 鈹�鈹� 搴曢儴鏃ュ織 鈹�鈹�
+ _logBox = new TextBox
+ {
+ IsReadOnly = true,
+ VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
+ FontFamily = new FontFamily("Consolas"),
+ FontSize = 11,
+ Margin = new Thickness(0, 8, 0, 0)
+ };
+ Grid.SetRow(_logBox, 2);
+ root.Children.Add(_logBox);
+
+ Content = root;
+ }
+
+ private static GridView BuildGridView()
+ {
+ var gv = new GridView();
+ AddCol(gv, "鏃堕棿", 85, nameof(FrameRow.Time));
+ AddCol(gv, "鎬荤嚎", 95, nameof(FrameRow.Bus));
+ AddCol(gv, "婧愯妭鐐�", 60, nameof(FrameRow.Src));
+ AddCol(gv, "甯D", 70, nameof(FrameRow.FrameId));
+ AddCol(gv, "鏁版嵁", 200, nameof(FrameRow.Data));
+ AddCol(gv, "绫诲瀷", 70, nameof(FrameRow.Kind));
+ return gv;
+ }
+
+ private static void AddCol(GridView gv, string header, double width, string binding)
+ {
+ gv.Columns.Add(new GridViewColumn
+ {
+ Header = header,
+ Width = width,
+ DisplayMemberBinding = new System.Windows.Data.Binding(binding)
+ });
+ }
+
+ // ==================== 甯ц〃涓庢爲鍒锋柊 ====================
+
+ /// <summary>鎬荤嚎绾ц瀵燂細璺敱鍙戠敓鍗宠涓�琛岋紙鍚嚜鍔ㄥ搷搴斿抚锛�</summary>
+ private void EnqueueBusRow(string busName, CanFrame frame, string srcNodeId)
+ {
+ _pendingRows.Enqueue(new FrameRow
+ {
+ Time = frame.Timestamp.ToString("HH:mm:ss.fff"),
+ Bus = busName,
+ Src = srcNodeId,
+ FrameId = frame.FrameIdHex,
+ Data = frame.DataHex,
+ Kind = "TX璺敱"
+ });
+ }
+
+ private void DrainPendingRows()
+ {
+ EnsureBusSubscriptions();
+
+ int n = 0;
+ while (_pendingRows.TryDequeue(out var row) && n < 50)
+ {
+ _rows.Insert(0, row);
+ n++;
+ }
+ while (_rows.Count > MaxFrameRows)
+ _rows.RemoveAt(_rows.Count - 1);
+
+ RefreshStatsLine();
+
+ // 姣忕鍒锋柊涓�娆¤妭鐐规爲锛堝弽鏄犳柇寮�浼氳瘽锛岄伩鍏嶅彧鍦ㄦ帴鍏ヤ簨浠朵腑鍒锋柊锛�
+ if (++_tickCount % 10 == 0) RefreshTree();
+ }
+
+ private void RefreshStatsLine()
+ {
+ var buses = _host.Manager.AllBuses().ToList();
+ long tx = buses.Sum(b => b.TotalFramesTransmitted);
+ long rx = buses.Sum(b => b.TotalFramesReceived);
+ _statsText.Text = $"浼氳瘽={_host.ActiveSessionCount} 绱={_host.AcceptedCount} " +
+ $"鎬荤嚎={buses.Count} TX={tx} RX={rx}";
+ }
+
+ private void RefreshTree()
+ {
+ _nodeTree.Items.Clear();
+ foreach (var bus in _host.Manager.AllBuses())
+ {
+ var busNode = new TreeViewItem { Header = bus.BusName + $"锛坽bus.State}锛�" };
+ foreach (var node in bus.GetAllNodes())
+ busNode.Items.Add(new TreeViewItem { Header = $"鑺傜偣 {node.NodeId} 路 {node.Name}" });
+ _nodeTree.Items.Add(busNode);
+ busNode.IsExpanded = true;
+ }
+ }
+
+ private void AppendLog(string msg)
+ {
+ _logBox.AppendText($"{DateTime.Now:HH:mm:ss.fff} {msg}{Environment.NewLine}");
+ _logBox.ScrollToEnd();
+ }
+
+ private static int ReadIntConfig(string key, int fallback)
+ => int.TryParse(ConfigurationManager.AppSettings[key], out int v) ? v : fallback;
+ }
+}
--
Gitblit v1.9.1