磁导航1.0内部交管和信号交互

This commit is contained in:
ykkokluo
2026-08-24 14:55:35 +08:00
parent cacc5fea5c
commit 244b0af6c8
60 changed files with 6395 additions and 294 deletions
@@ -0,0 +1,193 @@
using System;
using System.IO;
using System.Threading;
using LessokajiWeaverUtilities.Utilities;
using Newtonsoft.Json;
using SimpleCore;
using SimpleCore.Library;
using SimpleLite.RCS;
using StandardScene.Signal.Logic;
using StandardScene.Signal.Model;
using StandardScene.Signal.Ui;
using StandardScene.Utils;
namespace StandardScene.Signal
{
/// <summary>
/// 磁条交管进程:只针对 MagCar。同区名排队后,按「本线 + 冲突对向线」站点并集查占用,容量允许则发启动。
/// </summary>
/// <remarks>
/// 配置 <c>Config/Signal/mag-control-areas.json</c>。
/// 同「管控区名称」绑成一个路口排队;放行时合并本线与冲突对向线的管控站点列表统计当前占用。
/// 不使用 pending 在途占额。不处理 Mag2Car。
/// </remarks>
[MissionType(Name = "磁条交管进程", editor = typeof(MagTrafficMission))]
[I18N.DocumentTranslation(Name = "Magnetic Traffic Mission", locale = "en")]
public class MagTrafficMission : Mission
{
/// <summary>扫车周期(毫秒)。停在触发点的 MagCar 每拍排队判定一次。</summary>
[FieldMember] public int TickMs = 1000;
/// <summary>管控区表路径。相对路径只解析工作目录 <c>Config/Signal</c>。</summary>
[FieldMember] public string AreasPath = @"Config\Signal\mag-control-areas.json";
/// <summary>放行日志根目录。实际按车名落在其下 mag-traffic/car{name}/release_yyyyMMdd.log。</summary>
[FieldMember] public string LogDirectory = "log";
/// <summary>是否把交管放行写入本地文件留档。</summary>
[FieldMember] public bool EnableFileLog = true;
[JsonIgnore] private bool _started;
[JsonIgnore] private readonly MagControlRuntime _runtime = new MagControlRuntime();
[JsonIgnore] private CancellationTokenSource _cts;
[JsonIgnore] private Thread _thread;
public class MagTrafficMissionStatus : MissionStatus
{
public int RuleCount { get; set; }
public int WaitingAtTrigger { get; set; }
public int PendingInFlight { get; set; }
public int ScanTickCount { get; set; }
public string AreasFile { get; set; }
}
public override MissionStatus status { get; set; } = new MagTrafficMissionStatus();
public static Mission Create() => new MagTrafficMission();
[MethodMember(Name = "启动进程", Description = "加载管控区表,按区名排队后对 MagCar 发放行")]
public override void Execute()
{
try
{
MagTrafficFileLogger.Configure(LogDirectory, EnableFileLog);
ReloadConfig();
_started = true;
StartWatch();
MagTrafficFileLogger.WriteSystem($"进程启动 规则={((MagTrafficMissionStatus)status).RuleCount} 配置={((MagTrafficMissionStatus)status).AreasFile}");
Diagnosis.Post(
$"磁条交管进程已启动,规则={((MagTrafficMissionStatus)status).RuleCount}(仅 MagCar),放行日志={MagTrafficFileLogger.GetLogDirectory()}",
"MagTraffic", true);
}
catch (Exception ex)
{
status.status = "启动失败";
Diagnosis.Post($"磁条交管进程启动失败:{ExceptionFormatter.FormatEx(ex)}", "MagTraffic", true);
throw;
}
}
[MethodMember(Name = "重新加载配置", Description = "重新读取 mag-control-areas.json")]
public void ReloadConfig()
{
var file = SignalConfigStore.Resolve(AreasPath);
var rows = SignalConfigStore.Load<MagControlAreaModel>(file);
_runtime.Replace(rows);
var st = (MagTrafficMissionStatus)status;
st.AreasFile = file;
st.RuleCount = _runtime.RuleCount;
st.PendingInFlight = _runtime.PendingCount;
status.status = _started ? "运行中" : "配置已加载";
MagTrafficFileLogger.Configure(LogDirectory, EnableFileLog);
MagTrafficFileLogger.WriteSystem($"重新加载配置 规则={st.RuleCount} 文件={file}");
Diagnosis.Post($"磁条交管配置已加载:启用规则 {st.RuleCount} @ {file}", "MagTraffic", true);
}
[MethodMember(Name = "停止进程")]
public new void Stop()
{
_started = false;
StopWatch();
status.status = "已停止";
MagTrafficFileLogger.WriteSystem("进程停止");
Diagnosis.Post("磁条交管进程已停止", "MagTraffic", true);
}
[MethodMember(Name = "打开放行日志目录", Description = "打开 mag-traffic 目录(按 car{name} 分子目录),便于按车查阅放行记录")]
public void OpenReleaseLogFolder()
{
try
{
MagTrafficFileLogger.Configure(LogDirectory, EnableFileLog);
var dir = MagTrafficFileLogger.GetLogDirectory();
Directory.CreateDirectory(dir);
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = dir,
UseShellExecute = true
});
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开放行日志目录失败:{ex.Message}");
}
}
[MethodMember(Name = "打开管控区列表", Description = "编辑区名 / 兼容组 / 优先级 / 等待时长 / 触发点 / 区内站点 / 容量")]
public void OpenAreas()
{
try
{
var path = SignalConfigStore.Resolve(AreasPath);
ModelListPanel<MagControlAreaModel>.Open(
"磁条管控区配置",
path,
x => x == null ? "" : x.TriggerSit.ToString());
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开管控区列表失败:{ex.Message}");
}
}
private void StartWatch()
{
StopWatch();
_cts = new CancellationTokenSource();
_thread = new Thread(WatchLoop)
{
IsBackground = true,
Name = "MagTrafficWatch"
};
_thread.Start();
}
private void StopWatch()
{
try { _cts?.Cancel(); }
catch { }
try { _thread?.Join(2000); }
catch { }
_thread = null;
}
private void WatchLoop()
{
var token = _cts.Token;
var tick = 0;
var interval = Math.Max(200, TickMs);
while (!token.IsCancellationRequested)
{
try
{
var (waiting, _) = _runtime.ScanAndRelease();
var st = (MagTrafficMissionStatus)status;
st.WaitingAtTrigger = waiting;
st.PendingInFlight = _runtime.PendingCount;
st.RuleCount = _runtime.RuleCount;
st.ScanTickCount = ++tick;
if (_started)
status.status = $"运行中 规则{st.RuleCount} 等待{waiting}";
}
catch (Exception ex)
{
Diagnosis.Log($"磁条交管扫车异常:{ex.Message}", "MagTraffic", true);
}
try { Thread.Sleep(interval); }
catch (ThreadInterruptedException) { break; }
}
}
}
}
@@ -16,7 +16,7 @@ using StandardScene.Utils;
namespace StandardScene.Signal
{
/// <summary>
/// 第三方信号交互进程(场景 <c>scene.signal</c> 的唯一 Mission)。
/// 第三方信号交互进程(场景 <c>scene.signal</c>:PLC 握手 / 放行;磁条交管见 <c>MagTrafficMission</c>)。
/// </summary>
/// <remarks>
/// <para>
@@ -65,9 +65,9 @@ namespace StandardScene.Signal
/// <summary>
/// 西门子系列,传给 IoTClient <c>SiemensVersion</c>。
/// 常用 <c>S7_1500</c> / <c>S7_1200</c> / <c>S7_300</c>;连字符会当成下划线解析
/// 机构表 PLC类型为「跟随进程」时使用本项;机构单独指定则用机构值
/// </summary>
[FieldMember] public string PlcVersion = "S7_1500";
[FieldMember] public SignalPlcType PlcVersion = SignalPlcType.S7_1500;
/// <summary>机构未单独填 Port 时使用的 S7 端口,默认 102。</summary>
[FieldMember] public int PlcPort = 102;
@@ -84,6 +84,9 @@ namespace StandardScene.Signal
/// <summary>握手点表路径。按站点配置 JGControl / PointControl / TimedCharging / RateFlowControl。</summary>
[FieldMember] public string HandshakePointsPath = @"Config\Signal\handshake-points.json";
/// <summary>机构工位表。相对路径只解析工作目录 Config/Signal。</summary>
[FieldMember] public string DocksPath = @"Config\Signal\station-docks.json";
/// <summary>Execute 成功后为 trueReloadConfig 仅在已启动时重启 Worker / 扫车。</summary>
[JsonIgnore] private bool _started;
@@ -126,6 +129,12 @@ namespace StandardScene.Signal
/// <summary>实际解析到的握手点文件绝对路径。</summary>
public string HandshakePointsFile { get; set; }
/// <summary>实际解析到的工位文件绝对路径。</summary>
public string DocksFile { get; set; }
/// <summary>工位 JSON 条数。</summary>
public int DockCount { get; set; }
/// <summary>各机构运行态快照,供监视列表展示。</summary>
public List<PlcStationSnapshot> Stations { get; set; } = new List<PlcStationSnapshot>();
}
@@ -170,23 +179,26 @@ namespace StandardScene.Signal
public void ReloadConfig()
{
var stationsFile = SignalConfigStore.Resolve(StationsPath);
var docksFile = SignalConfigStore.Resolve(DocksPath);
var releaseFile = SignalConfigStore.Resolve(ReleasePointsPath);
var handshakeFile = SignalConfigStore.Resolve(HandshakePointsPath);
var stations = SignalConfigStore.Load<PlcStationModel>(stationsFile);
var bundles = PlcConfigLoader.Load(stationsFile, docksFile);
var points = SignalConfigStore.Load<ReleasePointModel>(releaseFile);
var handshake = SignalConfigStore.Load<HandshakePointModel>(handshakeFile);
var st = (SignalInteractMissionStatus)status;
st.StationCount = stations.Count;
st.StationCount = bundles.Count;
st.DockCount = bundles.Sum(b => b.Docks.Count);
st.ReleasePointCount = points.Count;
st.HandshakePointCount = handshake.Count;
st.StationsFile = stationsFile;
st.DocksFile = docksFile;
st.ReleasePointsFile = releaseFile;
st.HandshakePointsFile = handshakeFile;
st.Stations = stations.Select(s => new PlcStationSnapshot { JgName = s.JgName }).ToList();
st.Stations = bundles.Select(b => new PlcStationSnapshot { JgName = b.Station.JgName }).ToList();
status.status = _started ? "运行中" : "配置已加载";
Diagnosis.Post(
$"信号配置已加载:机构 {stations.Count};握手点 {handshake.Count} @ {handshakeFile};放行点 {points.Count} @ {releaseFile}",
$"信号配置已加载:机构 {bundles.Count},工位 {st.DockCount} @ {docksFile};握手点 {handshake.Count} @ {handshakeFile};放行点 {points.Count} @ {releaseFile}",
"Signal", true);
if (_started)
@@ -224,6 +236,24 @@ namespace StandardScene.Signal
}
}
/// <summary>CycleGUI 独立窗体编辑工位表;主键为机构名+工位编号。</summary>
[MethodMember(Name = "打开工位列表", Description = "按 PlcStationDockModel 增删改工位点位")]
public void OpenDocks()
{
try
{
var path = SignalConfigStore.Resolve(DocksPath);
ModelListPanel<PlcStationDockModel>.Open(
"机构工位配置",
path,
x => x == null ? "" : $"{x.JgName}/{x.ResolvedDockId}");
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开工位列表失败:{ex.Message}");
}
}
/// <summary>CycleGUI 独立窗体编辑握手点;主键展示为「站点号 + 信号类型」。</summary>
[MethodMember(Name = "打开握手点列表", Description = "按站点配置 JGControl / PointControl / TimedCharging / RateFlowControl")]
public void OpenHandshakePoints()
@@ -266,7 +296,7 @@ namespace StandardScene.Signal
get
{
lock (_workerLock)
return _workers.Select(w => w.Runtime).ToList();
return _workers.SelectMany(w => w.Docks).ToList();
}
}
@@ -285,25 +315,28 @@ namespace StandardScene.Signal
/// <summary>
/// 先停全部旧连接,再按机构表为每个非空名称建 <see cref="PlcStationWorker"/>。
/// 机构 Port&lt;=0 时回退进程级 <see cref="PlcPort"/>。
/// 机构 Port&lt;=0 时回退进程级 <see cref="PlcPort"/>PLC类型跟随进程时用 <see cref="PlcVersion"/>
/// </summary>
private void RestartWorkers()
{
StopWorkers();
var stationsFile = SignalConfigStore.Resolve(StationsPath);
var stations = SignalConfigStore.Load<PlcStationModel>(stationsFile)
.Where(s => !string.IsNullOrWhiteSpace(s.JgName))
.ToList();
var bundles = PlcConfigLoader.Load(
SignalConfigStore.Resolve(StationsPath),
SignalConfigStore.Resolve(DocksPath));
var created = new List<PlcStationWorker>();
lock (_workerLock)
{
foreach (var station in stations)
foreach (var bundle in bundles)
{
var station = bundle.Station;
var port = station.Port > 0 ? station.Port : PlcPort;
var runtime = new PlcStationRuntime(station);
var session = new SiemensPlcSession(PlcVersion, station.Ip, port, PlcSlot);
var worker = new PlcStationWorker(runtime, session, PlcPollMs, RefreshStatus);
var sync = new object();
var runtimes = bundle.Docks
.Select(d => new PlcStationRuntime(station, d, sync))
.ToList();
var session = new SiemensPlcSession(ResolvePlcType(station), station.Ip, port, PlcSlot);
var worker = new PlcStationWorker(station, runtimes, session, sync, PlcPollMs, RefreshStatus);
_workers.Add(worker);
created.Add(worker);
}
@@ -315,6 +348,15 @@ namespace StandardScene.Signal
PushStatus();
}
/// <summary>机构选跟随进程(含旧 JSON 缺省 0)时用进程 PlcVersion;进程若也是跟随进程则按 S7_1500。</summary>
private string ResolvePlcType(PlcStationModel station)
{
var type = station.PlcType == SignalPlcType. ? PlcVersion : station.PlcType;
if (type == SignalPlcType.)
type = SignalPlcType.S7_1500;
return type.ToString();
}
/// <summary>清空列表后再 Stop,避免扫车在停连接过程中仍拿到旧 Runtime。</summary>
private void StopWorkers()
{
@@ -345,7 +387,7 @@ namespace StandardScene.Signal
{
List<PlcStationSnapshot> snaps;
lock (_workerLock)
snaps = _workers.Select(w => w.Runtime.ToSnapshot()).ToList();
snaps = _workers.SelectMany(w => w.Docks.Select(d => d.ToSnapshot())).ToList();
var st = (SignalInteractMissionStatus)status;
st.Stations = snaps;