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>
|
/// 模拟器服务监控窗口:状态栏 + 连接节点树 + 实时帧表 + 日志区。
|
/// 纯代码构建 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 = "启动失败:端口被占用?";
|
_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, "帧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)
|
});
|
}
|
|
// ==================== 帧表与树刷新 ====================
|
|
/// <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;
|
}
|
}
|