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}"; } } }