Compare commits
2
Commits
1e780c2b65
...
cacc5fea5c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cacc5fea5c | ||
|
|
12c7c1d518 |
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<!--
|
||||
StandardScene 插件工程编译后,自动将产物复制到 build\plugins\(SimpleComposer / SimpleLite 宿主加载目录)。
|
||||
适用:StandardScene.Core + Magnetic / QrLidar / Devices / Protocol.VDA5050 等卫星插件。
|
||||
适用:StandardScene.Core + Magnetic / QrLidar / Devices / Protocol.VDA5050 / Signal 等卫星插件。
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<StandardSceneRepoRoot>$(MSBuildThisFileDirectory)</StandardSceneRepoRoot>
|
||||
@@ -12,6 +12,7 @@
|
||||
<IsStandardScenePluginCopy Condition="'$(MSBuildProjectName)' == 'StandardScene.QrLidar'">true</IsStandardScenePluginCopy>
|
||||
<IsStandardScenePluginCopy Condition="'$(MSBuildProjectName)' == 'StandardScene.Devices'">true</IsStandardScenePluginCopy>
|
||||
<IsStandardScenePluginCopy Condition="'$(MSBuildProjectName)' == 'StandardScene.Protocol.VDA5050'">true</IsStandardScenePluginCopy>
|
||||
<IsStandardScenePluginCopy Condition="'$(MSBuildProjectName)' == 'StandardScene.Signal'">true</IsStandardScenePluginCopy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="CopyStandardScenePluginOutputs"
|
||||
@@ -28,6 +29,10 @@
|
||||
<!-- Devices 插件运行时依赖 -->
|
||||
<StandardScenePluginCopyFile Include="$(TargetDir)leegKeys-sdk.dll"
|
||||
Condition="Exists('$(TargetDir)leegKeys-sdk.dll')" />
|
||||
<StandardScenePluginCopyFile Include="$(TargetDir)IoTClient.dll"
|
||||
Condition="Exists('$(TargetDir)IoTClient.dll')" />
|
||||
<StandardScenePluginCopyFile Include="$(NuGetPackageRoot)iotclient\1.0.40\lib\netstandard2.0\IoTClient.dll"
|
||||
Condition="'$(MSBuildProjectName)' == 'StandardScene.Signal' and Exists('$(NuGetPackageRoot)iotclient\1.0.40\lib\netstandard2.0\IoTClient.dll')" />
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(StandardScenePluginCopyFile)"
|
||||
|
||||
@@ -6,5 +6,6 @@ using System.Runtime.CompilerServices;
|
||||
[assembly: InternalsVisibleTo("StandardScene.Devices")]
|
||||
[assembly: InternalsVisibleTo("StandardScene.Magnetic")]
|
||||
[assembly: InternalsVisibleTo("StandardScene.QrLidar")]
|
||||
[assembly: InternalsVisibleTo("StandardScene.Signal")]
|
||||
// SimpleLite 反射 API 需读取 internal 字段袋(BasicTrackFields / KivaSiteFields 等)的 public 字段定义。
|
||||
[assembly: InternalsVisibleTo("SimpleLite")]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"Sit": 98,
|
||||
"SignalType": "JGControl",
|
||||
"SignalParams": "上线机构1;RequestIn"
|
||||
},
|
||||
{
|
||||
"Sit": 100,
|
||||
"SignalType": "JGControl",
|
||||
"SignalParams": "上线机构1;Arrived"
|
||||
},
|
||||
{
|
||||
"Sit": 199,
|
||||
"SignalType": "JGControl",
|
||||
"SignalParams": "下线机构1;RequestIn"
|
||||
},
|
||||
{
|
||||
"Sit": 201,
|
||||
"SignalType": "PointControl",
|
||||
"SignalParams": "UP1;下线机构1"
|
||||
},
|
||||
{
|
||||
"Sit": 50,
|
||||
"SignalType": "TimedCharging",
|
||||
"SignalParams": "300;95;20"
|
||||
},
|
||||
{
|
||||
"Sit": 60,
|
||||
"SignalType": "RateFlowControl",
|
||||
"SignalParams": "缓存区;A;2;60,61,62"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"Sit": 10,
|
||||
"IsUse": true
|
||||
},
|
||||
{
|
||||
"Sit": 20,
|
||||
"IsUse": false
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"JgName": "上线机构1",
|
||||
"Ip": "192.168.1.10",
|
||||
"Port": 102,
|
||||
"ReadDb": "DB1.DBB0",
|
||||
"WriteDb": "DB1.DBB1",
|
||||
"HeartDb": "DB1.DBX2.0",
|
||||
"JgInPointId": 99,
|
||||
"JgInContains": "99,100",
|
||||
"JgPointId": 100,
|
||||
"JgContains": "100,101,102",
|
||||
"LineType": "上线机构"
|
||||
},
|
||||
{
|
||||
"JgName": "下线机构1",
|
||||
"Ip": "192.168.1.11",
|
||||
"Port": 102,
|
||||
"ReadDb": "DB1.DBB0",
|
||||
"WriteDb": "DB1.DBB1",
|
||||
"HeartDb": "DB1.DBX2.0",
|
||||
"JgInPointId": 200,
|
||||
"JgInContains": "200",
|
||||
"JgPointId": 201,
|
||||
"JgContains": "201,202",
|
||||
"LineType": "单牵引下线机构"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using SimpleCore.Library;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.Signal.Model;
|
||||
using StandardScene.Signal.Plc;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 1s 扫全场车,对齐反编译 <c>DefaultPlugin.ExcuteThreadMethod</c> + <c>StarSignManage</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 每辆车每拍只看当前站点:
|
||||
/// <list type="number">
|
||||
/// <item>停稳:先按握手点类型分发 Handler,再执行放行点(放行点不读 PLC,可能把还在等允许进入的车开走,站点不要两边都配)。</item>
|
||||
/// <item>行驶中:仅 JGControl + RequestIn 调用 <c>RequestInJudge</c>,PLC 未允许进入则停车。</item>
|
||||
/// </list>
|
||||
/// 握手点解析优先站点 fields(<see cref="SignalCarAdapter.TryReadSignalStop"/>),没有再查 JSON。
|
||||
/// </remarks>
|
||||
public sealed class CarScanWorker : IDisposable
|
||||
{
|
||||
/// <summary>取当前全部机构运行态;由 Mission 提供,内部会拷贝列表。</summary>
|
||||
private readonly Func<IReadOnlyList<PlcStationRuntime>> _runtimes;
|
||||
|
||||
/// <summary>扫车间隔,下限 100ms,默认跟进程 TickMs=1000。</summary>
|
||||
private readonly int _tickMs;
|
||||
|
||||
/// <summary>保护握手点 / 放行点字典,热替换配置时与扫车拍互斥。</summary>
|
||||
private readonly object _configLock = new object();
|
||||
|
||||
/// <summary>站点 → 握手点。同一站点多条配置只留第一条。</summary>
|
||||
private Dictionary<int, HandshakePointModel> _handshake = new Dictionary<int, HandshakePointModel>();
|
||||
|
||||
/// <summary>站点 → 放行点。同一站点多条配置只留第一条。</summary>
|
||||
private Dictionary<int, ReleasePointModel> _release = new Dictionary<int, ReleasePointModel>();
|
||||
|
||||
/// <summary>限流区域上次记日志的车号,避免同一区域每秒刷屏。</summary>
|
||||
private readonly Dictionary<string, int> _rateFlowLog = new Dictionary<string, int>();
|
||||
|
||||
/// <summary>车辆没有 GetChargeTime 时,用首次看到「充电中」的时刻估算秒数。</summary>
|
||||
private readonly Dictionary<int, DateTime> _chargeStarted = new Dictionary<int, DateTime>();
|
||||
|
||||
/// <summary>未知 SignalType 的日志节流(同站点 10s 一次)。</summary>
|
||||
private readonly Dictionary<int, DateTime> _unknownTypeLog = new Dictionary<int, DateTime>();
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private Thread _thread;
|
||||
private int _tickCount;
|
||||
|
||||
public CarScanWorker(Func<IReadOnlyList<PlcStationRuntime>> runtimes, int tickMs)
|
||||
{
|
||||
_runtimes = runtimes ?? throw new ArgumentNullException(nameof(runtimes));
|
||||
_tickMs = Math.Max(100, tickMs);
|
||||
}
|
||||
|
||||
/// <summary>已完成的扫车拍数,写入进程 status 供监视。</summary>
|
||||
public int TickCount => _tickCount;
|
||||
|
||||
/// <summary>
|
||||
/// 热替换握手点 / 放行点。过滤掉站点号无效或信号类型为空的握手点。
|
||||
/// 不中断线程,下一拍生效。
|
||||
/// </summary>
|
||||
public void ReplaceConfig(IEnumerable<HandshakePointModel> handshake, IEnumerable<ReleasePointModel> release)
|
||||
{
|
||||
lock (_configLock)
|
||||
{
|
||||
_handshake = (handshake ?? Array.Empty<HandshakePointModel>())
|
||||
.Where(x => x != null && x.Sit > 0 && !string.IsNullOrWhiteSpace(x.SignalType))
|
||||
.GroupBy(x => x.Sit)
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
_release = (release ?? Array.Empty<ReleasePointModel>())
|
||||
.Where(x => x != null && x.Sit > 0)
|
||||
.GroupBy(x => x.Sit)
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>启动后台扫车线程。重复 Start 若线程仍活则忽略。</summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_thread != null && _thread.IsAlive) return;
|
||||
_cts = new CancellationTokenSource();
|
||||
_thread = new Thread(Loop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SignalScan"
|
||||
};
|
||||
_thread.Start();
|
||||
Diagnosis.Log("扫车握手线程已启动", "Signal", true);
|
||||
}
|
||||
|
||||
/// <summary>取消令牌并最多等 2s 退出。不抛异常,供进程停止时调用。</summary>
|
||||
public void Stop()
|
||||
{
|
||||
try { _cts?.Cancel(); }
|
||||
catch { }
|
||||
try { _thread?.Join(2000); }
|
||||
catch { }
|
||||
_thread = null;
|
||||
Diagnosis.Log("扫车握手线程已停止", "Signal", true);
|
||||
}
|
||||
|
||||
public void Dispose() => Stop();
|
||||
|
||||
private void Loop()
|
||||
{
|
||||
var token = _cts.Token;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
ScanOnce();
|
||||
Interlocked.Increment(ref _tickCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"检测信号停止的AGV是否可以放行异常:{ex.Message}", "Signal", true);
|
||||
}
|
||||
|
||||
try { Thread.Sleep(_tickMs); }
|
||||
catch (ThreadInterruptedException) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一拍:拷贝配置字典 → 按机构名查找闭包 → 遍历全部车。
|
||||
/// 查找机构时忽略大小写,与 JSON 里 JgName 对齐。
|
||||
/// </summary>
|
||||
internal void ScanOnce()
|
||||
{
|
||||
Dictionary<int, HandshakePointModel> handshake;
|
||||
Dictionary<int, ReleasePointModel> release;
|
||||
lock (_configLock)
|
||||
{
|
||||
handshake = _handshake;
|
||||
release = _release;
|
||||
}
|
||||
|
||||
PlcStationRuntime Find(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return null;
|
||||
var list = _runtimes() ?? Array.Empty<PlcStationRuntime>();
|
||||
return list.FirstOrDefault(x =>
|
||||
string.Equals(x.Config.JgName, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
foreach (var car in SignalCarAdapter.All())
|
||||
{
|
||||
if (car == null) continue;
|
||||
var sit = SignalCarAdapter.PositionId(car);
|
||||
var spec = ResolveSpec(sit, handshake);
|
||||
|
||||
if (SignalCarAdapter.IsStopped(car))
|
||||
{
|
||||
if (spec != null)
|
||||
DispatchStopped(car, spec, Find);
|
||||
ReleasePointHandler.OnStopped(car, sit, release);
|
||||
}
|
||||
else if (spec != null && spec.Is("JGControl"))
|
||||
{
|
||||
JgControlHandler.OnMoving(car, spec, Find);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 站点 fields 有 SignalType 则用地图点配置(老 SignalStop);
|
||||
/// 否则用 handshake-points.json。fields 存在但类型为空视为本站不握手。
|
||||
/// </summary>
|
||||
private SignalStopSpec ResolveSpec(int sit, IReadOnlyDictionary<int, HandshakePointModel> handshake)
|
||||
{
|
||||
if (sit <= 0) return null;
|
||||
|
||||
if (SignalCarAdapter.TryReadSignalStop(sit, out var type, out var parms))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(type))
|
||||
return null;
|
||||
return SignalStopSpec.Parse(type, parms);
|
||||
}
|
||||
|
||||
if (handshake != null && handshake.TryGetValue(sit, out var model))
|
||||
return SignalStopSpec.From(model);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停稳分发。TimedCharging 优先:充电点不应再走机构逻辑。
|
||||
/// 未识别的 SignalType 按站点节流打日志,避免刷屏。
|
||||
/// </summary>
|
||||
private void DispatchStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
|
||||
{
|
||||
if (spec.Is("TimedCharging"))
|
||||
{
|
||||
TimedChargeHandler.OnStopped(car, spec, _chargeStarted);
|
||||
return;
|
||||
}
|
||||
|
||||
if (spec.Is("JGControl"))
|
||||
{
|
||||
JgControlHandler.OnStopped(car, spec, find, _rateFlowLog);
|
||||
return;
|
||||
}
|
||||
|
||||
if (spec.Is("PointControl"))
|
||||
{
|
||||
PointControlHandler.OnStopped(car, spec, find);
|
||||
return;
|
||||
}
|
||||
|
||||
if (spec.Is("RateFlowControl"))
|
||||
{
|
||||
RateFlowHandler.OnStopped(car, spec, _rateFlowLog);
|
||||
return;
|
||||
}
|
||||
|
||||
var sit = SignalCarAdapter.PositionId(car);
|
||||
if (_unknownTypeLog.TryGetValue(sit, out var last) && (DateTime.Now - last).TotalSeconds < 10)
|
||||
return;
|
||||
_unknownTypeLog[sit] = DateTime.Now;
|
||||
Diagnosis.Log($"信号类型【{spec.SignalType}】未找到匹配的业务处理模块", "Signal", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SimpleCore.Library;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.Signal.Model;
|
||||
using StandardScene.Signal.Plc;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 机构进出站握手。对齐反编译 <c>DefaultPlugin.JGControl</c> / <c>JGJudge</c> / <c>RequestIn</c> / <c>Arrived</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <c>SignalParams</c> 分号三段:<c>机构名;动作;限流</c>。
|
||||
/// 动作 <c>RequestIn</c>(请求进入,配在机构口外等待点)或 <c>Arrived</c>(到位,配在机构内地标)。
|
||||
/// 第三段可选,格式与独立限流不同:<c>区域-车组-限额-点1,点2</c>(短横分隔,见 <see cref="RateFlowHandler"/>)。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// RequestIn 放行条件(需同时满足):PLC 读字节=允许进入、尚无请求进入车、写字节=无状态。
|
||||
/// 下线机构额外要求 <c>JgPointId</c> 上没有其它车。通过后写「进入中」、记下 RequestInAgvNo、停充并启动。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 车已在跑但本车不是请求进入车、且 PLC 不是允许进入时,立即停车(停稳拍与行驶拍都会判)。
|
||||
/// Arrived:若 InAgvNo 还不是本车,写「到位」、登记 InAgvNo、清空 RequestInAgvNo。
|
||||
/// 之后的「离开中 / 已离开」由 PLC Worker 的 <see cref="PlcStationLogic.AutoLeave"/> 推进。
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class JgControlHandler
|
||||
{
|
||||
/// <summary>停稳:可选限流 → RequestIn 或 Arrived。</summary>
|
||||
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find, IDictionary<string, int> rateFlowLog)
|
||||
{
|
||||
var jgName = spec.Part(0).Trim();
|
||||
var action = spec.Part(1).Trim();
|
||||
var rateFlow = spec.Part(2).Trim();
|
||||
if (string.IsNullOrEmpty(jgName))
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}停在地标{SignalCarAdapter.PositionId(car)},读取配置机构名称为空", "JGControl", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(rateFlow) && !RateFlowHandler.AllowPass(car, rateFlow, rateFlowLog))
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}停在地标{SignalCarAdapter.PositionId(car)},限流禁止通行", "JGControl", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(action, "RequestIn", StringComparison.OrdinalIgnoreCase))
|
||||
RequestIn(jgName, car, find);
|
||||
else if (string.Equals(action, "Arrived", StringComparison.OrdinalIgnoreCase))
|
||||
Arrived(jgName, car, find);
|
||||
}
|
||||
|
||||
/// <summary>行驶中只做 RequestIn 安全闸:未允许进入且不是本车占用请求槽则停车。</summary>
|
||||
public static void OnMoving(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
|
||||
{
|
||||
var jgName = spec.Part(0).Trim();
|
||||
var action = spec.Part(1).Trim();
|
||||
if (string.IsNullOrEmpty(jgName))
|
||||
return;
|
||||
if (string.Equals(action, "RequestIn", StringComparison.OrdinalIgnoreCase))
|
||||
RequestInJudge(jgName, car, find);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求进入。锁机构运行态,避免与 PLC Worker 同时改写字节 / 车号。
|
||||
/// 后半段与 <see cref="RequestInJudge"/> 相同,覆盖「刚放行后又被调度层启动」的竞态。
|
||||
/// </summary>
|
||||
private static void RequestIn(string jgName, Car car, Func<string, PlcStationRuntime> find)
|
||||
{
|
||||
var rt = find(jgName);
|
||||
if (rt == null)
|
||||
{
|
||||
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (rt.Sync)
|
||||
{
|
||||
if (rt.ReadSignal == SignalReadValue.允许进入 &&
|
||||
rt.RequestInAgvNo == 0 &&
|
||||
rt.ReadWriteSignal == SignalWriteValue.无状态)
|
||||
{
|
||||
if (rt.Config.LineType is SignalLineType.单牵引下线机构 or SignalLineType.双牵引下线机构)
|
||||
{
|
||||
var inside = SignalCarAdapter.CarsAt(rt.Config.JgPointId);
|
||||
if (inside.Count > 0)
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"{jgName} 机构内有AGV【{string.Join(",", inside.Select(x => x.id))}】暂不放行AGV{car.id}",
|
||||
"JGControl", true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Diagnosis.Log($"{jgName} 机构允许进入,通知AGV{car.id}启动,通知机构AGV进入中", "JGControl", true);
|
||||
rt.WriteSignal = SignalWriteValue.进入中;
|
||||
rt.RequestInAgvNo = car.id;
|
||||
SignalCarAdapter.StopCharge(car);
|
||||
SignalCarAdapter.Start(car);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rt.ReadSignal != SignalReadValue.允许进入 &&
|
||||
rt.RequestInAgvNo != car.id &&
|
||||
SignalCarAdapter.IsMoving(car))
|
||||
{
|
||||
SignalCarAdapter.Stop(car);
|
||||
Diagnosis.Log($"{jgName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>行驶中的 RequestIn 安全闸,条件与 RequestIn 后半段一致。</summary>
|
||||
private static void RequestInJudge(string jgName, Car car, Func<string, PlcStationRuntime> find)
|
||||
{
|
||||
var rt = find(jgName);
|
||||
if (rt == null)
|
||||
{
|
||||
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (rt.Sync)
|
||||
{
|
||||
if (rt.ReadSignal != SignalReadValue.允许进入 &&
|
||||
rt.RequestInAgvNo != car.id &&
|
||||
SignalCarAdapter.IsMoving(car))
|
||||
{
|
||||
SignalCarAdapter.Stop(car);
|
||||
Diagnosis.Log($"{jgName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到位。InAgvNo 已是本车则认为已报到,避免每秒重复写「到位」把 WriteSuccess 清掉。
|
||||
/// </summary>
|
||||
private static void Arrived(string jgName, Car car, Func<string, PlcStationRuntime> find)
|
||||
{
|
||||
var rt = find(jgName);
|
||||
if (rt == null)
|
||||
{
|
||||
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (rt.Sync)
|
||||
{
|
||||
if (rt.InAgvNo == car.id)
|
||||
return;
|
||||
Diagnosis.Log($"AGV{car.id}已到达机构{jgName}", "JGControl", true);
|
||||
rt.WriteSignal = SignalWriteValue.到位;
|
||||
rt.InAgvNo = car.id;
|
||||
rt.RequestInAgvNo = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using SimpleCore.Library;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.Signal.Model;
|
||||
using StandardScene.Signal.Plc;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 销钉 / 牵引棒升降后再放行。对齐反编译 <c>DefaultPlugin.PointControl</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>SignalParams</c>:<c>动作;机构名</c>。
|
||||
/// 动作:<c>UP1</c> 升销钉1,<c>UP2</c> 升销钉1+2,<c>Down1</c> / <c>Down2</c> 对应下降。
|
||||
/// 机构名可选:填了则必须等该机构读字节=允许离开才启动;空则只等销钉动作完成。
|
||||
/// 销钉指令通过 <see cref="SignalCarAdapter.Control"/> 反射调用车体 MethodMember(如 SetPin1Up)。
|
||||
/// </remarks>
|
||||
public static class PointControlHandler
|
||||
{
|
||||
/// <summary>停稳:先判机构是否允许离开,再下发销钉,全部到位后启动。</summary>
|
||||
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
|
||||
{
|
||||
var action = spec.Part(0).Trim();
|
||||
var jgName = spec.Part(1).Trim();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}停在地标{SignalCarAdapter.PositionId(car)},读取配置特殊点名称为空", "SetPin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
var waitLeave = false;
|
||||
if (!string.IsNullOrEmpty(jgName))
|
||||
{
|
||||
var rt = find(jgName);
|
||||
if (rt != null)
|
||||
{
|
||||
lock (rt.Sync)
|
||||
waitLeave = rt.ReadSignal != SignalReadValue.允许离开;
|
||||
}
|
||||
}
|
||||
|
||||
var pos = SignalCarAdapter.PositionId(car);
|
||||
switch (action)
|
||||
{
|
||||
case "UP1":
|
||||
PinThenGo(car, pos, waitLeave, jgName, "SetPin1Up");
|
||||
break;
|
||||
case "UP2":
|
||||
PinThenGo(car, pos, waitLeave, jgName, "SetPin1Up", "SetPin2Up");
|
||||
break;
|
||||
case "Down1":
|
||||
PinThenGo(car, pos, waitLeave, jgName, "SetPin1Down");
|
||||
break;
|
||||
case "Down2":
|
||||
PinThenGo(car, pos, waitLeave, jgName, "SetPin1Down", "SetPin2Down");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任一销钉未完成则本拍只下发指令、不启动;
|
||||
/// 全部完成且(无需等离开 或 已允许离开)才 Start。
|
||||
/// </summary>
|
||||
private static void PinThenGo(Car car, int pos, bool waitLeave, string jgName, params string[] actions)
|
||||
{
|
||||
var done = true;
|
||||
foreach (var a in actions)
|
||||
{
|
||||
if (!SignalCarAdapter.IsActionFinish(car, a))
|
||||
done = false;
|
||||
}
|
||||
|
||||
if (!done)
|
||||
{
|
||||
foreach (var a in actions)
|
||||
SignalCarAdapter.Control(car, a);
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitLeave)
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}停在地标{pos},需要读取{jgName}工位的允许离开", "SetPin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
SignalCarAdapter.Start(car);
|
||||
Diagnosis.Log($"AGV{car.id}停在地标{pos},牵引棒动作到位,已通知AGV启动", "SetPin", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SimpleCore.Library;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.Signal.Plc;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 区域车组限流。对齐反编译 <c>DefaultPlugin.RateFlowControl</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 独立握手类型 <c>RateFlowControl</c> 用分号:<c>区域;车组;限额;点1,点2</c>。
|
||||
/// 挂在 JGControl 第三段时用短横:<c>区域-车组-限额-点1,点2</c>(因为前两段机构名/动作已经占用分号)。
|
||||
/// 统计「站点列表上、同车组、且不是本车」的数量,小于限额才放行。
|
||||
/// 解析异常时放行(return true),避免配错把产线卡死。
|
||||
/// </remarks>
|
||||
public static class RateFlowHandler
|
||||
{
|
||||
/// <summary>独立限流点:通过则停充并启动。</summary>
|
||||
public static void OnStopped(Car car, SignalStopSpec spec, IDictionary<string, int> logByArea)
|
||||
{
|
||||
if (AllowPass(car, spec.SignalParams, logByArea, sep: ';'))
|
||||
{
|
||||
SignalCarAdapter.StopCharge(car);
|
||||
SignalCarAdapter.Start(car);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>JGControl 第三段限流,短横分隔。不通过时调用方应直接 return,不启动。</summary>
|
||||
public static bool AllowPass(Car car, string rateFlowString, IDictionary<string, int> logByArea) =>
|
||||
AllowPass(car, rateFlowString, logByArea, sep: '-');
|
||||
|
||||
/// <param name="sep">独立类型用 ';',JGControl 挂载用 '-'。</param>
|
||||
private static bool AllowPass(Car car, string raw, IDictionary<string, int> logByArea, char sep)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = (raw ?? "").Split(sep);
|
||||
var area = parts.Length > 0 ? parts[0] : "";
|
||||
var group = parts.Length > 1 ? parts[1] : "";
|
||||
var limit = 0;
|
||||
if (parts.Length > 2)
|
||||
int.TryParse(parts[2], out limit);
|
||||
var sites = new List<int>();
|
||||
if (parts.Length > 3)
|
||||
{
|
||||
foreach (var s in parts[3].Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (int.TryParse(s.Trim(), out var id))
|
||||
sites.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
var others = SignalCarAdapter.CarsAt(sites)
|
||||
.Where(x => x.id != car.id && string.Equals(SignalCarAdapter.GroupId(x), group, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
var count = others.Count;
|
||||
if (count < limit)
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"限流区域{area}限制AGV组{group}流量{limit}辆AGV,当前停放{count},放行AGV{car.id}",
|
||||
"RateFlow", true);
|
||||
Note(logByArea, area, car, raw);
|
||||
return true;
|
||||
}
|
||||
|
||||
Diagnosis.Log(
|
||||
$"限流区域{area}限制AGV组{group}流量{limit}辆AGV,当前停放{count},无法放行",
|
||||
"RateFlow", true);
|
||||
Note(logByArea, area, car, raw);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"处理区域限流业务异常:{ex.Message}", "RateFlow", true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>同一限流区域仅在「决策车号变化」时再打一条带原始参数的日志。</summary>
|
||||
private static void Note(IDictionary<string, int> logByArea, string area, Car car, string raw)
|
||||
{
|
||||
if (logByArea == null) return;
|
||||
var changed = !logByArea.TryGetValue(area, out var last) || last != car.id;
|
||||
logByArea[area] = car.id;
|
||||
if (changed)
|
||||
Diagnosis.Log($"AGV{car.id}停在地标{SignalCarAdapter.PositionId(car)},配置参数:【{raw}】", "RateFlow", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.Signal.Model;
|
||||
using StandardScene.Signal.Plc;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 无条件放行点。对齐反编译 <c>StarSignManage</c> / <c>StartSignModel</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 车在配置站点停稳且 <see cref="ReleasePointModel.IsUse"/> 为 true 时:停充并启动,不读 PLC。
|
||||
/// 用于任务在某点停住、充完电或人工暂停后需要自动续跑的站点。
|
||||
/// 机构口应配握手点,不要同时配放行点——扫车线程在握手 Handler 之后仍会执行本方法,可能提前开走。
|
||||
/// </remarks>
|
||||
public static class ReleasePointHandler
|
||||
{
|
||||
/// <summary>按当前站点号查放行表;未配置或未启用则什么都不做。</summary>
|
||||
public static void OnStopped(Car car, int sit, IReadOnlyDictionary<int, ReleasePointModel> points)
|
||||
{
|
||||
if (points == null || !points.TryGetValue(sit, out var p) || p == null || !p.IsUse)
|
||||
return;
|
||||
SignalCarAdapter.StopCharge(car);
|
||||
SignalCarAdapter.Start(car);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using StandardScene.Signal.Model;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 一条握手点配置的解析结果。参数格式与反编译 <c>SignalStop.SignalParams</c> 一致。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="SignalParams"/> 按分号切成 <see cref="Parts"/>,缺段时 <see cref="Part"/> 返回空串。
|
||||
/// 各类型约定:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>JGControl</c>:机构名;RequestIn|Arrived;[区域-车组-限额-点列表]</item>
|
||||
/// <item><c>PointControl</c>:UP1|UP2|Down1|Down2;机构名</item>
|
||||
/// <item><c>TimedCharging</c>:最长秒数;上限电量;下限电量</item>
|
||||
/// <item><c>RateFlowControl</c>:区域;车组;限额;点1,点2</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class SignalStopSpec
|
||||
{
|
||||
/// <summary>握手类型名,与 Handler 的 <see cref="Is"/> 比较时忽略大小写。</summary>
|
||||
public string SignalType { get; init; } = "";
|
||||
|
||||
/// <summary>原始参数串,限流独立类型会整段交给 <see cref="RateFlowHandler"/>。</summary>
|
||||
public string SignalParams { get; init; } = "";
|
||||
|
||||
/// <summary>按 ';' 切分,保留空段(与 Split None 一致,方便 Part(n) 按下标取)。</summary>
|
||||
public string[] Parts { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>取第 <paramref name="index"/> 段;越界或 null 返回空串。</summary>
|
||||
public string Part(int index)
|
||||
{
|
||||
if (index < 0 || index >= Parts.Length)
|
||||
return "";
|
||||
return Parts[index] ?? "";
|
||||
}
|
||||
|
||||
/// <summary>从类型名和参数串构造;类型会 Trim,参数保持原样以便日志对照现场 JSON。</summary>
|
||||
public static SignalStopSpec Parse(string signalType, string signalParams)
|
||||
{
|
||||
var type = (signalType ?? "").Trim();
|
||||
var raw = signalParams ?? "";
|
||||
return new SignalStopSpec
|
||||
{
|
||||
SignalType = type,
|
||||
SignalParams = raw,
|
||||
Parts = raw.Split(new[] { ';' }, StringSplitOptions.None)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>从握手点 Model 构造;model 为 null 时返回 null。</summary>
|
||||
public static SignalStopSpec From(HandshakePointModel model)
|
||||
{
|
||||
if (model == null)
|
||||
return null;
|
||||
return Parse(model.SignalType, model.SignalParams);
|
||||
}
|
||||
|
||||
/// <summary>类型名是否匹配(忽略大小写)。</summary>
|
||||
public bool Is(string type) =>
|
||||
string.Equals(SignalType, type, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SimpleCore.Library;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.Signal.Plc;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 站点定时充电。对齐反编译 <c>DefaultPlugin.TimedCharging</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>SignalParams</c>:<c>最长秒数;上限电量;下限电量</c>,某段填 0 表示该条件不启用。
|
||||
/// 正在充电:电量 ≥ 上限则停充;时长到且电量已过下限则停充;时长到但电量仍 ≤ 下限则继续充。
|
||||
/// 已停充且车仍停着:视为充电流程结束,启动放行。
|
||||
/// 充电秒数优先反射 <c>GetChargeTime</c>,没有则用 <paramref name="chargeStarted"/> 字典估算。
|
||||
/// </remarks>
|
||||
public static class TimedChargeHandler
|
||||
{
|
||||
/// <summary>停稳且本站是 TimedCharging 时由扫车线程调用。</summary>
|
||||
public static void OnStopped(Car car, SignalStopSpec spec, IDictionary<int, DateTime> chargeStarted)
|
||||
{
|
||||
int.TryParse(spec.Part(0), out var maxSeconds);
|
||||
int.TryParse(spec.Part(1), out var maxSoc);
|
||||
int.TryParse(spec.Part(2), out var minSoc);
|
||||
|
||||
var charged = SignalCarAdapter.ChargeSeconds(car, chargeStarted);
|
||||
var soc = SignalCarAdapter.Soc(car);
|
||||
var pos = SignalCarAdapter.PositionId(car);
|
||||
|
||||
if (SignalCarAdapter.IsCharging(car))
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"AGV{car.id}实时电量{soc}停在地标{pos}上已充电{charged}秒,信号停止类型为定时充电,参数为:{maxSeconds},{maxSoc},{minSoc}",
|
||||
"Charge", true);
|
||||
|
||||
if (maxSoc != 0 && soc >= maxSoc)
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}电量大于配置的最高充电电量,故停止充电", "Charge", true);
|
||||
SignalCarAdapter.StopCharge(car);
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxSeconds != 0 && charged >= maxSeconds)
|
||||
{
|
||||
if (minSoc != 0 && soc <= minSoc)
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}充电时长已到达{maxSeconds}秒,但仍未充电至{minSoc}继续充电", "Charge", true);
|
||||
return;
|
||||
}
|
||||
|
||||
Diagnosis.Log($"AGV{car.id}充电时长已到达{maxSeconds}秒,故停止充电", "Charge", true);
|
||||
SignalCarAdapter.StopCharge(car);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (SignalCarAdapter.IsStopped(car))
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}已经停止充电,故放行AGV", "Charge", true);
|
||||
SignalCarAdapter.Start(car);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
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> 的唯一 Mission)。
|
||||
/// </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>。
|
||||
/// 常用 <c>S7_1500</c> / <c>S7_1200</c> / <c>S7_300</c>;连字符会当成下划线解析。
|
||||
/// </summary>
|
||||
[FieldMember] public string PlcVersion = "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>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 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 releaseFile = SignalConfigStore.Resolve(ReleasePointsPath);
|
||||
var handshakeFile = SignalConfigStore.Resolve(HandshakePointsPath);
|
||||
var stations = SignalConfigStore.Load<PlcStationModel>(stationsFile);
|
||||
var points = SignalConfigStore.Load<ReleasePointModel>(releaseFile);
|
||||
var handshake = SignalConfigStore.Load<HandshakePointModel>(handshakeFile);
|
||||
|
||||
var st = (SignalInteractMissionStatus)status;
|
||||
st.StationCount = stations.Count;
|
||||
st.ReleasePointCount = points.Count;
|
||||
st.HandshakePointCount = handshake.Count;
|
||||
st.StationsFile = stationsFile;
|
||||
st.ReleasePointsFile = releaseFile;
|
||||
st.HandshakePointsFile = handshakeFile;
|
||||
st.Stations = stations.Select(s => new PlcStationSnapshot { JgName = s.JgName }).ToList();
|
||||
status.status = _started ? "运行中" : "配置已加载";
|
||||
Diagnosis.Post(
|
||||
$"信号配置已加载:机构 {stations.Count};握手点 {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 = "按站点配置 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.Select(w => w.Runtime).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"/>。
|
||||
/// </summary>
|
||||
private void RestartWorkers()
|
||||
{
|
||||
StopWorkers();
|
||||
var stationsFile = SignalConfigStore.Resolve(StationsPath);
|
||||
var stations = SignalConfigStore.Load<PlcStationModel>(stationsFile)
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.JgName))
|
||||
.ToList();
|
||||
|
||||
var created = new List<PlcStationWorker>();
|
||||
lock (_workerLock)
|
||||
{
|
||||
foreach (var station in stations)
|
||||
{
|
||||
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);
|
||||
_workers.Add(worker);
|
||||
created.Add(worker);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var worker in created)
|
||||
worker.Start();
|
||||
|
||||
PushStatus();
|
||||
}
|
||||
|
||||
/// <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.Select(w => w.Runtime.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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace StandardScene.Signal.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// 握手点配置。对应老地图点上的 <c>SignalStop</c>(SignalType + SignalParams)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 站点 fields 若已填写同名键,扫车优先用站点字段,本表该站点不再生效。
|
||||
/// SignalParams 按分号切分,各类型格式见 <see cref="StandardScene.Signal.Logic.SignalStopSpec"/>。
|
||||
/// 列表主键展示为「站点号 + 类型」,同一站点建议只配一条。
|
||||
/// </remarks>
|
||||
[Category("握手点数据管理")]
|
||||
[DisplayName("握手点配置列表")]
|
||||
public class HandshakePointModel
|
||||
{
|
||||
/// <summary>地图站点号,与车 PositionId(siteID / GetLastSite)对应。</summary>
|
||||
[DisplayName("站点")]
|
||||
public int Sit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 握手类型:<c>JGControl</c> 机构进出、
|
||||
/// <c>PointControl</c> 销钉升降、
|
||||
/// <c>TimedCharging</c> 定时充电、
|
||||
/// <c>RateFlowControl</c> 区域限流。
|
||||
/// 其它字符串会每 10s 打一次「未找到业务模块」日志。
|
||||
/// </summary>
|
||||
[DisplayName("信号类型")]
|
||||
public string SignalType { get; set; } = "JGControl";
|
||||
|
||||
/// <summary>
|
||||
/// 类型参数。示例:
|
||||
/// JGControl <c>上线机构1;RequestIn</c> / <c>上线机构1;Arrived</c> / 第三段限流 <c>缓存区-A-2-60,61</c>;
|
||||
/// PointControl <c>UP1;下线机构1</c>;
|
||||
/// TimedCharging <c>300;95;20</c>;
|
||||
/// RateFlowControl <c>缓存区;A;2;60,61,62</c>。
|
||||
/// </summary>
|
||||
[DisplayName("信号参数")]
|
||||
public string SignalParams { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace StandardScene.Signal.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC 机构静态配置,对应反编译 <c>PLCModel</c> 可存档字段。
|
||||
/// 读写运行态(当前字节、机构内车号)在 <see cref="StandardScene.Signal.Plc.PlcStationRuntime"/>,不进本类、不进 JSON。
|
||||
/// 机构名称不能重复,列表编辑器用 JgName 做主键。
|
||||
/// </summary>
|
||||
[Category("PLC数据管理")]
|
||||
[DisplayName("PLC配置列表(机构名称不能重复)")]
|
||||
public class PlcStationModel
|
||||
{
|
||||
/// <summary>逻辑名,握手点 SignalParams 第一段、Worker 线程名都用它。不能空、不能重复。</summary>
|
||||
[DisplayName("机构名称")]
|
||||
public string JgName { get; set; } = "";
|
||||
|
||||
/// <summary>西门子 PLC IP。</summary>
|
||||
[DisplayName("机构IP")]
|
||||
public string Ip { get; set; } = "127.0.0.1";
|
||||
|
||||
/// <summary>S7 端口。填 0 则用进程字段 PlcPort(默认 102)。</summary>
|
||||
[DisplayName("端口")]
|
||||
public int Port { get; set; } = 102;
|
||||
|
||||
/// <summary>
|
||||
/// 读字节地址(PLC→调度:允许进入/离开)。
|
||||
/// 支持 <c>DB1.DBB0</c> 或 Hsl 风格 <c>DB1.0</c>。空则跳过读取。
|
||||
/// </summary>
|
||||
[DisplayName("读地址")]
|
||||
public string ReadDb { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 写字节地址(调度→PLC:进入中/到位/离开中/已离开)。
|
||||
/// Worker 会先回读再按需写入。空则跳过写和回读。
|
||||
/// </summary>
|
||||
[DisplayName("写地址")]
|
||||
public string WriteDb { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 心跳位地址,如 <c>DB1.DBX2.0</c>。
|
||||
/// Worker 每拍读当前值再写反码,让 PLC 判断调度进程存活。空则不做心跳。
|
||||
/// </summary>
|
||||
[DisplayName("写心跳地址")]
|
||||
public string HeartDb { get; set; } = "";
|
||||
|
||||
/// <summary>机构进入地标(预留,与老 PLCModel 字段对齐;上线跟车主要用 JgInContains)。</summary>
|
||||
[DisplayName("机构进入地标")]
|
||||
public int JgInPointId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 进入过程包含的站点列表(逗号/分号/空格分隔)。
|
||||
/// 上线:写字节=进入中 且车已不在此列表 → 视为到位。
|
||||
/// </summary>
|
||||
[DisplayName("机构进入包含地标")]
|
||||
public string JgInContains { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 机构工作地标。下线 RequestIn 用它查「机构内是否已有车」;
|
||||
/// 下线/等待通行离站时,车必须停在此点才响应允许离开。
|
||||
/// </summary>
|
||||
[DisplayName("机构地标")]
|
||||
public int JgPointId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 机构占用包含的站点列表。车已不在此列表且写字节还不是已离开 → 写已离开。
|
||||
/// </summary>
|
||||
[DisplayName("机构包含地标")]
|
||||
public string JgContains { get; set; } = "";
|
||||
|
||||
/// <summary>上线 / 单牵引下线 / 双牵引下线 / 等待通行,决定 AutoLeave 分支。</summary>
|
||||
[DisplayName("上下机构")]
|
||||
public SignalLineType LineType { get; set; } = SignalLineType.未知;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace StandardScene.Signal.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个机构运行态快照,供 MissionStatus / 监视面板展示。
|
||||
/// 由 <see cref="StandardScene.Signal.Plc.PlcStationRuntime.ToSnapshot"/> 拷贝,UI 不要直接绑 Runtime。
|
||||
/// </summary>
|
||||
public class PlcStationSnapshot
|
||||
{
|
||||
/// <summary>机构逻辑名。</summary>
|
||||
[DisplayName("机构")] public string JgName { get; set; } = "";
|
||||
|
||||
/// <summary>最近一拍 PLC 读写是否成功。</summary>
|
||||
[DisplayName("连接")] public bool IsConnected { get; set; }
|
||||
|
||||
/// <summary>PLC→调度读字节的枚举名(允许进入 / 允许离开 / 无状态)。</summary>
|
||||
[DisplayName("读信号")] public string ReadSignal { get; set; } = "";
|
||||
|
||||
/// <summary>调度目标写字节的枚举名。</summary>
|
||||
[DisplayName("写信号")] public string WriteSignal { get; set; } = "";
|
||||
|
||||
/// <summary>从写地址回读到的枚举名,用于确认 PLC 是否已吃进目标值。</summary>
|
||||
[DisplayName("回读写值")] public string ReadWriteSignal { get; set; } = "";
|
||||
|
||||
/// <summary>已报到在机构内的 AGV 号,0 表示没有。</summary>
|
||||
[DisplayName("机构内车")] public int InAgvNo { get; set; }
|
||||
|
||||
/// <summary>已获准进入、正在往机构开的 AGV 号,0 表示没有。</summary>
|
||||
[DisplayName("请求进入车")] public int RequestInAgvNo { get; set; }
|
||||
|
||||
/// <summary>目标写字节是否已与回读一致。</summary>
|
||||
[DisplayName("写入成功")] public bool WriteSuccess { get; set; }
|
||||
|
||||
/// <summary>Worker 内存心跳翻转,表示线程在跑。</summary>
|
||||
[DisplayName("心跳")] public bool Heart { get; set; }
|
||||
|
||||
/// <summary>最近一次断线或周期异常原因。</summary>
|
||||
[DisplayName("最近错误")] public string LastError { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace StandardScene.Signal.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// 放行点配置。对应反编译 <c>StartSignModel</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 车停在 <see cref="Sit"/> 且 <see cref="IsUse"/> 为 true 时,进程停充并启动,不读 PLC。
|
||||
/// 同一站点不要同时配握手点:扫车先握手再放行,可能把还在等允许进入的车开走。
|
||||
/// 站点号不能重复,列表编辑器用 Sit 做主键。
|
||||
/// </remarks>
|
||||
[Category("放行点数据管理")]
|
||||
[DisplayName("放行点配置列表")]
|
||||
public class ReleasePointModel
|
||||
{
|
||||
/// <summary>要自动放行的地图站点号。</summary>
|
||||
[DisplayName("放行站点")]
|
||||
public int Sit { get; set; }
|
||||
|
||||
/// <summary>false 时该条保留在 JSON 里但不生效,方便现场临时关掉某个点。</summary>
|
||||
[DisplayName("启用")]
|
||||
public bool IsUse { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace StandardScene.Signal.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC → 调度:读字节含义。对齐反编译 <c>ENUM_READ_SIGNAL</c>。
|
||||
/// 机构用这一个字节告诉 AGV 现在能不能进、能不能出。
|
||||
/// </summary>
|
||||
public enum SignalReadValue : byte
|
||||
{
|
||||
/// <summary>0:空闲 / 未允许。RequestIn 必须看到写字节也是无状态才放行。</summary>
|
||||
无状态 = 0,
|
||||
|
||||
/// <summary>1:允许进入机构。扫车 RequestIn 的放行条件之一。</summary>
|
||||
允许进入 = 1,
|
||||
|
||||
/// <summary>2:允许离开机构。下线升销钉、等待通行机构、PointControl 等会等这个值。</summary>
|
||||
允许离开 = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调度 → PLC:写字节含义。对齐反编译 <c>ENUM_WRITE_SIGNAL</c>。
|
||||
/// AGV 用这一个字节向机构汇报当前处于进站流程的哪一步。
|
||||
/// </summary>
|
||||
public enum SignalWriteValue : byte
|
||||
{
|
||||
/// <summary>0:空闲。离站清空后回到此值,下一辆才能 RequestIn。</summary>
|
||||
无状态 = 0,
|
||||
|
||||
/// <summary>1:已获准进入、正在往机构开。RequestIn 成功时写入。</summary>
|
||||
进入中 = 1,
|
||||
|
||||
/// <summary>2:已到达机构内地标。Arrived 或上线 AutoLeave 检测到离开进入包含区时写入。</summary>
|
||||
到位 = 2,
|
||||
|
||||
/// <summary>3:正在离开机构。上线看到车在跑、下线/等待通行启动后写入。</summary>
|
||||
离开中 = 3,
|
||||
|
||||
/// <summary>4:已离开包含地标。PLC 确认后调度清零写字节和车号。</summary>
|
||||
已离开 = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 机构类型。对齐反编译 <c>ENUM_JGTYPE</c>。
|
||||
/// 决定 <see cref="StandardScene.Signal.Plc.PlcStationLogic.AutoLeave"/> 走哪套离站,以及 RequestIn 要不要查机构内是否有车。
|
||||
/// </summary>
|
||||
public enum SignalLineType : byte
|
||||
{
|
||||
/// <summary>0:未配置,AutoLeave 不处理。</summary>
|
||||
未知 = 0,
|
||||
|
||||
/// <summary>1:上线。进站靠扫车 RequestIn/Arrived,离站跟车位自动推进到位/离开中/已离开。</summary>
|
||||
上线机构 = 1,
|
||||
|
||||
/// <summary>2:单牵引下线。RequestIn 时若 JgPointId 上已有车则不放行;离站要升销钉1。</summary>
|
||||
单牵引下线机构 = 2,
|
||||
|
||||
/// <summary>3:双牵引下线。同单牵引,离站还需销钉2到位。</summary>
|
||||
双牵引下线机构 = 3,
|
||||
|
||||
/// <summary>4:等待通行。无销钉,允许离开后停充启动即可。</summary>
|
||||
等待通行机构 = 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using SimpleCore.Library;
|
||||
using StandardScene.Signal.Model;
|
||||
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC 字节含义转换,以及按机构类型自动推进离站状态机。对齐反编译 <c>PLCManager</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 由 <see cref="PlcStationWorker"/> 每 200ms 在已持有 <see cref="PlcStationRuntime.Sync"/> 时调用。
|
||||
/// 写字节含义见 <see cref="SignalWriteValue"/>:无状态 → 进入中 → 到位 → 离开中 → 已离开 → 清零。
|
||||
/// 「已离开」可由本侧写成功,或 PLC 回读写值变成已离开(对方清过)触发清空。
|
||||
/// </remarks>
|
||||
public static class PlcStationLogic
|
||||
{
|
||||
/// <summary>PLC 读字节 → 调度侧枚举。仅 1=允许进入、2=允许离开,其余当无状态。</summary>
|
||||
public static SignalReadValue ConvertRead(byte val) => val switch
|
||||
{
|
||||
1 => SignalReadValue.允许进入,
|
||||
2 => SignalReadValue.允许离开,
|
||||
_ => SignalReadValue.无状态
|
||||
};
|
||||
|
||||
/// <summary>PLC 写地址回读字节 → 调度侧枚举。1~4 对应进入中/到位/离开中/已离开。</summary>
|
||||
public static SignalWriteValue ConvertWrite(byte val) => val switch
|
||||
{
|
||||
1 => SignalWriteValue.进入中,
|
||||
2 => SignalWriteValue.到位,
|
||||
3 => SignalWriteValue.离开中,
|
||||
4 => SignalWriteValue.已离开,
|
||||
_ => SignalWriteValue.无状态
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 判断站点是否落在机构配置的包含列表里。
|
||||
/// 列表支持逗号、分号、空格分隔,与 JSON 里 <c>JgInContains</c> / <c>JgContains</c> 一致。
|
||||
/// </summary>
|
||||
public static bool ContainsSite(string csv, int siteId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(csv) || siteId <= 0)
|
||||
return false;
|
||||
return csv.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Any(s => int.TryParse(s.Trim(), out var n) && n == siteId);
|
||||
}
|
||||
|
||||
/// <summary>按 <see cref="PlcStationModel.LineType"/> 分发四套离站逻辑;未知类型不处理。</summary>
|
||||
public static void AutoLeave(PlcStationRuntime rt)
|
||||
{
|
||||
if (rt?.Config == null) return;
|
||||
switch (rt.Config.LineType)
|
||||
{
|
||||
case SignalLineType.上线机构:
|
||||
AutoLeaveUpLine(rt);
|
||||
break;
|
||||
case SignalLineType.单牵引下线机构:
|
||||
AutoLeaveDownLine(rt, pin2: false);
|
||||
break;
|
||||
case SignalLineType.双牵引下线机构:
|
||||
AutoLeaveDownLine(rt, pin2: true);
|
||||
break;
|
||||
case SignalLineType.等待通行机构:
|
||||
AutoLeaveWaitSit(rt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 离站完成:写字节清无状态、清空机构内车号。
|
||||
/// 若仍挂着 RequestInAgvNo(上线途中 PLC 已报已离开),一并清掉以免卡死下一辆。
|
||||
/// </summary>
|
||||
private static void ClearLeft(PlcStationRuntime rt, bool fromReadback)
|
||||
{
|
||||
rt.WriteSignal = SignalWriteValue.无状态;
|
||||
rt.InAgvNo = 0;
|
||||
if (rt.RequestInAgvNo > 0)
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"{rt.Config.JgName} {(fromReadback ? "回读已离开" : "写已离开成功")},清空请求进入车 {rt.RequestInAgvNo}",
|
||||
"JGControl", true);
|
||||
rt.RequestInAgvNo = 0;
|
||||
}
|
||||
else
|
||||
Diagnosis.Log($"{rt.Config.JgName} 已离开,清空信号", "JGControl", true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上线机构:进站过程由扫车 RequestIn/Arrived 启动,离站由本方法跟车位推进。
|
||||
/// 进入中且车已离开 JgInContains → 视为到位;
|
||||
/// 到位且车在跑 → 离开中;
|
||||
/// 离开中且车已不在 JgContains → 已离开。
|
||||
/// </summary>
|
||||
private static void AutoLeaveUpLine(PlcStationRuntime rt)
|
||||
{
|
||||
if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.已离开)
|
||||
ClearLeft(rt, fromReadback: false);
|
||||
if (rt.ReadWriteSignal == SignalWriteValue.已离开)
|
||||
ClearLeft(rt, fromReadback: true);
|
||||
|
||||
if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.进入中)
|
||||
{
|
||||
var car = SignalCarAdapter.Find(rt.RequestInAgvNo);
|
||||
if (car == null) return;
|
||||
if (!ContainsSite(rt.Config.JgInContains, SignalCarAdapter.PositionId(car)))
|
||||
{
|
||||
rt.WriteSignal = SignalWriteValue.到位;
|
||||
rt.InAgvNo = car.id;
|
||||
rt.RequestInAgvNo = 0;
|
||||
Diagnosis.Log($"AGV{car.id} 已到达机构 {rt.Config.JgName}", "JGControl", true);
|
||||
}
|
||||
}
|
||||
else if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.到位)
|
||||
{
|
||||
var car = SignalCarAdapter.Find(rt.InAgvNo);
|
||||
if (car == null || SignalCarAdapter.IsRunning(car))
|
||||
rt.WriteSignal = SignalWriteValue.离开中;
|
||||
}
|
||||
else if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.离开中)
|
||||
{
|
||||
var car = SignalCarAdapter.Find(rt.InAgvNo);
|
||||
if (car == null || !ContainsSite(rt.Config.JgContains, SignalCarAdapter.PositionId(car)))
|
||||
rt.WriteSignal = SignalWriteValue.已离开;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下线机构:车已在机构内(InAgvNo),等允许离开后升销钉、停充、启动,写离开中;
|
||||
/// 车离开 JgContains 后写已离开。
|
||||
/// <paramref name="pin2"/> 为 true 时双牵引,需销钉1+2都到位。
|
||||
/// </summary>
|
||||
private static void AutoLeaveDownLine(PlcStationRuntime rt, bool pin2)
|
||||
{
|
||||
if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.已离开)
|
||||
{
|
||||
ClearLeft(rt, fromReadback: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rt.ReadWriteSignal == SignalWriteValue.已离开)
|
||||
ClearLeft(rt, fromReadback: true);
|
||||
|
||||
if (rt.InAgvNo == 0) return;
|
||||
var car = SignalCarAdapter.Find(rt.InAgvNo);
|
||||
if (car == null) return;
|
||||
|
||||
var pos = SignalCarAdapter.PositionId(car);
|
||||
if (rt.ReadSignal == SignalReadValue.允许离开 && pos == rt.Config.JgPointId)
|
||||
{
|
||||
if (rt.WriteSignal == SignalWriteValue.离开中) return;
|
||||
var pinOk = SignalCarAdapter.IsActionFinish(car, "SetPin1Up")
|
||||
&& (!pin2 || SignalCarAdapter.IsActionFinish(car, "SetPin2Up"))
|
||||
&& SignalCarAdapter.IsActionFinish(car, "StopCharge");
|
||||
if (pinOk)
|
||||
{
|
||||
SignalCarAdapter.StopCharge(car);
|
||||
SignalCarAdapter.Start(car);
|
||||
rt.WriteSignal = SignalWriteValue.离开中;
|
||||
Diagnosis.Log($"{rt.Config.JgName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SignalCarAdapter.Control(car, "SetPin1Up");
|
||||
if (pin2) SignalCarAdapter.Control(car, "SetPin2Up");
|
||||
SignalCarAdapter.Control(car, "StopCharge");
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(rt.Config.JgContains)
|
||||
&& !ContainsSite(rt.Config.JgContains, pos)
|
||||
&& rt.ReadWriteSignal != SignalWriteValue.已离开)
|
||||
{
|
||||
rt.WriteSignal = SignalWriteValue.已离开;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待通行机构:无销钉,允许离开且停在 JgPointId 时停充并启动,写离开中;
|
||||
/// 离开包含地标后写已离开。
|
||||
/// </summary>
|
||||
private static void AutoLeaveWaitSit(PlcStationRuntime rt)
|
||||
{
|
||||
if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.已离开)
|
||||
{
|
||||
ClearLeft(rt, fromReadback: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rt.ReadWriteSignal == SignalWriteValue.已离开)
|
||||
ClearLeft(rt, fromReadback: true);
|
||||
|
||||
if (rt.InAgvNo == 0) return;
|
||||
var car = SignalCarAdapter.Find(rt.InAgvNo);
|
||||
if (car == null) return;
|
||||
|
||||
var pos = SignalCarAdapter.PositionId(car);
|
||||
if (rt.ReadSignal == SignalReadValue.允许离开 && pos == rt.Config.JgPointId)
|
||||
{
|
||||
if (rt.WriteSignal == SignalWriteValue.离开中) return;
|
||||
if (SignalCarAdapter.IsActionFinish(car, "StopCharge"))
|
||||
{
|
||||
SignalCarAdapter.Start(car);
|
||||
rt.WriteSignal = SignalWriteValue.离开中;
|
||||
Diagnosis.Log($"{rt.Config.JgName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
|
||||
}
|
||||
else
|
||||
SignalCarAdapter.Control(car, "StopCharge");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(rt.Config.JgContains)
|
||||
&& !ContainsSite(rt.Config.JgContains, pos)
|
||||
&& rt.ReadWriteSignal != SignalWriteValue.已离开)
|
||||
{
|
||||
rt.WriteSignal = SignalWriteValue.已离开;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using SimpleCore.Library;
|
||||
using StandardScene.Signal.Model;
|
||||
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个机构的运行态(不进 JSON)。扫车线程改写目标写字节,PLC Worker 负责真正下发。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 赋值 <see cref="WriteSignal"/> 会把 <see cref="WriteSuccess"/> 置 false,下一拍 Worker 发现
|
||||
/// 目标与回读不一致就会重写 PLC。回读与目标一致后再标成功,AutoLeave 才继续往下一状态走。
|
||||
/// 所有读写应在 <see cref="Sync"/> 内进行。
|
||||
/// </remarks>
|
||||
public sealed class PlcStationRuntime
|
||||
{
|
||||
private SignalWriteValue _writeSignal;
|
||||
private SignalReadValue _readSignal;
|
||||
private SignalWriteValue _readWriteSignal;
|
||||
|
||||
public PlcStationRuntime(PlcStationModel config)
|
||||
{
|
||||
Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
/// <summary>本机构静态配置(IP、DB 地址、地标、机构类型)。</summary>
|
||||
public PlcStationModel Config { get; }
|
||||
|
||||
/// <summary>扫车线程与 PLC Worker 共用运行态,写入时加此锁。</summary>
|
||||
public object Sync { get; } = new object();
|
||||
|
||||
/// <summary>PLC→调度:允许进入 / 允许离开 / 无状态。变化时打诊断。</summary>
|
||||
public SignalReadValue ReadSignal
|
||||
{
|
||||
get => _readSignal;
|
||||
set
|
||||
{
|
||||
if (_readSignal != value)
|
||||
Diagnosis.Log($"{Config.JgName} 读信号 {_readSignal} → {value}", "PLC", true);
|
||||
_readSignal = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调度→PLC 的目标写字节。赋值即视为尚未写成功,驱动 Worker 重写。
|
||||
/// </summary>
|
||||
public SignalWriteValue WriteSignal
|
||||
{
|
||||
get => _writeSignal;
|
||||
set
|
||||
{
|
||||
if (_writeSignal != value)
|
||||
Diagnosis.Log($"{Config.JgName} 写信号 {_writeSignal} → {value}", "PLC", true);
|
||||
_writeSignal = value;
|
||||
WriteSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从写地址回读到的当前值,用来确认 PLC 是否已吃进目标字节。</summary>
|
||||
public SignalWriteValue ReadWriteSignal
|
||||
{
|
||||
get => _readWriteSignal;
|
||||
set
|
||||
{
|
||||
if (_readWriteSignal != value)
|
||||
Diagnosis.Log($"{Config.JgName} 回读写值 {_readWriteSignal} → {value}", "PLC", true);
|
||||
_readWriteSignal = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>已报到在机构内的 AGV 号;0 表示没有。Arrived / 上线自动到位 时写入。</summary>
|
||||
public int InAgvNo { get; set; }
|
||||
|
||||
/// <summary>已获准进入、正在往机构开的 AGV 号;到位或离站清空后为 0。</summary>
|
||||
public int RequestInAgvNo { get; set; }
|
||||
|
||||
/// <summary>最近一拍 PLC 读写是否成功。心跳或读写失败会立刻变 false。</summary>
|
||||
public bool IsConnected { get; set; }
|
||||
|
||||
/// <summary>目标写字节是否已与 PLC 回读一致。改 WriteSignal 时自动清掉。</summary>
|
||||
public bool WriteSuccess { get; set; }
|
||||
|
||||
/// <summary>内存侧心跳翻转标志,供监视面板看到 Worker 在跑;真正写 PLC 的是 HeartDb 位翻转。</summary>
|
||||
public bool Heart { get; set; }
|
||||
|
||||
/// <summary>最近一次断线或周期异常原因,连接正常时清空。</summary>
|
||||
public string LastError { get; set; } = "";
|
||||
|
||||
/// <summary>拷贝一份给 MissionStatus / 监视列表,避免 UI 直接绑运行态。</summary>
|
||||
public PlcStationSnapshot ToSnapshot() => new PlcStationSnapshot
|
||||
{
|
||||
JgName = Config.JgName,
|
||||
IsConnected = IsConnected,
|
||||
ReadSignal = ReadSignal.ToString(),
|
||||
WriteSignal = WriteSignal.ToString(),
|
||||
ReadWriteSignal = ReadWriteSignal.ToString(),
|
||||
InAgvNo = InAgvNo,
|
||||
RequestInAgvNo = RequestInAgvNo,
|
||||
WriteSuccess = WriteSuccess,
|
||||
Heart = Heart,
|
||||
LastError = LastError ?? ""
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using SimpleCore.Library;
|
||||
using StandardScene.Signal.Model;
|
||||
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// 单机构 200ms 循环:心跳、读允许进出、回读写值、按需写入、自动离站。
|
||||
/// 对齐反编译 <c>PLCManager.ExcuteThreadMethod</c>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 一拍顺序固定,失败则本拍标断线并 return,避免用脏数据推进状态机:
|
||||
/// 1. 翻转内存心跳标志;若配置了 HeartDb,读当前位再写反码(PLC 侧看翻转判断调度活着)。
|
||||
/// 2. 读 ReadDb → <see cref="PlcStationRuntime.ReadSignal"/>。
|
||||
/// 3. 回读 WriteDb → ReadWriteSignal;与目标 WriteSignal 一致则 WriteSuccess=true。
|
||||
/// 4. 目标与回读不一致且尚未成功时写入目标字节。
|
||||
/// 5. <see cref="PlcStationLogic.AutoLeave"/>。
|
||||
/// 读写均在 <see cref="PlcStationRuntime.Sync"/> 内,与扫车 Handler 互斥。
|
||||
/// </remarks>
|
||||
public sealed class PlcStationWorker : IDisposable
|
||||
{
|
||||
private readonly SiemensPlcSession _session;
|
||||
private readonly int _pollMs;
|
||||
private readonly Action _onTick;
|
||||
private CancellationTokenSource _cts;
|
||||
private Thread _thread;
|
||||
|
||||
/// <summary>断线日志节流起点,避免 200ms 打满诊断。</summary>
|
||||
private DateTime _lastFailLog = DateTime.MinValue;
|
||||
|
||||
public PlcStationWorker(PlcStationRuntime runtime, SiemensPlcSession session, int pollMs, Action onTick = null)
|
||||
{
|
||||
Runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_pollMs = Math.Max(50, pollMs);
|
||||
_onTick = onTick;
|
||||
}
|
||||
|
||||
/// <summary>本机构运行态,扫车线程按 JgName 查找后加 Sync 读写。</summary>
|
||||
public PlcStationRuntime Runtime { get; }
|
||||
|
||||
/// <summary>启动后台线程。重复 Start 若仍存活则忽略。</summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_thread != null && _thread.IsAlive) return;
|
||||
_cts = new CancellationTokenSource();
|
||||
_thread = new Thread(Loop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"PlcStation:{Runtime.Config.JgName}"
|
||||
};
|
||||
_thread.Start();
|
||||
Diagnosis.Log($"{Runtime.Config.JgName} Worker 已启动 → {Runtime.Config.Ip}:{Runtime.Config.Port}", "PLC", true);
|
||||
}
|
||||
|
||||
/// <summary>取消循环、最多等 2s、关闭 S7 连接并标断线。</summary>
|
||||
public void Stop()
|
||||
{
|
||||
try { _cts?.Cancel(); }
|
||||
catch { }
|
||||
try { _thread?.Join(2000); }
|
||||
catch { }
|
||||
_thread = null;
|
||||
_session.Close();
|
||||
Runtime.IsConnected = false;
|
||||
Diagnosis.Log($"{Runtime.Config.JgName} Worker 已停止", "PLC", true);
|
||||
}
|
||||
|
||||
public void Dispose() => Stop();
|
||||
|
||||
private void Loop()
|
||||
{
|
||||
var token = _cts.Token;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (Runtime.Sync)
|
||||
Tick();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Runtime.LastError = ex.Message;
|
||||
Runtime.IsConnected = false;
|
||||
LogFail($"{Runtime.Config.JgName} 周期异常 {ex.Message}");
|
||||
}
|
||||
|
||||
try { _onTick?.Invoke(); }
|
||||
catch { }
|
||||
|
||||
try { Thread.Sleep(_pollMs); }
|
||||
catch (ThreadInterruptedException) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>单拍 PLC 交互。心跳地址为空则跳过心跳;读写地址为空则跳过对应步骤。</summary>
|
||||
internal void Tick()
|
||||
{
|
||||
var cfg = Runtime.Config;
|
||||
Runtime.Heart = !Runtime.Heart;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cfg.HeartDb))
|
||||
{
|
||||
if (_session.TryReadBool(cfg.HeartDb, out var bit))
|
||||
{
|
||||
_session.TryWriteBool(cfg.HeartDb, !bit);
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
else
|
||||
{
|
||||
MarkDisconnected("心跳读写失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cfg.ReadDb))
|
||||
{
|
||||
if (_session.TryReadByte(cfg.ReadDb, out var rb))
|
||||
Runtime.ReadSignal = PlcStationLogic.ConvertRead(rb);
|
||||
else
|
||||
{
|
||||
MarkDisconnected("读地址失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cfg.WriteDb))
|
||||
{
|
||||
if (_session.TryReadByte(cfg.WriteDb, out var wb))
|
||||
{
|
||||
Runtime.ReadWriteSignal = PlcStationLogic.ConvertWrite(wb);
|
||||
if (Runtime.ReadWriteSignal == Runtime.WriteSignal)
|
||||
Runtime.WriteSuccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MarkDisconnected("写地址回读失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Runtime.WriteSignal != Runtime.ReadWriteSignal && !Runtime.WriteSuccess)
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"AGV→机构 {cfg.JgName} 写入【{Runtime.WriteSignal}】请求进入{Runtime.RequestInAgvNo} 到达{Runtime.InAgvNo}",
|
||||
"JGControl", true);
|
||||
_session.TryWriteByte(cfg.WriteDb, (byte)Runtime.WriteSignal);
|
||||
}
|
||||
}
|
||||
|
||||
Runtime.IsConnected = true;
|
||||
Runtime.LastError = "";
|
||||
PlcStationLogic.AutoLeave(Runtime);
|
||||
}
|
||||
|
||||
private void MarkDisconnected(string reason)
|
||||
{
|
||||
Runtime.IsConnected = false;
|
||||
Runtime.LastError = reason;
|
||||
LogFail($"{Runtime.Config.JgName} {reason}");
|
||||
}
|
||||
|
||||
/// <summary>同一机构失败日志最少间隔 5 秒。</summary>
|
||||
private void LogFail(string msg)
|
||||
{
|
||||
if ((DateTime.Now - _lastFailLog).TotalSeconds < 5) return;
|
||||
_lastFailLog = DateTime.Now;
|
||||
Diagnosis.Log(msg, "PLC", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using IoTClient.Clients.PLC;
|
||||
using IoTClient.Common.Enums;
|
||||
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// 西门子 S7 长连接封装(IoTClient)。一个机构一个实例,由对应 Worker 独占。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 字节地址兼容老 Hsl 写法 <c>DB1.0</c> 与 IoTClient 写法 <c>DB1.DBB0</c>;
|
||||
/// 心跳位地址保持原样(如 <c>DB1.DBX2.0</c>),不做 DBB 转换。
|
||||
/// 连接失败时读/写返回 false,由 Worker 标断线,不在这里抛给扫车线程。
|
||||
/// </remarks>
|
||||
public sealed class SiemensPlcSession : IDisposable
|
||||
{
|
||||
/// <summary>匹配 Hsl 风格「DB块号.字节偏移」,捕获组 1=DB,2=偏移。</summary>
|
||||
private static readonly Regex BareDbByte = new Regex(@"^DB(\d+)\.(\d+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private readonly SiemensVersion _version;
|
||||
private readonly string _ip;
|
||||
private readonly int _port;
|
||||
private readonly byte _slot;
|
||||
private SiemensClient _client;
|
||||
private readonly object _sync = new object();
|
||||
|
||||
/// <param name="version">进程字段 PlcVersion,如 S7_1500。</param>
|
||||
/// <param name="ip">机构 IP。</param>
|
||||
/// <param name="port">S7 端口,通常 102。</param>
|
||||
/// <param name="slot">机架槽号,S7-1500 常见 1。</param>
|
||||
public SiemensPlcSession(string version, string ip, int port, int slot)
|
||||
{
|
||||
_version = ParseVersion(version);
|
||||
_ip = ip ?? "";
|
||||
_port = port > 0 ? port : 102;
|
||||
_slot = (byte)Math.Max(0, slot);
|
||||
}
|
||||
|
||||
/// <summary>当前客户端是否已 Open 成功。未创建客户端时为 false。</summary>
|
||||
public bool Connected
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync) return _client?.Connected == true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>惰性创建客户端并 Open。已连接则直接 true;失败返回 false 不抛。</summary>
|
||||
public bool EnsureOpen()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_client == null)
|
||||
_client = new SiemensClient(_version, _ip, _port, _slot);
|
||||
if (_client.Connected)
|
||||
return true;
|
||||
var r = _client.Open();
|
||||
return r != null && r.IsSucceed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>读一字节。地址会先规范化;未连接或失败时 value=0 且返回 false。</summary>
|
||||
public bool TryReadByte(string address, out byte value)
|
||||
{
|
||||
value = 0;
|
||||
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var r = _client.ReadByte(NormalizeByteAddress(address));
|
||||
if (r == null || !r.IsSucceed)
|
||||
return false;
|
||||
value = r.Value;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>写一字节(进入中/到位等)。失败返回 false。</summary>
|
||||
public bool TryWriteByte(string address, byte value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var r = _client.Write(NormalizeByteAddress(address), value);
|
||||
return r != null && r.IsSucceed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>读心跳位。地址不做 DBB 转换。</summary>
|
||||
public bool TryReadBool(string address, out bool value)
|
||||
{
|
||||
value = false;
|
||||
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var r = _client.ReadBoolean(address.Trim());
|
||||
if (r == null || !r.IsSucceed)
|
||||
return false;
|
||||
value = r.Value;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>写心跳位(通常写当前值的反码,让 PLC 看到翻转)。</summary>
|
||||
public bool TryWriteBool(string address, bool value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var r = _client.Write(address.Trim(), value);
|
||||
return r != null && r.IsSucceed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>关闭并丢弃客户端,下次 EnsureOpen 会新建。</summary>
|
||||
public void Close()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
try { _client?.Close(); }
|
||||
catch { }
|
||||
_client = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => Close();
|
||||
|
||||
/// <summary>
|
||||
/// <c>DB1.0</c> → <c>DB1.DBB0</c>;已是 DBB/DBW/DBX 的地址原样返回。
|
||||
/// </summary>
|
||||
internal static string NormalizeByteAddress(string address)
|
||||
{
|
||||
var addr = address.Trim();
|
||||
var m = BareDbByte.Match(addr);
|
||||
return m.Success ? $"DB{m.Groups[1].Value}.DBB{m.Groups[2].Value}" : addr;
|
||||
}
|
||||
|
||||
/// <summary>空或无法解析时默认 S7_1500。允许把 S7-1500 写成带连字符。</summary>
|
||||
internal static SiemensVersion ParseVersion(string version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
return SiemensVersion.S7_1500;
|
||||
return Enum.TryParse(version.Replace("-", "_"), true, out SiemensVersion parsed)
|
||||
? parsed
|
||||
: SiemensVersion.S7_1500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using SimpleCore;
|
||||
using SimpleCore.PropType;
|
||||
using SimpleLite.RCS;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.Signal.Site;
|
||||
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// 信号插件访问 AGV / 站点的适配层。不引用 Magnetic.dll,按方法名反射启停和牵引棒。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// MagCar / Mag2Car 上有对应 MethodMember(StartMag2Car、SetPin1Up 等)时生效;
|
||||
/// 没有 <c>IsActionFinish</c> 则视为动作已完成,避免非磁导航车卡在销钉等待。
|
||||
/// 启停回退顺序:StartMag2Car / StartMagCar → SendStart(0)。
|
||||
/// </remarks>
|
||||
public static class SignalCarAdapter
|
||||
{
|
||||
/// <summary>当前内核里的全部车;SimpleLib 异常时返回空,避免扫车线程崩掉。</summary>
|
||||
public static IEnumerable<Car> All()
|
||||
{
|
||||
try
|
||||
{
|
||||
return SimpleLib.GetAllCars().OfType<Car>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<Car>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>按车号取车;id≤0 返回 null。</summary>
|
||||
public static Car Find(int id)
|
||||
{
|
||||
if (id <= 0) return null;
|
||||
return SimpleLib.GetCar(id) as Car;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前站点:优先 <c>siteID</c>,否则 <c>GetLastSite()</c>。
|
||||
/// 握手 / 放行 / 限流都用这个号去对 JSON 里的 Sit。
|
||||
/// </summary>
|
||||
public static int PositionId(Car car)
|
||||
{
|
||||
if (car == null) return 0;
|
||||
try
|
||||
{
|
||||
if (car.siteID > 0) return car.siteID;
|
||||
var last = car.GetLastSite();
|
||||
return last > 0 ? last : 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>读车 status.enums["State"],没有则空串。</summary>
|
||||
public static string GetState(Car car)
|
||||
{
|
||||
if (car?.status?.enums != null &&
|
||||
car.status.enums.TryGetValue("State", out var state) &&
|
||||
!string.IsNullOrWhiteSpace(state))
|
||||
return state;
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>State 为「运行中」或 Running。</summary>
|
||||
public static bool IsMoving(Car car)
|
||||
{
|
||||
var state = GetState(car);
|
||||
return state == "运行中" ||
|
||||
string.Equals(state, "Running", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对齐反编译 <c>RunStatus==停止</c>:非运行中即视为可握手,含充电中 / 停止中 / WaitPass。
|
||||
/// 充电点必须把「充电中」算停稳,否则 TimedCharging 不会进 Handler。
|
||||
/// </summary>
|
||||
public static bool IsStopped(Car car)
|
||||
{
|
||||
if (car == null) return false;
|
||||
if (IsMoving(car)) return false;
|
||||
var state = GetState(car);
|
||||
if (state == "停止中" || state == "充电中" ||
|
||||
string.Equals(state, "Stopping", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(state, "Charging", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(state, "WaitPass", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
return !IsRunning(car);
|
||||
}
|
||||
|
||||
/// <summary>正在充电(中英文 State)。</summary>
|
||||
public static bool IsCharging(Car car)
|
||||
{
|
||||
var state = GetState(car);
|
||||
return state == "充电中" ||
|
||||
string.Equals(state, "Charging", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 车仍占资源:运行中,或有 pendingLocks,或 tags 含 occupied。
|
||||
/// 上线机构「到位后开始离开」用这个判断,比单纯看 State 更早切到离开中。
|
||||
/// </summary>
|
||||
public static bool IsRunning(Car car)
|
||||
{
|
||||
if (car == null) return false;
|
||||
if (IsMoving(car)) return true;
|
||||
if (car.status?.pendingLocks != null && car.status.pendingLocks.Length > 0)
|
||||
return true;
|
||||
if (car.tags != null && car.tags.Contains("occupied"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>电量百分比,读 enums["Soc"];解析失败当 0。</summary>
|
||||
public static double Soc(Car car)
|
||||
{
|
||||
if (car?.status?.enums != null &&
|
||||
car.status.enums.TryGetValue("Soc", out var soc) &&
|
||||
double.TryParse(soc, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
|
||||
return value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>限流用的车组:fields["group"] 或 ["GroupID"]。</summary>
|
||||
public static string GroupId(Car car)
|
||||
{
|
||||
if (car?.fields == null) return "";
|
||||
if (car.fields.TryGetValue("group", out var g) && !string.IsNullOrWhiteSpace(g))
|
||||
return g;
|
||||
if (car.fields.TryGetValue("GroupID", out var g2) && !string.IsNullOrWhiteSpace(g2))
|
||||
return g2;
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>当前停在给定站点集合上的全部车(按 PositionId)。</summary>
|
||||
public static IReadOnlyList<Car> CarsAt(IEnumerable<int> siteIds)
|
||||
{
|
||||
var set = new HashSet<int>(siteIds ?? Array.Empty<int>());
|
||||
if (set.Count == 0)
|
||||
return Array.Empty<Car>();
|
||||
return All().Where(c => set.Contains(PositionId(c))).ToList();
|
||||
}
|
||||
|
||||
/// <summary>当前停在单个站点上的车。下线 RequestIn 用 JgPointId 查机构内是否已有车。</summary>
|
||||
public static IReadOnlyList<Car> CarsAt(int siteId)
|
||||
{
|
||||
if (siteId <= 0) return Array.Empty<Car>();
|
||||
return CarsAt(new[] { siteId });
|
||||
}
|
||||
|
||||
/// <summary>读站点 fields 指定键。站点不存在或值为空返回 false。</summary>
|
||||
public static bool TryReadSiteField(int siteId, string key, out string value)
|
||||
{
|
||||
value = "";
|
||||
if (siteId <= 0 || string.IsNullOrWhiteSpace(key))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var site = SimpleLib.GetSite(siteId);
|
||||
if (site?.fields == null)
|
||||
return false;
|
||||
if (!site.fields.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw))
|
||||
return false;
|
||||
value = raw;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 地图点是否自带 SignalStop(SignalType 字段存在且非空)。
|
||||
/// SignalParams 可缺省。有类型则扫车不再查 handshake-points.json。
|
||||
/// </summary>
|
||||
public static bool TryReadSignalStop(int siteId, out string signalType, out string signalParams)
|
||||
{
|
||||
signalType = "";
|
||||
signalParams = "";
|
||||
if (!TryReadSiteField(siteId, SignalSiteFields.SignalType, out signalType))
|
||||
return false;
|
||||
TryReadSiteField(siteId, SignalSiteFields.SignalParams, out signalParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 已充电秒数。优先反射 GetChargeTime(int/double/TimeSpan);
|
||||
/// 没有则从首次看到充电中的时刻算,离开充电态时清字典。
|
||||
/// </summary>
|
||||
public static int ChargeSeconds(Car car, IDictionary<int, DateTime> startedAt)
|
||||
{
|
||||
if (car == null) return 0;
|
||||
var mi = car.GetType().GetMethod("GetChargeTime", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (mi != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var r = mi.Invoke(car, null);
|
||||
if (r is int i) return i;
|
||||
if (r is double d) return (int)d;
|
||||
if (r is TimeSpan ts) return (int)ts.TotalSeconds;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (IsCharging(car))
|
||||
{
|
||||
if (!startedAt.ContainsKey(car.id))
|
||||
startedAt[car.id] = DateTime.Now;
|
||||
return (int)(DateTime.Now - startedAt[car.id]).TotalSeconds;
|
||||
}
|
||||
|
||||
startedAt.Remove(car.id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>放行:Mag2 → Mag → SendStart(0)。</summary>
|
||||
public static void Start(Car car)
|
||||
{
|
||||
if (car == null) return;
|
||||
if (TryInvoke(car, "StartMag2Car") || TryInvoke(car, "StartMagCar"))
|
||||
return;
|
||||
TryInvoke(car, "SendStart", new object[] { (ushort)0 });
|
||||
}
|
||||
|
||||
/// <summary>急停:Mag2 → Mag → SendStop(0)。机构未允许进入时调用。</summary>
|
||||
public static void Stop(Car car)
|
||||
{
|
||||
if (car == null) return;
|
||||
if (TryInvoke(car, "StopMag2Car") || TryInvoke(car, "StopMagCar"))
|
||||
return;
|
||||
TryInvoke(car, "SendStop", new object[] { (ushort)0 });
|
||||
}
|
||||
|
||||
/// <summary>停充。握手放行前通常先停充再启动。</summary>
|
||||
public static void StopCharge(Car car)
|
||||
{
|
||||
if (car == null) return;
|
||||
TryInvoke(car, "StopCharge");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销钉/停充等动作是否完成。车类型没有 IsActionFinish 时返回 true(非磁导航不阻塞)。
|
||||
/// 反射异常也当完成,避免把产线卡死。
|
||||
/// </summary>
|
||||
public static bool IsActionFinish(Car car, string action)
|
||||
{
|
||||
if (car == null) return false;
|
||||
if (string.IsNullOrWhiteSpace(action)) return true;
|
||||
var mi = car.GetType().GetMethod("IsActionFinish", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (mi == null)
|
||||
return true;
|
||||
try
|
||||
{
|
||||
var r = mi.Invoke(car, new object[] { action });
|
||||
return r is true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>下发动作:先按动作名直接调 MethodMember,没有再 ImmediateCommand(action)。</summary>
|
||||
public static void Control(Car car, string action)
|
||||
{
|
||||
if (car == null || string.IsNullOrWhiteSpace(action)) return;
|
||||
if (TryInvoke(car, action)) return;
|
||||
TryInvoke(car, "ImmediateCommand", new object[] { action });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按名称找实例方法。无参与带参分开匹配,重载参数个数不对就跳过。
|
||||
/// 调用抛错返回 false,让上层试下一个名字。
|
||||
/// </summary>
|
||||
private static bool TryInvoke(object target, string name, object[] args = null)
|
||||
{
|
||||
var type = target.GetType();
|
||||
var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
.Where(m => string.Equals(m.Name, name, StringComparison.Ordinal));
|
||||
foreach (var mi in methods)
|
||||
{
|
||||
var ps = mi.GetParameters();
|
||||
if (args == null)
|
||||
{
|
||||
if (ps.Length != 0) continue;
|
||||
try { mi.Invoke(target, null); return true; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (ps.Length != args.Length) continue;
|
||||
try { mi.Invoke(target, args); return true; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace StandardScene.Signal
|
||||
{
|
||||
/// <summary>
|
||||
/// 信号插件三张 JSON 的路径解析与读写。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 只使用 SimpleLite 工作目录 <c>Config/Signal/*.json</c>,不读、不拷 <c>plugins/Config/Signal</c>。
|
||||
/// 文件不存在时 Load 返回空表;Save 会创建目录。
|
||||
/// 迷毂数据中心走同一套文件;保存后需在本进程点「重新加载配置」。
|
||||
/// 枚举按名字序列化(<see cref="StringEnumConverter"/>),与样例里 <c>"上线机构"</c> 一致。
|
||||
/// </remarks>
|
||||
public static class SignalConfigStore
|
||||
{
|
||||
/// <summary>保存时串行写盘,避免 CycleGUI 与迷毂 API 同时 Save 互相覆盖一半。</summary>
|
||||
private static readonly object FileLock = new object();
|
||||
|
||||
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = { new StringEnumConverter() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 相对路径固定落到工作目录(SimpleLite.exe 所在目录)下的 <c>Config/Signal</c>。
|
||||
/// 已是绝对路径则原样返回。文件不存在也返回该路径,不回退插件目录。
|
||||
/// </summary>
|
||||
public static string Resolve(string configuredPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuredPath))
|
||||
configuredPath = Path.Combine("Config", "Signal", "stations.json");
|
||||
|
||||
if (Path.IsPathRooted(configuredPath))
|
||||
return configuredPath;
|
||||
|
||||
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, configuredPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反序列化为列表。文件不存在、空、或 JSON 损坏时返回空列表,不抛给启动流程。
|
||||
/// </summary>
|
||||
public static List<T> Load<T>(string path) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
return new List<T>();
|
||||
var json = File.ReadAllText(path);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return new List<T>();
|
||||
return JsonConvert.DeserializeObject<List<T>>(json, JsonSettings) ?? new List<T>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<T>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>缩进写入;自动建目录。与 Load 共用枚举按名序列化。</summary>
|
||||
public static void Save<T>(string path, IReadOnlyList<T> items)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(items ?? Array.Empty<T>(), JsonSettings);
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
lock (FileLock)
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace StandardScene.Signal.Site
|
||||
{
|
||||
/// <summary>
|
||||
/// 站点 fields 键名,替代老地图 <c>SignalStop</c> 对象。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 地图编辑器若未挂字段袋,仍可手工在站点 fields 写入这两个键。
|
||||
/// 扫车解析顺序:站点有非空 SignalType → 用地图点;否则回退 <c>handshake-points.json</c>。
|
||||
/// 键名必须与迷毂/地图侧约定完全一致,改这里要同步改地图字段袋。
|
||||
/// </remarks>
|
||||
public static class SignalSiteFields
|
||||
{
|
||||
/// <summary>握手类型,取值同 <see cref="StandardScene.Signal.Model.HandshakePointModel.SignalType"/>。</summary>
|
||||
public const string SignalType = "SignalType";
|
||||
|
||||
/// <summary>握手参数,取值同 <see cref="StandardScene.Signal.Model.HandshakePointModel.SignalParams"/>。</summary>
|
||||
public const string SignalParams = "SignalParams";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<!--
|
||||
scene.signal:第三方 PLC 信号交互插件。
|
||||
产物 StandardScene.Signal.dll + StandardScene.Signal.scene.json,拷到 SimpleLite/plugins。
|
||||
运行时由「信号交互进程」拉起:每机构 200ms S7 轮询 + 1s 扫车握手。
|
||||
Config/Signal/*.json 仅留在工程内作样例,运行时读写 SimpleLite 工作目录 Config/Signal,不部署到 plugins。
|
||||
-->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>StandardScene.Signal</RootNamespace>
|
||||
<AssemblyName>StandardScene.Signal</AssemblyName>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Deterministic>true</Deterministic>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<NoWarn>$(NoWarn);NU1701;CS0618;CS0612;MSB3277;CA1416</NoWarn>
|
||||
<AssemblySearchPaths>{HintPathFromItem};{TargetFrameworkDirectory};{RawFileName};{GAC}</AssemblySearchPaths>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="StandardScene.Signal.scene.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<!-- 样例 JSON 只留源码树,不复制到输出/plugins;运行时读写工作目录 Config/Signal -->
|
||||
<None Update="Config\Signal\*.json" CopyToOutputDirectory="Never" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StandardScene.Core\StandardScene.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="CommonUsage">
|
||||
<HintPath>$(StandRefDir)CommonUsage.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="CycleGUI">
|
||||
<HintPath>$(StandRefDir)CycleGUI.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="LessokajiWeaverUtilities">
|
||||
<HintPath>$(StandRefDir)LessokajiWeaverUtilities.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleCore">
|
||||
<HintPath>$(StandRefDir)SimpleCore.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleLite">
|
||||
<HintPath>$(StandSimpleLiteDir)SimpleLite.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Topaz">
|
||||
<HintPath>$(StandRefDir)Topaz.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IoTClient" Version="1.0.40" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "scene.signal",
|
||||
"displayName": "第三方信号交互",
|
||||
"assembly": "StandardScene.Signal.dll",
|
||||
"coreVersion": ">=1.0.0",
|
||||
"requiresCore": "StandardScene.dll",
|
||||
"provides": {
|
||||
"missionTypes": [ "SignalInteractMission" ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using CycleGUI;
|
||||
|
||||
namespace StandardScene.Signal.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// CycleGUI 独立窗口打开某一类 Model 列表(进程工具栏「打开机构/握手点/放行点列表」触发)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 每种 T 共用一个静态窗口:已打开则前置,关掉后下次再 DeclarePanel。
|
||||
/// 真正的增删改在 <see cref="ModelTableEditor{T}"/>,保存直接写 JSON,
|
||||
/// 不自动 Reload 进程内 Worker,需再到进程上点「重新加载配置」。
|
||||
/// </remarks>
|
||||
public static class ModelListPanel<T> where T : class, new()
|
||||
{
|
||||
/// <summary>当前已声明的面板;关闭或异常后置 null,允许再次 Open。</summary>
|
||||
private static Panel _panel;
|
||||
|
||||
private static readonly ModelTableEditor<T> Editor = new ModelTableEditor<T>();
|
||||
|
||||
/// <param name="title">窗口标题。</param>
|
||||
/// <param name="path">JSON 绝对路径(应先经 SignalConfigStore.Resolve)。</param>
|
||||
/// <param name="keySelector">主键,添加/保存时查重(机构名、站点号等)。</param>
|
||||
public static void Open(string title, string path, Func<T, string> keySelector)
|
||||
{
|
||||
Editor.Bind(path, keySelector ?? (_ => ""), typeof(T).Name);
|
||||
|
||||
if (_panel != null)
|
||||
{
|
||||
try { _panel.BringToFront(); return; }
|
||||
catch { _panel = null; }
|
||||
}
|
||||
|
||||
var panel = GUI.DeclarePanel()
|
||||
.ShowTitle(title)
|
||||
.SetDefaultDocking(Panel.Docking.None)
|
||||
.InitSize(1100, 720)
|
||||
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
|
||||
_panel = panel;
|
||||
panel.IfTerminalQuit(() => { if (_panel == panel) _panel = null; });
|
||||
|
||||
panel.Define(pb =>
|
||||
{
|
||||
if (pb.Closing())
|
||||
{
|
||||
panel.Exit();
|
||||
_panel = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Editor.Render(pb);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CycleGUI;
|
||||
using Newtonsoft.Json;
|
||||
using StandardScene.Utils;
|
||||
|
||||
namespace StandardScene.Signal.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// 按 Model 的 <see cref="DisplayNameAttribute"/> 反射生成可增删改表格。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 列顺序取属性 MetadataToken(源码声明顺序)。
|
||||
/// 跳过不可写、索引器、<see cref="BrowsableAttribute"/> 为 false、<see cref="JsonIgnoreAttribute"/> 的属性。
|
||||
/// 编辑区用独立字典暂存,点「添加」或「保存当前行」才反写对象并 <see cref="SignalConfigStore.Save"/>。
|
||||
/// CycleGUI 控件 id 带 <c>_idPrefix</c>,三种表同时开窗时不会抢同一组控件状态。
|
||||
/// </remarks>
|
||||
public sealed class ModelTableEditor<T> where T : class, new()
|
||||
{
|
||||
private static PropertyInfo[] Props => _props ??= Discover();
|
||||
private static PropertyInfo[] _props;
|
||||
|
||||
private List<T> _items = new List<T>();
|
||||
|
||||
/// <summary>-1 表示未选中,此时「保存/删除」会提示先选行,「添加」写入新行。</summary>
|
||||
private int _selected = -1;
|
||||
|
||||
private string _status = "";
|
||||
private string _path = "";
|
||||
private string _idPrefix = typeof(T).Name;
|
||||
private Func<T, string> _keySelector = _ => "";
|
||||
|
||||
/// <summary>字符串/数值编辑缓存,键为属性名。</summary>
|
||||
private readonly Dictionary<string, string> _text = new Dictionary<string, string>();
|
||||
|
||||
private readonly Dictionary<string, bool> _bools = new Dictionary<string, bool>();
|
||||
|
||||
/// <summary>枚举下拉当前下标,写入时按 Enum.GetNames 转回枚举值。</summary>
|
||||
private readonly Dictionary<string, int> _enumIdx = new Dictionary<string, int>();
|
||||
|
||||
public int Count => _items.Count;
|
||||
|
||||
public string Status => _status;
|
||||
|
||||
/// <summary>绑定 JSON 路径并立即 Reload。同一编辑器换文件时调用。</summary>
|
||||
public void Bind(string path, Func<T, string> keySelector, string idPrefix)
|
||||
{
|
||||
_path = path ?? "";
|
||||
_keySelector = keySelector ?? (_ => "");
|
||||
_idPrefix = string.IsNullOrWhiteSpace(idPrefix) ? typeof(T).Name : idPrefix;
|
||||
Reload();
|
||||
}
|
||||
|
||||
/// <summary>从磁盘重读,丢弃未保存的编辑区内容。</summary>
|
||||
public void Reload()
|
||||
{
|
||||
_items = SignalConfigStore.Load<T>(_path);
|
||||
_selected = -1;
|
||||
_status = $"已加载 {_items.Count} 条 · {_path}";
|
||||
ClearEditor();
|
||||
}
|
||||
|
||||
/// <summary>列表 + 编辑区 + 添加/保存/删除/清空/刷新。每帧由面板 Define 调用。</summary>
|
||||
public void Render(PanelBuilder pb)
|
||||
{
|
||||
var headers = Props.Select(DisplayOf).Concat(new[] { "行操作" }).ToArray();
|
||||
pb.SeparatorText($"列表({_items.Count})");
|
||||
pb.Table($"dc-list-{_idPrefix}", headers, _items.Count, (row, i) =>
|
||||
{
|
||||
var item = _items[i];
|
||||
foreach (var p in Props)
|
||||
row.Label(FormatCell(p.GetValue(item)));
|
||||
if (row.ButtonGroup(new[] { "选择" }, new[] { "选择该行" }) == 0)
|
||||
{
|
||||
_selected = i;
|
||||
LoadEditor(item);
|
||||
}
|
||||
}, height: 10, enableSearch: true);
|
||||
|
||||
pb.SeparatorText(_selected >= 0 ? $"编辑第 {_selected + 1} 行" : "编辑(未选中则「添加」写入新行)");
|
||||
DrawEditor(pb);
|
||||
|
||||
if (pb.Button("添加", distinct: $"dc-{_idPrefix}-add")) Add();
|
||||
pb.SameLine(8);
|
||||
if (pb.Button("保存当前行", distinct: $"dc-{_idPrefix}-save")) SaveRow();
|
||||
pb.SameLine(8);
|
||||
if (pb.Button("删除当前行", distinct: $"dc-{_idPrefix}-del")) ConfirmDelete();
|
||||
pb.SameLine(8);
|
||||
if (pb.Button("清空编辑区", distinct: $"dc-{_idPrefix}-clear"))
|
||||
{
|
||||
_selected = -1;
|
||||
ClearEditor();
|
||||
_status = "已清空编辑区,可添加新行";
|
||||
}
|
||||
pb.SameLine(8);
|
||||
if (pb.Button("从文件刷新", distinct: $"dc-{_idPrefix}-reload"))
|
||||
Reload();
|
||||
|
||||
if (!string.IsNullOrEmpty(_status))
|
||||
{
|
||||
pb.Separator();
|
||||
pb.Label(_status);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>发现可编辑列。Browsable=false 或 JsonIgnore 的运行态字段不会进表。</summary>
|
||||
private static PropertyInfo[] Discover()
|
||||
{
|
||||
return typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
|
||||
.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
|
||||
.Where(p => p.GetCustomAttribute<JsonIgnoreAttribute>() == null)
|
||||
.OrderBy(p => p.MetadataToken)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string DisplayOf(PropertyInfo p) =>
|
||||
p.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? p.Name;
|
||||
|
||||
private static string FormatCell(object value)
|
||||
{
|
||||
if (value == null) return "";
|
||||
if (value is bool b) return b ? "是" : "否";
|
||||
return Convert.ToString(value) ?? "";
|
||||
}
|
||||
|
||||
private void DrawEditor(PanelBuilder pb)
|
||||
{
|
||||
for (var i = 0; i < Props.Length; i++)
|
||||
{
|
||||
var p = Props[i];
|
||||
var label = $"{_idPrefix}-{i + 1}. {DisplayOf(p)}";
|
||||
var type = p.PropertyType;
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
if (!_bools.ContainsKey(p.Name)) _bools[p.Name] = false;
|
||||
var v = _bools[p.Name];
|
||||
pb.CheckBox(label, ref v);
|
||||
_bools[p.Name] = v;
|
||||
}
|
||||
else if (type.IsEnum)
|
||||
{
|
||||
var names = Enum.GetNames(type);
|
||||
if (names.Length == 0) continue;
|
||||
if (!_enumIdx.ContainsKey(p.Name)) _enumIdx[p.Name] = 0;
|
||||
var idx = _enumIdx[p.Name];
|
||||
if (idx < 0 || idx >= names.Length) idx = 0;
|
||||
pb.DropdownBox(label, names, ref idx);
|
||||
_enumIdx[p.Name] = idx;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_text.ContainsKey(p.Name)) _text[p.Name] = "";
|
||||
var (text, _) = pb.TextInput(label, _text[p.Name], alwaysReturnString: true);
|
||||
_text[p.Name] = text ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadEditor(T item)
|
||||
{
|
||||
foreach (var p in Props)
|
||||
{
|
||||
var value = p.GetValue(item);
|
||||
if (p.PropertyType == typeof(bool))
|
||||
_bools[p.Name] = value is true;
|
||||
else if (p.PropertyType.IsEnum)
|
||||
{
|
||||
var names = Enum.GetNames(p.PropertyType);
|
||||
var name = value?.ToString() ?? names.FirstOrDefault() ?? "";
|
||||
var idx = Array.IndexOf(names, name);
|
||||
_enumIdx[p.Name] = idx >= 0 ? idx : 0;
|
||||
}
|
||||
else
|
||||
_text[p.Name] = value == null ? "" : Convert.ToString(value) ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearEditor()
|
||||
{
|
||||
_text.Clear();
|
||||
_bools.Clear();
|
||||
_enumIdx.Clear();
|
||||
foreach (var p in Props)
|
||||
{
|
||||
if (p.PropertyType == typeof(bool))
|
||||
_bools[p.Name] = false;
|
||||
else if (p.PropertyType.IsEnum)
|
||||
_enumIdx[p.Name] = 0;
|
||||
else if (p.PropertyType == typeof(int) || p.PropertyType == typeof(byte) || p.PropertyType == typeof(ushort))
|
||||
_text[p.Name] = "0";
|
||||
else
|
||||
_text[p.Name] = DefaultText(p);
|
||||
}
|
||||
}
|
||||
|
||||
private static string DefaultText(PropertyInfo p)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fresh = new T();
|
||||
var v = p.GetValue(fresh);
|
||||
return v == null ? "" : Convert.ToString(v) ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>把编辑区打成对象。主键空、整数解析失败时返回 false 并给出中文 err。</summary>
|
||||
private bool TryBuild(out T item, out string err)
|
||||
{
|
||||
item = new T();
|
||||
err = "";
|
||||
foreach (var p in Props)
|
||||
{
|
||||
try
|
||||
{
|
||||
object boxed;
|
||||
if (p.PropertyType == typeof(bool))
|
||||
boxed = _bools.TryGetValue(p.Name, out var b) && b;
|
||||
else if (p.PropertyType.IsEnum)
|
||||
{
|
||||
var names = Enum.GetNames(p.PropertyType);
|
||||
var idx = _enumIdx.TryGetValue(p.Name, out var e) ? e : 0;
|
||||
if (idx < 0 || idx >= names.Length) idx = 0;
|
||||
boxed = Enum.Parse(p.PropertyType, names[idx]);
|
||||
}
|
||||
else if (p.PropertyType == typeof(int))
|
||||
{
|
||||
var raw = _text.TryGetValue(p.Name, out var t) ? t : "0";
|
||||
if (!int.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n))
|
||||
{
|
||||
err = $"{DisplayOf(p)} 必须是整数";
|
||||
return false;
|
||||
}
|
||||
boxed = n;
|
||||
}
|
||||
else if (p.PropertyType == typeof(ushort))
|
||||
{
|
||||
var raw = _text.TryGetValue(p.Name, out var t) ? t : "0";
|
||||
if (!ushort.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n))
|
||||
{
|
||||
err = $"{DisplayOf(p)} 必须是 0-65535";
|
||||
return false;
|
||||
}
|
||||
boxed = n;
|
||||
}
|
||||
else if (p.PropertyType == typeof(byte))
|
||||
{
|
||||
var raw = _text.TryGetValue(p.Name, out var t) ? t : "0";
|
||||
if (!byte.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n))
|
||||
{
|
||||
err = $"{DisplayOf(p)} 必须是 0-255";
|
||||
return false;
|
||||
}
|
||||
boxed = n;
|
||||
}
|
||||
else
|
||||
boxed = _text.TryGetValue(p.Name, out var s) ? s ?? "" : "";
|
||||
p.SetValue(item, boxed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
err = $"{DisplayOf(p)} 写入失败:{ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var key = (_keySelector(item) ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
err = "主键不能为空";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>主键已存在则拒绝。成功后立刻写盘并把选中行指到新行。</summary>
|
||||
private void Add()
|
||||
{
|
||||
if (!TryBuild(out var item, out var err))
|
||||
{
|
||||
CycleUiHelper.Alert("错误", err);
|
||||
return;
|
||||
}
|
||||
|
||||
var key = _keySelector(item);
|
||||
if (_items.Any(x => string.Equals(_keySelector(x), key, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
CycleUiHelper.Alert("错误", $"主键「{key}」已存在");
|
||||
return;
|
||||
}
|
||||
|
||||
_items.Add(item);
|
||||
_selected = _items.Count - 1;
|
||||
SignalConfigStore.Save(_path, _items);
|
||||
_status = $"已添加 {key}";
|
||||
}
|
||||
|
||||
/// <summary>覆盖当前选中行。主键与其它行冲突则拒绝。</summary>
|
||||
private void SaveRow()
|
||||
{
|
||||
if (_selected < 0 || _selected >= _items.Count)
|
||||
{
|
||||
CycleUiHelper.Alert("提示", "请先选择要保存的行,或用「添加」写入新行");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryBuild(out var item, out var err))
|
||||
{
|
||||
CycleUiHelper.Alert("错误", err);
|
||||
return;
|
||||
}
|
||||
|
||||
var key = _keySelector(item);
|
||||
if (_items.Where((_, i) => i != _selected)
|
||||
.Any(x => string.Equals(_keySelector(x), key, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
CycleUiHelper.Alert("错误", $"主键「{key}」已存在");
|
||||
return;
|
||||
}
|
||||
|
||||
_items[_selected] = item;
|
||||
SignalConfigStore.Save(_path, _items);
|
||||
_status = $"已保存 {key}";
|
||||
CycleUiHelper.Alert("提示", "保存成功");
|
||||
}
|
||||
|
||||
/// <summary>确认框通过后删除选中行并写盘。</summary>
|
||||
private void ConfirmDelete()
|
||||
{
|
||||
if (_selected < 0 || _selected >= _items.Count)
|
||||
{
|
||||
CycleUiHelper.Alert("提示", "请先选择要删除的行");
|
||||
return;
|
||||
}
|
||||
|
||||
var key = _keySelector(_items[_selected]);
|
||||
CycleUiHelper.ConfirmThen($"删除「{key}」?", () =>
|
||||
{
|
||||
_items.RemoveAt(_selected);
|
||||
_selected = -1;
|
||||
ClearEditor();
|
||||
SignalConfigStore.Save(_path, _items);
|
||||
_status = $"已删除 {key}";
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardScene.Fass2Simulato
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardScene.Magnetic.Tests", "StandardScene.Magnetic.Tests\StandardScene.Magnetic.Tests.csproj", "{7C13685A-D666-4404-8CD7-19AA9BFC907B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardScene.Signal", "StandardScene.Signal\StandardScene.Signal.csproj", "{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -111,6 +113,18 @@ Global
|
||||
{7C13685A-D666-4404-8CD7-19AA9BFC907B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7C13685A-D666-4404-8CD7-19AA9BFC907B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7C13685A-D666-4404-8CD7-19AA9BFC907B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7E5050AA-0009-4A11-9C22-0A0B0C0D0E09}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#r "D:\工作\stand\SimpleLite\SimpleLite.dll"
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
var asm = typeof(SimpleLite.RCS.Mission).Assembly;
|
||||
Console.WriteLine("asm=" + asm.FullName);
|
||||
foreach (var t in asm.GetTypes().OrderBy(x => x.FullName))
|
||||
{
|
||||
var n = t.FullName ?? "";
|
||||
if (n.IndexOf("Data", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| n.IndexOf("Json", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| n.IndexOf("Table", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| n.IndexOf("Center", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| n.IndexOf("Store", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| n.Contains("UI"))
|
||||
Console.WriteLine(n);
|
||||
}
|
||||
Reference in New Issue
Block a user