磁导航1.0内部交管和信号交互
This commit is contained in:
@@ -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 成功后为 true;ReloadConfig 仅在已启动时重启 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<=0 时回退进程级 <see cref="PlcPort"/>。
|
||||
/// 机构 Port<=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;
|
||||
|
||||
Reference in New Issue
Block a user