402 lines
18 KiB
C#
402 lines
18 KiB
C#
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 成功后为 true;ReloadConfig 仅在已启动时重启 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<=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}";
|
||
}
|
||
}
|
||
}
|