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 _pendingLogs = new Queue(); 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 ReadVehiclesFromGrid() { var list = new List(); 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 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(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 batch; lock (_logLock) { if (_pendingLogs.Count == 0) { return; } batch = new List(_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; } } }