Files
StandardSence/StandardScene.Fass2Simulator/MainWindow.xaml.cs
T

366 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string> _pendingLogLines = new Queue<string>();
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("就绪。配置参数后点击「启动模拟」开始联调。完整日志将写入 logs 目录。");
}
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<string> batch;
lock (_logQueueLock)
{
if (_pendingLogLines.Count == 0)
{
return;
}
batch = new List<string>(_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 ?? "logs"
};
}
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;
}
}
}