Files

631 lines
23 KiB
C#
Raw Permalink 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.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;
}
}
}