Files
StandardSence/StandardScene.Signal/Mission/SignalInteractMission.cs
T

402 lines
18 KiB
C#
Raw 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.Linq;
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.Plc;
using StandardScene.Signal.Ui;
using StandardScene.Utils;
namespace StandardScene.Signal
{
/// <summary>
/// 第三方信号交互进程(场景 <c>scene.signal</c>PLC 握手 / 放行;磁条交管见 <c>MagTrafficMission</c>)。
/// </summary>
/// <remarks>
/// <para>
/// 对齐梅山吉利反编译插件 <c>CoreClass30.A20050.Plugin.DefaultPlugin</c>
/// 一边用西门子 S7 和机构 PLC 交换进出站字节,一边按站点对停稳 AGV 做握手 / 放行。
/// </para>
/// <para>
/// 两条独立循环,不要合成一次 HTTP 或任务编排节点:
/// </para>
/// <list type="bullet">
/// <item>
/// <term>PLC Worker</term>
/// <description>
/// 每个机构一条后台线程,默认 200ms:心跳翻转、读「允许进入/离开」、回读写字节、按需写入、
/// <see cref="PlcStationLogic.AutoLeave"/> 推进离站状态机。
/// </description>
/// </item>
/// <item>
/// <term>扫车线程</term>
/// <description>
/// <see cref="CarScanWorker"/> 默认 1s 扫全场车。停稳则按握手点类型分发给
/// JGControl / PointControl / TimedCharging / RateFlowControl,再执行放行点;
/// 行驶中仅对 JGControl 的 RequestIn 做「未允许进入则急停」。
/// </description>
/// </item>
/// </list>
/// <para>
/// 配置三张 JSON(工作目录 <c>Config/Signal/</c>,可由迷毂数据中心改,保存后点「重新加载配置」):
/// 机构 <c>stations.json</c>、握手点 <c>handshake-points.json</c>、放行点 <c>release-points.json</c>。
/// 站点 fields 若已写 <c>SignalType</c>/<c>SignalParams</c>,优先于握手点表。
/// </para>
/// <para>
/// CycleGUI 进程工具栏必须点「信号交互进程」,不要打开开放泛型编辑器。
/// 列表窗体只改 JSON,不经过本进程的 HTTP;改完需本进程已启动才会重启 Worker。
/// </para>
/// </remarks>
[MissionType(Name = "信号交互进程", editor = typeof(SignalInteractMission))]
[I18N.DocumentTranslation(Name = "Signal Interact Mission", locale = "en")]
public class SignalInteractMission : Mission
{
/// <summary>扫车周期(毫秒)。对齐反编译 DefaultPlugin 主循环,现场一般保持 1000。</summary>
[FieldMember] public int TickMs = 1000;
/// <summary>单机构 PLC 轮询周期(毫秒)。对齐 PLCManager,现场一般保持 200。</summary>
[FieldMember] public int PlcPollMs = 200;
/// <summary>
/// 西门子系列,传给 IoTClient <c>SiemensVersion</c>。
/// 机构表 PLC类型为「跟随进程」时使用本项;机构单独指定则用机构值。
/// </summary>
[FieldMember] public SignalPlcType PlcVersion = SignalPlcType.S7_1500;
/// <summary>机构未单独填 Port 时使用的 S7 端口,默认 102。</summary>
[FieldMember] public int PlcPort = 102;
/// <summary>S7 机架槽号,S7-1500 常见为 1;写入 <see cref="SiemensPlcSession"/>。</summary>
[FieldMember] public int PlcSlot = 1;
/// <summary>机构表路径。相对路径只解析工作目录 <c>Config/Signal</c>,不读插件目录。</summary>
[FieldMember] public string StationsPath = @"Config\Signal\stations.json";
/// <summary>放行点表路径。车停在这些站点且启用时,无条件停充并启动,不读 PLC。</summary>
[FieldMember] public string ReleasePointsPath = @"Config\Signal\release-points.json";
/// <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;
/// <summary>保护 <see cref="_workers"/>:启动、停止、状态快照、扫车查找机构时共用。</summary>
[JsonIgnore] private readonly object _workerLock = new object();
/// <summary>当前在跑的机构 Worker,与 stations.json 一一对应(空名称的机构不会创建)。</summary>
[JsonIgnore] private readonly List<PlcStationWorker> _workers = new List<PlcStationWorker>();
/// <summary>全场扫车握手线程;停止进程时置空。</summary>
[JsonIgnore] private CarScanWorker _scan;
/// <summary>PLC 每 tick 都会回调刷新状态;用计数把 UI 刷新降到约每 5 个 poll 一次。</summary>
[JsonIgnore] private int _statusTick;
/// <summary>进程监视面板展示的运行摘要(机构连接数、扫车次数、各站读写字节)。</summary>
public class SignalInteractMissionStatus : MissionStatus
{
/// <summary>当前 Worker 数量,等于已加载且名称非空的机构数。</summary>
public int StationCount { get; set; }
/// <summary>最近一次快照里 <c>IsConnected == true</c> 的机构数。</summary>
public int ConnectedCount { get; set; }
/// <summary>放行点 JSON 条数(含未启用)。</summary>
public int ReleasePointCount { get; set; }
/// <summary>握手点 JSON 条数。</summary>
public int HandshakePointCount { get; set; }
/// <summary><see cref="CarScanWorker.TickCount"/>,用于确认扫车线程是否在跑。</summary>
public int ScanTickCount { get; set; }
/// <summary>实际解析到的机构文件绝对路径。</summary>
public string StationsFile { get; set; }
/// <summary>实际解析到的放行点文件绝对路径。</summary>
public string ReleasePointsFile { get; set; }
/// <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>();
}
public override MissionStatus status { get; set; } = new SignalInteractMissionStatus();
/// <summary>SimpleLite 按 MissionType 反射创建实例时调用。</summary>
public static Mission Create() => new SignalInteractMission();
/// <summary>
/// 加载三张 JSON,拉起全部 PLC Worker 和扫车线程。
/// 重复点击会先停再建;失败时 status 为「启动失败」并记诊断。
/// </summary>
[MethodMember(Name = "启动进程", Description = "加载配置,启动 PLC Worker 与扫车握手")]
public override void Execute()
{
try
{
ReloadConfig();
_started = true;
RestartWorkers();
RestartScan();
var st = (SignalInteractMissionStatus)status;
Diagnosis.Post(
$"信号交互进程已启动,机构={st.StationCount},握手点={st.HandshakePointCount},放行点={st.ReleasePointCount}",
"Signal", true);
}
catch (Exception ex)
{
status.status = "启动失败";
Diagnosis.Post($"信号交互进程启动失败:{ExceptionFormatter.FormatEx(ex)}", "Signal", true);
throw;
}
}
/// <summary>
/// 重新读 JSON 并刷新 status 里的路径/条数。
/// 进程已启动时:重建 PLC Worker,并向扫车线程热替换握手点/放行点(不必停进程)。
/// 迷毂数据中心改完文件后应调本方法,否则内存里仍是旧表。
/// </summary>
[MethodMember(Name = "重新加载配置", Description = "重新读取 JSON;若进程已启动则重启 Worker/扫车")]
public void ReloadConfig()
{
var stationsFile = SignalConfigStore.Resolve(StationsPath);
var docksFile = SignalConfigStore.Resolve(DocksPath);
var releaseFile = SignalConfigStore.Resolve(ReleasePointsPath);
var handshakeFile = SignalConfigStore.Resolve(HandshakePointsPath);
var bundles = PlcConfigLoader.Load(stationsFile, docksFile);
var points = SignalConfigStore.Load<ReleasePointModel>(releaseFile);
var handshake = SignalConfigStore.Load<HandshakePointModel>(handshakeFile);
var st = (SignalInteractMissionStatus)status;
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 = bundles.Select(b => new PlcStationSnapshot { JgName = b.Station.JgName }).ToList();
status.status = _started ? "运行中" : "配置已加载";
Diagnosis.Post(
$"信号配置已加载:机构 {bundles.Count},工位 {st.DockCount} @ {docksFile};握手点 {handshake.Count} @ {handshakeFile};放行点 {points.Count} @ {releaseFile}",
"Signal", true);
if (_started)
{
RestartWorkers();
_scan?.ReplaceConfig(handshake, points);
}
}
/// <summary>停扫车、停全部 PLC 连接。隐藏了基类 Stop,工具栏显示本方法。</summary>
[MethodMember(Name = "停止进程")]
public new void Stop()
{
_started = false;
try { _scan?.Stop(); }
catch { }
_scan = null;
StopWorkers();
status.status = "已停止";
Diagnosis.Post("信号交互进程已停止", "Signal", true);
}
/// <summary>CycleGUI 独立窗体编辑机构表;保存立刻写 JSON,需「重新加载配置」才进 Worker。</summary>
[MethodMember(Name = "打开机构列表", Description = "按 PlcStationModel 增删改机构配置")]
public void OpenStations()
{
try
{
var path = SignalConfigStore.Resolve(StationsPath);
ModelListPanel<PlcStationModel>.Open("PLC 机构配置", path, x => x?.JgName ?? "");
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开机构列表失败:{ex.Message}");
}
}
/// <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()
{
try
{
var path = SignalConfigStore.Resolve(HandshakePointsPath);
ModelListPanel<HandshakePointModel>.Open(
"握手点配置",
path,
x => x == null ? "" : $"{x.Sit} {x.SignalType}");
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开握手点列表失败:{ex.Message}");
}
}
/// <summary>CycleGUI 独立窗体编辑放行点;主键为站点号。</summary>
[MethodMember(Name = "打开放行点列表", Description = "按 ReleasePointModel 增删改放行点")]
public void OpenReleasePoints()
{
try
{
var path = SignalConfigStore.Resolve(ReleasePointsPath);
ModelListPanel<ReleasePointModel>.Open("放行点配置", path, x => x == null ? "" : x.Sit.ToString());
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开放行点列表失败:{ex.Message}");
}
}
/// <summary>
/// 扫车 Handler 用机构名查找运行态。返回拷贝列表,避免长时间持锁;
/// 运行态对象本身仍由各 Worker 持有,读写需 <see cref="PlcStationRuntime.Sync"/>。
/// </summary>
internal IReadOnlyList<PlcStationRuntime> Runtimes
{
get
{
lock (_workerLock)
return _workers.SelectMany(w => w.Docks).ToList();
}
}
/// <summary>停旧扫车线程,按当前 JSON 新建并启动。</summary>
private void RestartScan()
{
try { _scan?.Stop(); }
catch { }
var handshake = SignalConfigStore.Load<HandshakePointModel>(SignalConfigStore.Resolve(HandshakePointsPath));
var release = SignalConfigStore.Load<ReleasePointModel>(SignalConfigStore.Resolve(ReleasePointsPath));
_scan = new CarScanWorker(() => Runtimes, TickMs);
_scan.ReplaceConfig(handshake, release);
_scan.Start();
}
/// <summary>
/// 先停全部旧连接,再按机构表为每个非空名称建 <see cref="PlcStationWorker"/>。
/// 机构 Port&lt;=0 时回退进程级 <see cref="PlcPort"/>PLC类型跟随进程时用 <see cref="PlcVersion"/>。
/// </summary>
private void RestartWorkers()
{
StopWorkers();
var bundles = PlcConfigLoader.Load(
SignalConfigStore.Resolve(StationsPath),
SignalConfigStore.Resolve(DocksPath));
var created = new List<PlcStationWorker>();
lock (_workerLock)
{
foreach (var bundle in bundles)
{
var station = bundle.Station;
var port = station.Port > 0 ? station.Port : PlcPort;
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);
}
}
foreach (var worker in created)
worker.Start();
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()
{
List<PlcStationWorker> copy;
lock (_workerLock)
{
copy = _workers.ToList();
_workers.Clear();
}
foreach (var w in copy)
{
try { w.Stop(); }
catch { }
}
}
/// <summary>PLC Worker 每周期回调;约每 5 次才刷一次监视面板,避免 200ms 打满 UI。</summary>
private void RefreshStatus()
{
if (Interlocked.Increment(ref _statusTick) % 5 != 0)
return;
PushStatus();
}
/// <summary>把各机构快照和扫车次数写进 MissionStatus,供 SimpleLite 进程监视。</summary>
private void PushStatus()
{
List<PlcStationSnapshot> snaps;
lock (_workerLock)
snaps = _workers.SelectMany(w => w.Docks.Select(d => d.ToSnapshot())).ToList();
var st = (SignalInteractMissionStatus)status;
st.Stations = snaps;
st.ConnectedCount = snaps.Count(s => s.IsConnected);
st.StationCount = snaps.Count;
st.ScanTickCount = _scan?.TickCount ?? 0;
if (_started)
status.status = $"运行中 {st.ConnectedCount}/{st.StationCount} 已连接 扫车{_scan?.TickCount ?? 0}";
}
}
}