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;
}
}