磁导航1.0内部交管和信号交互

This commit is contained in:
ykkokluo
2026-08-24 14:55:35 +08:00
parent cacc5fea5c
commit 244b0af6c8
60 changed files with 6395 additions and 294 deletions
@@ -2,31 +2,46 @@
{
"Sit": 98,
"SignalType": "JGControl",
"SignalParams": "上线机构1;RequestIn"
"JgName": "上线机构1",
"DockId": "dock1",
"Action": "RequestIn"
},
{
"Sit": 100,
"SignalType": "JGControl",
"SignalParams": "上线机构1;Arrived"
"JgName": "上线机构1",
"DockId": "dock1",
"Action": "Arrived"
},
{
"Sit": 108,
"SignalType": "JGControl",
"JgName": "上线机构1",
"DockId": "dock2",
"Action": "RequestIn"
},
{
"Sit": 199,
"SignalType": "JGControl",
"SignalParams": "下线机构1;RequestIn"
"JgName": "下线机构1",
"DockId": "dock1",
"Action": "RequestIn"
},
{
"Sit": 201,
"SignalType": "PointControl",
"SignalParams": "UP1;下线机构1"
"JgName": "下线机构1",
"DockId": "dock1",
"Action": "UP1"
},
{
"Sit": 50,
"SignalType": "TimedCharging",
"SignalParams": "300;95;20"
"ExtraParams": "300;95;20"
},
{
"Sit": 60,
"SignalType": "RateFlowControl",
"SignalParams": "缓存区;A;2;60,61,62"
"ExtraParams": "缓存区;A;2;60,61,62"
}
]
@@ -0,0 +1,134 @@
[
{
"TriggerSit": 4,
"AreaName": "路口中",
"CompatGroup": "NS-W",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "5,104,100,103,6",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 26,
"AreaName": "路口中",
"CompatGroup": "NS-E",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "6,103,100,104,5",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 38,
"AreaName": "路口中",
"CompatGroup": "EW-N",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "27,101,100,102,28",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 45,
"AreaName": "路口中",
"CompatGroup": "EW-S",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "28,102,100,101,27",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 1,
"AreaName": "西南角",
"CompatGroup": "SW-W",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "2",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 47,
"AreaName": "西南角",
"CompatGroup": "SW-S",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "2",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 8,
"AreaName": "西北角",
"CompatGroup": "NW-W",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "9",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 36,
"AreaName": "西北角",
"CompatGroup": "NW-N",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "9",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 16,
"AreaName": "东南角",
"CompatGroup": "SE-E",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "17",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 43,
"AreaName": "东南角",
"CompatGroup": "SE-S",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "17",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 23,
"AreaName": "东北角",
"CompatGroup": "NE-E",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "24",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 30,
"AreaName": "东北角",
"CompatGroup": "NE-N",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "24",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
}
]
@@ -0,0 +1,134 @@
[
{
"TriggerSit": 4,
"AreaName": "路口中",
"CompatGroup": "NS-W",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "5,104,100,103,6",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 26,
"AreaName": "路口中",
"CompatGroup": "NS-E",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "6,103,100,104,5",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 38,
"AreaName": "路口中",
"CompatGroup": "EW-N",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "27,101,100,102,28",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 45,
"AreaName": "路口中",
"CompatGroup": "EW-S",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "28,102,100,101,27",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 1,
"AreaName": "西南角",
"CompatGroup": "SW-W",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "2",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 47,
"AreaName": "西南角",
"CompatGroup": "SW-S",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "2",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 8,
"AreaName": "西北角",
"CompatGroup": "NW-W",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "9",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 36,
"AreaName": "西北角",
"CompatGroup": "NW-N",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "9",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 16,
"AreaName": "东南角",
"CompatGroup": "SE-E",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "17",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 43,
"AreaName": "东南角",
"CompatGroup": "SE-S",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "17",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 23,
"AreaName": "东北角",
"CompatGroup": "NE-E",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "24",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
},
{
"TriggerSit": 30,
"AreaName": "东北角",
"CompatGroup": "NE-N",
"Priority": 10,
"WaitSeconds": 12,
"ControlArea": "24",
"ControlCapacity": 1,
"ControlReleaseStartStop": 11,
"IsUse": true
}
]
@@ -0,0 +1,40 @@
{
"version": 1,
"tables": [
{
"id": "stations",
"title": "PLC机构",
"category": "PLC数据管理",
"fileName": "stations.json",
"model": "PlcStationModel"
},
{
"id": "docks",
"title": "机构工位",
"category": "PLC数据管理",
"fileName": "station-docks.json",
"model": "PlcStationDockModel"
},
{
"id": "handshake",
"title": "握手点",
"category": "握手点数据管理",
"fileName": "handshake-points.json",
"model": "HandshakePointModel"
},
{
"id": "release",
"title": "放行点",
"category": "放行点数据管理",
"fileName": "release-points.json",
"model": "ReleasePointModel"
},
{
"id": "mag-control",
"title": "磁条管控区",
"category": "磁条交管",
"fileName": "mag-control-areas.json",
"model": "MagControlAreaModel"
}
]
}
@@ -0,0 +1,62 @@
[
{
"JgName": "上线机构1",
"DockId": "dock1",
"AllowInByte": 0,
"AllowInBit": 0,
"AllowOutByte": 0,
"AllowOutBit": 1,
"EnteringByte": 2,
"EnteringBit": 0,
"ArrivedByte": 2,
"ArrivedBit": 1,
"LeavingByte": 2,
"LeavingBit": 2,
"LeftByte": 2,
"LeftBit": 3,
"JgInPointId": 99,
"JgInContains": "99,100",
"JgPointId": 100,
"JgContains": "100,101"
},
{
"JgName": "上线机构1",
"DockId": "dock2",
"AllowInByte": 0,
"AllowInBit": 2,
"AllowOutByte": 0,
"AllowOutBit": 3,
"EnteringByte": 2,
"EnteringBit": 4,
"ArrivedByte": 2,
"ArrivedBit": 5,
"LeavingByte": 2,
"LeavingBit": 6,
"LeftByte": 2,
"LeftBit": 7,
"JgInPointId": 109,
"JgInContains": "109,110",
"JgPointId": 110,
"JgContains": "110,111"
},
{
"JgName": "下线机构1",
"DockId": "dock1",
"AllowInByte": 0,
"AllowInBit": 0,
"AllowOutByte": 0,
"AllowOutBit": 1,
"EnteringByte": 2,
"EnteringBit": 0,
"ArrivedByte": 2,
"ArrivedBit": 1,
"LeavingByte": 2,
"LeavingBit": 2,
"LeftByte": 2,
"LeftBit": 3,
"JgInPointId": 200,
"JgInContains": "200",
"JgPointId": 201,
"JgContains": "201,202"
}
]
@@ -3,26 +3,20 @@
"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",
"PlcType": "S7_1500",
"DbNumber": 100,
"HeartByte": 32,
"HeartBit": 0,
"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",
"PlcType": "S7_1500",
"DbNumber": 100,
"HeartByte": 32,
"HeartBit": 0,
"LineType": "单牵引下线机构"
}
]
+17 -4
View File
@@ -139,13 +139,26 @@ namespace StandardScene.Signal.Logic
release = _release;
}
PlcStationRuntime Find(string name)
PlcStationRuntime Find(string name, string dockId)
{
if (string.IsNullOrWhiteSpace(name))
return null;
var list = _runtimes() ?? Array.Empty<PlcStationRuntime>();
return list.FirstOrDefault(x =>
string.Equals(x.Config.JgName, name, StringComparison.OrdinalIgnoreCase));
var ofStation = list.Where(x =>
string.Equals(x.JgName, name, StringComparison.OrdinalIgnoreCase)).ToList();
if (ofStation.Count == 0)
return null;
if (!string.IsNullOrWhiteSpace(dockId))
{
var hit = ofStation.FirstOrDefault(x =>
string.Equals(x.DockId, dockId.Trim(), StringComparison.OrdinalIgnoreCase));
if (hit != null)
return hit;
}
return ofStation.FirstOrDefault(x =>
string.Equals(x.DockId, PlcConfigLoader.DefaultDockId, StringComparison.OrdinalIgnoreCase))
?? ofStation[0];
}
foreach (var car in SignalCarAdapter.All())
@@ -192,7 +205,7 @@ namespace StandardScene.Signal.Logic
/// 停稳分发。TimedCharging 优先:充电点不应再走机构逻辑。
/// 未识别的 SignalType 按站点节流打日志,避免刷屏。
/// </summary>
private void DispatchStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
private void DispatchStopped(Car car, SignalStopSpec spec, Func<string, string, PlcStationRuntime> find)
{
if (spec.Is("TimedCharging"))
{
+24 -31
View File
@@ -30,11 +30,12 @@ namespace StandardScene.Signal.Logic
public static class JgControlHandler
{
/// <summary>停稳:可选限流 → RequestIn 或 Arrived。</summary>
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find, IDictionary<string, int> rateFlowLog)
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, 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();
var jgName = spec.JgName;
var action = spec.Action;
var dockId = spec.DockId;
var rateFlow = spec.ExtraParams;
if (string.IsNullOrEmpty(jgName))
{
Diagnosis.Log($"AGV{car.id}停在地标{SignalCarAdapter.PositionId(car)},读取配置机构名称为空", "JGControl", true);
@@ -48,29 +49,25 @@ namespace StandardScene.Signal.Logic
}
if (string.Equals(action, "RequestIn", StringComparison.OrdinalIgnoreCase))
RequestIn(jgName, car, find);
RequestIn(jgName, dockId, car, find);
else if (string.Equals(action, "Arrived", StringComparison.OrdinalIgnoreCase))
Arrived(jgName, car, find);
Arrived(jgName, dockId, car, find);
}
/// <summary>行驶中只做 RequestIn 安全闸:未允许进入且不是本车占用请求槽则停车。</summary>
public static void OnMoving(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
public static void OnMoving(Car car, SignalStopSpec spec, Func<string, string, PlcStationRuntime> find)
{
var jgName = spec.Part(0).Trim();
var action = spec.Part(1).Trim();
var jgName = spec.JgName;
var action = spec.Action;
if (string.IsNullOrEmpty(jgName))
return;
if (string.Equals(action, "RequestIn", StringComparison.OrdinalIgnoreCase))
RequestInJudge(jgName, car, find);
RequestInJudge(jgName, spec.DockId, car, find);
}
/// <summary>
/// 请求进入。锁机构运行态,避免与 PLC Worker 同时改写字节 / 车号。
/// 后半段与 <see cref="RequestInJudge"/> 相同,覆盖「刚放行后又被调度层启动」的竞态。
/// </summary>
private static void RequestIn(string jgName, Car car, Func<string, PlcStationRuntime> find)
private static void RequestIn(string jgName, string dockId, Car car, Func<string, string, PlcStationRuntime> find)
{
var rt = find(jgName);
var rt = find(jgName, dockId);
if (rt == null)
{
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
@@ -83,19 +80,19 @@ namespace StandardScene.Signal.Logic
rt.RequestInAgvNo == 0 &&
rt.ReadWriteSignal == SignalWriteValue.)
{
if (rt.Config.LineType is SignalLineType.线 or SignalLineType.线)
if (rt.Station.LineType is SignalLineType.线 or SignalLineType.线)
{
var inside = SignalCarAdapter.CarsAt(rt.Config.JgPointId);
var inside = SignalCarAdapter.CarsAt(rt.Dock.JgPointId);
if (inside.Count > 0)
{
Diagnosis.Log(
$"{jgName} 机构内有AGV【{string.Join(",", inside.Select(x => x.id))}】暂不放行AGV{car.id}",
$"{rt.DisplayName} 机构内有AGV【{string.Join(",", inside.Select(x => x.id))}】暂不放行AGV{car.id}",
"JGControl", true);
return;
}
}
Diagnosis.Log($"{jgName} 机构允许进入,通知AGV{car.id}启动,通知机构AGV进入中", "JGControl", true);
Diagnosis.Log($"{rt.DisplayName} 机构允许进入,通知AGV{car.id}启动,通知机构AGV进入中", "JGControl", true);
rt.WriteSignal = SignalWriteValue.;
rt.RequestInAgvNo = car.id;
SignalCarAdapter.StopCharge(car);
@@ -108,15 +105,14 @@ namespace StandardScene.Signal.Logic
SignalCarAdapter.IsMoving(car))
{
SignalCarAdapter.Stop(car);
Diagnosis.Log($"{jgName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
Diagnosis.Log($"{rt.DisplayName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
}
}
}
/// <summary>行驶中的 RequestIn 安全闸,条件与 RequestIn 后半段一致。</summary>
private static void RequestInJudge(string jgName, Car car, Func<string, PlcStationRuntime> find)
private static void RequestInJudge(string jgName, string dockId, Car car, Func<string, string, PlcStationRuntime> find)
{
var rt = find(jgName);
var rt = find(jgName, dockId);
if (rt == null)
{
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
@@ -130,17 +126,14 @@ namespace StandardScene.Signal.Logic
SignalCarAdapter.IsMoving(car))
{
SignalCarAdapter.Stop(car);
Diagnosis.Log($"{jgName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
Diagnosis.Log($"{rt.DisplayName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
}
}
}
/// <summary>
/// 到位。InAgvNo 已是本车则认为已报到,避免每秒重复写「到位」把 WriteSuccess 清掉。
/// </summary>
private static void Arrived(string jgName, Car car, Func<string, PlcStationRuntime> find)
private static void Arrived(string jgName, string dockId, Car car, Func<string, string, PlcStationRuntime> find)
{
var rt = find(jgName);
var rt = find(jgName, dockId);
if (rt == null)
{
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
@@ -151,7 +144,7 @@ namespace StandardScene.Signal.Logic
{
if (rt.InAgvNo == car.id)
return;
Diagnosis.Log($"AGV{car.id}已到达机构{jgName}", "JGControl", true);
Diagnosis.Log($"AGV{car.id}已到达机构{rt.DisplayName}", "JGControl", true);
rt.WriteSignal = SignalWriteValue.;
rt.InAgvNo = car.id;
rt.RequestInAgvNo = 0;
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using SimpleLite.RCS.CarTypes;
namespace StandardScene.Signal.Logic
{
/// <summary>
/// 磁条管控区占用判定:他车占用「当前策略」管控站点列表的数量 ≥ 容量则不放行;本车不计入。
/// 磁条交管进程只对停在触发点的 MagCar 调用,满足后发启动。
/// </summary>
public static class MagControlAreaGate
{
public const byte StartStopPass = 1;
public const byte StartStopControlStart = 11;
private static readonly char[] SiteIdSeparators = { ',', ';', '|', ' ', '\t', '[', ']', '(', ')', '{', '}' };
public static byte NormalizeReleaseStartStop(byte value)
{
if (value == StartStopPass || value == StartStopControlStart)
return value;
return StartStopControlStart;
}
public static int NormalizeCapacity(int capacity)
{
return capacity < 1 ? 1 : capacity;
}
public static int[] ParseSiteIds(string text, int fallbackSiteId)
{
var ids = new List<int>();
if (!string.IsNullOrWhiteSpace(text))
{
var parts = text.Split(SiteIdSeparators, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
if (int.TryParse(part.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var id) &&
id > 0 && !ids.Contains(id))
{
ids.Add(id);
}
}
}
if (ids.Count == 0 && fallbackSiteId > 0)
ids.Add(fallbackSiteId);
return ids.ToArray();
}
public static string NormalizeCompatGroup(string value)
{
return (value ?? string.Empty).Trim();
}
public static bool SitesIntersect(IReadOnlyList<int> a, IReadOnlyList<int> b)
{
if (a == null || b == null || a.Count == 0 || b.Count == 0)
return false;
for (var i = 0; i < a.Count; i++)
{
var left = a[i];
if (left <= 0)
continue;
for (var j = 0; j < b.Count; j++)
{
if (b[j] == left)
return true;
}
}
return false;
}
public static MagControlReleaseDecision Evaluate(
int selfCarId,
int triggerSit,
IReadOnlyList<int> areaSiteIds,
int capacity,
byte releaseStartStop,
IReadOnlyList<(int CarId, IReadOnlyList<int> OccupiedSiteIds)> occupancies)
{
var sites = areaSiteIds == null || areaSiteIds.Count == 0
? (triggerSit > 0 ? new[] { triggerSit } : Array.Empty<int>())
: areaSiteIds;
capacity = NormalizeCapacity(capacity);
releaseStartStop = NormalizeReleaseStartStop(releaseStartStop);
var others = 0;
if (occupancies != null)
{
foreach (var occupancy in occupancies)
{
if (occupancy.CarId == selfCarId)
continue;
if (OccupiesListedSites(occupancy.OccupiedSiteIds, sites))
others++;
}
}
var canRelease = others < capacity;
var areaId = string.Join(",", sites);
return new MagControlReleaseDecision
{
RuleFound = true,
CanRelease = canRelease,
AreaId = areaId,
Capacity = capacity,
OthersInArea = others,
ReleaseStartStop = releaseStartStop,
Reason = canRelease
? "area clear"
: $"area occupied cars={others}, capacity={capacity}"
};
}
/// <summary>统计占用管控区内部站点的车辆数,可排除排队车和在途车。</summary>
public static int CountOccupyingCars(
IReadOnlyList<(int CarId, IReadOnlyList<int> OccupiedSiteIds)> occupancies,
IReadOnlyList<int> areaSiteIds,
HashSet<int> excludeCarIds)
{
if (occupancies == null || occupancies.Count == 0 ||
areaSiteIds == null || areaSiteIds.Count == 0)
return 0;
var others = 0;
foreach (var occupancy in occupancies)
{
if (excludeCarIds != null && excludeCarIds.Contains(occupancy.CarId))
continue;
if (OccupiesListedSites(occupancy.OccupiedSiteIds, areaSiteIds))
others++;
}
return others;
}
public static bool OccupiesListedSites(IReadOnlyList<int> occupiedSiteIds, IReadOnlyList<int> areaSiteIds)
{
if (occupiedSiteIds == null || occupiedSiteIds.Count == 0 ||
areaSiteIds == null || areaSiteIds.Count == 0)
return false;
for (var i = 0; i < occupiedSiteIds.Count; i++)
{
var occupied = occupiedSiteIds[i];
if (occupied <= 0)
continue;
for (var j = 0; j < areaSiteIds.Count; j++)
{
if (areaSiteIds[j] == occupied)
return true;
}
}
return false;
}
/// <summary>统计一辆车占用的站点:物理 siteID + holdingLocks。</summary>
public static IReadOnlyList<int> CollectOccupiedSiteIds(Car car)
{
var ids = new List<int>();
if (car == null)
return ids;
try
{
if (car.siteID > 0)
ids.Add(car.siteID);
}
catch
{
}
var holding = car.status?.holdingLocks;
if (holding == null)
return ids;
foreach (var lockId in holding)
{
if (lockId > 0 && !ids.Contains(lockId))
ids.Add(lockId);
}
return ids;
}
}
}
@@ -0,0 +1,479 @@
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>
/// 磁条交管运行态:按管控区名称分组排队;放行按「本线 + 冲突对向线」站点并集查当前占用。
/// 不维护 pending 在途占额。
/// </summary>
public sealed class MagControlRuntime
{
private readonly object _lock = new object();
private Dictionary<int, MagControlAreaModel> _byTrigger = new Dictionary<int, MagControlAreaModel>();
private readonly Dictionary<int, DateTime> _releaseLog = new Dictionary<int, DateTime>();
private readonly Dictionary<int, Arrival> _arrivals = new Dictionary<int, Arrival>();
public int RuleCount
{
get
{
lock (_lock)
return _byTrigger.Count;
}
}
/// <summary>已不再使用 pending;保留属性避免旧 UI 绑定报错,恒为 0。</summary>
public int PendingCount => 0;
public void Replace(IEnumerable<MagControlAreaModel> rows)
{
var map = (rows ?? Array.Empty<MagControlAreaModel>())
.Where(x => x != null && x.TriggerSit > 0 && x.IsUse)
.GroupBy(x => x.TriggerSit)
.ToDictionary(g => g.Key, g => g.First());
lock (_lock)
{
_byTrigger = map;
_arrivals.Clear();
}
}
/// <summary>
/// 同名区排队;放行前查本线 + 冲突对向线站点并集上的当前占用。
/// 另计:已放行仍停在冲突触发点上的运行中车辆(避免 site 未进区时空窗连放)。
/// </summary>
public (int Waiting, int Released) ScanAndRelease()
{
List<MagControlAreaModel> rules;
lock (_lock)
rules = _byTrigger.Values.ToList();
if (rules.Count == 0)
return (0, 0);
var snapshots = SnapshotCars();
var occupancies = snapshots
.Select(s => (s.CarId, s.OccupiedSiteIds))
.ToList();
var groups = rules
.GroupBy(AreaKey, StringComparer.OrdinalIgnoreCase)
.Select(g => BuildGroup(g.Key, g.ToList()))
.ToList();
lock (_lock)
SweepArrivals(snapshots);
var waiting = 0;
var released = 0;
foreach (var group in groups)
{
var waiters = CollectWaiters(group, snapshots);
waiting += waiters.Count;
if (waiters.Count == 0)
continue;
// 停稳排队的车不计入区内占用;本拍已放行的车单独记冲突占额
var exclude = new HashSet<int>(waiters.Select(w => w.CarId));
var releasedThisScan = new List<Waiter>();
waiters.Sort((a, b) => CompareWaiters(a, b, group.WaitSeconds));
foreach (var waiter in waiters)
{
var checkSites = BuildConflictSites(group, waiter);
var capacity = MagControlAreaGate.NormalizeCapacity(waiter.Capacity);
var others = CountConflictingOccupancy(
group, waiter, checkSites, occupancies, snapshots, exclude, releasedThisScan);
if (others >= capacity)
continue;
SignalCarAdapter.StopCharge(waiter.Car);
SignalCarAdapter.StartMagCar(waiter.Car);
released++;
exclude.Add(waiter.CarId);
releasedThisScan.Add(waiter);
NoteRelease(waiter, group, others, capacity, checkSites);
}
}
return (waiting, released);
}
/// <summary>
/// 冲突占用 = 并集站点上的当前车
/// + 本区冲突触发点上仍「运行中」的车(刚放行、site 尚未进 ControlArea
/// + 本拍已放行且与本线冲突的车。
/// </summary>
private static int CountConflictingOccupancy(
AreaGroup group,
Waiter waiter,
IReadOnlyList<int> checkSites,
List<(int CarId, IReadOnlyList<int> OccupiedSiteIds)> occupancies,
List<CarSnapshot> snapshots,
HashSet<int> exclude,
List<Waiter> releasedThisScan)
{
var others = MagControlAreaGate.CountOccupyingCars(occupancies, checkSites, exclude);
var counted = new HashSet<int>();
foreach (var snap in snapshots)
{
if (snap == null || exclude.Contains(snap.CarId) || counted.Contains(snap.CarId))
continue;
if (!snap.IsMagCar)
continue;
// 已在并集站点上:CountOccupyingCars 已计,这里补「运行中仍在冲突触发点」
if (snap.IsStopped)
continue;
if (snap.Sit <= 0 || !group.Triggers.Contains(snap.Sit))
continue;
if (!group.RouteByTrigger.TryGetValue(snap.Sit, out var route))
continue;
if (!Conflicts(waiter.CompatGroup, waiter.AreaSites, route.CompatGroup, route.AreaSites))
continue;
// 若 site 已在 checkSites 里,上面 others 可能已计(触发点一般不在列表)
if (MagControlAreaGate.OccupiesListedSites(snap.OccupiedSiteIds, checkSites))
continue;
counted.Add(snap.CarId);
others++;
}
foreach (var releasedCar in releasedThisScan)
{
if (releasedCar == null || counted.Contains(releasedCar.CarId))
continue;
if (!Conflicts(
waiter.CompatGroup, waiter.AreaSites,
releasedCar.CompatGroup, releasedCar.AreaSites))
continue;
counted.Add(releasedCar.CarId);
others++;
}
return others;
}
/// <summary>单车判定:本线 + 同名区内冲突对向线站点并集的当前占用。</summary>
public MagControlReleaseDecision Decide(int carId, int triggerSit)
{
MagControlAreaModel rule;
List<MagControlAreaModel> sameArea;
lock (_lock)
{
if (triggerSit <= 0 || !_byTrigger.TryGetValue(triggerSit, out rule) || rule == null)
{
return new MagControlReleaseDecision
{
RuleFound = false,
Reason = "no mag-control rule"
};
}
var key = AreaKey(rule);
sameArea = _byTrigger.Values
.Where(r => string.Equals(AreaKey(r), key, StringComparison.OrdinalIgnoreCase))
.ToList();
}
var group = BuildGroup(AreaKey(rule), sameArea);
if (!group.RouteByTrigger.TryGetValue(triggerSit, out var route))
{
return new MagControlReleaseDecision
{
RuleFound = false,
Reason = "no mag-control route"
};
}
var waiter = new Waiter
{
CarId = carId,
TriggerSit = triggerSit,
CompatGroup = route.CompatGroup,
AreaSites = route.AreaSites,
Capacity = route.Capacity
};
var checkSites = BuildConflictSites(group, waiter);
var occupancies = new List<(int CarId, IReadOnlyList<int> OccupiedSiteIds)>();
foreach (var car in SignalCarAdapter.All())
{
if (car == null)
continue;
occupancies.Add((car.id, MagControlAreaGate.CollectOccupiedSiteIds(car)));
}
return MagControlAreaGate.Evaluate(
carId,
triggerSit,
checkSites,
rule.ControlCapacity,
rule.ControlReleaseStartStop,
occupancies);
}
internal static string AreaKey(MagControlAreaModel rule)
{
if (rule == null)
return "#sit:0";
var name = (rule.AreaName ?? string.Empty).Trim();
return name.Length > 0 ? name : "#sit:" + rule.TriggerSit;
}
internal static int CompareWaiters(Waiter a, Waiter b, int waitSeconds)
{
if (waitSeconds > 0)
{
if (a.Priority < b.Priority &&
(b.ArrivedAt - a.ArrivedAt).TotalSeconds > waitSeconds)
return -1;
if (b.Priority < a.Priority &&
(a.ArrivedAt - b.ArrivedAt).TotalSeconds > waitSeconds)
return 1;
}
var byPriority = b.Priority.CompareTo(a.Priority);
if (byPriority != 0)
return byPriority;
var byArrival = a.ArrivedAt.CompareTo(b.ArrivedAt);
if (byArrival != 0)
return byArrival;
return a.CarId.CompareTo(b.CarId);
}
internal static bool Conflicts(
string compatA,
IReadOnlyList<int> sitesA,
string compatB,
IReadOnlyList<int> sitesB)
{
if (string.Equals(compatA ?? "", compatB ?? "", StringComparison.OrdinalIgnoreCase))
return true;
return MagControlAreaGate.SitesIntersect(sitesA, sitesB);
}
/// <summary>
/// 本线站点 + 同名区内所有与本线冲突的对向/交叉线路站点并集。
/// 例如西环 NS-W 会并入东/北/南列表,100/101/104 有车时都能挡住触发点 4。
/// </summary>
private static IReadOnlyList<int> BuildConflictSites(AreaGroup group, Waiter waiter)
{
var set = new HashSet<int>();
if (waiter?.AreaSites != null)
{
foreach (var site in waiter.AreaSites)
{
if (site > 0)
set.Add(site);
}
}
if (group?.RouteByTrigger == null)
return set.OrderBy(x => x).ToList();
foreach (var route in group.RouteByTrigger.Values)
{
if (route?.AreaSites == null)
continue;
if (!Conflicts(waiter.CompatGroup, waiter.AreaSites, route.CompatGroup, route.AreaSites))
continue;
foreach (var site in route.AreaSites)
{
if (site > 0)
set.Add(site);
}
}
return set.OrderBy(x => x).ToList();
}
private static AreaGroup BuildGroup(string key, List<MagControlAreaModel> rows)
{
var ordered = rows.OrderBy(r => r.TriggerSit).ToList();
var triggers = new HashSet<int>(ordered.Select(r => r.TriggerSit));
var waitSeconds = 0;
var routeByTrigger = new Dictionary<int, RouteInfo>();
foreach (var row in ordered)
{
waitSeconds = Math.Max(waitSeconds, row.WaitSeconds);
var sites = MagControlAreaGate.ParseSiteIds(row.ControlArea, row.TriggerSit);
routeByTrigger[row.TriggerSit] = new RouteInfo
{
CompatGroup = MagControlAreaGate.NormalizeCompatGroup(row.CompatGroup),
Capacity = MagControlAreaGate.NormalizeCapacity(row.ControlCapacity),
AreaSites = sites,
Priority = row.Priority
};
}
return new AreaGroup
{
Key = key,
WaitSeconds = waitSeconds,
Triggers = triggers,
RouteByTrigger = routeByTrigger
};
}
private List<CarSnapshot> SnapshotCars()
{
var list = new List<CarSnapshot>();
foreach (var car in SignalCarAdapter.All())
{
if (car == null)
continue;
list.Add(new CarSnapshot
{
Car = car,
CarId = car.id,
OccupiedSiteIds = MagControlAreaGate.CollectOccupiedSiteIds(car),
IsMagCar = SignalCarAdapter.IsMagCar(car),
IsStopped = SignalCarAdapter.IsStopped(car),
Sit = SignalCarAdapter.PositionId(car)
});
}
return list;
}
private List<Waiter> CollectWaiters(AreaGroup group, List<CarSnapshot> snapshots)
{
var waiters = new List<Waiter>();
var now = DateTime.Now;
foreach (var snap in snapshots)
{
if (!snap.IsMagCar || !snap.IsStopped)
continue;
if (snap.Sit <= 0 || !group.Triggers.Contains(snap.Sit))
continue;
DateTime arrivedAt;
lock (_lock)
{
if (!_arrivals.TryGetValue(snap.CarId, out var arrival) || arrival.TriggerSit != snap.Sit)
{
arrival = new Arrival { TriggerSit = snap.Sit, ArrivedAt = now };
_arrivals[snap.CarId] = arrival;
}
arrivedAt = arrival.ArrivedAt;
}
if (!group.RouteByTrigger.TryGetValue(snap.Sit, out var route))
continue;
waiters.Add(new Waiter
{
Car = snap.Car,
CarId = snap.CarId,
TriggerSit = snap.Sit,
AreaKey = group.Key,
CompatGroup = route.CompatGroup,
Capacity = route.Capacity,
AreaSites = route.AreaSites,
Priority = route.Priority,
ArrivedAt = arrivedAt
});
}
return waiters;
}
private void SweepArrivals(List<CarSnapshot> snapshots)
{
var present = new HashSet<int>();
foreach (var snap in snapshots)
{
if (!snap.IsMagCar || !snap.IsStopped || snap.Sit <= 0)
continue;
if (_arrivals.TryGetValue(snap.CarId, out var arrival) && arrival.TriggerSit == snap.Sit)
present.Add(snap.CarId);
}
var stale = _arrivals.Keys.Where(id => !present.Contains(id)).ToList();
foreach (var id in stale)
_arrivals.Remove(id);
}
private void NoteRelease(
Waiter waiter,
AreaGroup group,
int others,
int capacity,
IReadOnlyList<int> checkSites)
{
var groupName = string.IsNullOrEmpty(waiter.CompatGroup) ? "-" : waiter.CompatGroup;
var carName = waiter.Car?.name;
var namePart = string.IsNullOrWhiteSpace(carName) ? "" : $" name={carName}";
var sites = waiter.AreaSites == null || waiter.AreaSites.Count == 0
? "-"
: string.Join(",", waiter.AreaSites);
var conflict = checkSites == null || checkSites.Count == 0
? "-"
: string.Join(",", checkSites);
var message =
$"放行 MagCar={waiter.CarId}{namePart} 区={group.Key} 组={groupName} 触发点={waiter.TriggerSit} " +
$"本线={sites} 冲突并集={conflict} 当前占用={others}/{capacity}";
MagTrafficFileLogger.Write(carName, waiter.CarId, message);
if (_releaseLog.TryGetValue(waiter.CarId, out var last) && (DateTime.Now - last).TotalSeconds < 5)
return;
_releaseLog[waiter.CarId] = DateTime.Now;
Diagnosis.Log($"磁条交管{message}", "MagTraffic", true);
}
internal sealed class Waiter
{
public Car Car;
public int CarId;
public int TriggerSit;
public string AreaKey;
public string CompatGroup;
public int Capacity;
public IReadOnlyList<int> AreaSites;
public int Priority;
public DateTime ArrivedAt;
}
private sealed class Arrival
{
public int TriggerSit;
public DateTime ArrivedAt;
}
private sealed class RouteInfo
{
public string CompatGroup;
public int Capacity;
public IReadOnlyList<int> AreaSites;
public int Priority;
}
private sealed class AreaGroup
{
public string Key;
public int WaitSeconds;
public HashSet<int> Triggers;
public Dictionary<int, RouteInfo> RouteByTrigger;
}
private sealed class CarSnapshot
{
public Car Car;
public int CarId;
public IReadOnlyList<int> OccupiedSiteIds;
public bool IsMagCar;
public bool IsStopped;
public int Sit;
}
}
}
@@ -0,0 +1,136 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text;
namespace StandardScene.Signal.Logic
{
/// <summary>
/// 磁条交管放行留档,按车名分目录:log/mag-traffic/car{name}/release_yyyyMMdd.log
/// 进程启停等写到 log/mag-traffic/_sys/sys_yyyyMMdd.log
/// </summary>
public static class MagTrafficFileLogger
{
private static readonly ConcurrentDictionary<string, object> CarLocks = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
private static readonly object SysLock = new object();
private static readonly char[] InvalidNameChars = Path.GetInvalidFileNameChars();
private static string _baseDirectory = "log";
private static bool _enabled = true;
public static void Configure(string baseDirectory, bool enabled)
{
_baseDirectory = string.IsNullOrWhiteSpace(baseDirectory) ? "log" : baseDirectory.Trim();
_enabled = enabled;
}
public static string GetLogDirectory()
{
return Path.Combine(ResolveBaseDirectory(), "mag-traffic");
}
/// <summary>目录名 car{name};名称为空时回退 car{id}。</summary>
public static string GetCarFolderKey(string carName, int carId)
{
var safe = SanitizeName(carName);
if (string.IsNullOrWhiteSpace(safe))
{
safe = carId > 0 ? carId.ToString() : "unknown";
}
return "car" + safe;
}
public static string GetCarLogDirectory(string carName, int carId = 0)
{
return Path.Combine(GetLogDirectory(), GetCarFolderKey(carName, carId));
}
public static string GetCurrentCarLogFilePath(string carName, int carId = 0)
{
return Path.Combine(GetCarLogDirectory(carName, carId), $"release_{DateTime.Now:yyyyMMdd}.log");
}
public static string GetCurrentSysLogFilePath()
{
return Path.Combine(GetLogDirectory(), "_sys", $"sys_{DateTime.Now:yyyyMMdd}.log");
}
/// <summary>按车名写入放行记录。</summary>
public static void Write(string carName, int carId, string message)
{
if (!_enabled || string.IsNullOrWhiteSpace(message))
{
return;
}
var folderKey = GetCarFolderKey(carName, carId);
var carLock = CarLocks.GetOrAdd(folderKey, _ => new object());
lock (carLock)
{
try
{
var directory = Path.Combine(GetLogDirectory(), folderKey);
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, $"release_{DateTime.Now:yyyyMMdd}.log");
File.AppendAllText(
path,
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}",
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}
catch
{
}
}
}
/// <summary>进程启停、重载配置等非单车事件。</summary>
public static void WriteSystem(string message)
{
if (!_enabled || string.IsNullOrWhiteSpace(message))
{
return;
}
lock (SysLock)
{
try
{
var path = GetCurrentSysLogFilePath();
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? GetLogDirectory());
File.AppendAllText(
path,
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}",
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}
catch
{
}
}
}
private static string SanitizeName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return "";
}
var chars = name.Trim().Select(ch => InvalidNameChars.Contains(ch) || ch <= 31 ? '_' : ch).ToArray();
var safe = new string(chars).Trim('.', ' ');
while (safe.Contains("__"))
{
safe = safe.Replace("__", "_");
}
return safe.Length > 80 ? safe.Substring(0, 80) : safe;
}
private static string ResolveBaseDirectory()
{
return Path.IsPathRooted(_baseDirectory)
? _baseDirectory
: Path.Combine(AppContext.BaseDirectory, _baseDirectory);
}
}
}
@@ -18,10 +18,10 @@ namespace StandardScene.Signal.Logic
public static class PointControlHandler
{
/// <summary>停稳:先判机构是否允许离开,再下发销钉,全部到位后启动。</summary>
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, string, PlcStationRuntime> find)
{
var action = spec.Part(0).Trim();
var jgName = spec.Part(1).Trim();
var action = spec.Action;
var jgName = spec.JgName;
if (string.IsNullOrEmpty(action))
{
Diagnosis.Log($"AGV{car.id}停在地标{SignalCarAdapter.PositionId(car)},读取配置特殊点名称为空", "SetPin", true);
@@ -31,7 +31,7 @@ namespace StandardScene.Signal.Logic
var waitLeave = false;
if (!string.IsNullOrEmpty(jgName))
{
var rt = find(jgName);
var rt = find(jgName, spec.DockId);
if (rt != null)
{
lock (rt.Sync)
+87 -20
View File
@@ -4,30 +4,24 @@ using StandardScene.Signal.Model;
namespace StandardScene.Signal.Logic
{
/// <summary>
/// 一条握手点配置的解析结果。参数格式与反编译 <c>SignalStop.SignalParams</c> 一致
/// 一条握手点配置的解析结果。新列(机构/工位/动作/附加参数)优先,旧 SignalParams 作回退
/// </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 JgName { get; init; } = "";
public string DockId { get; init; } = "";
public string Action { get; init; } = "";
public string ExtraParams { get; init; } = "";
public string Part(int index)
{
if (index < 0 || index >= Parts.Length)
@@ -35,29 +29,102 @@ namespace StandardScene.Signal.Logic
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
var parsed = new SignalStopSpec
{
SignalType = type,
SignalParams = raw,
Parts = raw.Split(new[] { ';' }, StringSplitOptions.None)
};
return EnrichFromParts(parsed);
}
/// <summary>从握手点 Model 构造;model 为 null 时返回 null。</summary>
public static SignalStopSpec From(HandshakePointModel model)
{
if (model == null)
return null;
return Parse(model.SignalType, model.SignalParams);
var type = (model.SignalType ?? "").Trim();
if (string.Equals(type, "TimedCharging", StringComparison.OrdinalIgnoreCase) ||
string.Equals(type, "RateFlowControl", StringComparison.OrdinalIgnoreCase))
{
var raw = First(model.ExtraParams, model.SignalParams);
return Parse(type, raw);
}
var parsed = Parse(type, model.SignalParams ?? "");
var jg = First(model.JgName, parsed.JgName);
var action = First(model.Action, parsed.Action);
var dock = First(model.DockId, parsed.DockId);
var extra = First(model.ExtraParams, parsed.ExtraParams);
return new SignalStopSpec
{
SignalType = parsed.SignalType,
SignalParams = parsed.SignalParams,
Parts = parsed.Parts,
JgName = jg,
DockId = dock,
Action = action,
ExtraParams = extra
};
}
/// <summary>类型名是否匹配(忽略大小写)。</summary>
public bool Is(string type) =>
string.Equals(SignalType, type, StringComparison.OrdinalIgnoreCase);
private static SignalStopSpec EnrichFromParts(SignalStopSpec parsed)
{
var type = parsed.SignalType;
if (string.Equals(type, "TimedCharging", StringComparison.OrdinalIgnoreCase) ||
string.Equals(type, "RateFlowControl", StringComparison.OrdinalIgnoreCase))
{
return new SignalStopSpec
{
SignalType = parsed.SignalType,
SignalParams = parsed.SignalParams,
Parts = parsed.Parts,
ExtraParams = parsed.SignalParams
};
}
var point = string.Equals(type, "PointControl", StringComparison.OrdinalIgnoreCase);
var jg = point ? parsed.Part(1).Trim() : parsed.Part(0).Trim();
var action = point ? parsed.Part(0).Trim() : parsed.Part(1).Trim();
var dock = "";
var extra = "";
var p2 = parsed.Part(2).Trim();
if (LooksLikeDock(p2))
dock = p2;
else if (LooksLikeRateFlow(p2))
extra = p2;
if (string.IsNullOrEmpty(extra))
extra = parsed.Part(3).Trim();
return new SignalStopSpec
{
SignalType = parsed.SignalType,
SignalParams = parsed.SignalParams,
Parts = parsed.Parts,
JgName = jg,
DockId = dock,
Action = action,
ExtraParams = extra
};
}
private static string First(string a, string b)
{
if (!string.IsNullOrWhiteSpace(a))
return a.Trim();
return (b ?? "").Trim();
}
private static bool LooksLikeRateFlow(string s) =>
!string.IsNullOrEmpty(s) && s.IndexOf('-') >= 0;
private static bool LooksLikeDock(string s) =>
!string.IsNullOrEmpty(s) && s.IndexOf('-') < 0 && s.IndexOf(',') < 0;
}
}
@@ -0,0 +1,193 @@
using System;
using System.IO;
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.Ui;
using StandardScene.Utils;
namespace StandardScene.Signal
{
/// <summary>
/// 磁条交管进程:只针对 MagCar。同区名排队后,按「本线 + 冲突对向线」站点并集查占用,容量允许则发启动。
/// </summary>
/// <remarks>
/// 配置 <c>Config/Signal/mag-control-areas.json</c>。
/// 同「管控区名称」绑成一个路口排队;放行时合并本线与冲突对向线的管控站点列表统计当前占用。
/// 不使用 pending 在途占额。不处理 Mag2Car。
/// </remarks>
[MissionType(Name = "磁条交管进程", editor = typeof(MagTrafficMission))]
[I18N.DocumentTranslation(Name = "Magnetic Traffic Mission", locale = "en")]
public class MagTrafficMission : Mission
{
/// <summary>扫车周期(毫秒)。停在触发点的 MagCar 每拍排队判定一次。</summary>
[FieldMember] public int TickMs = 1000;
/// <summary>管控区表路径。相对路径只解析工作目录 <c>Config/Signal</c>。</summary>
[FieldMember] public string AreasPath = @"Config\Signal\mag-control-areas.json";
/// <summary>放行日志根目录。实际按车名落在其下 mag-traffic/car{name}/release_yyyyMMdd.log。</summary>
[FieldMember] public string LogDirectory = "log";
/// <summary>是否把交管放行写入本地文件留档。</summary>
[FieldMember] public bool EnableFileLog = true;
[JsonIgnore] private bool _started;
[JsonIgnore] private readonly MagControlRuntime _runtime = new MagControlRuntime();
[JsonIgnore] private CancellationTokenSource _cts;
[JsonIgnore] private Thread _thread;
public class MagTrafficMissionStatus : MissionStatus
{
public int RuleCount { get; set; }
public int WaitingAtTrigger { get; set; }
public int PendingInFlight { get; set; }
public int ScanTickCount { get; set; }
public string AreasFile { get; set; }
}
public override MissionStatus status { get; set; } = new MagTrafficMissionStatus();
public static Mission Create() => new MagTrafficMission();
[MethodMember(Name = "启动进程", Description = "加载管控区表,按区名排队后对 MagCar 发放行")]
public override void Execute()
{
try
{
MagTrafficFileLogger.Configure(LogDirectory, EnableFileLog);
ReloadConfig();
_started = true;
StartWatch();
MagTrafficFileLogger.WriteSystem($"进程启动 规则={((MagTrafficMissionStatus)status).RuleCount} 配置={((MagTrafficMissionStatus)status).AreasFile}");
Diagnosis.Post(
$"磁条交管进程已启动,规则={((MagTrafficMissionStatus)status).RuleCount}(仅 MagCar),放行日志={MagTrafficFileLogger.GetLogDirectory()}",
"MagTraffic", true);
}
catch (Exception ex)
{
status.status = "启动失败";
Diagnosis.Post($"磁条交管进程启动失败:{ExceptionFormatter.FormatEx(ex)}", "MagTraffic", true);
throw;
}
}
[MethodMember(Name = "重新加载配置", Description = "重新读取 mag-control-areas.json")]
public void ReloadConfig()
{
var file = SignalConfigStore.Resolve(AreasPath);
var rows = SignalConfigStore.Load<MagControlAreaModel>(file);
_runtime.Replace(rows);
var st = (MagTrafficMissionStatus)status;
st.AreasFile = file;
st.RuleCount = _runtime.RuleCount;
st.PendingInFlight = _runtime.PendingCount;
status.status = _started ? "运行中" : "配置已加载";
MagTrafficFileLogger.Configure(LogDirectory, EnableFileLog);
MagTrafficFileLogger.WriteSystem($"重新加载配置 规则={st.RuleCount} 文件={file}");
Diagnosis.Post($"磁条交管配置已加载:启用规则 {st.RuleCount} @ {file}", "MagTraffic", true);
}
[MethodMember(Name = "停止进程")]
public new void Stop()
{
_started = false;
StopWatch();
status.status = "已停止";
MagTrafficFileLogger.WriteSystem("进程停止");
Diagnosis.Post("磁条交管进程已停止", "MagTraffic", true);
}
[MethodMember(Name = "打开放行日志目录", Description = "打开 mag-traffic 目录(按 car{name} 分子目录),便于按车查阅放行记录")]
public void OpenReleaseLogFolder()
{
try
{
MagTrafficFileLogger.Configure(LogDirectory, EnableFileLog);
var dir = MagTrafficFileLogger.GetLogDirectory();
Directory.CreateDirectory(dir);
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = dir,
UseShellExecute = true
});
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开放行日志目录失败:{ex.Message}");
}
}
[MethodMember(Name = "打开管控区列表", Description = "编辑区名 / 兼容组 / 优先级 / 等待时长 / 触发点 / 区内站点 / 容量")]
public void OpenAreas()
{
try
{
var path = SignalConfigStore.Resolve(AreasPath);
ModelListPanel<MagControlAreaModel>.Open(
"磁条管控区配置",
path,
x => x == null ? "" : x.TriggerSit.ToString());
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开管控区列表失败:{ex.Message}");
}
}
private void StartWatch()
{
StopWatch();
_cts = new CancellationTokenSource();
_thread = new Thread(WatchLoop)
{
IsBackground = true,
Name = "MagTrafficWatch"
};
_thread.Start();
}
private void StopWatch()
{
try { _cts?.Cancel(); }
catch { }
try { _thread?.Join(2000); }
catch { }
_thread = null;
}
private void WatchLoop()
{
var token = _cts.Token;
var tick = 0;
var interval = Math.Max(200, TickMs);
while (!token.IsCancellationRequested)
{
try
{
var (waiting, _) = _runtime.ScanAndRelease();
var st = (MagTrafficMissionStatus)status;
st.WaitingAtTrigger = waiting;
st.PendingInFlight = _runtime.PendingCount;
st.RuleCount = _runtime.RuleCount;
st.ScanTickCount = ++tick;
if (_started)
status.status = $"运行中 规则{st.RuleCount} 等待{waiting}";
}
catch (Exception ex)
{
Diagnosis.Log($"磁条交管扫车异常:{ex.Message}", "MagTraffic", true);
}
try { Thread.Sleep(interval); }
catch (ThreadInterruptedException) { break; }
}
}
}
}
@@ -16,7 +16,7 @@ using StandardScene.Utils;
namespace StandardScene.Signal
{
/// <summary>
/// 第三方信号交互进程(场景 <c>scene.signal</c> 的唯一 Mission)。
/// 第三方信号交互进程(场景 <c>scene.signal</c>:PLC 握手 / 放行;磁条交管见 <c>MagTrafficMission</c>)。
/// </summary>
/// <remarks>
/// <para>
@@ -65,9 +65,9 @@ namespace StandardScene.Signal
/// <summary>
/// 西门子系列,传给 IoTClient <c>SiemensVersion</c>。
/// 常用 <c>S7_1500</c> / <c>S7_1200</c> / <c>S7_300</c>;连字符会当成下划线解析
/// 机构表 PLC类型为「跟随进程」时使用本项;机构单独指定则用机构值
/// </summary>
[FieldMember] public string PlcVersion = "S7_1500";
[FieldMember] public SignalPlcType PlcVersion = SignalPlcType.S7_1500;
/// <summary>机构未单独填 Port 时使用的 S7 端口,默认 102。</summary>
[FieldMember] public int PlcPort = 102;
@@ -84,6 +84,9 @@ namespace StandardScene.Signal
/// <summary>握手点表路径。按站点配置 JGControl / PointControl / TimedCharging / RateFlowControl。</summary>
[FieldMember] public string HandshakePointsPath = @"Config\Signal\handshake-points.json";
/// <summary>机构工位表。相对路径只解析工作目录 Config/Signal。</summary>
[FieldMember] public string DocksPath = @"Config\Signal\station-docks.json";
/// <summary>Execute 成功后为 trueReloadConfig 仅在已启动时重启 Worker / 扫车。</summary>
[JsonIgnore] private bool _started;
@@ -126,6 +129,12 @@ namespace StandardScene.Signal
/// <summary>实际解析到的握手点文件绝对路径。</summary>
public string HandshakePointsFile { get; set; }
/// <summary>实际解析到的工位文件绝对路径。</summary>
public string DocksFile { get; set; }
/// <summary>工位 JSON 条数。</summary>
public int DockCount { get; set; }
/// <summary>各机构运行态快照,供监视列表展示。</summary>
public List<PlcStationSnapshot> Stations { get; set; } = new List<PlcStationSnapshot>();
}
@@ -170,23 +179,26 @@ namespace StandardScene.Signal
public void ReloadConfig()
{
var stationsFile = SignalConfigStore.Resolve(StationsPath);
var docksFile = SignalConfigStore.Resolve(DocksPath);
var releaseFile = SignalConfigStore.Resolve(ReleasePointsPath);
var handshakeFile = SignalConfigStore.Resolve(HandshakePointsPath);
var stations = SignalConfigStore.Load<PlcStationModel>(stationsFile);
var bundles = PlcConfigLoader.Load(stationsFile, docksFile);
var points = SignalConfigStore.Load<ReleasePointModel>(releaseFile);
var handshake = SignalConfigStore.Load<HandshakePointModel>(handshakeFile);
var st = (SignalInteractMissionStatus)status;
st.StationCount = stations.Count;
st.StationCount = bundles.Count;
st.DockCount = bundles.Sum(b => b.Docks.Count);
st.ReleasePointCount = points.Count;
st.HandshakePointCount = handshake.Count;
st.StationsFile = stationsFile;
st.DocksFile = docksFile;
st.ReleasePointsFile = releaseFile;
st.HandshakePointsFile = handshakeFile;
st.Stations = stations.Select(s => new PlcStationSnapshot { JgName = s.JgName }).ToList();
st.Stations = bundles.Select(b => new PlcStationSnapshot { JgName = b.Station.JgName }).ToList();
status.status = _started ? "运行中" : "配置已加载";
Diagnosis.Post(
$"信号配置已加载:机构 {stations.Count};握手点 {handshake.Count} @ {handshakeFile};放行点 {points.Count} @ {releaseFile}",
$"信号配置已加载:机构 {bundles.Count},工位 {st.DockCount} @ {docksFile};握手点 {handshake.Count} @ {handshakeFile};放行点 {points.Count} @ {releaseFile}",
"Signal", true);
if (_started)
@@ -224,6 +236,24 @@ namespace StandardScene.Signal
}
}
/// <summary>CycleGUI 独立窗体编辑工位表;主键为机构名+工位编号。</summary>
[MethodMember(Name = "打开工位列表", Description = "按 PlcStationDockModel 增删改工位点位")]
public void OpenDocks()
{
try
{
var path = SignalConfigStore.Resolve(DocksPath);
ModelListPanel<PlcStationDockModel>.Open(
"机构工位配置",
path,
x => x == null ? "" : $"{x.JgName}/{x.ResolvedDockId}");
}
catch (Exception ex)
{
CycleUiHelper.Alert("错误", $"打开工位列表失败:{ex.Message}");
}
}
/// <summary>CycleGUI 独立窗体编辑握手点;主键展示为「站点号 + 信号类型」。</summary>
[MethodMember(Name = "打开握手点列表", Description = "按站点配置 JGControl / PointControl / TimedCharging / RateFlowControl")]
public void OpenHandshakePoints()
@@ -266,7 +296,7 @@ namespace StandardScene.Signal
get
{
lock (_workerLock)
return _workers.Select(w => w.Runtime).ToList();
return _workers.SelectMany(w => w.Docks).ToList();
}
}
@@ -285,25 +315,28 @@ namespace StandardScene.Signal
/// <summary>
/// 先停全部旧连接,再按机构表为每个非空名称建 <see cref="PlcStationWorker"/>。
/// 机构 Port&lt;=0 时回退进程级 <see cref="PlcPort"/>。
/// 机构 Port&lt;=0 时回退进程级 <see cref="PlcPort"/>PLC类型跟随进程时用 <see cref="PlcVersion"/>
/// </summary>
private void RestartWorkers()
{
StopWorkers();
var stationsFile = SignalConfigStore.Resolve(StationsPath);
var stations = SignalConfigStore.Load<PlcStationModel>(stationsFile)
.Where(s => !string.IsNullOrWhiteSpace(s.JgName))
.ToList();
var bundles = PlcConfigLoader.Load(
SignalConfigStore.Resolve(StationsPath),
SignalConfigStore.Resolve(DocksPath));
var created = new List<PlcStationWorker>();
lock (_workerLock)
{
foreach (var station in stations)
foreach (var bundle in bundles)
{
var station = bundle.Station;
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);
var sync = new object();
var runtimes = bundle.Docks
.Select(d => new PlcStationRuntime(station, d, sync))
.ToList();
var session = new SiemensPlcSession(ResolvePlcType(station), station.Ip, port, PlcSlot);
var worker = new PlcStationWorker(station, runtimes, session, sync, PlcPollMs, RefreshStatus);
_workers.Add(worker);
created.Add(worker);
}
@@ -315,6 +348,15 @@ namespace StandardScene.Signal
PushStatus();
}
/// <summary>机构选跟随进程(含旧 JSON 缺省 0)时用进程 PlcVersion;进程若也是跟随进程则按 S7_1500。</summary>
private string ResolvePlcType(PlcStationModel station)
{
var type = station.PlcType == SignalPlcType. ? PlcVersion : station.PlcType;
if (type == SignalPlcType.)
type = SignalPlcType.S7_1500;
return type.ToString();
}
/// <summary>清空列表后再 Stop,避免扫车在停连接过程中仍拿到旧 Runtime。</summary>
private void StopWorkers()
{
@@ -345,7 +387,7 @@ namespace StandardScene.Signal
{
List<PlcStationSnapshot> snaps;
lock (_workerLock)
snaps = _workers.Select(w => w.Runtime.ToSnapshot()).ToList();
snaps = _workers.SelectMany(w => w.Docks.Select(d => d.ToSnapshot())).ToList();
var st = (SignalInteractMissionStatus)status;
st.Stations = snaps;
@@ -1,41 +1,47 @@
using System.ComponentModel;
using Newtonsoft.Json;
namespace StandardScene.Signal.Model
{
/// <summary>
/// 握手点配置。对应老地图点上的 <c>SignalStop</c>SignalType + SignalParams
/// 握手点。JGControl / PointControl 填机构、工位、动作;定时充电 / 限流走附加参数
/// 旧 JSON 仅有 SignalParams 时仍可解析。
/// </summary>
/// <remarks>
/// 站点 fields 若已填写同名键,扫车优先用站点字段,本表该站点不再生效。
/// SignalParams 按分号切分,各类型格式见 <see cref="StandardScene.Signal.Logic.SignalStopSpec"/>。
/// 列表主键展示为「站点号 + 类型」,同一站点建议只配一条。
/// </remarks>
[SignalTable("handshake", "handshake-points.json", "握手点", Order = 3)]
[Category("握手点数据管理")]
[DisplayName("握手点配置列表")]
public class HandshakePointModel
{
/// <summary>地图站点号,与车 PositionIdsiteID / GetLastSite)对应。</summary>
[DisplayName("站点")]
public int Sit { get; set; }
/// <summary>
/// 握手类型:<c>JGControl</c> 机构进出、
/// <c>PointControl</c> 销钉升降、
/// <c>TimedCharging</c> 定时充电、
/// <c>RateFlowControl</c> 区域限流。
/// 其它字符串会每 10s 打一次「未找到业务模块」日志。
/// </summary>
/// <summary>JGControl / PointControl / TimedCharging / RateFlowControl。</summary>
[DisplayName("信号类型")]
[SignalSelect("JGControl", "PointControl", "TimedCharging", "RateFlowControl")]
public string SignalType { get; set; } = "JGControl";
[DisplayName("机构名称")]
public string JgName { get; set; } = "";
[DisplayName("工位编号")]
public string DockId { get; set; } = "";
/// <summary>JGControlRequestIn / ArrivedPointControlUP1 / UP2 / Down1 / Down2。</summary>
[DisplayName("动作")]
[SignalSelect("RequestIn", "Arrived", "UP1", "UP2", "Down1", "Down2")]
public string Action { get; set; } = "";
/// <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>。
/// TimedCharging:最长秒;上限电量;下限电量。
/// RateFlowControl:区域;车组;限额;点列表。
/// JGControl 可选限流:区域-车组-限额-点列表。
/// </summary>
[DisplayName("信号参数")]
public string SignalParams { get; set; } = "";
[DisplayName("附加参数")]
public string ExtraParams { get; set; } = "";
/// <summary>旧字段。新配置可空,加载时由上面几列拼回。</summary>
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string SignalParams { get; set; }
}
}
@@ -0,0 +1,60 @@
using System.ComponentModel;
namespace StandardScene.Signal.Model
{
/// <summary>
/// 磁条管控区一行。仅 MagCar:车在触发点停稳等待,进程按区名排队后发启动。
/// </summary>
/// <remarks>
/// 主键为 <see cref="TriggerSit"/>。同 <see cref="AreaName"/> 的多行绑成一个路口排队。
/// 区名为空时,该触发点自成一区(兼容旧表)。
/// 放行时查本行 <see cref="ControlArea"/> 与同区冲突对向线站点的并集当前占用;不用 pending。
/// <see cref="ControlReleaseStartStop"/> 保留兼容,MagCar 放行走 FASS 1.0 启动指令,不使用该字节。
/// </remarks>
[SignalTable("mag-control", "mag-control-areas.json", "磁条管控区", Order = 5)]
[Category("磁条交管")]
[DisplayName("管控区配置列表")]
public class MagControlAreaModel
{
/// <summary>管控触发站点。MagCar 到此停稳后进入该区排队。</summary>
[DisplayName("管控触发站点")]
public int TriggerSit { get; set; }
/// <summary>管控区名称。同名多行绑成一个路口;空则本触发点单独成区。</summary>
[DisplayName("管控区名称")]
public string AreaName { get; set; } = "";
/// <summary>
/// 兼容组。同区里互不干扰的线路填不同组名;会打架的填同组。
/// 空则与同区其他空组互斥。站点列表有交集时即使组名不同也互斥。
/// </summary>
[DisplayName("兼容组")]
public string CompatGroup { get; set; } = "";
/// <summary>优先级,数值越大越先放。干道高、支路低。只在冲突线路之间生效。</summary>
[DisplayName("优先级")]
public int Priority { get; set; }
/// <summary>
/// 等待时长(秒)。同区取最长;≤0 不做插队。只在冲突线路之间生效。
/// </summary>
[DisplayName("等待时长")]
public int WaitSeconds { get; set; }
/// <summary>本线路路口内部站点,如 <c>10,11</c>。不要把排队触发点写进来。空则回退触发点。</summary>
[DisplayName("管控区站点列表")]
public string ControlArea { get; set; } = "";
/// <summary>本线路区内最多几辆。含占用本行站点的车和与本行冲突的在途车。默认 1。</summary>
[DisplayName("区内最多几辆")]
public int ControlCapacity { get; set; } = 1;
/// <summary>兼容字段,MagCar 放行不使用。</summary>
[DisplayName("放行后启停字节")]
public byte ControlReleaseStartStop { get; set; } = 11;
/// <summary>false 时该条保留在 JSON 里但不生效。</summary>
[DisplayName("启用")]
public bool IsUse { get; set; } = true;
}
}
@@ -0,0 +1,106 @@
using System.ComponentModel;
using Newtonsoft.Json;
namespace StandardScene.Signal.Model
{
/// <summary>
/// 机构工位:一块 DB 上的一组 BOOL 点 + 地标。同一机构可多行(dock1 / dock2 / …)。
/// </summary>
[SignalTable("docks", "station-docks.json", "机构工位", Order = 2)]
[Category("PLC数据管理")]
[DisplayName("机构工位列表(机构名称+工位编号不能重复)")]
public class PlcStationDockModel
{
/// <summary>外键,对应 <see cref="PlcStationModel.JgName"/>。</summary>
[Category("基本")]
[DisplayName("机构名称")]
public string JgName { get; set; } = "";
/// <summary>机构内唯一。空则按 default。</summary>
[Category("基本")]
[DisplayName("工位编号")]
public string DockId { get; set; } = "dock1";
[Category("PLC 读 BOOL")]
[DisplayName("允许进入字节")]
public int AllowInByte { get; set; }
[Category("PLC 读 BOOL")]
[DisplayName("允许进入位")]
public int AllowInBit { get; set; }
[Category("PLC 读 BOOL")]
[DisplayName("允许离开字节")]
public int AllowOutByte { get; set; }
[Category("PLC 读 BOOL")]
[DisplayName("允许离开位")]
public int AllowOutBit { get; set; } = 1;
[Category("PLC 写 BOOL")]
[DisplayName("进入中字节")]
public int EnteringByte { get; set; } = 2;
[Category("PLC 写 BOOL")]
[DisplayName("进入中位")]
public int EnteringBit { get; set; }
[Category("PLC 写 BOOL")]
[DisplayName("到位字节")]
public int ArrivedByte { get; set; } = 2;
[Category("PLC 写 BOOL")]
[DisplayName("到位位")]
public int ArrivedBit { get; set; } = 1;
[Category("PLC 写 BOOL")]
[DisplayName("离开中字节")]
public int LeavingByte { get; set; } = 2;
[Category("PLC 写 BOOL")]
[DisplayName("离开中位")]
public int LeavingBit { get; set; } = 2;
[Category("PLC 写 BOOL")]
[DisplayName("已离开字节")]
public int LeftByte { get; set; } = 2;
[Category("PLC 写 BOOL")]
[DisplayName("已离开位")]
public int LeftBit { get; set; } = 3;
[Category("地标")]
[DisplayName("进入地标")]
public int JgInPointId { get; set; }
[Category("地标")]
[DisplayName("进入包含地标")]
public string JgInContains { get; set; } = "";
[Category("地标")]
[DisplayName("机构地标")]
public int JgPointId { get; set; }
[Category("地标")]
[DisplayName("机构包含地标")]
public string JgContains { get; set; } = "";
/// <summary>旧配置合成:按一字节枚举读写,不走 BOOL。</summary>
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool UseByteIo { get; set; }
/// <summary>仅 UseByteIo 时使用的读字节地址。</summary>
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ReadDb { get; set; }
/// <summary>仅 UseByteIo 时使用的写字节地址。</summary>
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string WriteDb { get; set; }
public string ResolvedDockId =>
string.IsNullOrWhiteSpace(DockId) ? "default" : DockId.Trim();
}
}
+47 -45
View File
@@ -1,17 +1,17 @@
using System.ComponentModel;
using Newtonsoft.Json;
namespace StandardScene.Signal.Model
{
/// <summary>
/// PLC 机构静态配置,对应反编译 <c>PLCModel</c> 可存档字段
/// 读写运行态(当前字节、机构内车号)在 <see cref="StandardScene.Signal.Plc.PlcStationRuntime"/>,不进本类、不进 JSON。
/// 机构名称不能重复,列表编辑器用 JgName 做主键。
/// PLC 机构:一把 S7 连接 + 一块 DB。点位和地标在 <see cref="PlcStationDockModel"/>
/// </summary>
[SignalTable("stations", "stations.json", "PLC机构", Order = 1)]
[Category("PLC数据管理")]
[DisplayName("PLC配置列表(机构名称不能重复)")]
public class PlcStationModel
{
/// <summary>逻辑名,握手点 SignalParams 第一段、Worker 线程名都用它。不能空、不能重复。</summary>
/// <summary>逻辑名,握手点机构名、Worker 线程名都用它。不能空、不能重复。</summary>
[DisplayName("机构名称")]
public string JgName { get; set; } = "";
@@ -23,53 +23,55 @@ namespace StandardScene.Signal.Model
[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>西门子系列。跟随进程则用信号交互进程的 PlcVersion。</summary>
[DisplayName("PLC类型")]
public SignalPlcType PlcType { get; set; } = SignalPlcType.;
/// <summary>
/// 写字节地址(调度→PLC:进入中/到位/离开中/已离开)。
/// Worker 会先回读再按需写入。空则跳过写和回读。
/// </summary>
[DisplayName("写地址")]
public string WriteDb { get; set; } = "";
/// <summary>DB 块号。工位 BOOL 与心跳都拼到这块 DB。</summary>
[DisplayName("DB块号")]
public int DbNumber { get; set; } = 100;
/// <summary>
/// 心跳位地址,如 <c>DB1.DBX2.0</c>。
/// Worker 每拍读当前值再写反码,让 PLC 判断调度进程存活。空则不做心跳。
/// </summary>
[DisplayName("写心跳地址")]
public string HeartDb { get; set; } = "";
/// <summary>心跳字节偏移。未填 HeartDb 时拼 DB{n}.DBX{HeartByte}.{HeartBit}。</summary>
[DisplayName("心跳字节")]
public int HeartByte { get; set; } = 32;
/// <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>心跳位,07。</summary>
[DisplayName("心跳位")]
public int HeartBit { get; set; }
/// <summary>上线 / 单牵引下线 / 双牵引下线 / 等待通行,决定 AutoLeave 分支。</summary>
[DisplayName("上下机构")]
public SignalLineType LineType { get; set; } = SignalLineType.;
/// <summary>旧配置读字节地址。有工位表后不再使用;无工位表时合成 default 工位。</summary>
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ReadDb { get; set; }
/// <summary>旧配置写字节地址。</summary>
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string WriteDb { get; set; }
/// <summary>旧配置心跳位整段地址,优先于心跳字节/位。</summary>
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string HeartDb { get; set; }
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int JgInPointId { get; set; }
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string JgInContains { get; set; }
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int JgPointId { get; set; }
[Browsable(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string JgContains { get; set; }
}
}
@@ -10,6 +10,7 @@ namespace StandardScene.Signal.Model
/// 同一站点不要同时配握手点:扫车先握手再放行,可能把还在等允许进入的车开走。
/// 站点号不能重复,列表编辑器用 Sit 做主键。
/// </remarks>
[SignalTable("release", "release-points.json", "放行点", Order = 4)]
[Category("放行点数据管理")]
[DisplayName("放行点配置列表")]
public class ReleasePointModel
+28
View File
@@ -38,6 +38,34 @@ namespace StandardScene.Signal.Model
= 4
}
/// <summary>
/// 西门子系列,名称与 IoTClient <c>SiemensVersion</c> 对齐(不含 <c>None</c>)。
/// 机构表选「跟随进程」时用信号交互进程的 <c>PlcVersion</c>。
/// </summary>
public enum SignalPlcType : byte
{
/// <summary>0:未单独指定,建连时用进程字段 PlcVersion。</summary>
= 0,
/// <summary>S7-1500,现场最常见。</summary>
S7_1500 = 1,
/// <summary>S7-1200。</summary>
S7_1200 = 2,
/// <summary>S7-300。</summary>
S7_300 = 3,
/// <summary>S7-400。</summary>
S7_400 = 4,
/// <summary>S7-200 Smart。</summary>
S7_200Smart = 5,
/// <summary>S7-200。</summary>
S7_200 = 6
}
/// <summary>
/// 机构类型。对齐反编译 <c>ENUM_JGTYPE</c>。
/// 决定 <see cref="StandardScene.Signal.Plc.PlcStationLogic.AutoLeave"/> 走哪套离站,以及 RequestIn 要不要查机构内是否有车。
@@ -0,0 +1,18 @@
using System;
namespace StandardScene.Signal.Model
{
/// <summary>
/// 迷毂数据中心把该字符串字段渲染成下拉。反射列定义时读取。
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class SignalSelectAttribute : Attribute
{
public SignalSelectAttribute(params string[] options)
{
Options = options ?? Array.Empty<string>();
}
public string[] Options { get; }
}
}
@@ -0,0 +1,27 @@
using System;
namespace StandardScene.Signal.Model
{
/// <summary>
/// 标记一张迷毂数据中心表。迷毂从插件 DLL 反射本特性,不再依赖 signal-tables.json 的列定义。
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class SignalTableAttribute : Attribute
{
public SignalTableAttribute(string id, string fileName, string title)
{
Id = id ?? "";
FileName = fileName ?? "";
Title = title ?? "";
}
public string Id { get; }
public string FileName { get; }
public string Title { get; }
/// <summary>侧栏顺序,越小越靠前。</summary>
public int Order { get; set; }
}
}
+63
View File
@@ -0,0 +1,63 @@
using System;
using System.Text.RegularExpressions;
namespace StandardScene.Signal.Plc
{
/// <summary>西门子 DB 字节/位地址拼装与缓冲区位操作。</summary>
internal static class PlcAddress
{
private static readonly Regex Addr = new Regex(
@"^DB(\d+)\.(?:DBB|DBX|DBW)?(\d+)(?:\.(\d+))?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static string Bit(int db, int absByte, int bit) =>
$"DB{Math.Max(1, db)}.DBX{Math.Max(0, absByte)}.{ClampBit(bit)}";
public static string Byte(int db, int absByte) =>
$"DB{Math.Max(1, db)}.DBB{Math.Max(0, absByte)}";
public static int ClampBit(int bit)
{
if (bit < 0) return 0;
if (bit > 7) return 7;
return bit;
}
public static bool TryParse(string address, out int db, out int absByte, out int bit)
{
db = 0;
absByte = 0;
bit = 0;
if (string.IsNullOrWhiteSpace(address))
return false;
var m = Addr.Match(address.Trim());
if (!m.Success)
return false;
db = int.Parse(m.Groups[1].Value);
absByte = int.Parse(m.Groups[2].Value);
if (m.Groups[3].Success)
bit = ClampBit(int.Parse(m.Groups[3].Value));
return true;
}
public static bool GetBit(byte[] buffer, int originByte, int absByte, int bit)
{
var i = absByte - originByte;
if (buffer == null || i < 0 || i >= buffer.Length)
return false;
return (buffer[i] & (1 << ClampBit(bit))) != 0;
}
public static void SetBit(byte[] buffer, int originByte, int absByte, int bit, bool value)
{
var i = absByte - originByte;
if (buffer == null || i < 0 || i >= buffer.Length)
return;
var mask = (byte)(1 << ClampBit(bit));
if (value)
buffer[i] |= mask;
else
buffer[i] = (byte)(buffer[i] & ~mask);
}
}
}
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StandardScene.Signal.Model;
namespace StandardScene.Signal.Plc
{
/// <summary>
/// 加载机构 + 工位。没有工位表时,用旧机构行的 ReadDb/WriteDb/地标合成 default 工位(一字节枚举)。
/// </summary>
internal static class PlcConfigLoader
{
public const string DefaultDockId = "default";
public sealed class StationBundle
{
public PlcStationModel Station { get; init; }
public List<PlcStationDockModel> Docks { get; init; }
}
public static List<StationBundle> Load(string stationsPath, string docksPath)
{
var stations = SignalConfigStore.Load<PlcStationModel>(stationsPath)
.Where(s => s != null && !string.IsNullOrWhiteSpace(s.JgName))
.ToList();
var docks = SignalConfigStore.Load<PlcStationDockModel>(docksPath)
.Where(d => d != null && !string.IsNullOrWhiteSpace(d.JgName))
.ToList();
var byName = docks
.GroupBy(d => d.JgName.Trim(), StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
var list = new List<StationBundle>();
foreach (var station in stations)
{
if (!byName.TryGetValue(station.JgName.Trim(), out var rows) || rows.Count == 0)
rows = HasLegacyIo(station) ? new List<PlcStationDockModel> { Synthesize(station) } : new List<PlcStationDockModel>();
list.Add(new StationBundle { Station = station, Docks = rows });
}
return list;
}
public static int ResolveDb(PlcStationModel station)
{
if (station == null)
return 100;
if (station.DbNumber > 0)
return station.DbNumber;
if (PlcAddress.TryParse(station.HeartDb, out var db, out _, out _))
return db;
if (PlcAddress.TryParse(station.ReadDb, out db, out _, out _))
return db;
if (PlcAddress.TryParse(station.WriteDb, out db, out _, out _))
return db;
return 100;
}
public static string ResolveHeartAddress(PlcStationModel station)
{
if (station == null)
return "";
if (!string.IsNullOrWhiteSpace(station.HeartDb))
return station.HeartDb.Trim();
return PlcAddress.Bit(ResolveDb(station), station.HeartByte, station.HeartBit);
}
private static bool HasLegacyIo(PlcStationModel station) =>
station != null &&
(!string.IsNullOrWhiteSpace(station.ReadDb) ||
!string.IsNullOrWhiteSpace(station.WriteDb) ||
station.JgPointId > 0 ||
!string.IsNullOrWhiteSpace(station.JgContains));
private static PlcStationDockModel Synthesize(PlcStationModel station) => new PlcStationDockModel
{
JgName = station.JgName,
DockId = DefaultDockId,
UseByteIo = true,
ReadDb = station.ReadDb,
WriteDb = station.WriteDb,
JgInPointId = station.JgInPointId,
JgInContains = station.JgInContains,
JgPointId = station.JgPointId,
JgContains = station.JgContains
};
}
}
+15 -15
View File
@@ -48,8 +48,8 @@ namespace StandardScene.Signal.Plc
/// <summary>按 <see cref="PlcStationModel.LineType"/> 分发四套离站逻辑;未知类型不处理。</summary>
public static void AutoLeave(PlcStationRuntime rt)
{
if (rt?.Config == null) return;
switch (rt.Config.LineType)
if (rt?.Station == null || rt.Dock == null) return;
switch (rt.Station.LineType)
{
case SignalLineType.线:
AutoLeaveUpLine(rt);
@@ -77,12 +77,12 @@ namespace StandardScene.Signal.Plc
if (rt.RequestInAgvNo > 0)
{
Diagnosis.Log(
$"{rt.Config.JgName} {(fromReadback ? "" : "")},清空请求进入车 {rt.RequestInAgvNo}",
$"{rt.DisplayName} {(fromReadback ? "" : "")},清空请求进入车 {rt.RequestInAgvNo}",
"JGControl", true);
rt.RequestInAgvNo = 0;
}
else
Diagnosis.Log($"{rt.Config.JgName} 已离开,清空信号", "JGControl", true);
Diagnosis.Log($"{rt.DisplayName} 已离开,清空信号", "JGControl", true);
}
/// <summary>
@@ -102,12 +102,12 @@ namespace StandardScene.Signal.Plc
{
var car = SignalCarAdapter.Find(rt.RequestInAgvNo);
if (car == null) return;
if (!ContainsSite(rt.Config.JgInContains, SignalCarAdapter.PositionId(car)))
if (!ContainsSite(rt.Dock.JgInContains, SignalCarAdapter.PositionId(car)))
{
rt.WriteSignal = SignalWriteValue.;
rt.InAgvNo = car.id;
rt.RequestInAgvNo = 0;
Diagnosis.Log($"AGV{car.id} 已到达机构 {rt.Config.JgName}", "JGControl", true);
Diagnosis.Log($"AGV{car.id} 已到达机构 {rt.DisplayName}", "JGControl", true);
}
}
else if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.)
@@ -119,7 +119,7 @@ namespace StandardScene.Signal.Plc
else if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.)
{
var car = SignalCarAdapter.Find(rt.InAgvNo);
if (car == null || !ContainsSite(rt.Config.JgContains, SignalCarAdapter.PositionId(car)))
if (car == null || !ContainsSite(rt.Dock.JgContains, SignalCarAdapter.PositionId(car)))
rt.WriteSignal = SignalWriteValue.;
}
}
@@ -145,7 +145,7 @@ namespace StandardScene.Signal.Plc
if (car == null) return;
var pos = SignalCarAdapter.PositionId(car);
if (rt.ReadSignal == SignalReadValue. && pos == rt.Config.JgPointId)
if (rt.ReadSignal == SignalReadValue. && pos == rt.Dock.JgPointId)
{
if (rt.WriteSignal == SignalWriteValue.) return;
var pinOk = SignalCarAdapter.IsActionFinish(car, "SetPin1Up")
@@ -156,7 +156,7 @@ namespace StandardScene.Signal.Plc
SignalCarAdapter.StopCharge(car);
SignalCarAdapter.Start(car);
rt.WriteSignal = SignalWriteValue.;
Diagnosis.Log($"{rt.Config.JgName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
Diagnosis.Log($"{rt.DisplayName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
}
else
{
@@ -165,8 +165,8 @@ namespace StandardScene.Signal.Plc
SignalCarAdapter.Control(car, "StopCharge");
}
}
else if (!string.IsNullOrEmpty(rt.Config.JgContains)
&& !ContainsSite(rt.Config.JgContains, pos)
else if (!string.IsNullOrEmpty(rt.Dock.JgContains)
&& !ContainsSite(rt.Dock.JgContains, pos)
&& rt.ReadWriteSignal != SignalWriteValue.)
{
rt.WriteSignal = SignalWriteValue.;
@@ -193,20 +193,20 @@ namespace StandardScene.Signal.Plc
if (car == null) return;
var pos = SignalCarAdapter.PositionId(car);
if (rt.ReadSignal == SignalReadValue. && pos == rt.Config.JgPointId)
if (rt.ReadSignal == SignalReadValue. && pos == rt.Dock.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);
Diagnosis.Log($"{rt.DisplayName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
}
else
SignalCarAdapter.Control(car, "StopCharge");
}
else if (!string.IsNullOrEmpty(rt.Config.JgContains)
&& !ContainsSite(rt.Config.JgContains, pos)
else if (!string.IsNullOrEmpty(rt.Dock.JgContains)
&& !ContainsSite(rt.Dock.JgContains, pos)
&& rt.ReadWriteSignal != SignalWriteValue.)
{
rt.WriteSignal = SignalWriteValue.;
+25 -28
View File
@@ -5,91 +5,88 @@ using StandardScene.Signal.Model;
namespace StandardScene.Signal.Plc
{
/// <summary>
/// 单个机构的运行态(不进 JSON)。扫车线程改写目标写字节PLC Worker 负责真正下发。
/// 单个工位运行态(不进 JSON)。扫车改写目标写状态PLC Worker 负责下发 BOOL(或旧一字节枚举)
/// </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)
public PlcStationRuntime(PlcStationModel station, PlcStationDockModel dock, object sync = null)
{
Config = config ?? throw new ArgumentNullException(nameof(config));
Station = station ?? throw new ArgumentNullException(nameof(station));
Dock = dock ?? throw new ArgumentNullException(nameof(dock));
Sync = sync ?? new object();
}
/// <summary>本机构静态配置(IP、DB 地址、地标、机构类型)。</summary>
public PlcStationModel Config { get; }
public PlcStationModel Station { get; }
/// <summary>扫车线程与 PLC Worker 共用运行态,写入时加此锁。</summary>
public object Sync { get; } = new object();
public PlcStationDockModel Dock { get; }
/// <summary>兼容旧代码:机构连接配置(不含工位地标)。</summary>
public PlcStationModel Config => Station;
public object Sync { get; }
public string JgName => Station.JgName ?? "";
public string DockId => Dock.ResolvedDockId;
public string DisplayName =>
string.IsNullOrEmpty(DockId) || string.Equals(DockId, PlcConfigLoader.DefaultDockId, StringComparison.OrdinalIgnoreCase)
? JgName
: $"{JgName}/{DockId}";
/// <summary>PLC→调度:允许进入 / 允许离开 / 无状态。变化时打诊断。</summary>
public SignalReadValue ReadSignal
{
get => _readSignal;
set
{
if (_readSignal != value)
Diagnosis.Log($"{Config.JgName} 读信号 {_readSignal} → {value}", "PLC", true);
Diagnosis.Log($"{DisplayName} 读信号 {_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);
Diagnosis.Log($"{DisplayName} 写信号 {_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);
Diagnosis.Log($"{DisplayName} 回读写值 {_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,
JgName = DisplayName,
IsConnected = IsConnected,
ReadSignal = ReadSignal.ToString(),
WriteSignal = WriteSignal.ToString(),
+199 -64
View File
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using SimpleCore.Library;
using StandardScene.Signal.Model;
@@ -6,41 +8,43 @@ using StandardScene.Signal.Model;
namespace StandardScene.Signal.Plc
{
/// <summary>
/// 单机构 200ms 循环:心跳、读允许进出、回读写值、按需写入、自动离站。
/// 对齐反编译 <c>PLCManager.ExcuteThreadMethod</c>。
/// 单机构 200ms 循环:心跳、按工位读 BOOL(或旧一字节)、回写变化位、自动离站。
/// </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 readonly PlcStationModel _station;
private readonly List<PlcStationRuntime> _docks;
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)
public PlcStationWorker(
PlcStationModel station,
IEnumerable<PlcStationRuntime> docks,
SiemensPlcSession session,
object sync,
int pollMs,
Action onTick = null)
{
Runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_station = station ?? throw new ArgumentNullException(nameof(station));
_docks = (docks ?? Array.Empty<PlcStationRuntime>()).Where(d => d != null).ToList();
_session = session ?? throw new ArgumentNullException(nameof(session));
Sync = sync ?? new object();
_pollMs = Math.Max(50, pollMs);
_onTick = onTick;
foreach (var d in _docks)
d.Heart = false;
}
/// <summary>本机构运行态,扫车线程按 JgName 查找后加 Sync 读写。</summary>
public PlcStationRuntime Runtime { get; }
public object Sync { get; }
public IReadOnlyList<PlcStationRuntime> Docks => _docks;
public PlcStationRuntime Runtime => _docks.Count > 0 ? _docks[0] : null;
/// <summary>启动后台线程。重复 Start 若仍存活则忽略。</summary>
public void Start()
{
if (_thread != null && _thread.IsAlive) return;
@@ -48,13 +52,12 @@ namespace StandardScene.Signal.Plc
_thread = new Thread(Loop)
{
IsBackground = true,
Name = $"PlcStation:{Runtime.Config.JgName}"
Name = $"PlcStation:{_station.JgName}"
};
_thread.Start();
Diagnosis.Log($"{Runtime.Config.JgName} Worker 已启动 → {Runtime.Config.Ip}:{Runtime.Config.Port}", "PLC", true);
Diagnosis.Log($"{_station.JgName} Worker 已启动 → {_station.Ip}:{_station.Port} 工位{_docks.Count}", "PLC", true);
}
/// <summary>取消循环、最多等 2s、关闭 S7 连接并标断线。</summary>
public void Stop()
{
try { _cts?.Cancel(); }
@@ -63,8 +66,9 @@ namespace StandardScene.Signal.Plc
catch { }
_thread = null;
_session.Close();
Runtime.IsConnected = false;
Diagnosis.Log($"{Runtime.Config.JgName} Worker 已停止", "PLC", true);
foreach (var d in _docks)
d.IsConnected = false;
Diagnosis.Log($"{_station.JgName} Worker 已停止", "PLC", true);
}
public void Dispose() => Stop();
@@ -76,14 +80,17 @@ namespace StandardScene.Signal.Plc
{
try
{
lock (Runtime.Sync)
lock (Sync)
Tick();
}
catch (Exception ex)
{
Runtime.LastError = ex.Message;
Runtime.IsConnected = false;
LogFail($"{Runtime.Config.JgName} 周期异常 {ex.Message}");
foreach (var d in _docks)
{
d.LastError = ex.Message;
d.IsConnected = false;
}
LogFail($"{_station.JgName} 周期异常 {ex.Message}");
}
try { _onTick?.Invoke(); }
@@ -94,17 +101,18 @@ namespace StandardScene.Signal.Plc
}
}
/// <summary>单拍 PLC 交互。心跳地址为空则跳过心跳;读写地址为空则跳过对应步骤。</summary>
internal void Tick()
{
var cfg = Runtime.Config;
Runtime.Heart = !Runtime.Heart;
var heart = !_docks.Select(d => d.Heart).FirstOrDefault();
foreach (var d in _docks)
d.Heart = heart;
if (!string.IsNullOrWhiteSpace(cfg.HeartDb))
var heartAddr = PlcConfigLoader.ResolveHeartAddress(_station);
if (!string.IsNullOrWhiteSpace(heartAddr))
{
if (_session.TryReadBool(cfg.HeartDb, out var bit))
if (_session.TryReadBool(heartAddr, out var bit))
{
_session.TryWriteBool(cfg.HeartDb, !bit);
_session.TryWriteBool(heartAddr, !bit);
Thread.Sleep(20);
}
else
@@ -114,56 +122,183 @@ namespace StandardScene.Signal.Plc
}
}
var boolDocks = _docks.Where(d => !d.Dock.UseByteIo).ToList();
var byteDocks = _docks.Where(d => d.Dock.UseByteIo).ToList();
if (boolDocks.Count > 0 && !TickBoolDocks(boolDocks))
return;
foreach (var rt in byteDocks)
{
if (!TickByteDock(rt))
return;
}
foreach (var rt in _docks)
{
rt.IsConnected = true;
rt.LastError = "";
PlcStationLogic.AutoLeave(rt);
}
}
private bool TickBoolDocks(List<PlcStationRuntime> docks)
{
var db = PlcConfigLoader.ResolveDb(_station);
var bytes = new List<int>();
foreach (var rt in docks)
CollectBytes(rt.Dock, bytes);
if (bytes.Count == 0)
return true;
var origin = bytes.Min();
var last = bytes.Max();
var len = (ushort)(last - origin + 1);
if (!_session.TryReadBytes(PlcAddress.Byte(db, origin), len, out var buffer) ||
buffer == null || buffer.Length < len)
{
MarkDisconnected("BOOL 块读失败");
return false;
}
var changedBytes = new HashSet<int>();
foreach (var rt in docks)
{
var dock = rt.Dock;
var allowIn = PlcAddress.GetBit(buffer, origin, dock.AllowInByte, dock.AllowInBit);
var allowOut = PlcAddress.GetBit(buffer, origin, dock.AllowOutByte, dock.AllowOutBit);
rt.ReadSignal = allowIn
? SignalReadValue.
: allowOut
? SignalReadValue.
: SignalReadValue.;
var readWrite = DecodeWrite(buffer, origin, dock);
rt.ReadWriteSignal = readWrite;
if (readWrite == rt.WriteSignal)
rt.WriteSuccess = true;
if (rt.WriteSignal != readWrite && !rt.WriteSuccess)
{
Diagnosis.Log(
$"AGV→机构 {rt.DisplayName} 写入【{rt.WriteSignal}】请求进入{rt.RequestInAgvNo} 到达{rt.InAgvNo}",
"JGControl", true);
EncodeWrite(buffer, origin, dock, rt.WriteSignal, changedBytes);
}
}
foreach (var abs in changedBytes)
{
var i = abs - origin;
if (i < 0 || i >= buffer.Length)
continue;
if (!_session.TryWriteByte(PlcAddress.Byte(db, abs), buffer[i]))
{
MarkDisconnected("BOOL 写失败");
return false;
}
}
return true;
}
private bool TickByteDock(PlcStationRuntime rt)
{
var cfg = rt.Dock;
if (!string.IsNullOrWhiteSpace(cfg.ReadDb))
{
if (_session.TryReadByte(cfg.ReadDb, out var rb))
Runtime.ReadSignal = PlcStationLogic.ConvertRead(rb);
rt.ReadSignal = PlcStationLogic.ConvertRead(rb);
else
{
MarkDisconnected("读地址失败");
return;
MarkDisconnected($"{rt.DisplayName} 读地址失败");
return false;
}
}
if (!string.IsNullOrWhiteSpace(cfg.WriteDb))
if (string.IsNullOrWhiteSpace(cfg.WriteDb))
return true;
if (!_session.TryReadByte(cfg.WriteDb, out var wb))
{
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);
}
MarkDisconnected($"{rt.DisplayName} 写地址回读失败");
return false;
}
Runtime.IsConnected = true;
Runtime.LastError = "";
PlcStationLogic.AutoLeave(Runtime);
rt.ReadWriteSignal = PlcStationLogic.ConvertWrite(wb);
if (rt.ReadWriteSignal == rt.WriteSignal)
rt.WriteSuccess = true;
if (rt.WriteSignal != rt.ReadWriteSignal && !rt.WriteSuccess)
{
Diagnosis.Log(
$"AGV→机构 {rt.DisplayName} 写入【{rt.WriteSignal}】请求进入{rt.RequestInAgvNo} 到达{rt.InAgvNo}",
"JGControl", true);
_session.TryWriteByte(cfg.WriteDb, (byte)rt.WriteSignal);
}
return true;
}
private static void CollectBytes(PlcStationDockModel dock, List<int> bytes)
{
if (dock == null) return;
bytes.Add(dock.AllowInByte);
bytes.Add(dock.AllowOutByte);
bytes.Add(dock.EnteringByte);
bytes.Add(dock.ArrivedByte);
bytes.Add(dock.LeavingByte);
bytes.Add(dock.LeftByte);
}
private static SignalWriteValue DecodeWrite(byte[] buffer, int origin, PlcStationDockModel dock)
{
if (PlcAddress.GetBit(buffer, origin, dock.LeftByte, dock.LeftBit))
return SignalWriteValue.;
if (PlcAddress.GetBit(buffer, origin, dock.LeavingByte, dock.LeavingBit))
return SignalWriteValue.;
if (PlcAddress.GetBit(buffer, origin, dock.ArrivedByte, dock.ArrivedBit))
return SignalWriteValue.;
if (PlcAddress.GetBit(buffer, origin, dock.EnteringByte, dock.EnteringBit))
return SignalWriteValue.;
return SignalWriteValue.;
}
private static void EncodeWrite(
byte[] buffer,
int origin,
PlcStationDockModel dock,
SignalWriteValue value,
HashSet<int> changedBytes)
{
void Set(int abs, int bit, bool on)
{
var before = PlcAddress.GetBit(buffer, origin, abs, bit);
if (before == on) return;
PlcAddress.SetBit(buffer, origin, abs, bit, on);
changedBytes.Add(abs);
}
Set(dock.EnteringByte, dock.EnteringBit, value == SignalWriteValue.);
Set(dock.ArrivedByte, dock.ArrivedBit, value == SignalWriteValue.);
Set(dock.LeavingByte, dock.LeavingBit, value == SignalWriteValue.);
Set(dock.LeftByte, dock.LeftBit, value == SignalWriteValue.);
}
private void MarkDisconnected(string reason)
{
Runtime.IsConnected = false;
Runtime.LastError = reason;
LogFail($"{Runtime.Config.JgName} {reason}");
foreach (var d in _docks)
{
d.IsConnected = false;
d.LastError = reason;
}
LogFail($"{_station.JgName} {reason}");
}
/// <summary>同一机构失败日志最少间隔 5 秒。</summary>
private void LogFail(string msg)
{
if ((DateTime.Now - _lastFailLog).TotalSeconds < 5) return;
if ((DateTime.Now - _lastFailLog).TotalSeconds < 5)
return;
_lastFailLog = DateTime.Now;
Diagnosis.Log(msg, "PLC", true);
}
+21 -1
View File
@@ -25,7 +25,7 @@ namespace StandardScene.Signal.Plc
private SiemensClient _client;
private readonly object _sync = new object();
/// <param name="version">进程字段 PlcVersion,如 S7_1500。</param>
/// <param name="version">西门子系列名,如 S7_1500;空或无法解析时默认 S7_1500。</param>
/// <param name="ip">机构 IP。</param>
/// <param name="port">S7 端口,通常 102。</param>
/// <param name="slot">机架槽号,S7-1500 常见 1。</param>
@@ -103,6 +103,26 @@ namespace StandardScene.Signal.Plc
}
}
/// <summary>从起始字节连续读 length 字节,用于工位 BOOL 拆位。</summary>
public bool TryReadBytes(string startAddress, ushort length, out byte[] data)
{
data = Array.Empty<byte>();
if (string.IsNullOrWhiteSpace(startAddress) || length == 0 || !EnsureOpen())
return false;
try
{
var r = _client.Read(NormalizeByteAddress(startAddress), length, false);
if (r == null || !r.IsSucceed || r.Value == null)
return false;
data = r.Value;
return true;
}
catch
{
return false;
}
}
/// <summary>读心跳位。地址不做 DBB 转换。</summary>
public bool TryReadBool(string address, out bool value)
{
@@ -224,6 +224,13 @@ namespace StandardScene.Signal.Plc
return 0;
}
/// <summary>类型名恰好为 MagCar(不含 Mag2Car)。磁条交管只放行这种车。</summary>
public static bool IsMagCar(Car car)
{
if (car == null) return false;
return string.Equals(car.GetType().Name, "MagCar", StringComparison.Ordinal);
}
/// <summary>放行:Mag2 → Mag → SendStart(0)。</summary>
public static void Start(Car car)
{
@@ -233,6 +240,15 @@ namespace StandardScene.Signal.Plc
TryInvoke(car, "SendStart", new object[] { (ushort)0 });
}
/// <summary>仅 MagCar 发 FASS 1.0 启动(0x01)。不调 StartMag2Car。</summary>
public static void StartMagCar(Car car)
{
if (!IsMagCar(car)) return;
if (TryInvoke(car, "StartMagCar"))
return;
TryInvoke(car, "SendStart", new object[] { (ushort)0 });
}
/// <summary>急停:Mag2 → Mag → SendStop(0)。机构未允许进入时调用。</summary>
public static void Stop(Car car)
{
+1 -1
View File
@@ -7,7 +7,7 @@ using Newtonsoft.Json.Converters;
namespace StandardScene.Signal
{
/// <summary>
/// 信号插件三张 JSON 的路径解析与读写。
/// 信号插件 JSON 的路径解析与读写。
/// </summary>
/// <remarks>
/// 只使用 SimpleLite 工作目录 <c>Config/Signal/*.json</c>,不读、不拷 <c>plugins/Config/Signal</c>。
@@ -26,7 +26,8 @@
<ItemGroup>
<None Update="StandardScene.Signal.scene.json" CopyToOutputDirectory="PreserveNewest" />
<!-- 样例 JSON 只留源码树,不复制到输出/plugins;运行时读写工作目录 Config/Signal -->
<None Update="Config\Signal\signal-tables.json" CopyToOutputDirectory="PreserveNewest" />
<!-- 业务 JSON 样例只留源码树,不复制到输出/plugins;运行时读写工作目录 Config/Signal -->
<None Update="Config\Signal\*.json" CopyToOutputDirectory="Never" />
</ItemGroup>
@@ -5,6 +5,6 @@
"coreVersion": ">=1.0.0",
"requiresCore": "StandardScene.dll",
"provides": {
"missionTypes": [ "SignalInteractMission" ]
"missionTypes": [ "SignalInteractMission", "MagTrafficMission" ]
}
}