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 { /// /// 模拟器服务监控窗口:状态栏 + 连接节点树 + 实时帧表 + 日志区。 /// 纯代码构建 UI(不走 XAML 标记编译链路);帧表经 100ms 定时器批量刷新,上限 2000 行。 /// 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 _pendingRows = new(); private readonly ObservableCollection _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 = "启动失败:端口被占用?"; _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(); })); }; } /// 总线帧事件挂接(宿主启动后总线才存在,故在定时器里惰性补挂) 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 _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, "帧ID", 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) }); } // ==================== 帧表与树刷新 ==================== /// 总线级观察:路由发生即记一行(含自动响应帧) 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; } }