Files
StandardSence/StandardScene.MagCarSimulator/MainForm.cs
T

631 lines
23 KiB
C#
Raw Normal View History

2026-08-24 14:55:35 +08:00
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;
namespace StandardScene.MagCarSimulator
{
public sealed class MainForm : Form
{
private const int MaxLogLines = 400;
private const int MaxPendingLogs = 300;
private readonly MagCarSimConfig _config;
private readonly MagCarSimHost _host;
private readonly StringBuilder _logBuffer = new StringBuilder();
private readonly Queue<string> _pendingLogs = new Queue<string>();
private readonly object _logLock = new object();
private readonly Timer _uiTimer;
private bool _logFlushScheduled;
private int _logLineCount;
private TextBox _mapPathBox;
private CheckBox _useTagValueBox;
private Label _mapInfoLabel;
private DataGridView _grid;
private ListBox _siteList;
private TextBox _logBox;
private Button _listenButton;
private Button _loopButton;
private Button _stopButton;
private Button _releaseButton;
private Button _addButton;
private Button _removeButton;
private Button _saveButton;
public MainForm()
{
_config = MagCarSimConfig.Load();
MagCarSimLog.Configure(_config);
_host = new MagCarSimHost(_config);
Text = "MagCar 模拟器(FASS 1.0 / SimpleLite 地图循环)";
Width = 1280;
Height = 820;
MinimumSize = new Size(980, 640);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Microsoft YaHei UI", 9F);
BuildLayout();
LoadConfigToUi();
MagCarSimLog.MessageWritten += OnLogMessage;
_uiTimer = new Timer { Interval = 250 };
_uiTimer.Tick += (_, __) => RefreshGridStatus();
_uiTimer.Start();
FormClosed += (_, __) =>
{
MagCarSimLog.MessageWritten -= OnLogMessage;
_uiTimer.Stop();
_host.Dispose();
MagCarSimLog.Shutdown();
};
AppendLog($"就绪。文件日志目录: {MagCarSimLog.GetLogDirectory()}。加载 SimpleLite 地图后启动监听,再开始循环。普通站可叠车;停止点和交管点同时只允许一辆。");
TryLoadMap(_config.MapPath, silent: true);
}
private void BuildLayout()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 4,
Padding = new Padding(8)
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 42));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 58));
Controls.Add(root);
root.Controls.Add(BuildToolbar(), 0, 0);
_grid = BuildGrid();
root.Controls.Add(_grid, 0, 1);
root.Controls.Add(BuildVehicleButtons(), 0, 2);
var split = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Vertical,
SplitterDistance = 280
};
_siteList = new ListBox { Dock = DockStyle.Fill, IntegralHeight = false };
var sitePanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(0, 4, 4, 0) };
sitePanel.Controls.Add(_siteList);
sitePanel.Controls.Add(new Label
{
Text = "地图站点(带 [停止] 的为 Mag_NeedStop",
Dock = DockStyle.Top,
Height = 22
});
split.Panel1.Controls.Add(sitePanel);
_logBox = new TextBox
{
Dock = DockStyle.Fill,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both,
WordWrap = false,
Font = new Font("Consolas", 9F)
};
var logPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(4, 4, 0, 0) };
logPanel.Controls.Add(_logBox);
logPanel.Controls.Add(new Label
{
Text = "协议 / 运动日志",
Dock = DockStyle.Top,
Height = 22
});
split.Panel2.Controls.Add(logPanel);
root.Controls.Add(split, 0, 3);
}
private Control BuildToolbar()
{
var panel = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ColumnCount = 8,
RowCount = 2,
Padding = new Padding(0, 0, 0, 6)
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.Controls.Add(new Label { Text = "地图", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 0);
_mapPathBox = new TextBox { Dock = DockStyle.Fill, Width = 480 };
panel.Controls.Add(_mapPathBox, 1, 0);
var browse = new Button { Text = "浏览…", AutoSize = true };
browse.Click += (_, __) => BrowseMap();
panel.Controls.Add(browse, 2, 0);
var load = new Button { Text = "加载地图", AutoSize = true };
load.Click += (_, __) => TryLoadMap(_mapPathBox.Text, silent: false);
panel.Controls.Add(load, 3, 0);
_listenButton = new Button { Text = "启动监听", AutoSize = true };
_listenButton.Click += (_, __) => StartListen();
panel.Controls.Add(_listenButton, 4, 0);
_loopButton = new Button { Text = "开始循环", AutoSize = true };
_loopButton.Click += (_, __) => StartLoop();
panel.Controls.Add(_loopButton, 5, 0);
_stopButton = new Button { Text = "停止模拟", AutoSize = true, Enabled = false };
_stopButton.Click += (_, __) => StopSim();
panel.Controls.Add(_stopButton, 6, 0);
_saveButton = new Button { Text = "保存配置", AutoSize = true };
_saveButton.Click += (_, __) => SaveConfig();
panel.Controls.Add(_saveButton, 7, 0);
_useTagValueBox = new CheckBox { Text = "用 TagValue 作为节点号", AutoSize = true, Anchor = AnchorStyles.Left };
panel.SetColumnSpan(_useTagValueBox, 2);
panel.Controls.Add(_useTagValueBox, 0, 1);
_mapInfoLabel = new Label
{
Text = "未加载地图",
AutoSize = true,
Anchor = AnchorStyles.Left,
ForeColor = Color.DimGray
};
panel.SetColumnSpan(_mapInfoLabel, 6);
panel.Controls.Add(_mapInfoLabel, 2, 1);
return panel;
}
private Control BuildVehicleButtons()
{
var panel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
WrapContents = false,
Padding = new Padding(0, 4, 0, 4)
};
_addButton = new Button { Text = "添加车辆", AutoSize = true };
_addButton.Click += (_, __) => AddVehicleRow();
_removeButton = new Button { Text = "删除选中", AutoSize = true };
_removeButton.Click += (_, __) => RemoveSelectedVehicle();
_releaseButton = new Button { Text = "放行选中车", AutoSize = true };
_releaseButton.Click += (_, __) => ReleaseSelected();
panel.Controls.Add(_addButton);
panel.Controls.Add(_removeButton);
panel.Controls.Add(_releaseButton);
panel.Controls.Add(new Label
{
Text = "普通站可叠车。停止点和交管点同时只允许一辆,后车停在上一站保持运行中。仅 Mag_NeedStop 停车等 0x01。",
AutoSize = true,
Padding = new Padding(12, 8, 0, 0),
ForeColor = Color.DimGray
});
return panel;
}
private DataGridView BuildGrid()
{
var grid = new DataGridView
{
Dock = DockStyle.Fill,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
RowHeadersVisible = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
MultiSelect = false,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
BackgroundColor = Color.White
};
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Name", HeaderText = "名称", FillWeight = 80 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Code", HeaderText = "车号", FillWeight = 50 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Port", HeaderText = "端口", FillWeight = 55 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Start", HeaderText = "起点", FillWeight = 50 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "End", HeaderText = "终点", FillWeight = 50 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Interval", HeaderText = "间隔ms", FillWeight = 60 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Site", HeaderText = "当前站", ReadOnly = true, FillWeight = 55 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Node", HeaderText = "节点", ReadOnly = true, FillWeight = 50 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "State", HeaderText = "状态", ReadOnly = true, FillWeight = 70 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Wait", HeaderText = "等待", ReadOnly = true, FillWeight = 55 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Clients", HeaderText = "连接", ReadOnly = true, FillWeight = 45 });
grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Path", HeaderText = "循环路径", ReadOnly = true, FillWeight = 160 });
return grid;
}
private void LoadConfigToUi()
{
_mapPathBox.Text = _config.MapPath ?? "";
_useTagValueBox.Checked = _config.UseTagValueAsNode;
_grid.Rows.Clear();
foreach (var vehicle in _config.Vehicles)
{
_grid.Rows.Add(
vehicle.Name,
vehicle.VehicleCode,
vehicle.ListenPort,
vehicle.StartSiteId,
vehicle.EndSiteId,
vehicle.IntervalMs,
"",
"",
"",
"",
"",
"");
}
}
private List<MagCarSimVehicleConfig> ReadVehiclesFromGrid()
{
var list = new List<MagCarSimVehicleConfig>();
foreach (DataGridViewRow row in _grid.Rows)
{
if (row.IsNewRow)
{
continue;
}
var code = ToUShort(row.Cells["Code"].Value, 1);
list.Add(new MagCarSimVehicleConfig
{
Name = Convert.ToString(row.Cells["Name"].Value),
VehicleCode = code,
ListenPort = ToInt(row.Cells["Port"].Value, 5001),
StartSiteId = ToInt(row.Cells["Start"].Value, 1),
EndSiteId = ToInt(row.Cells["End"].Value, 2),
IntervalMs = ToInt(row.Cells["Interval"].Value, _config.DefaultIntervalMs),
LoopSiteIds = PreserveLoop(code)
});
}
return list;
}
private List<int> PreserveLoop(ushort vehicleCode)
{
var existing = _config.Vehicles?.Find(v => v.VehicleCode == vehicleCode);
if (existing?.LoopSiteIds == null || existing.LoopSiteIds.Count == 0)
{
return null;
}
return new List<int>(existing.LoopSiteIds);
}
private void BrowseMap()
{
using var dialog = new OpenFileDialog
{
Filter = "SimpleLite 地图 (*.json)|*.json|所有文件 (*.*)|*.*",
Title = "选择 SimpleLite 地图"
};
if (!string.IsNullOrWhiteSpace(_mapPathBox.Text) && File.Exists(_mapPathBox.Text))
{
dialog.InitialDirectory = Path.GetDirectoryName(_mapPathBox.Text);
dialog.FileName = Path.GetFileName(_mapPathBox.Text);
}
if (dialog.ShowDialog(this) == DialogResult.OK)
{
_mapPathBox.Text = dialog.FileName;
TryLoadMap(dialog.FileName, silent: false);
}
}
private void TryLoadMap(string path, bool silent)
{
if (string.IsNullOrWhiteSpace(path))
{
if (!silent)
{
MessageBox.Show(this, "请先选择 SimpleLite 地图文件。", "加载地图", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
return;
}
try
{
_config.UseTagValueAsNode = _useTagValueBox.Checked;
var map = _host.LoadMap(path);
_mapPathBox.Text = map.FilePath;
RefreshSiteList(map);
var stops = map.ListNeedStopSiteIds();
_mapInfoLabel.Text = $"站点 {map.Sites.Count},路径 {map.Tracks.Count},停止点 {(stops.Count == 0 ? "无(循环将一直过站)" : string.Join(",", stops))}";
}
catch (Exception ex)
{
_mapInfoLabel.Text = "地图加载失败";
if (!silent)
{
MessageBox.Show(this, ex.Message, "加载地图失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
AppendLog("地图未加载:" + ex.Message);
}
}
}
private void RefreshSiteList(SimpleLiteMap map)
{
_siteList.Items.Clear();
foreach (var site in map.Sites.Values)
{
var tag = site.NeedStop ? " [停止]" : "";
var node = map.ResolveNode(site.Id);
_siteList.Items.Add($"站 {site.Id} node={node}{tag}");
}
}
private void StartListen()
{
try
{
ApplyGridToHost();
_host.StartListen();
SetRunningUi(true, looping: false);
AppendLog("TCP 监听已启动。请在 SimpleLite 中把 MagCar 的 address/Port/VehicleCode 配成与上表一致。");
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "启动监听失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void StartLoop()
{
try
{
if (!_host.IsListening)
{
ApplyGridToHost();
}
_host.StartLoops();
SetRunningUi(true, looping: true);
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "开始循环失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void StopSim()
{
_host.StopAll();
SetRunningUi(false, looping: false);
AppendLog("模拟已停止。");
RefreshGridStatus();
}
private void ApplyGridToHost()
{
_config.MapPath = _mapPathBox.Text;
_config.UseTagValueAsNode = _useTagValueBox.Checked;
_config.Vehicles = ReadVehiclesFromGrid();
if (!string.IsNullOrWhiteSpace(_config.MapPath))
{
TryLoadMap(_config.MapPath, silent: false);
}
_host.RebuildVehicles(_config.Vehicles);
}
private void AddVehicleRow()
{
if (_host.IsListening)
{
MessageBox.Show(this, "运行中不能改车辆列表,请先停止模拟。", "添加车辆", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var nextCode = _grid.Rows.Count + 1;
_grid.Rows.Add($"AGV-{nextCode}", nextCode, 5000 + nextCode, 1, 6, _config.DefaultIntervalMs, "", "", "", "", "", "");
}
private void RemoveSelectedVehicle()
{
if (_host.IsListening)
{
MessageBox.Show(this, "运行中不能改车辆列表,请先停止模拟。", "删除车辆", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (_grid.CurrentRow == null)
{
return;
}
_grid.Rows.Remove(_grid.CurrentRow);
}
private void ReleaseSelected()
{
if (_grid.CurrentRow == null)
{
MessageBox.Show(this, "请先选中一辆车。", "放行", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var code = ToUShort(_grid.CurrentRow.Cells["Code"].Value, 0);
if (!_host.TryRelease(code))
{
MessageBox.Show(this, $"未找到车号 {code},请先启动监听。", "放行", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private void SaveConfig()
{
try
{
_config.MapPath = _mapPathBox.Text;
_config.UseTagValueAsNode = _useTagValueBox.Checked;
_config.Vehicles = ReadVehiclesFromGrid();
_config.Save();
AppendLog("配置已保存到 " + MagCarSimConfig.SettingsPath);
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "保存失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void SetRunningUi(bool listening, bool looping)
{
_listenButton.Enabled = !listening;
_loopButton.Enabled = !looping;
_stopButton.Enabled = listening;
_addButton.Enabled = !listening;
_removeButton.Enabled = !listening;
_grid.ReadOnly = listening;
_useTagValueBox.Enabled = !listening;
}
private void RefreshGridStatus()
{
var snapshots = _host.Snapshots();
if (snapshots.Count == 0)
{
return;
}
for (var i = 0; i < _grid.Rows.Count && i < snapshots.Count; i++)
{
var snap = snapshots[i];
var row = _grid.Rows[i];
row.Cells["Site"].Value = snap.CurrentSiteId;
row.Cells["Node"].Value = snap.Node;
row.Cells["State"].Value = snap.StateText;
row.Cells["Wait"].Value = snap.WaitingRelease ? "等放行" : (snap.HoldingForStop ? "让行" : "");
row.Cells["Clients"].Value = snap.ClientCount;
row.Cells["Path"].Value = snap.PathText;
row.DefaultCellStyle.BackColor = snap.WaitingRelease
? Color.FromArgb(255, 236, 179)
: snap.HoldingForStop
? Color.FromArgb(207, 232, 255)
: Color.White;
}
}
private void OnLogMessage(string message)
{
lock (_logLock)
{
while (_pendingLogs.Count >= MaxPendingLogs)
{
_pendingLogs.Dequeue();
}
_pendingLogs.Enqueue(message);
}
if (_logFlushScheduled || !IsHandleCreated)
{
return;
}
_logFlushScheduled = true;
try
{
BeginInvoke(new Action(FlushLogs));
}
catch
{
_logFlushScheduled = false;
}
}
private void FlushLogs()
{
_logFlushScheduled = false;
List<string> batch;
lock (_logLock)
{
if (_pendingLogs.Count == 0)
{
return;
}
batch = new List<string>(_pendingLogs.Count);
while (_pendingLogs.Count > 0)
{
batch.Add(_pendingLogs.Dequeue());
}
}
foreach (var line in batch)
{
_logLineCount++;
_logBuffer.AppendLine(line);
}
while (_logLineCount > MaxLogLines)
{
var text = _logBuffer.ToString();
var firstBreak = text.IndexOf('\n');
if (firstBreak < 0)
{
break;
}
_logBuffer.Remove(0, firstBreak + 1);
_logLineCount--;
}
_logBox.Text = _logBuffer.ToString();
_logBox.SelectionStart = _logBox.TextLength;
_logBox.ScrollToCaret();
}
private void AppendLog(string message)
{
lock (_logLock)
{
while (_pendingLogs.Count >= MaxPendingLogs)
{
_pendingLogs.Dequeue();
}
_pendingLogs.Enqueue(message);
}
if (IsHandleCreated)
{
if (!_logFlushScheduled)
{
_logFlushScheduled = true;
BeginInvoke(new Action(FlushLogs));
}
}
else
{
FlushLogs();
}
}
private static int ToInt(object value, int fallback)
{
return int.TryParse(Convert.ToString(value), out var n) ? n : fallback;
}
private static ushort ToUShort(object value, ushort fallback)
{
return ushort.TryParse(Convert.ToString(value), out var n) ? n : fallback;
}
}
}