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