using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Windows; using System.Windows.Media; using System.Windows.Threading; namespace StandardScene.Fass2Simulator { public partial class MainWindow : Window { private const int MaxLogLines = 800; private readonly StringBuilder _logBuffer = new StringBuilder(); private int _logLineCount; private Fass2SimConfig _config; private Fass2SimRuntime _runtime; private readonly DispatcherTimer _uiTimer; private readonly Queue _pendingLogLines = new Queue(); private readonly object _logQueueLock = new object(); private bool _logFlushScheduled; public MainWindow() { InitializeComponent(); _config = Fass2SimBootstrap.LoadConfig(); Fass2SimFileLogger.Configure(_config); LoadConfigToUi(_config); SetRunningUi(false); UpdateLogFilePathText(); Fass2SimLog.MessageWritten += OnLogMessage; _uiTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; _uiTimer.Tick += (_, __) => RefreshStatus(); _uiTimer.Start(); RefreshStatus(); Closed += (_, __) => { Fass2SimLog.MessageWritten -= OnLogMessage; _runtime?.Dispose(); Fass2SimFileLogger.Shutdown(); }; AppendLog("就绪。配置参数后点击「启动模拟」开始联调。完整日志将写入 log 目录。"); } private void StartButton_Click(object sender, RoutedEventArgs e) { if (_runtime != null && _runtime.IsRunning) { return; } try { _config = ReadConfigFromUi(); Fass2SimFileLogger.Configure(_config); Fass2SimFileLogger.BeginSession(_config); UpdateLogFilePathText(); _runtime?.Dispose(); _runtime = Fass2SimBootstrap.CreateRuntime(_config); _runtime.Start(); SetRunningUi(true); RefreshStatus(); } catch (Exception ex) { MessageBox.Show(this, ex.Message, "启动失败", MessageBoxButton.OK, MessageBoxImage.Error); } } private void StopButton_Click(object sender, RoutedEventArgs e) { if (_runtime == null) { return; } _runtime.Stop(); SetRunningUi(false); RefreshStatus(); } private void ClearLogButton_Click(object sender, RoutedEventArgs e) { _logBuffer.Clear(); _logLineCount = 0; LogTextBox.Clear(); } private void OpenLogFolderButton_Click(object sender, RoutedEventArgs e) { var directory = Fass2SimFileLogger.GetLogDirectory(); Directory.CreateDirectory(directory); Process.Start(new ProcessStartInfo { FileName = directory, UseShellExecute = true }); } private void UpdateLogFilePathText() { if (!_config.EnableFileLog) { LogFilePathText.Text = "文件日志已关闭(appsettings.json: EnableFileLog=false)"; return; } var path = Fass2SimFileLogger.CurrentFilePath; if (string.IsNullOrEmpty(path)) { LogFilePathText.Text = $"日志目录: {Fass2SimFileLogger.GetLogDirectory()}(启动模拟后生成会话日志)"; return; } LogFilePathText.Text = $"日志文件: {path}"; } private void OnLogMessage(string message) { lock (_logQueueLock) { _pendingLogLines.Enqueue(message); } if (_logFlushScheduled) { return; } _logFlushScheduled = true; Dispatcher.BeginInvoke(new Action(FlushPendingLogs), DispatcherPriority.Background); } private void FlushPendingLogs() { _logFlushScheduled = false; List batch; lock (_logQueueLock) { if (_pendingLogLines.Count == 0) { return; } batch = new List(_pendingLogLines.Count); while (_pendingLogLines.Count > 0) { batch.Add(_pendingLogLines.Dequeue()); } } foreach (var line in batch) { AppendLog(line); } } private void AppendLog(string message) { if (string.IsNullOrEmpty(message)) { return; } _logBuffer.AppendLine(message); _logLineCount++; while (_logLineCount > MaxLogLines) { var bufferText = _logBuffer.ToString(); var firstBreak = bufferText.IndexOf('\n'); if (firstBreak < 0) { _logBuffer.Clear(); _logLineCount = 0; break; } _logBuffer.Remove(0, firstBreak + 1); _logLineCount--; } LogTextBox.Text = _logBuffer.ToString(); LogTextBox.CaretIndex = LogTextBox.Text.Length; LogTextBox.ScrollToEnd(); } private void RefreshStatus() { if (_runtime == null || !_runtime.IsRunning) { if (_runtime == null) { ApplyIdleStatus( _config.InitialNode.ToString(), "-", "0", Fass2SimProtocol.StateText(_config.InitialState), "0", "-", "0", $"{_config.BatteryCharge}%", "-", "0", "0", "0", "无"); } return; } var snap = _runtime.Vehicle.Snapshot(); var expected = _runtime.Actions.ExpectedStation; var pending = expected == null ? "-" : Fass2SimActionResolver.DescribePending(expected, snap); if (string.IsNullOrEmpty(pending)) { pending = "-"; } var distanceText = snap.Distance.ToString(); var targetNodeText = "-"; if (_runtime.Motion.TryGetSegmentStatus(out var targetNode, out var segmentLengthMm, out var progressMm)) { targetNodeText = targetNode.ToString(); distanceText = $"{progressMm} / {segmentLengthMm}"; } ApplyIdleStatus( snap.Node.ToString(), targetNodeText, distanceText, Fass2SimProtocol.StateText(snap.State), snap.Task.ToString(), snap.StartStop.ToString(), snap.Lift.ToString(), $"{snap.BatteryCharge}%", pending, snap.StateReportsSent.ToString(), snap.CommandsReceived.ToString(), snap.AckReceived.ToString(), snap.LastAckUtc == DateTime.MinValue ? "无" : $"{(DateTime.UtcNow - snap.LastAckUtc).TotalSeconds:F1}s 前"); } private void ApplyIdleStatus( string node, string targetNode, string distance, string state, string task, string startStop, string lift, string battery, string pending, string reports, string commands, string ack, string lastAck) { NodeText.Text = node; TargetNodeText.Text = targetNode; DistanceText.Text = distance; StateText.Text = state; TaskText.Text = task; StartStopText.Text = startStop; LiftText.Text = lift; BatteryText.Text = battery; PendingText.Text = pending; ReportsText.Text = reports; CommandsText.Text = commands; AckText.Text = ack; LastAckText.Text = lastAck; } private void LoadConfigToUi(Fass2SimConfig config) { VehicleCodeBox.Text = config.VehicleCode.ToString(); ListenAddressBox.Text = config.VehicleListenAddress; ListenPortBox.Text = config.VehicleListenPort.ToString(); SchedulerHostBox.Text = config.SchedulerHost; SchedulerPortBox.Text = config.SchedulerListenPort.ToString(); ReportIntervalBox.Text = config.ReportIntervalMs.ToString(); InitialNodeBox.Text = config.InitialNode.ToString(); AutoContinueBox.IsChecked = config.AutoContinueOnPass; AutoCompleteBox.IsChecked = config.AutoCompleteStationActions; LogRawFramesBox.IsChecked = config.LogRawFrames; } private Fass2SimConfig ReadConfigFromUi() { return new Fass2SimConfig { VehicleCode = ParseUShort(VehicleCodeBox.Text, "车号"), VehicleListenAddress = ListenAddressBox.Text?.Trim() ?? "0.0.0.0", VehicleListenPort = ParseInt(ListenPortBox.Text, "车体监听端口"), SchedulerHost = SchedulerHostBox.Text?.Trim() ?? "127.0.0.1", SchedulerListenPort = ParseInt(SchedulerPortBox.Text, "调度监听端口"), ReportIntervalMs = ParseInt(ReportIntervalBox.Text, "上报周期"), InitialNode = ParseUShort(InitialNodeBox.Text, "初始节点"), InitialState = _config?.InitialState ?? 0, BatteryCharge = _config?.BatteryCharge ?? 100, CarLength = _config?.CarLength ?? 1200, CarWidth = _config?.CarWidth ?? 800, DefaultSpeed = _config?.DefaultSpeed ?? 500, DefaultSegmentDistance = _config?.DefaultSegmentDistance ?? 1000, SecondsPerSegment = _config?.SecondsPerSegment ?? 2, ActionDelayMs = _config?.ActionDelayMs ?? 1000, NodeProfilesPath = _config?.NodeProfilesPath ?? "sim-nodes.json", AutoContinueOnPass = AutoContinueBox.IsChecked == true, AutoCompleteStationActions = AutoCompleteBox.IsChecked == true, LogRawFrames = LogRawFramesBox.IsChecked == true, EnableFileLog = _config?.EnableFileLog ?? true, LogDirectory = _config?.LogDirectory ?? "log" }; } private void SetRunningUi(bool running) { StartButton.IsEnabled = !running; StopButton.IsEnabled = running; VehicleCodeBox.IsEnabled = !running; ListenAddressBox.IsEnabled = !running; ListenPortBox.IsEnabled = !running; SchedulerHostBox.IsEnabled = !running; SchedulerPortBox.IsEnabled = !running; ReportIntervalBox.IsEnabled = !running; InitialNodeBox.IsEnabled = !running; AutoContinueBox.IsEnabled = !running; AutoCompleteBox.IsEnabled = !running; LogRawFramesBox.IsEnabled = !running; RunStateText.Text = running ? "运行中" : "已停止"; RunStateText.Foreground = running ? new SolidColorBrush(Color.FromRgb(22, 163, 74)) : new SolidColorBrush(Color.FromRgb(107, 114, 128)); } private static ushort ParseUShort(string text, string fieldName) { if (!ushort.TryParse(text?.Trim(), out var value)) { throw new InvalidOperationException($"{fieldName} 无效:{text}"); } return value; } private static int ParseInt(string text, string fieldName) { if (!int.TryParse(text?.Trim(), out var value)) { throw new InvalidOperationException($"{fieldName} 无效:{text}"); } return value; } } }