2.0协议完善加简单交管配置
This commit is contained in:
+550
-124
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using SimpleCore;
|
||||
using SimpleCore.Library;
|
||||
using SimpleLite;
|
||||
using SimpleLite.Props;
|
||||
using SimpleLite.RCS;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.CarTypes;
|
||||
using StandardScene.Chained;
|
||||
using StandardScene.Magnetic.Tasking;
|
||||
using StandardScene.Model;
|
||||
|
||||
namespace StandardScene
|
||||
{
|
||||
/// <summary>
|
||||
/// 磁导航 FASS 2.0 环线任务进程。
|
||||
/// <para>任务分配/触发/流量逻辑继承 Core <see cref="LoopMission"/>。</para>
|
||||
/// <para>导航执行由 <see cref="Mag2Car"/> 拦截 Loop 编译脚本(含 <c>Mag2Go</c> + <c>goalSite</c>),
|
||||
/// 改为单次全程 <c>0xB1</c> 状态机任务。</para>
|
||||
/// </summary>
|
||||
[MissionType(Name = "磁导航FASS2环线", editor = typeof(Mag2LoopMission))]
|
||||
public class Mag2LoopMission : LoopMission
|
||||
{
|
||||
[JsonIgnore] public override MissionStatus status { get; set; } = new LoopMissionStatus();
|
||||
|
||||
/// <summary>
|
||||
/// Mag2 空闲时可能尚未 TrafficReset(holdingLocks 为空),仍按上报站点/siteID 匹配在站车辆,
|
||||
/// 否则 AutoLoop 永远找不到车、加不上 goalSite。
|
||||
/// </summary>
|
||||
protected override Car FindCarArrivedAtSite(int siteId)
|
||||
{
|
||||
var byLock = base.FindCarArrivedAtSite(siteId);
|
||||
if (byLock != null)
|
||||
return byLock;
|
||||
|
||||
try
|
||||
{
|
||||
return SimpleLib.GetAllCars()
|
||||
.OfType<Mag2Car>()
|
||||
.FirstOrDefault(car =>
|
||||
{
|
||||
if (car?.tags == null || car.tags.Contains("occupied") || !car.tags.Contains("Online"))
|
||||
return false;
|
||||
if (car.status?.pendingLocks != null && car.status.pendingLocks.Length > 0)
|
||||
return false;
|
||||
if (car.status?.holdingLocks != null && car.status.holdingLocks.Length > 1)
|
||||
return false;
|
||||
|
||||
var physical = car.siteID > 0 ? car.siteID : car.GetLastSite();
|
||||
return physical == siteId;
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分配前尽量补上本站交管锁,保证 SelectCar/GoSite 的 GetLastSite 可用。
|
||||
/// </summary>
|
||||
protected override void AssignCarToTarget(Car car, int targetSiteId)
|
||||
{
|
||||
if (car is Mag2Car mag2)
|
||||
{
|
||||
EnsureMag2HoldingCurrentSite(mag2);
|
||||
}
|
||||
|
||||
base.AssignCarToTarget(car, targetSiteId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用当前占点匹配全部 LoopTask 展开路径(不过滤 IsViaPoint)。
|
||||
/// 路径用 BFS,避免 FindRoute 污染交管锁。
|
||||
/// </summary>
|
||||
public static List<Fass2ChainMatch> FindChainsContaining(int siteId)
|
||||
{
|
||||
var matches = new List<Fass2ChainMatch>();
|
||||
if (siteId <= 0)
|
||||
{
|
||||
return matches;
|
||||
}
|
||||
|
||||
IEnumerable<AbstractLoopMission> missions = null;
|
||||
try
|
||||
{
|
||||
missions = SimpleProject.proj?.Missions?.OfType<AbstractLoopMission>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
missions = null;
|
||||
}
|
||||
|
||||
if (missions == null)
|
||||
{
|
||||
return matches;
|
||||
}
|
||||
|
||||
foreach (var mission in missions)
|
||||
{
|
||||
IReadOnlyList<LoopTask> tasks;
|
||||
try
|
||||
{
|
||||
tasks = mission.GetTasks();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tasks == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
var match = TryMatchTask(task, siteId);
|
||||
if (match != null)
|
||||
{
|
||||
matches.Add(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
public static bool TryResolveBelongingChain(
|
||||
int siteId,
|
||||
int? preferGoalSiteId,
|
||||
out Fass2ChainMatch selected,
|
||||
out string reason)
|
||||
{
|
||||
var matches = FindChainsContaining(siteId);
|
||||
return Fass2ReconnectChainSelector.TrySelect(matches, preferGoalSiteId, out selected, out reason);
|
||||
}
|
||||
|
||||
private static Fass2ChainMatch TryMatchTask(LoopTask task, int siteId)
|
||||
{
|
||||
if (task == null || task.CurrentStationId <= 0 || task.TargetStationId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<int> fullPath;
|
||||
try
|
||||
{
|
||||
fullPath = Fass2RouteHelper.GetSitesBetween(task.CurrentStationId, task.TargetStationId, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fullPath == null || fullPath.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var index = fullPath.IndexOf(siteId);
|
||||
if (index < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var remaining = fullPath.Skip(index).ToList();
|
||||
return new Fass2ChainMatch
|
||||
{
|
||||
TaskId = task.Id,
|
||||
TaskPriority = task.Priority,
|
||||
CurrentSiteId = siteId,
|
||||
TargetSiteId = task.TargetStationId,
|
||||
IndexInPath = index,
|
||||
DistanceToTarget = Math.Max(0, fullPath.Count - index - 1),
|
||||
IsAtStartPoint = index == 0,
|
||||
IsAtEndPoint = index == fullPath.Count - 1,
|
||||
FullPath = fullPath,
|
||||
RemainingPath = remaining
|
||||
};
|
||||
}
|
||||
|
||||
private static void EnsureMag2HoldingCurrentSite(Mag2Car car)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (car.status?.holdingLocks != null && car.status.holdingLocks.Length == 1)
|
||||
return;
|
||||
|
||||
var siteId = car.siteID > 0 ? car.siteID : car.GetLastSite();
|
||||
if (siteId <= 0)
|
||||
return;
|
||||
|
||||
var site = SimpleLib.GetSite(siteId);
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
car.TrafficReset(site, true, strict: false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 交管失败时仍允许打上 goalSite,后续 GoSite/状态机会再处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using SimpleLite.Props;
|
||||
using SimpleLite.RCS;
|
||||
using StandardScene.Chained;
|
||||
|
||||
namespace StandardScene
|
||||
{
|
||||
/// <summary>
|
||||
/// 磁导航 FASS 2.0 环线任务进程。
|
||||
/// <para>任务分配/触发/流量逻辑完全继承 Core <see cref="LoopMission"/>,不修改 Core。</para>
|
||||
/// <para>导航执行由 <see cref="MagFass2Car"/> 拦截 Loop 编译脚本(含 <c>MagFass2Go</c> + <c>goalSite</c>),
|
||||
/// 改为单次全程 <c>0xB1</c> 状态机任务。</para>
|
||||
/// <para>场景内可继续使用原 <see cref="LoopMission"/>;本类型仅作磁导航 FASS2 场景标识。</para>
|
||||
/// </summary>
|
||||
[MissionType(Name = "磁导航FASS2环线", editor = typeof(MagFass2LoopMission))]
|
||||
public class MagFass2LoopMission : LoopMission
|
||||
{
|
||||
[JsonIgnore] public override MissionStatus status { get; set; } = new LoopMissionStatus();
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ namespace StandardScene.Magnetic
|
||||
{
|
||||
/// <summary>
|
||||
/// scene.mag 平台画像:磁导航场景插件。
|
||||
/// <para>车型:<see cref="MagCar"/>(FASS 1.0 TCP)、<see cref="MagFass2Car"/>(FASS 2.0 UDP/任务状态机)。
|
||||
/// Loop 导航:继续使用 Core <c>LoopMission</c> 或本插件 <c>MagFass2LoopMission</c> 分配 <c>goalSite</c>,
|
||||
/// <see cref="MagFass2Car"/> 拦截编译脚本后一次下发全程 <c>0xB1</c>。</para>
|
||||
/// <para>车型:<see cref="MagCar"/>(FASS 1.0 TCP)、<see cref="Mag2Car"/>(FASS 2.0 UDP/任务状态机)。
|
||||
/// Loop 导航:继续使用 Core <c>LoopMission</c> 或本插件 <c>Mag2LoopMission</c> 分配 <c>goalSite</c>,
|
||||
/// <see cref="Mag2Car"/> 拦截编译脚本后一次下发全程 <c>0xB1</c>。</para>
|
||||
/// 宿主(SimpleLite)加载本 dll 后反射实例化并 OnActivate / 注册。
|
||||
/// </summary>
|
||||
public sealed class MagneticSceneProfile : NavigationProfileBase
|
||||
@@ -23,12 +23,12 @@ namespace StandardScene.Magnetic
|
||||
public override IReadOnlyList<Type> CarTypes => new[]
|
||||
{
|
||||
typeof(MagCar),
|
||||
typeof(MagFass2Car),
|
||||
typeof(Mag2Car),
|
||||
};
|
||||
|
||||
public override void OnActivate(ISceneContext context)
|
||||
{
|
||||
context.Log($"{DisplayName} 已激活(车型:MagCar / MagFass2Car)");
|
||||
context.Log($"{DisplayName} 已激活(车型:MagCar / Mag2Car)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StandardScene.Magnetic.Tests")]
|
||||
@@ -9,7 +9,7 @@ namespace StandardScene.Magnetic.Protocol
|
||||
/// </summary>
|
||||
public static class Fass2StateCodec
|
||||
{
|
||||
public static byte[] BuildState(Fass2StateReport report, string carType = "MagFass2")
|
||||
public static byte[] BuildState(Fass2StateReport report, string carType = "Mag2")
|
||||
{
|
||||
if (report == null)
|
||||
{
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace StandardScene.Magnetic.Protocol
|
||||
}
|
||||
|
||||
Diagnosis.Post(
|
||||
$"Fass2UdpHub 收到 Car={carCode} 的状态,但未注册该编号(已注册: {registered})。请检查 MagFass2Car.VehicleCode 与模拟器车号是否一致,并重新启动场景。",
|
||||
$"Fass2UdpHub 收到 Car={carCode} 的状态,但未注册该编号(已注册: {registered})。请检查 Mag2Car.VehicleCode 与模拟器车号是否一致,并重新启动场景。",
|
||||
"Fass2UdpHub",
|
||||
true);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"coreVersion": ">=1.0.0",
|
||||
"requiresCore": "StandardScene.dll",
|
||||
"provides": {
|
||||
"carTypes": [ "MagCar", "MagFass2Car" ],
|
||||
"missionTypes": [ "EventCarMission", "MagFass2LoopMission" ]
|
||||
"carTypes": [ "MagCar", "Mag2Car" ],
|
||||
"missionTypes": [ "EventCarMission", "Mag2LoopMission" ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// 比对期望节点与上报节点,判定到站与动作是否完成(对齐 backend <c>CarResponseService</c>)。
|
||||
/// 默认只闭环机构类动作;轨迹字段(Speed/Orientation 等)需显式开启。
|
||||
/// </summary>
|
||||
public static class Fass2ActionResolver
|
||||
{
|
||||
private static readonly (string Name, Func<Fass2NodeMessage, byte> Get, Action<Fass2NodeMessage, byte> Set)[] ActionFields =
|
||||
/// <summary>是否在停车站比对 Speed/Orientation/Direction/Byroad 等轨迹字段。</summary>
|
||||
public static bool VerifyTrajectoryFields { get; set; }
|
||||
|
||||
private static readonly (string Name, Func<Fass2NodeMessage, byte> Get, Action<Fass2NodeMessage, byte> Set)[] MechanismFields =
|
||||
{
|
||||
("StartStop", n => n.StartStop, (n, v) => n.StartStop = v),
|
||||
("Direction", n => n.Direction, (n, v) => n.Direction = v),
|
||||
("Orientation", n => n.Orientation, (n, v) => n.Orientation = v),
|
||||
("Byroad", n => n.Byroad, (n, v) => n.Byroad = v),
|
||||
("Obstacle", n => n.Obstacle, (n, v) => n.Obstacle = v),
|
||||
("Audio", n => n.Audio, (n, v) => n.Audio = v),
|
||||
("Light", n => n.Light, (n, v) => n.Light = v),
|
||||
@@ -28,6 +28,13 @@ namespace StandardScene.Magnetic.Tasking
|
||||
("Shutdown", n => n.Shutdown, (n, v) => n.Shutdown = v)
|
||||
};
|
||||
|
||||
private static readonly (string Name, Func<Fass2NodeMessage, byte> Get, Action<Fass2NodeMessage, byte> Set)[] TrajectoryFields =
|
||||
{
|
||||
("Direction", n => n.Direction, (n, v) => n.Direction = v),
|
||||
("Orientation", n => n.Orientation, (n, v) => n.Orientation = v),
|
||||
("Byroad", n => n.Byroad, (n, v) => n.Byroad = v)
|
||||
};
|
||||
|
||||
public static bool IsAtNode(Fass2StateReport report, ushort targetNode)
|
||||
{
|
||||
return report?.Node != null && report.Node.Node == targetNode;
|
||||
@@ -35,7 +42,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
|
||||
public static bool IsVehicleMoving(byte vehicleState)
|
||||
{
|
||||
return vehicleState == 1;
|
||||
return vehicleState is 1 or 5;
|
||||
}
|
||||
|
||||
public static bool RequiresActionWait(Fass2NodeMessage expected)
|
||||
@@ -45,12 +52,28 @@ namespace StandardScene.Magnetic.Tasking
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expected.StartStop is Fass2TaskBuilder.StartStopStop or Fass2TaskBuilder.StartStopPrecision)
|
||||
if (expected.StartStop is Fass2TaskBuilder.StartStopStop
|
||||
or Fass2TaskBuilder.StartStopPrecision
|
||||
or Fass2TaskBuilder.StartStopControlStop
|
||||
or Fass2TaskBuilder.StartStopControlStart)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var field in ActionFields)
|
||||
foreach (var field in MechanismFields)
|
||||
{
|
||||
if (field.Get(expected) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!VerifyTrajectoryFields)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var field in TrajectoryFields)
|
||||
{
|
||||
if (field.Get(expected) != 0)
|
||||
{
|
||||
@@ -83,12 +106,16 @@ namespace StandardScene.Magnetic.Tasking
|
||||
return true;
|
||||
}
|
||||
|
||||
// 过站(StartStop=1) 只要求到点且非运行态,不要求 Orientation 等轨迹字段到位。
|
||||
if (expected.StartStop == Fass2TaskBuilder.StartStopPass)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expected.StartStop == Fass2TaskBuilder.StartStopControlStop)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ListPendingFields(expected, actual).Count == 0;
|
||||
}
|
||||
|
||||
@@ -105,18 +132,25 @@ namespace StandardScene.Magnetic.Tasking
|
||||
pending.Add("StartStop");
|
||||
}
|
||||
|
||||
if (expected.Speed != 0 && actual.Speed != expected.Speed)
|
||||
if (VerifyTrajectoryFields)
|
||||
{
|
||||
pending.Add("Speed");
|
||||
}
|
||||
|
||||
foreach (var field in ActionFields)
|
||||
{
|
||||
if (field.Name == "StartStop")
|
||||
if (expected.Speed != 0 && actual.Speed != expected.Speed)
|
||||
{
|
||||
continue;
|
||||
pending.Add("Speed");
|
||||
}
|
||||
|
||||
foreach (var field in TrajectoryFields)
|
||||
{
|
||||
var expectedValue = field.Get(expected);
|
||||
if (expectedValue != 0 && field.Get(actual) != expectedValue)
|
||||
{
|
||||
pending.Add(field.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var field in MechanismFields)
|
||||
{
|
||||
var expectedValue = field.Get(expected);
|
||||
if (expectedValue != 0 && field.Get(actual) != expectedValue)
|
||||
{
|
||||
@@ -143,19 +177,27 @@ namespace StandardScene.Magnetic.Tasking
|
||||
hasPatch = true;
|
||||
}
|
||||
|
||||
if (expected.Speed != 0 && (actual == null || actual.Speed != expected.Speed))
|
||||
if (VerifyTrajectoryFields)
|
||||
{
|
||||
patch.Speed = expected.Speed;
|
||||
hasPatch = true;
|
||||
}
|
||||
|
||||
foreach (var field in ActionFields)
|
||||
{
|
||||
if (field.Name == "StartStop")
|
||||
if (expected.Speed != 0 && (actual == null || actual.Speed != expected.Speed))
|
||||
{
|
||||
continue;
|
||||
patch.Speed = expected.Speed;
|
||||
hasPatch = true;
|
||||
}
|
||||
|
||||
foreach (var field in TrajectoryFields)
|
||||
{
|
||||
var expectedValue = field.Get(expected);
|
||||
if (expectedValue != 0 && (actual == null || field.Get(actual) != expectedValue))
|
||||
{
|
||||
field.Set(patch, expectedValue);
|
||||
hasPatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var field in MechanismFields)
|
||||
{
|
||||
var expectedValue = field.Get(expected);
|
||||
if (expectedValue != 0 && (actual == null || field.Get(actual) != expectedValue))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
using StandardScene.Magnetic.Protocol;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// 管控停止(StartStop=12)按车放行:地图站点保持 12,仅改本车任务报文。
|
||||
/// </summary>
|
||||
public sealed class Fass2ControlAreaInfo
|
||||
{
|
||||
public int SiteId { get; set; }
|
||||
public string AreaId { get; set; } = string.Empty;
|
||||
public int[] AreaSiteIds { get; set; } = Array.Empty<int>();
|
||||
public int Capacity { get; set; } = 1;
|
||||
public byte ReleaseStartStop { get; set; } = Fass2TaskBuilder.StartStopControlStart;
|
||||
}
|
||||
|
||||
public sealed class Fass2ControlOccupancy
|
||||
{
|
||||
public Fass2ControlOccupancy(int carId, IReadOnlyList<int> occupiedSiteIds)
|
||||
{
|
||||
CarId = carId;
|
||||
OccupiedSiteIds = occupiedSiteIds ?? Array.Empty<int>();
|
||||
}
|
||||
|
||||
public int CarId { get; }
|
||||
public IReadOnlyList<int> OccupiedSiteIds { get; }
|
||||
}
|
||||
|
||||
public sealed class Fass2ControlReleaseCheck
|
||||
{
|
||||
public bool CanRelease { get; set; }
|
||||
public string AreaId { get; set; } = string.Empty;
|
||||
public int Capacity { get; set; } = 1;
|
||||
public int OthersInArea { get; set; }
|
||||
public byte ReleaseStartStop { get; set; } = Fass2TaskBuilder.StartStopControlStart;
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public static class Fass2ControlAreaGate
|
||||
{
|
||||
private static readonly char[] SiteIdSeparators = { ',', ';', '|', ' ', '\t', '[', ']', '(', ')', '{', '}' };
|
||||
|
||||
public static bool IsControlStop(byte startStop)
|
||||
{
|
||||
return startStop == Fass2TaskBuilder.StartStopControlStop;
|
||||
}
|
||||
|
||||
public static bool AllowsLeave(byte startStop)
|
||||
{
|
||||
return startStop == Fass2TaskBuilder.StartStopPass
|
||||
|| startStop == Fass2TaskBuilder.StartStopControlStart;
|
||||
}
|
||||
|
||||
public static Fass2ControlAreaInfo Resolve(Fass2SiteActionData data, int siteId)
|
||||
{
|
||||
data ??= new Fass2SiteActionData();
|
||||
var areaSites = ParseSiteIds(data.ControlArea, siteId);
|
||||
var capacity = data.ControlCapacity.GetValueOrDefault(1);
|
||||
if (capacity < 1)
|
||||
{
|
||||
capacity = 1;
|
||||
}
|
||||
|
||||
var release = data.ControlReleaseStartStop ?? Fass2TaskBuilder.StartStopControlStart;
|
||||
if (release != Fass2TaskBuilder.StartStopPass &&
|
||||
release != Fass2TaskBuilder.StartStopControlStart)
|
||||
{
|
||||
release = Fass2TaskBuilder.StartStopControlStart;
|
||||
}
|
||||
|
||||
return new Fass2ControlAreaInfo
|
||||
{
|
||||
SiteId = siteId,
|
||||
AreaSiteIds = areaSites,
|
||||
AreaId = string.Join(",", areaSites),
|
||||
Capacity = capacity,
|
||||
ReleaseStartStop = release
|
||||
};
|
||||
}
|
||||
|
||||
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 int LimitWindowCount(
|
||||
IReadOnlyList<Fass2NodeMessage> nodes,
|
||||
int startIndex,
|
||||
int count,
|
||||
int releasedIndex)
|
||||
{
|
||||
if (nodes == null || count <= 0 || startIndex < 0 || startIndex >= nodes.Count)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var limited = Math.Min(count, nodes.Count - startIndex);
|
||||
for (var i = 0; i < limited; i++)
|
||||
{
|
||||
var node = nodes[startIndex + i];
|
||||
if (node != null && IsControlStop(node.StartStop) && releasedIndex != startIndex + i)
|
||||
{
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return limited;
|
||||
}
|
||||
|
||||
public static Fass2ControlReleaseCheck Evaluate(
|
||||
int selfCarId,
|
||||
Fass2ControlAreaInfo area,
|
||||
IReadOnlyList<Fass2ControlOccupancy> occupancies)
|
||||
{
|
||||
area ??= new Fass2ControlAreaInfo();
|
||||
if (area.AreaSiteIds == null || area.AreaSiteIds.Length == 0)
|
||||
{
|
||||
area.AreaSiteIds = area.SiteId > 0 ? new[] { area.SiteId } : Array.Empty<int>();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(area.AreaId))
|
||||
{
|
||||
area.AreaId = string.Join(",", area.AreaSiteIds);
|
||||
}
|
||||
|
||||
if (area.Capacity < 1)
|
||||
{
|
||||
area.Capacity = 1;
|
||||
}
|
||||
|
||||
var others = 0;
|
||||
if (occupancies != null)
|
||||
{
|
||||
foreach (var occupancy in occupancies)
|
||||
{
|
||||
if (occupancy == null || occupancy.CarId == selfCarId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (OccupiesListedSites(occupancy.OccupiedSiteIds, area.AreaSiteIds))
|
||||
{
|
||||
others++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var canRelease = others < area.Capacity;
|
||||
return new Fass2ControlReleaseCheck
|
||||
{
|
||||
CanRelease = canRelease,
|
||||
AreaId = area.AreaId,
|
||||
Capacity = area.Capacity,
|
||||
OthersInArea = others,
|
||||
ReleaseStartStop = area.ReleaseStartStop == 0
|
||||
? Fass2TaskBuilder.StartStopControlStart
|
||||
: area.ReleaseStartStop,
|
||||
Reason = canRelease
|
||||
? "area clear"
|
||||
: $"area occupied cars={others}, capacity={area.Capacity}"
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using SimpleCore;
|
||||
using SimpleCore.PropType;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// 按 holdingLocks + 物理 siteID 统计管控站点列表上的他车占用。
|
||||
/// </summary>
|
||||
internal static class Fass2ControlAreaOccupancy
|
||||
{
|
||||
public static Fass2ControlReleaseCheck Check(Car self, int controlSiteId)
|
||||
{
|
||||
var site = SimpleLib.GetSite(controlSiteId);
|
||||
var area = Fass2ControlAreaGate.Resolve(Fass2SiteFieldReader.Read(site), controlSiteId);
|
||||
var occupancies = new List<Fass2ControlOccupancy>();
|
||||
|
||||
IEnumerable<AbstractCar> cars;
|
||||
try
|
||||
{
|
||||
cars = SimpleLib.GetAllCars();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new Fass2ControlReleaseCheck
|
||||
{
|
||||
CanRelease = false,
|
||||
AreaId = area.AreaId,
|
||||
Capacity = area.Capacity,
|
||||
ReleaseStartStop = area.ReleaseStartStop,
|
||||
Reason = "car list unavailable: " + ex.Message
|
||||
};
|
||||
}
|
||||
|
||||
if (cars != null)
|
||||
{
|
||||
foreach (var other in cars.OfType<Car>())
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
occupancies.Add(new Fass2ControlOccupancy(other.id, CollectOccupiedSiteIds(other)));
|
||||
}
|
||||
}
|
||||
|
||||
return Fass2ControlAreaGate.Evaluate(self?.id ?? 0, area, occupancies);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<int> CollectOccupiedSiteIds(Car car)
|
||||
{
|
||||
var ids = new List<int>();
|
||||
if (car.siteID > 0)
|
||||
{
|
||||
ids.Add(car.siteID);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
internal static class Fass2PathFinder
|
||||
{
|
||||
public static List<int> GetSitesBetween(int startSiteId, int endSiteId)
|
||||
public static List<int> GetSitesBetweenBfs(int startSiteId, int endSiteId)
|
||||
{
|
||||
if (startSiteId == endSiteId)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// 断线重连后,用当前占点匹配 tasklist 链路的结果。
|
||||
/// </summary>
|
||||
public sealed class Fass2ChainMatch
|
||||
{
|
||||
public int TaskId { get; set; }
|
||||
public int TaskPriority { get; set; }
|
||||
public int CurrentSiteId { get; set; }
|
||||
public int TargetSiteId { get; set; }
|
||||
public int IndexInPath { get; set; }
|
||||
public int DistanceToTarget { get; set; }
|
||||
public bool IsAtStartPoint { get; set; }
|
||||
public bool IsAtEndPoint { get; set; }
|
||||
public IReadOnlyList<int> FullPath { get; set; } = new List<int>();
|
||||
public IReadOnlyList<int> RemainingPath { get; set; } = new List<int>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var position = IsAtStartPoint ? "start" : (IsAtEndPoint ? "end" : "mid");
|
||||
return $"task={TaskId} {CurrentSiteId}->{TargetSiteId} pos={position} remain={DistanceToTarget} prio={TaskPriority}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一站多链路时的固定裁决:原 goal 仍在剩余路径 → 起点 → 优先级 → 距目标更近。
|
||||
/// </summary>
|
||||
public static class Fass2ReconnectChainSelector
|
||||
{
|
||||
public static bool TrySelect(
|
||||
IReadOnlyList<Fass2ChainMatch> matches,
|
||||
int? preferGoalSiteId,
|
||||
out Fass2ChainMatch selected,
|
||||
out string reason)
|
||||
{
|
||||
selected = null;
|
||||
reason = "no chain";
|
||||
if (matches == null || matches.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidates = matches.Where(m => m != null && m.TargetSiteId > 0).ToList();
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preferGoalSiteId != null && preferGoalSiteId.Value > 0)
|
||||
{
|
||||
var keepGoal = candidates
|
||||
.Where(m => RemainingContains(m, preferGoalSiteId.Value))
|
||||
.ToList();
|
||||
if (keepGoal.Count == 1)
|
||||
{
|
||||
selected = keepGoal[0];
|
||||
reason = $"preferGoal={preferGoalSiteId.Value}";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keepGoal.Count > 1)
|
||||
{
|
||||
candidates = keepGoal;
|
||||
reason = $"preferGoal={preferGoalSiteId.Value} narrowed={keepGoal.Count}";
|
||||
}
|
||||
}
|
||||
|
||||
var ranked = candidates
|
||||
.OrderByDescending(m => m.IsAtStartPoint ? 1 : 0)
|
||||
.ThenByDescending(m => m.TaskPriority)
|
||||
.ThenBy(m => m.DistanceToTarget)
|
||||
.ThenBy(m => m.TaskId)
|
||||
.ToList();
|
||||
|
||||
if (ranked.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var best = ranked[0];
|
||||
if (ranked.Count > 1)
|
||||
{
|
||||
var second = ranked[1];
|
||||
var tied = best.IsAtStartPoint == second.IsAtStartPoint &&
|
||||
best.TaskPriority == second.TaskPriority &&
|
||||
best.DistanceToTarget == second.DistanceToTarget;
|
||||
if (tied && best.TargetSiteId != second.TargetSiteId)
|
||||
{
|
||||
reason = $"ambiguous {best} vs {second}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
selected = best;
|
||||
if (string.IsNullOrEmpty(reason) || reason == "no chain")
|
||||
{
|
||||
reason = $"selected {best}";
|
||||
}
|
||||
else
|
||||
{
|
||||
reason = $"{reason}; selected {best}";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool RemainingContains(Fass2ChainMatch match, int siteId)
|
||||
{
|
||||
if (match.RemainingPath != null)
|
||||
{
|
||||
for (var i = 0; i < match.RemainingPath.Count; i++)
|
||||
{
|
||||
if (match.RemainingPath[i] == siteId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return match.TargetSiteId == siteId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using SimpleCore;
|
||||
using SimpleCore.Compiler;
|
||||
using SimpleCore.Library;
|
||||
using SimpleCore.PropType;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// 从站点序列构建 <see cref="SegmentPlan"/>,供交管 Forecast 与任务下发共用同一路径。
|
||||
/// </summary>
|
||||
internal static class Fass2RouteHelper
|
||||
{
|
||||
public static List<int> GetSitesBetween(int startSiteId, int endSiteId, AbstractCar car)
|
||||
{
|
||||
if (startSiteId == endSiteId)
|
||||
{
|
||||
return new List<int> { startSiteId };
|
||||
}
|
||||
|
||||
if (car != null)
|
||||
{
|
||||
var startSite = SimpleLib.GetSite(startSiteId);
|
||||
var endSite = SimpleLib.GetSite(endSiteId);
|
||||
if (startSite != null && endSite != null)
|
||||
{
|
||||
var plan = new SegmentPlan { usingCar = car };
|
||||
plan.fields["allow_destination_on_route"] = "true";
|
||||
plan.FindRoute(startSite, endSite);
|
||||
var sites = ExtractSiteIds(plan);
|
||||
if (sites.Count > 0)
|
||||
{
|
||||
return sites;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Fass2PathFinder.GetSitesBetweenBfs(startSiteId, endSiteId);
|
||||
}
|
||||
|
||||
public static void ForecastTrafficSequence(AbstractCar car, IReadOnlyList<int> siteIds, int fromIndex)
|
||||
{
|
||||
if (car == null || siteIds == null || siteIds.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("traffic forecast requires car and siteIds");
|
||||
}
|
||||
|
||||
fromIndex = Math.Max(0, Math.Min(fromIndex, siteIds.Count - 1));
|
||||
if (fromIndex >= siteIds.Count - 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var routeSiteIds = new List<int>();
|
||||
for (var i = fromIndex; i < siteIds.Count; i++)
|
||||
{
|
||||
routeSiteIds.Add(siteIds[i]);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var plan = BuildSegmentPlan(car, routeSiteIds);
|
||||
var program = plan.Compile("fass2-traffic", false);
|
||||
program.Forecast();
|
||||
}
|
||||
catch (Exception ex) when (IsRecoverableForecastFailure(ex))
|
||||
{
|
||||
// 双车 Multi-car search Exhausted 时降级:只建 pending/seqScope,交由 TryLock 现场占点
|
||||
ApplyManualTrafficSequence(car, routeSiteIds);
|
||||
Diagnosis.Post(
|
||||
$"Mag2Car forecast fallback manual sequence car={car.id}: {ex.Message}",
|
||||
"Mag2Car",
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不走 SimpleCore 多车 Forecast,直接写入交管序列(持锁起点已由 TrafficReset 保证)。
|
||||
/// </summary>
|
||||
public static void ApplyManualTrafficSequence(AbstractCar car, IReadOnlyList<int> routeSiteIds)
|
||||
{
|
||||
if (car == null || routeSiteIds == null || routeSiteIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scope = new int[routeSiteIds.Count];
|
||||
for (var i = 0; i < routeSiteIds.Count; i++)
|
||||
{
|
||||
scope[i] = routeSiteIds[i];
|
||||
}
|
||||
|
||||
var pending = new int[Math.Max(0, scope.Length - 1)];
|
||||
for (var i = 0; i < pending.Length; i++)
|
||||
{
|
||||
pending[i] = scope[i + 1];
|
||||
}
|
||||
|
||||
car.status.seqScope = scope;
|
||||
car.status.pendingLocks = pending;
|
||||
car.status.seqPtr = scope.Length > 1 ? 1 : 0;
|
||||
car.status.escape = Array.Empty<int>();
|
||||
}
|
||||
|
||||
private static bool IsRecoverableForecastFailure(Exception ex)
|
||||
{
|
||||
for (var cur = ex; cur != null; cur = cur.InnerException)
|
||||
{
|
||||
var msg = cur.Message ?? string.Empty;
|
||||
if (msg.IndexOf("Multi-car traffic search", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
msg.IndexOf("Exhausted", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
msg.IndexOf("BadForecast", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
cur.GetType().Name.IndexOf("Forecast", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static SegmentPlan BuildSegmentPlan(AbstractCar car, IReadOnlyList<int> siteIds)
|
||||
{
|
||||
if (car == null || siteIds == null || siteIds.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("segment plan requires car and siteIds");
|
||||
}
|
||||
|
||||
var plan = new SegmentPlan { usingCar = car };
|
||||
plan.fields["allow_destination_on_route"] = "true";
|
||||
|
||||
var firstSite = SimpleLib.GetSite(siteIds[0]);
|
||||
if (firstSite == null)
|
||||
{
|
||||
throw new InvalidOperationException($"site {siteIds[0]} not found");
|
||||
}
|
||||
|
||||
plan.segments.Add(firstSite);
|
||||
|
||||
for (var i = 0; i < siteIds.Count - 1; i++)
|
||||
{
|
||||
var dstSite = SimpleLib.GetSite(siteIds[i + 1]);
|
||||
if (dstSite == null)
|
||||
{
|
||||
throw new InvalidOperationException($"site {siteIds[i + 1]} not found");
|
||||
}
|
||||
|
||||
var track = FindTrack(siteIds[i], siteIds[i + 1]);
|
||||
if (track == null)
|
||||
{
|
||||
throw new InvalidOperationException($"no track from site {siteIds[i]} to {siteIds[i + 1]}");
|
||||
}
|
||||
|
||||
plan.segments.Add(track);
|
||||
plan.segments.Add(dstSite);
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
public static List<int> ExtractSiteIds(SegmentPlan plan)
|
||||
{
|
||||
var sites = new List<int>();
|
||||
if (plan?.segments == null)
|
||||
{
|
||||
return sites;
|
||||
}
|
||||
|
||||
foreach (var segment in plan.segments)
|
||||
{
|
||||
if (segment is Site site)
|
||||
{
|
||||
sites.Add(site.id);
|
||||
}
|
||||
}
|
||||
|
||||
return sites;
|
||||
}
|
||||
|
||||
public static Track FindTrack(int fromSiteId, int toSiteId)
|
||||
{
|
||||
foreach (var track in SimpleLib.GetAllTracks())
|
||||
{
|
||||
if (track.direction == 0)
|
||||
{
|
||||
if ((track.siteA == fromSiteId && track.siteB == toSiteId) ||
|
||||
(track.siteB == fromSiteId && track.siteA == toSiteId))
|
||||
{
|
||||
return track;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (track.direction == 1 && track.siteA == fromSiteId && track.siteB == toSiteId)
|
||||
{
|
||||
return track;
|
||||
}
|
||||
|
||||
if (track.direction == 2 && track.siteB == fromSiteId && track.siteA == toSiteId)
|
||||
{
|
||||
return track;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,15 @@ namespace StandardScene.Magnetic.Tasking
|
||||
data.Shutdown = TryReadByte(site.fields, Fass2SiteFields.Shutdown);
|
||||
data.PrecisionStop = TryReadBool(site.fields, Fass2SiteFields.PrecisionStop);
|
||||
data.WaitMode = TryReadString(site.fields, Fass2SiteFields.WaitMode);
|
||||
data.ControlArea = TryReadString(site.fields, Fass2SiteFields.ControlArea);
|
||||
data.ControlCapacity = TryReadInt(site.fields, Fass2SiteFields.ControlCapacity);
|
||||
data.ControlReleaseStartStop = TryReadByte(site.fields, Fass2SiteFields.ControlReleaseStartStop);
|
||||
|
||||
if (!data.ControlCapacity.HasValue)
|
||||
{
|
||||
TryFillRegionCapacityFallback(site.fields, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -99,5 +108,40 @@ namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
return fields.TryGetValue(key, out var text) ? text : null;
|
||||
}
|
||||
|
||||
private static int? TryReadInt(Dictionary<string, string> fields, string key)
|
||||
{
|
||||
if (!fields.TryGetValue(key, out var text) || string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void TryFillRegionCapacityFallback(Dictionary<string, string> fields, Fass2SiteActionData data)
|
||||
{
|
||||
foreach (var pair in fields)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pair.Key) ||
|
||||
!pair.Key.StartsWith("Region", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (int.TryParse(pair.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var maxCount) &&
|
||||
maxCount > 0)
|
||||
{
|
||||
data.ControlCapacity = maxCount;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ namespace StandardScene.Magnetic.Tasking
|
||||
public const string Shutdown = "Fass2_Shutdown";
|
||||
public const string PrecisionStop = "Fass2_PrecisionStop";
|
||||
public const string WaitMode = "Fass2_WaitMode";
|
||||
public const string ControlArea = "Fass2_ControlArea";
|
||||
public const string ControlCapacity = "Fass2_ControlCapacity";
|
||||
public const string ControlReleaseStartStop = "Fass2_ControlReleaseStartStop";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,5 +49,8 @@ namespace StandardScene.Magnetic.Tasking
|
||||
public byte? Shutdown { get; set; }
|
||||
public bool PrecisionStop { get; set; }
|
||||
public string WaitMode { get; set; }
|
||||
public string ControlArea { get; set; }
|
||||
public int? ControlCapacity { get; set; }
|
||||
public byte? ControlReleaseStartStop { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,23 @@ using SimpleCore.PropType;
|
||||
using StandardScene.Magnetic.Protocol;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// 将站点路径与站点/边 fields 合并为 FASS 2.0 <c>0xB1</c> 节点序列。
|
||||
/// 对齐 backend <c>CarRequestService.GetSendNodes</c> 的组包规则。
|
||||
/// 对齐 backend <c>CarRequestService.GetSendNodes</c>:边属性挂在节点<b>出边</b>上。
|
||||
/// </summary>
|
||||
public static class Fass2TaskBuilder
|
||||
{
|
||||
public const byte StartStopPass = 1;
|
||||
public const byte StartStopStop = 2;
|
||||
public const byte StartStopControlStart = 11;
|
||||
public const byte StartStopControlStop = 12;
|
||||
public const byte StartStopPrecision = 22;
|
||||
|
||||
public static Fass2TaskPlan BuildPath(int startSiteId, int goalSiteId, Fass2TaskBuildOptions options,
|
||||
Func<int, ushort> resolveNodeId)
|
||||
Func<int, ushort> resolveNodeId, AbstractCar routingCar = null)
|
||||
{
|
||||
if (resolveNodeId == null)
|
||||
{
|
||||
@@ -26,7 +27,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
}
|
||||
|
||||
options ??= new Fass2TaskBuildOptions();
|
||||
var siteIds = Fass2PathFinder.GetSitesBetween(startSiteId, goalSiteId);
|
||||
var siteIds = Fass2RouteHelper.GetSitesBetween(startSiteId, goalSiteId, routingCar);
|
||||
if (siteIds.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"no path from site {startSiteId} to {goalSiteId}");
|
||||
@@ -45,9 +46,17 @@ namespace StandardScene.Magnetic.Tasking
|
||||
}
|
||||
|
||||
public static Fass2NodeMessage[] BuildSegment(int srcSiteId, int dstSiteId, Fass2TaskBuildOptions options,
|
||||
Func<int, ushort> resolveNodeId)
|
||||
Func<int, ushort> resolveNodeId, AbstractCar routingCar = null)
|
||||
{
|
||||
return BuildPath(srcSiteId, dstSiteId, options, resolveNodeId).Nodes.ToArray();
|
||||
var plan = BuildPath(srcSiteId, dstSiteId, options, resolveNodeId, routingCar);
|
||||
var nodes = plan.Nodes;
|
||||
var array = new Fass2NodeMessage[nodes.Count];
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
array[i] = nodes[i];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<Fass2NodeMessage[]> SplitBatches(IReadOnlyList<Fass2NodeMessage> nodes, int maxPerFrame = 10)
|
||||
@@ -88,14 +97,15 @@ namespace StandardScene.Magnetic.Tasking
|
||||
var site = SimpleLib.GetSite(siteId);
|
||||
var siteAction = Fass2SiteFieldReader.Read(site);
|
||||
var isLast = i == siteIds.Count - 1;
|
||||
var prevSiteId = i > 0 ? siteIds[i - 1] : siteId;
|
||||
var nextSiteId = isLast ? -1 : siteIds[i + 1];
|
||||
|
||||
Fass2TrackMotionData trackMotion = null;
|
||||
Site prevSite = null;
|
||||
if (i > 0)
|
||||
Site currentSite = site;
|
||||
Site nextSite = null;
|
||||
if (!isLast)
|
||||
{
|
||||
prevSite = SimpleLib.GetSite(prevSiteId);
|
||||
var track = FindTrack(prevSiteId, siteId);
|
||||
nextSite = SimpleLib.GetSite(nextSiteId);
|
||||
var track = Fass2RouteHelper.FindTrack(siteId, nextSiteId);
|
||||
trackMotion = Fass2TrackFieldReader.Read(track);
|
||||
}
|
||||
|
||||
@@ -103,9 +113,11 @@ namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
Node = resolveNodeId(siteId),
|
||||
StartStop = ResolveStartStop(siteAction, isLast),
|
||||
Distance = i > 0 ? ComputeEdgeDistance(prevSite, site) : (ushort)0,
|
||||
Speed = SpeedToProtocol(trackMotion?.Speed ?? options.DefaultSpeed, options.CarSpeed),
|
||||
Orientation = ResolveOrientation(prevSite, site, trackMotion),
|
||||
Distance = !isLast ? ComputeEdgeDistance(currentSite, nextSite) : (ushort)0,
|
||||
Speed = !isLast
|
||||
? SpeedToProtocol(trackMotion?.Speed ?? options.DefaultSpeed)
|
||||
: (ushort)0,
|
||||
Orientation = !isLast ? ResolveOrientation(currentSite, nextSite, trackMotion) : (byte)0,
|
||||
Byroad = siteAction.Byroad ?? trackMotion?.Byroad ?? 0,
|
||||
Direction = siteAction.Direction ?? trackMotion?.Direction ?? 0,
|
||||
Lift = siteAction.Lift ?? 0,
|
||||
@@ -185,42 +197,17 @@ namespace StandardScene.Magnetic.Tasking
|
||||
return (ushort)Math.Min(ushort.MaxValue, Math.Max(1, Math.Round(length)));
|
||||
}
|
||||
|
||||
private static ushort SpeedToProtocol(double trackSpeed, double carSpeed)
|
||||
/// <summary>
|
||||
/// 协议速度单位 0.1 m/min;地图 Speed 默认 m/min,≤1 视为旧版 m/s 兼容。
|
||||
/// </summary>
|
||||
public static ushort SpeedToProtocol(double speed)
|
||||
{
|
||||
var value = trackSpeed <= 0 ? carSpeed : trackSpeed;
|
||||
var protocolSpeed = value <= 1
|
||||
? (int)Math.Round(value * 600)
|
||||
: (int)Math.Round(value * 10);
|
||||
return (ushort)Math.Max(1, Math.Min(10000, protocolSpeed));
|
||||
}
|
||||
|
||||
private static Track FindTrack(int fromSiteId, int toSiteId)
|
||||
{
|
||||
foreach (var track in SimpleLib.GetAllTracks())
|
||||
if (speed > 0 && speed <= 1)
|
||||
{
|
||||
if (track.direction == 0)
|
||||
{
|
||||
if ((track.siteA == fromSiteId && track.siteB == toSiteId) ||
|
||||
(track.siteB == fromSiteId && track.siteA == toSiteId))
|
||||
{
|
||||
return track;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (track.direction == 1 && track.siteA == fromSiteId && track.siteB == toSiteId)
|
||||
{
|
||||
return track;
|
||||
}
|
||||
|
||||
if (track.direction == 2 && track.siteB == fromSiteId && track.siteA == toSiteId)
|
||||
{
|
||||
return track;
|
||||
}
|
||||
speed *= 60;
|
||||
}
|
||||
|
||||
return null;
|
||||
return (ushort)Math.Max(1, Math.Min(10000, Math.Round(speed * 10)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@ namespace StandardScene.Magnetic.Tasking
|
||||
public DateTime LastDispatchAt { get; set; } = DateTime.MinValue;
|
||||
public ulong LastActionId { get; set; }
|
||||
public DateTime LastActionSentAt { get; set; } = DateTime.MinValue;
|
||||
|
||||
/// <summary>交管窗口未齐、正在等锁;此期间不消耗移动超时预算。</summary>
|
||||
public bool WaitingForTraffic { get; set; }
|
||||
|
||||
/// <summary>停在管控停止点等待按车放行;此期间不消耗移动超时预算。</summary>
|
||||
public bool WaitingForRelease { get; set; }
|
||||
|
||||
/// <summary>本任务已对哪个路径下标发过放行报文;-1 表示尚未放行。</summary>
|
||||
public int ControlReleasedIndex { get; set; } = -1;
|
||||
|
||||
/// <summary>最近一次规划的站点链路,供断线重连判断是否仍在原路径上。</summary>
|
||||
public int[] RouteSiteIds { get; set; } = Array.Empty<int>();
|
||||
}
|
||||
|
||||
public static class Fass2TaskPersistence
|
||||
@@ -47,6 +59,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
public const string TagTaskId = "Fass2Task_TaskId";
|
||||
public const string TagFieldsSignature = "Fass2Task_FieldsSig";
|
||||
public const string TagDefaultSpeed = "Fass2Task_DefaultSpeed";
|
||||
public const string TagRouteSites = "Fass2Task_RouteSites";
|
||||
|
||||
public static void Save(TagSet tags, Fass2TaskContext context)
|
||||
{
|
||||
@@ -62,6 +75,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
SetTag(tags, TagTaskId, context.TaskId.ToString());
|
||||
SetTag(tags, TagFieldsSignature, context.FieldsSignature ?? string.Empty);
|
||||
SetTag(tags, TagDefaultSpeed, context.DefaultSpeed.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
SetTag(tags, TagRouteSites, FormatIntList(context.RouteSiteIds));
|
||||
}
|
||||
|
||||
public static bool TryLoad(TagSet tags, out Fass2TaskContext context)
|
||||
@@ -93,6 +107,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var defaultSpeed);
|
||||
|
||||
tags.TryGetValue(TagRouteSites, out var routeSitesText);
|
||||
context = new Fass2TaskContext
|
||||
{
|
||||
Phase = phase,
|
||||
@@ -101,7 +116,8 @@ namespace StandardScene.Magnetic.Tasking
|
||||
CurrentIndex = currentIndex,
|
||||
TaskId = taskId,
|
||||
FieldsSignature = fieldsSignature ?? string.Empty,
|
||||
DefaultSpeed = defaultSpeed
|
||||
DefaultSpeed = defaultSpeed,
|
||||
RouteSiteIds = ParseIntList(routeSitesText)
|
||||
};
|
||||
return true;
|
||||
}
|
||||
@@ -120,6 +136,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
RemoveTag(tags, TagTaskId);
|
||||
RemoveTag(tags, TagFieldsSignature);
|
||||
RemoveTag(tags, TagDefaultSpeed);
|
||||
RemoveTag(tags, TagRouteSites);
|
||||
}
|
||||
|
||||
private static void SetTag(TagSet tags, string key, string value)
|
||||
@@ -145,5 +162,30 @@ namespace StandardScene.Magnetic.Tasking
|
||||
value = 0;
|
||||
return tags.TryGetValue(key, out var text) && int.TryParse(text, out value);
|
||||
}
|
||||
|
||||
private static string FormatIntList(int[] values)
|
||||
{
|
||||
return values == null || values.Length == 0 ? string.Empty : string.Join(",", values);
|
||||
}
|
||||
|
||||
private static int[] ParseIntList(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
|
||||
var parts = text.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var ids = new List<int>(parts.Length);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (int.TryParse(part.Trim(), out var id) && id > 0)
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return ids.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
public sealed class Fass2TaskBuildOptions
|
||||
{
|
||||
public bool UseTagValueAsNode { get; set; }
|
||||
public double DefaultSpeed { get; set; } = 0.2;
|
||||
public double DefaultSpeed { get; set; } = 12;
|
||||
public double CarSpeed { get; set; } = 1;
|
||||
public int MaxNodesPerFrame { get; set; } = 10;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,303 @@
|
||||
using SimpleCore;
|
||||
using SimpleCore.Compiler;
|
||||
using SimpleCore.PropType;
|
||||
using SimpleCore.Traffic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// 方案 B′:用任务下发的 siteIds 经 SegmentPlan.Forecast 建立 seqScope/pendingLocks,再段级 TryLock/Leave。
|
||||
/// </summary>
|
||||
internal sealed class Fass2TrafficLocker
|
||||
{
|
||||
private readonly AbstractCar _car;
|
||||
private readonly Action<string> _log;
|
||||
|
||||
public Fass2TrafficLocker(AbstractCar car, Action<string> log)
|
||||
{
|
||||
_car = car ?? throw new ArgumentNullException(nameof(car));
|
||||
_log = log ?? (_ => { });
|
||||
}
|
||||
|
||||
public void PrepareSequence(IReadOnlyList<int> siteIds, int fromIndex)
|
||||
{
|
||||
if (siteIds == null || siteIds.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("traffic plan siteIds is empty");
|
||||
}
|
||||
|
||||
fromIndex = Math.Max(0, Math.Min(fromIndex, siteIds.Count - 1));
|
||||
var startSiteId = siteIds[fromIndex];
|
||||
var startSite = SimpleLib.GetSite(startSiteId);
|
||||
if (startSite == null)
|
||||
{
|
||||
throw new InvalidOperationException($"traffic start site invalid: {startSiteId}");
|
||||
}
|
||||
|
||||
// TrafficReset 会 programs.Clear() 把 now 置空;TryLock 在真正占点前要求 programs.now != null,
|
||||
// 否则抛 Program obsoleted。环线拦截路径仍处在 move 脚本的 actualSendScript 中,需保留/重建锚点。
|
||||
var keepProgram = _car.status.programs.now;
|
||||
|
||||
_log($"prepare reset start={startSiteId}, goal={siteIds[siteIds.Count - 1]}, fromIndex={fromIndex}, route=[{string.Join(",", siteIds)}]");
|
||||
try
|
||||
{
|
||||
_car.TrafficReset(startSite, makeAvailable: true, strict: false);
|
||||
}
|
||||
catch (Exception ex) when (IsStartOccupiedByOtherCar(ex))
|
||||
{
|
||||
// 起点被他车占用时不能 Reset;若本车已持有该站则跳过 Reset 继续建序,否则交给上层等待。
|
||||
if (Array.IndexOf(_car.status.holdingLocks, startSiteId) < 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"traffic start site {startSiteId} occupied by another car", ex);
|
||||
}
|
||||
|
||||
_log($"prepare reset skipped, already holding start={startSiteId}, ex={ex.Message}");
|
||||
}
|
||||
|
||||
EnsureProgramAnchor(keepProgram);
|
||||
|
||||
if (fromIndex >= siteIds.Count - 1)
|
||||
{
|
||||
LogTrafficState("prepare single-site");
|
||||
return;
|
||||
}
|
||||
|
||||
Fass2RouteHelper.ForecastTrafficSequence(_car, siteIds, fromIndex);
|
||||
LogTrafficState("prepare forecast ok");
|
||||
}
|
||||
|
||||
public void RebaseFrom(IReadOnlyList<int> siteIds, int fromIndex, int windowSize)
|
||||
{
|
||||
PrepareSequence(siteIds, fromIndex);
|
||||
EnsureWindowLocked(siteIds, fromIndex, Math.Max(1, windowSize));
|
||||
}
|
||||
|
||||
private static bool IsStartOccupiedByOtherCar(Exception ex)
|
||||
{
|
||||
for (var cur = ex; cur != null; cur = cur.InnerException)
|
||||
{
|
||||
var msg = cur.Message ?? string.Empty;
|
||||
if (msg.IndexOf("already locks it", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
msg.IndexOf("unavailable site", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryLockNext(int siteId)
|
||||
{
|
||||
if (!_car.status.usage.Get().scheduling)
|
||||
{
|
||||
throw new InvalidOperationException("abandoned");
|
||||
}
|
||||
|
||||
if (Array.IndexOf(_car.status.holdingLocks, siteId) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_car.status.pendingLocks.Length > 0 && _car.status.pendingLocks[0] != siteId)
|
||||
{
|
||||
// 窗口重入时可能再次请求已锁过的更早站点;交给 EnsureWindowLocked 跳过,避免 UDP 线程抛异常。
|
||||
_log($"lock skip site={siteId}, expect pending0={_car.status.pendingLocks[0]}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureProgramAnchor(keepProgram: null);
|
||||
|
||||
if (TrafficControl.TryLock(_car, siteId))
|
||||
{
|
||||
_log($"lock ok site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
return true;
|
||||
}
|
||||
|
||||
_log($"lock wait site={siteId}, blockedBy={FormatBlocked()}, TCStat={FormatTcStat()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LeavePassed(int siteId)
|
||||
{
|
||||
if (_car.status.holdingLocks.Length <= 1)
|
||||
{
|
||||
_log($"leave skip site={siteId}, holding count={_car.status.holdingLocks.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.IndexOf(_car.status.holdingLocks, siteId) < 0)
|
||||
{
|
||||
_log($"leave skip site={siteId}, not in holding");
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureProgramAnchor(keepProgram: null);
|
||||
TrafficControl.Leave(_car, siteId);
|
||||
_log($"leave site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放路径上严格位于 keepFromIndex 之前的持锁站点(当前站及前方窗口保留)。
|
||||
/// </summary>
|
||||
public void ReleaseLocksBehind(IReadOnlyList<int> siteIds, int keepFromIndex)
|
||||
{
|
||||
if (siteIds == null || siteIds.Count == 0 || _car.status.holdingLocks.Length <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
keepFromIndex = Math.Max(0, Math.Min(keepFromIndex, siteIds.Count - 1));
|
||||
var holdingSnapshot = _car.status.holdingLocks.ToArray();
|
||||
foreach (var siteId in holdingSnapshot)
|
||||
{
|
||||
if (_car.status.holdingLocks.Length <= 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var index = IndexOfSite(siteIds, siteId);
|
||||
if (index < 0 || index >= keepFromIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LeavePassed(siteId);
|
||||
}
|
||||
}
|
||||
|
||||
private static int IndexOfSite(IReadOnlyList<int> siteIds, int siteId)
|
||||
{
|
||||
for (var i = 0; i < siteIds.Count; i++)
|
||||
{
|
||||
if (siteIds[i] == siteId)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int EnsureWindowLocked(IReadOnlyList<int> siteIds, int fromIndex, int windowSize)
|
||||
{
|
||||
if (siteIds == null || siteIds.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
fromIndex = Math.Max(0, Math.Min(fromIndex, siteIds.Count - 1));
|
||||
windowSize = Math.Max(1, Math.Min(windowSize, 10));
|
||||
var maxIndex = Math.Min(siteIds.Count - 1, fromIndex + windowSize - 1);
|
||||
|
||||
// 返回值必须是从 fromIndex 起的连续已占/新锁站数,供 0xB1 窗口切片使用。
|
||||
// UDP Tick 会反复进入:已在 holding 的站直接计数,禁止再次对已消费的 pending 站 TryLock。
|
||||
var lockedCount = 0;
|
||||
for (var i = fromIndex; i <= maxIndex; i++)
|
||||
{
|
||||
var siteId = siteIds[i];
|
||||
if (Array.IndexOf(_car.status.holdingLocks, siteId) >= 0)
|
||||
{
|
||||
lockedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_car.status.pendingLocks.Length == 0 || _car.status.pendingLocks[0] != siteId)
|
||||
{
|
||||
_log(
|
||||
$"window gap site={siteId}, pending0={(_car.status.pendingLocks.Length > 0 ? _car.status.pendingLocks[0].ToString() : "-")}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!TryLockNext(siteId))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
lockedCount++;
|
||||
}
|
||||
|
||||
if (lockedCount == 0)
|
||||
{
|
||||
lockedCount = 1;
|
||||
_log($"window warn: no holding overlap index={fromIndex}, site={siteIds[fromIndex]}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
}
|
||||
|
||||
_log($"window locked={lockedCount}/{windowSize}, fromIndex={fromIndex}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}]");
|
||||
return lockedCount;
|
||||
}
|
||||
|
||||
public void FinalizeAtSite(int siteId)
|
||||
{
|
||||
_log($"finalize site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SimpleCore.TryLock 在写锁前检查 programs.now;为空则抛 Program obsoleted。
|
||||
/// </summary>
|
||||
private void EnsureProgramAnchor(CarProgram keepProgram)
|
||||
{
|
||||
if (_car.status.programs.now != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (keepProgram != null)
|
||||
{
|
||||
_car.status.programs.now = keepProgram;
|
||||
_log($"restore programs.now after TrafficReset, name={keepProgram.name}, state={keepProgram.status.state}");
|
||||
return;
|
||||
}
|
||||
|
||||
var dummy = new CarProgram
|
||||
{
|
||||
name = $"fass2-traffic-anchor:{_car.id}",
|
||||
plans = new[]
|
||||
{
|
||||
new SegmentPlan { usingCar = _car }
|
||||
}
|
||||
};
|
||||
dummy.status.state = CarProgram.StatusEnum.Dummy;
|
||||
_car.status.programs.now = dummy;
|
||||
_log($"install dummy programs.now for TryLock, name={dummy.name}");
|
||||
}
|
||||
|
||||
private void LogTrafficState(string prefix)
|
||||
{
|
||||
_log(
|
||||
$"{prefix}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}], seqScope=[{FormatInts(_car.status.seqScope)}], seqPtr={_car.status.seqPtr}");
|
||||
}
|
||||
|
||||
private string FormatBlocked()
|
||||
{
|
||||
if (_car.status.blockedBy == null || _car.status.blockedBy.Length == 0)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
return string.Join(",", _car.status.blockedBy.Select(p => $"{p.Item1}:{p.Item2}"));
|
||||
}
|
||||
|
||||
private string FormatTcStat()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tc = _car.status.TCStat;
|
||||
return string.IsNullOrWhiteSpace(tc) ? "-" : tc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatInts(int[] values)
|
||||
{
|
||||
return values == null || values.Length == 0 ? string.Empty : string.Join(",", values);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -6,9 +6,9 @@ using System.Text;
|
||||
namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// MagFass2Car 本地文件日志,按协议车号分目录:logs/car{VehicleCode}/magfass2_yyyyMMdd.log
|
||||
/// Mag2Car 本地文件日志,按协议车号分目录:logs/car{VehicleCode}/mag2_yyyyMMdd.log
|
||||
/// </summary>
|
||||
public static class MagFass2CarFileLogger
|
||||
public static class Mag2CarFileLogger
|
||||
{
|
||||
private static readonly ConcurrentDictionary<ushort, object> CarLocks = new ConcurrentDictionary<ushort, object>();
|
||||
private static string _baseDirectory = "logs";
|
||||
@@ -27,7 +27,7 @@ namespace StandardScene.Magnetic.Tasking
|
||||
|
||||
public static string GetCurrentLogFilePath(ushort vehicleCode)
|
||||
{
|
||||
var fileName = $"magfass2_{DateTime.Now:yyyyMMdd}.log";
|
||||
var fileName = $"mag2_{DateTime.Now:yyyyMMdd}.log";
|
||||
return Path.Combine(GetCarLogDirectory(vehicleCode), fileName);
|
||||
}
|
||||
|
||||
+34
-34
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>MagFass2Car Guide</title>
|
||||
<title>Mag2Car Guide</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 18mm 16mm; }
|
||||
body { font-family: "Microsoft YaHei", "Segoe UI", sans-serif; color: #222; line-height: 1.55; font-size: 11pt; }
|
||||
@@ -25,10 +25,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MagFass2Car(FASS 2.0)使用指南</h1>
|
||||
<h1>Mag2Car(FASS 2.0)使用指南</h1>
|
||||
<blockquote><p>适用对象:第一次接触磁导航 + FASS 2.0 的调试/实施人员 </p></blockquote>
|
||||
<blockquote><p>插件:`StandardScene.Magnetic.dll` </p></blockquote>
|
||||
<blockquote><p>车型:`MagFass2Car` </p></blockquote>
|
||||
<blockquote><p>车型:`Mag2Car` </p></blockquote>
|
||||
<blockquote><p>最后更新:2026-07</p></blockquote>
|
||||
<hr/>
|
||||
<h2>目录</h2>
|
||||
@@ -57,7 +57,7 @@
|
||||
</ol>
|
||||
<hr/>
|
||||
<h2>1. 这套系统能做什么</h2>
|
||||
<p><code>MagFass2Car</code> 是 StandardScene **磁导航平台插件**里的 FASS 2.0 车型,主要能力:</p>
|
||||
<p><code>Mag2Car</code> 是 StandardScene **磁导航平台插件**里的 FASS 2.0 车型,主要能力:</p>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>能力</th>
|
||||
@@ -96,12 +96,12 @@
|
||||
<h3>步骤 1:加载插件</h3>
|
||||
<p>确认场景已加载 <code>scene.mag</code>(<code>StandardScene.Magnetic.scene.json</code>),提供:</p>
|
||||
<ul>
|
||||
<li>车型:<code>MagCar</code>、<code>MagFass2Car</code></li>
|
||||
<li>进程:<code>EventCarMission</code>、<code>MagFass2LoopMission</code></li>
|
||||
<li>车型:<code>MagCar</code>、<code>Mag2Car</code></li>
|
||||
<li>进程:<code>EventCarMission</code>、<code>Mag2LoopMission</code></li>
|
||||
</ul>
|
||||
<h3>步骤 2:添加一辆车</h3>
|
||||
<ol>
|
||||
<li>在场景中添加车型 **<code>MagFass2Car</code>**</li>
|
||||
<li>在场景中添加车型 **<code>Mag2Car</code>**</li>
|
||||
<li>填写基本参数(UDP 模式示例):</li>
|
||||
</ol>
|
||||
<table>
|
||||
@@ -150,8 +150,8 @@
|
||||
<p>**方式 A — 手动 Go(Web/调度下发)** </p>
|
||||
<p>用系统自带的「去某站」功能,内部会编译脚本,例如:</p>
|
||||
<pre><code>
|
||||
agv.MagFass2Go(1, 2, 0.2);
|
||||
agv.MagFass2Go(2, 5, 0.2);
|
||||
agv.Mag2Go(1, 2, 0.2);
|
||||
agv.Mag2Go(2, 5, 0.2);
|
||||
</code></pre>
|
||||
<p>**方式 B — LoopMission 环线** </p>
|
||||
<ol>
|
||||
@@ -165,11 +165,11 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
<pre><code>
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 业务层:LoopMission / 手动 Go / 脚本 │
|
||||
│ (分配 goalSite 或编译多行 MagFass2Go) │
|
||||
│ (分配 goalSite 或编译多行 Mag2Go) │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ MagFass2Car 执行层 │
|
||||
│ Mag2Car 执行层 │
|
||||
│ · 脚本拦截(Loop + goalSite)→ 一次全程任务 │
|
||||
│ · Fass2TaskBuilder:路径 + fields → 节点序列 │
|
||||
│ · Fass2TaskStateMachine:滑动窗口下发 + 动作闭环 │
|
||||
@@ -186,7 +186,7 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
<p>**关键理解:**</p>
|
||||
<ul>
|
||||
<li>**LoopMission(Core)** 只负责「给谁分配哪个目标站」(写 <code>goalSite</code> 标签)</li>
|
||||
<li>**MagFass2Car** 负责「怎么走过去」(拼包、下发、等到站)</li>
|
||||
<li>**Mag2Car** 负责「怎么走过去」(拼包、下发、等到站)</li>
|
||||
<li>**阶段 4** 后,Loop 不再逐行执行多段脚本,而是**拦截为一次全程任务**(可关)</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
@@ -195,8 +195,8 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
<p>文件:<code>StandardScene.Magnetic.scene.json</code></p>
|
||||
<pre><code>
|
||||
{
|
||||
"carTypes": [ "MagCar", "MagFass2Car" ],
|
||||
"missionTypes": [ "EventCarMission", "MagFass2LoopMission" ]
|
||||
"carTypes": [ "MagCar", "Mag2Car" ],
|
||||
"missionTypes": [ "EventCarMission", "Mag2LoopMission" ]
|
||||
}
|
||||
</code></pre>
|
||||
<h3>4.2 选哪个 Mission?</h3>
|
||||
@@ -210,21 +210,21 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
<tbody><tr>
|
||||
<td><code>LoopMission</code></td>
|
||||
<td>Core</td>
|
||||
<td>通用环线,MagFass2Car 同样可用</td>
|
||||
<td>通用环线,Mag2Car 同样可用</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>磁导航FASS2环线</code>(<code>MagFass2LoopMission</code>)</td>
|
||||
<td><code>磁导航FASS2环线</code>(<code>Mag2LoopMission</code>)</td>
|
||||
<td>Magnetic 插件</td>
|
||||
<td>逻辑与 LoopMission 相同,名称标识磁导航 FASS2 场景</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FASS车辆事件进程</code>(<code>EventCarMission</code>)</td>
|
||||
<td>Magnetic 插件</td>
|
||||
<td>**仅 MagCar(FASS 1.0)**,不驱动 MagFass2Car</td>
|
||||
<td>**仅 MagCar(FASS 1.0)**,不驱动 Mag2Car</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<blockquote><p>新手建议:MagFass2Car 环线用 **`LoopMission`** 或 **`磁导航FASS2环线`** 均可。</p></blockquote>
|
||||
<blockquote><p>新手建议:Mag2Car 环线用 **`LoopMission`** 或 **`磁导航FASS2环线`** 均可。</p></blockquote>
|
||||
<h3>4.3 tasklist.json 最小示例</h3>
|
||||
<pre><code>
|
||||
{
|
||||
@@ -253,7 +253,7 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
<p>含义:车到站 1 → 自动分配目标站 2;到站 2 → 自动分配目标站 1,形成环线。</p>
|
||||
<hr/>
|
||||
<h2>5. 车辆参数说明</h2>
|
||||
<p>在场景编辑器中选中 <code>MagFass2Car</code>,常用字段如下。</p>
|
||||
<p>在场景编辑器中选中 <code>Mag2Car</code>,常用字段如下。</p>
|
||||
<h3>5.1 通讯</h3>
|
||||
<table>
|
||||
<thead><tr>
|
||||
@@ -426,8 +426,8 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
<p>**流程**:</p>
|
||||
<pre><code>
|
||||
FindRoute → Compile → 多行脚本:
|
||||
agv.MagFass2Go(1,2,speed);
|
||||
agv.MagFass2Go(2,3,speed);
|
||||
agv.Mag2Go(1,2,speed);
|
||||
agv.Mag2Go(2,3,speed);
|
||||
...
|
||||
→ SelfEvaluating 逐行执行
|
||||
→ 每行:锁终点 → 发 0xB1 → 等到站 → 释放起点
|
||||
@@ -435,7 +435,7 @@ FindRoute → Compile → 多行脚本:
|
||||
<p>**特点**:每**条边**一段任务,多次 <code>taskId</code>,与交管「逐段锁点」一致。</p>
|
||||
<hr/>
|
||||
<h3>方式 2:Loop 全程任务(阶段 4,默认)</h3>
|
||||
<p>**触发**:<code>goalSite</code> 标签 + 编译脚本含 <code>MagFass2Go</code> + <code>EnableLoopTaskDrive=true</code>。</p>
|
||||
<p>**触发**:<code>goalSite</code> 标签 + 编译脚本含 <code>Mag2Go</code> + <code>EnableLoopTaskDrive=true</code>。</p>
|
||||
<p>**流程**:</p>
|
||||
<pre><code>
|
||||
LoopMission 写 goalSite
|
||||
@@ -446,7 +446,7 @@ LoopMission 写 goalSite
|
||||
</code></pre>
|
||||
<p>**特点**:业务上一次任务,协议上仍遵守每帧 ≤10 站。</p>
|
||||
<hr/>
|
||||
<h3>方式 3:单段 MagFass2Go(脚本一行)</h3>
|
||||
<h3>方式 3:单段 Mag2Go(脚本一行)</h3>
|
||||
<p>**触发**:脚本里只有一行,或无 <code>goalSite</code> 的测试。</p>
|
||||
<p>**流程**:与方式 1 的单行相同,状态机只跑 <code>src → dst</code> 一段。</p>
|
||||
<hr/>
|
||||
@@ -631,7 +631,7 @@ LoopMission 写 goalSite
|
||||
</tr>
|
||||
<tr>
|
||||
<td>车辆开关</td>
|
||||
<td><code>MagFass2Car.UseTagValueAsNode = true</code></td>
|
||||
<td><code>Mag2Car.UseTagValueAsNode = true</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>示例</td>
|
||||
@@ -1295,7 +1295,7 @@ LoopMission 写 goalSite
|
||||
<td><code>Magnet</code></td>
|
||||
<td>bool</td>
|
||||
<td>—</td>
|
||||
<td><code>true</code> 启用 <code>MagneticTrackCoder</code>(<code>agv.MagGo</code> 等),与 <code>MagFass2Go</code> 模板独立</td>
|
||||
<td><code>true</code> 启用 <code>MagneticTrackCoder</code>(<code>agv.MagGo</code> 等),与 <code>Mag2Go</code> 模板独立</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ReverseDst</code></td>
|
||||
@@ -1433,7 +1433,7 @@ Fass2_Obstacle = 2
|
||||
2. 匹配 tasklist:currentStationId = N → 分配 targetStationId
|
||||
3. 写 tags:goalSite = 目标站 ID
|
||||
4. LoopStartAction 调用 GoSite
|
||||
5. MagFass2Car 拦截 → 全程任务到 goalSite
|
||||
5. Mag2Car 拦截 → 全程任务到 goalSite
|
||||
6. 到达后 GoSite 清理 goalSite / occupied / loopAssigned
|
||||
7. 下一轮任务匹配
|
||||
</code></pre>
|
||||
@@ -1462,13 +1462,13 @@ Fass2_Obstacle = 2
|
||||
<ul>
|
||||
<li>**不需要改 Core**;<code>LoopMission</code> 逻辑完全沿用</li>
|
||||
<li><code>磁导航FASS2环线</code> 仅为场景标识,行为与 <code>LoopMission</code> 相同</li>
|
||||
<li>真正区别在 **车型** 是否 <code>MagFass2Car</code> 且 <code>EnableLoopTaskDrive=true</code></li>
|
||||
<li>真正区别在 **车型** 是否 <code>Mag2Car</code> 且 <code>EnableLoopTaskDrive=true</code></li>
|
||||
</ul>
|
||||
<h3>9.4 关闭 Loop 拦截(恢复多段脚本)</h3>
|
||||
<pre><code>
|
||||
EnableLoopTaskDrive = false
|
||||
</code></pre>
|
||||
<p>Loop 仍走 Compile 多行 <code>MagFass2Go</code>,每行一段边。</p>
|
||||
<p>Loop 仍走 Compile 多行 <code>Mag2Go</code>,每行一段边。</p>
|
||||
<hr/>
|
||||
<h2>10. 任务状态机与协议下发</h2>
|
||||
<h3>10.1 状态流转</h3>
|
||||
@@ -1595,7 +1595,7 @@ Idle → Planning → Moving → AtStation → SegmentDone → … → Complete
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>日志前缀:<code>[MagFass2Car:车名(id)]</code>,任务状态机:<code>task-sm</code>。</p>
|
||||
<p>日志前缀:<code>[Mag2Car:车名(id)]</code>,任务状态机:<code>task-sm</code>。</p>
|
||||
<hr/>
|
||||
<h2>12. 常见问题排查</h2>
|
||||
<h3>Q1:车显示离线(UDP)</h3>
|
||||
@@ -1698,13 +1698,13 @@ Idle → Planning → Moving → AtStation → SegmentDone → … → Complete
|
||||
<p>看日志:</p>
|
||||
<ul>
|
||||
<li><code>loop goal drive intercept</code> → 全程任务(阶段 4)</li>
|
||||
<li><code>script begin</code> + 多行 <code>MagFass2Go</code> → 多段脚本</li>
|
||||
<li><code>script begin</code> + 多行 <code>Mag2Go</code> → 多段脚本</li>
|
||||
</ul>
|
||||
<h3>Q6:与 MagCar 混用注意事项</h3>
|
||||
<ul>
|
||||
<li><code>EventCarMission</code> **只控制 MagCar**</li>
|
||||
<li>同场景两种车型时,tasklist 和车辆类型要对应</li>
|
||||
<li>FASS 1.0 用 <code>MagCar</code>,FASS 2.0 用 <code>MagFass2Car</code></li>
|
||||
<li>FASS 1.0 用 <code>MagCar</code>,FASS 2.0 用 <code>Mag2Car</code></li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<h2>13. 参数推荐与检查清单</h2>
|
||||
@@ -1722,7 +1722,7 @@ MoveTimeoutSeconds = 120
|
||||
<h3>13.2 上线前检查清单</h3>
|
||||
<ul>
|
||||
<li>[ ] 插件 <code>StandardScene.Magnetic.dll</code> 已加载</li>
|
||||
<li>[ ] 车型为 <code>MagFass2Car</code>(不是 <code>MagCar</code>)</li>
|
||||
<li>[ ] 车型为 <code>Mag2Car</code>(不是 <code>MagCar</code>)</li>
|
||||
<li>[ ] <code>VehicleCode</code>、IP、端口与车体一致</li>
|
||||
<li>[ ] UDP 能收到 100B,日志有 <code>0x10</code> 应答</li>
|
||||
<li>[ ] 关键站点 <code>TagValue</code> / <code>Fass2_*</code> 已配置</li>
|
||||
@@ -1785,7 +1785,7 @@ MoveTimeoutSeconds = 120
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>车型</td>
|
||||
<td><code>CarTypes/MagFass2Car.cs</code></td>
|
||||
<td><code>CarTypes/Mag2Car.cs</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>协议</td>
|
||||
@@ -1805,7 +1805,7 @@ MoveTimeoutSeconds = 120
|
||||
</tr>
|
||||
<tr>
|
||||
<td>环线 Mission</td>
|
||||
<td><code>Chained/MagFass2LoopMission.cs</code></td>
|
||||
<td><code>Chained/Mag2LoopMission.cs</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>站点字段常量</td>
|
||||
+66
-38
@@ -1,8 +1,8 @@
|
||||
# MagFass2Car(FASS 2.0)使用指南
|
||||
# Mag2Car(FASS 2.0)使用指南
|
||||
|
||||
> 适用对象:第一次接触磁导航 + FASS 2.0 的调试/实施人员
|
||||
> 插件:`StandardScene.Magnetic.dll`
|
||||
> 车型:`MagFass2Car`
|
||||
> 车型:`Mag2Car`
|
||||
> 最后更新:2026-07
|
||||
|
||||
---
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
## 1. 这套系统能做什么
|
||||
|
||||
`MagFass2Car` 是 StandardScene **磁导航平台插件**里的 FASS 2.0 车型,主要能力:
|
||||
`Mag2Car` 是 StandardScene **磁导航平台插件**里的 FASS 2.0 车型,主要能力:
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
@@ -54,12 +54,12 @@
|
||||
|
||||
确认场景已加载 `scene.mag`(`StandardScene.Magnetic.scene.json`),提供:
|
||||
|
||||
- 车型:`MagCar`、`MagFass2Car`
|
||||
- 进程:`EventCarMission`、`MagFass2LoopMission`
|
||||
- 车型:`MagCar`、`Mag2Car`
|
||||
- 进程:`EventCarMission`、`Mag2LoopMission`
|
||||
|
||||
### 步骤 2:添加一辆车
|
||||
|
||||
1. 在场景中添加车型 **`MagFass2Car`**
|
||||
1. 在场景中添加车型 **`Mag2Car`**
|
||||
2. 填写基本参数(UDP 模式示例):
|
||||
|
||||
| 参数 | 示例值 | 含义 |
|
||||
@@ -86,8 +86,8 @@
|
||||
用系统自带的「去某站」功能,内部会编译脚本,例如:
|
||||
|
||||
```text
|
||||
agv.MagFass2Go(1, 2, 0.2);
|
||||
agv.MagFass2Go(2, 5, 0.2);
|
||||
agv.Mag2Go(1, 2, 0.2);
|
||||
agv.Mag2Go(2, 5, 0.2);
|
||||
```
|
||||
|
||||
**方式 B — LoopMission 环线**
|
||||
@@ -103,11 +103,11 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 业务层:LoopMission / 手动 Go / 脚本 │
|
||||
│ (分配 goalSite 或编译多行 MagFass2Go) │
|
||||
│ (分配 goalSite 或编译多行 Mag2Go) │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ MagFass2Car 执行层 │
|
||||
│ Mag2Car 执行层 │
|
||||
│ · 脚本拦截(Loop + goalSite)→ 一次全程任务 │
|
||||
│ · Fass2TaskBuilder:路径 + fields → 节点序列 │
|
||||
│ · Fass2TaskStateMachine:滑动窗口下发 + 动作闭环 │
|
||||
@@ -125,7 +125,7 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
**关键理解:**
|
||||
|
||||
- **LoopMission(Core)** 只负责「给谁分配哪个目标站」(写 `goalSite` 标签)
|
||||
- **MagFass2Car** 负责「怎么走过去」(拼包、下发、等到站)
|
||||
- **Mag2Car** 负责「怎么走过去」(拼包、下发、等到站)
|
||||
- **阶段 4** 后,Loop 不再逐行执行多段脚本,而是**拦截为一次全程任务**(可关)
|
||||
|
||||
---
|
||||
@@ -138,8 +138,8 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
|
||||
```json
|
||||
{
|
||||
"carTypes": [ "MagCar", "MagFass2Car" ],
|
||||
"missionTypes": [ "EventCarMission", "MagFass2LoopMission" ]
|
||||
"carTypes": [ "MagCar", "Mag2Car" ],
|
||||
"missionTypes": [ "EventCarMission", "Mag2LoopMission" ]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -147,11 +147,11 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
|
||||
| Mission | 来源 | 适用 |
|
||||
|---------|------|------|
|
||||
| `LoopMission` | Core | 通用环线,MagFass2Car 同样可用 |
|
||||
| `磁导航FASS2环线`(`MagFass2LoopMission`) | Magnetic 插件 | 逻辑与 LoopMission 相同,名称标识磁导航 FASS2 场景 |
|
||||
| `FASS车辆事件进程`(`EventCarMission`) | Magnetic 插件 | **仅 MagCar(FASS 1.0)**,不驱动 MagFass2Car |
|
||||
| `LoopMission` | Core | 通用环线,Mag2Car 同样可用 |
|
||||
| `磁导航FASS2环线`(`Mag2LoopMission`) | Magnetic 插件 | 逻辑与 LoopMission 相同,名称标识磁导航 FASS2 场景 |
|
||||
| `FASS车辆事件进程`(`EventCarMission`) | Magnetic 插件 | **仅 MagCar(FASS 1.0)**,不驱动 Mag2Car |
|
||||
|
||||
> 新手建议:MagFass2Car 环线用 **`LoopMission`** 或 **`磁导航FASS2环线`** 均可。
|
||||
> 新手建议:Mag2Car 环线用 **`LoopMission`** 或 **`磁导航FASS2环线`** 均可。
|
||||
|
||||
### 4.3 tasklist.json 最小示例
|
||||
|
||||
@@ -186,7 +186,7 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
|
||||
## 5. 车辆参数说明
|
||||
|
||||
在场景编辑器中选中 `MagFass2Car`,常用字段如下。
|
||||
在场景编辑器中选中 `Mag2Car`,常用字段如下。
|
||||
|
||||
### 5.1 通讯
|
||||
|
||||
@@ -269,8 +269,8 @@ agv.MagFass2Go(2, 5, 0.2);
|
||||
|
||||
```text
|
||||
FindRoute → Compile → 多行脚本:
|
||||
agv.MagFass2Go(1,2,speed);
|
||||
agv.MagFass2Go(2,3,speed);
|
||||
agv.Mag2Go(1,2,speed);
|
||||
agv.Mag2Go(2,3,speed);
|
||||
...
|
||||
→ SelfEvaluating 逐行执行
|
||||
→ 每行:锁终点 → 发 0xB1 → 等到站 → 释放起点
|
||||
@@ -282,7 +282,7 @@ FindRoute → Compile → 多行脚本:
|
||||
|
||||
### 方式 2:Loop 全程任务(阶段 4,默认)
|
||||
|
||||
**触发**:`goalSite` 标签 + 编译脚本含 `MagFass2Go` + `EnableLoopTaskDrive=true`。
|
||||
**触发**:`goalSite` 标签 + 编译脚本含 `Mag2Go` + `EnableLoopTaskDrive=true`。
|
||||
|
||||
**流程**:
|
||||
|
||||
@@ -298,7 +298,7 @@ LoopMission 写 goalSite
|
||||
|
||||
---
|
||||
|
||||
### 方式 3:单段 MagFass2Go(脚本一行)
|
||||
### 方式 3:单段 Mag2Go(脚本一行)
|
||||
|
||||
**触发**:脚本里只有一行,或无 `goalSite` 的测试。
|
||||
|
||||
@@ -370,7 +370,7 @@ LoopMission 写 goalSite
|
||||
|------|------|
|
||||
| 映射协议字段 | `Node`(节点号) |
|
||||
| 何时配置 | 磁导航地标号与地图 `site.id` **不一致**时必须配置 |
|
||||
| 车辆开关 | `MagFass2Car.UseTagValueAsNode = true` |
|
||||
| 车辆开关 | `Mag2Car.UseTagValueAsNode = true` |
|
||||
| 示例 | `TagValue = 10086` |
|
||||
| 注意 | 未配置且 `UseTagValueAsNode=false` 时,直接用 `site.id` 作为节点号 |
|
||||
|
||||
@@ -387,9 +387,9 @@ LoopMission 写 goalSite
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 映射协议字段 | 无(当前版本不直接写入节点块) |
|
||||
| 映射协议字段 | 无(不写入节点块) |
|
||||
| 规划值 | `None` / `Button` / `Plc` / `Api` |
|
||||
| 当前状态 | 字段已定义,**等待放行联动逻辑待扩展**;可通过修改 `Fass2_StartStop` 或外部改 fields 触发重发 |
|
||||
| 当前状态 | 人工/外部放行源预留。**管控区按车放行**请用 `Fass2_StartStop=12` + `Fass2_ControlArea`(站点列表),不要靠改地图 `Fass2_StartStop` 放行 |
|
||||
|
||||
---
|
||||
|
||||
@@ -401,8 +401,8 @@ LoopMission 写 goalSite
|
||||
|----|---------------------------|----------|
|
||||
| `1` | 过站(不停) | 路径中间站、通道站 |
|
||||
| `2` | 普通停车 | 工位、等待点、装卸点 |
|
||||
| `11` | 管控启动 | 管控区入口,需授权后启动 |
|
||||
| `12` | 管控停止 | 管控区停车等待 |
|
||||
| `11` | 管控启动 | 本车放行后写入 **0xA1 / 后续 0xB1**(地图站点仍保持 12) |
|
||||
| `12` | 管控停止 | 管控区停车,等调度按车放行 |
|
||||
| `22` | 精准停止 | 装配、对接,精度要求高 |
|
||||
|
||||
**默认规则**(未显式配置 `Fass2_StartStop` 时,`Fass2TaskBuilder` 自动决定):
|
||||
@@ -413,7 +413,17 @@ LoopMission 写 goalSite
|
||||
| 末站(goalSite) | `2`(停车) |
|
||||
| 末站 + `Fass2_PrecisionStop=true` | `22`(精准停) |
|
||||
|
||||
**动作完成判定**:若配置了非过站值,状态机要求上报 `Node.StartStop` 与期望值一致,且车体非运行态(`State≠1`),才推进下一段。
|
||||
**动作完成判定**:`2`/`22` 要求上报 `StartStop` 一致且车体非运行态。`12` **不会**因上报 12 就推进:必须等同区占用条件满足后,调度把本车期望值改为 `11`(或 `1`),车体回显后再离站。
|
||||
|
||||
**管控停止按车放行(`12`)**:
|
||||
|
||||
- 地图 `Fass2_StartStop` **始终保持 12**,不要改成 1 来放行(否则下一辆车的 0xB1 也会变成过站)。
|
||||
- `0xB1` 窗口截断在第一个尚未放行的 12,其后站点不会出现在同一帧里。
|
||||
- 本车到站并停稳后,统计 `Fass2_ControlArea` **站点列表**上的他车占用(`holdingLocks` + Mag2 `siteID`)。他车数 **≥** `Fass2_ControlCapacity`(默认 1,即数量与容量相等)则不放行;本车不计入,避免停在列表内时永远走不了。
|
||||
- 放行只改**本车**报文:`0xA1` 把当前节点 `12→11`(可用 `Fass2_ControlReleaseStartStop=1` 改为过站),并补发含后续站的 `0xB1`。下一辆车仍会收到 12。
|
||||
- 等待放行期间不消耗移动超时。断线重连若仍停在该管控点,会重新判定占用;条件满足则再次发放行报文。
|
||||
|
||||
相关站点字段:`Fass2_ControlArea`(站点 ID 数组,如 `10,11,12`)、`Fass2_ControlCapacity`、`Fass2_ControlReleaseStartStop`。未配或解析不出站点时,只统计当前管控点本身。
|
||||
|
||||
**与 `Fass2_PrecisionStop` 关系**:
|
||||
|
||||
@@ -614,6 +624,9 @@ LoopMission 写 goalSite
|
||||
| `Fass2_Tray` | byte | `Tray` | 是(非 0 时) |
|
||||
| `Fass2_Shutdown` | byte | `Shutdown` | 是(非 0 时) |
|
||||
| `Fass2_WaitMode` | string | — | 预留 |
|
||||
| `Fass2_ControlArea` | string | — | 管控站点列表,如 `10,11,12` |
|
||||
| `Fass2_ControlCapacity` | int | — | 列表内他车数达到该值则不放行,默认 1 |
|
||||
| `Fass2_ControlReleaseStartStop` | byte | 放行后的 `StartStop` | `11`(默认)或 `1` |
|
||||
|
||||
---
|
||||
|
||||
@@ -632,7 +645,7 @@ LoopMission 写 goalSite
|
||||
| `magSelect` | byte | `Byroad` | 磁导航 coder 分叉序号;`Fass2_Byroad` 为空时作为 Byroad 备选 |
|
||||
| `Fass2_Direction` | byte | `Direction` | 边级车头旋转;站点未覆盖时生效 |
|
||||
| `Fass2_Orientation` | byte | `Orientation` | 边级朝向;站点未覆盖时生效 |
|
||||
| `Magnet` | bool | — | `true` 启用 `MagneticTrackCoder`(`agv.MagGo` 等),与 `MagFass2Go` 模板独立 |
|
||||
| `Magnet` | bool | — | `true` 启用 `MagneticTrackCoder`(`agv.MagGo` 等),与 `Mag2Go` 模板独立 |
|
||||
| `ReverseDst` | int | — | 倒车目的地站 ID;磁 coder 跳过该边 |
|
||||
|
||||
**距离 `Distance`**:由地图坐标自动计算(上一站与本站欧氏距离,mm 取整),一般无需手配。
|
||||
@@ -717,6 +730,21 @@ Fass2_Charge = 2 # 开始充电
|
||||
|
||||
---
|
||||
|
||||
#### 场景 E2:管控停止(按车放行)
|
||||
|
||||
在 12 站点填写要监控的站点列表;**地图 `Fass2_StartStop` 保持 12**。
|
||||
|
||||
```text
|
||||
Fass2_StartStop = 12
|
||||
Fass2_ControlArea = 10,11,12 # 这些站上有他车且数量达到容量则不放行
|
||||
Fass2_ControlCapacity = 1
|
||||
# Fass2_ControlReleaseStartStop = 11 # 默认;改 1 则放行后按过站离站
|
||||
```
|
||||
|
||||
列表 `10,11,12` 上已有 1 辆他车时,本车停在 12 等待;他车离开列表后,调度只给本车发 `0xA1`(12→11)。下一辆车仍会先收到 12。
|
||||
|
||||
---
|
||||
|
||||
#### 场景 F:Y 型磁分叉口
|
||||
|
||||
**站点**(推荐在岔口站配):
|
||||
@@ -792,7 +820,7 @@ Fass2_Obstacle = 2
|
||||
2. 匹配 tasklist:currentStationId = N → 分配 targetStationId
|
||||
3. 写 tags:goalSite = 目标站 ID
|
||||
4. LoopStartAction 调用 GoSite
|
||||
5. MagFass2Car 拦截 → 全程任务到 goalSite
|
||||
5. Mag2Car 拦截 → 全程任务到 goalSite
|
||||
6. 到达后 GoSite 清理 goalSite / occupied / loopAssigned
|
||||
7. 下一轮任务匹配
|
||||
```
|
||||
@@ -809,7 +837,7 @@ Fass2_Obstacle = 2
|
||||
|
||||
- **不需要改 Core**;`LoopMission` 逻辑完全沿用
|
||||
- `磁导航FASS2环线` 仅为场景标识,行为与 `LoopMission` 相同
|
||||
- 真正区别在 **车型** 是否 `MagFass2Car` 且 `EnableLoopTaskDrive=true`
|
||||
- 真正区别在 **车型** 是否 `Mag2Car` 且 `EnableLoopTaskDrive=true`
|
||||
|
||||
### 9.4 关闭 Loop 拦截(恢复多段脚本)
|
||||
|
||||
@@ -817,7 +845,7 @@ Fass2_Obstacle = 2
|
||||
EnableLoopTaskDrive = false
|
||||
```
|
||||
|
||||
Loop 仍走 Compile 多行 `MagFass2Go`,每行一段边。
|
||||
Loop 仍走 Compile 多行 `Mag2Go`,每行一段边。
|
||||
|
||||
---
|
||||
|
||||
@@ -890,7 +918,7 @@ Idle → Planning → Moving → AtStation → SegmentDone → … → Complete
|
||||
| 重置UDP监听 | — | 重新注册 UDP 会话 |
|
||||
| 重置TCP连接 | — | 关闭长连接(TCP 模式) |
|
||||
|
||||
日志前缀:`[MagFass2Car:车名(id)]`,任务状态机:`task-sm`。
|
||||
日志前缀:`[Mag2Car:车名(id)]`,任务状态机:`task-sm`。
|
||||
|
||||
---
|
||||
|
||||
@@ -936,13 +964,13 @@ Idle → Planning → Moving → AtStation → SegmentDone → … → Complete
|
||||
看日志:
|
||||
|
||||
- `loop goal drive intercept` → 全程任务(阶段 4)
|
||||
- `script begin` + 多行 `MagFass2Go` → 多段脚本
|
||||
- `script begin` + 多行 `Mag2Go` → 多段脚本
|
||||
|
||||
### Q6:与 MagCar 混用注意事项
|
||||
|
||||
- `EventCarMission` **只控制 MagCar**
|
||||
- 同场景两种车型时,tasklist 和车辆类型要对应
|
||||
- FASS 1.0 用 `MagCar`,FASS 2.0 用 `MagFass2Car`
|
||||
- FASS 1.0 用 `MagCar`,FASS 2.0 用 `Mag2Car`
|
||||
|
||||
---
|
||||
|
||||
@@ -964,7 +992,7 @@ MoveTimeoutSeconds = 120
|
||||
### 13.2 上线前检查清单
|
||||
|
||||
- [ ] 插件 `StandardScene.Magnetic.dll` 已加载
|
||||
- [ ] 车型为 `MagFass2Car`(不是 `MagCar`)
|
||||
- [ ] 车型为 `Mag2Car`(不是 `MagCar`)
|
||||
- [ ] `VehicleCode`、IP、端口与车体一致
|
||||
- [ ] UDP 能收到 100B,日志有 `0x10` 应答
|
||||
- [ ] 关键站点 `TagValue` / `Fass2_*` 已配置
|
||||
@@ -993,12 +1021,12 @@ MoveTimeoutSeconds = 120
|
||||
|
||||
| 模块 | 路径 |
|
||||
|------|------|
|
||||
| 车型 | `CarTypes/MagFass2Car.cs` |
|
||||
| 车型 | `CarTypes/Mag2Car.cs` |
|
||||
| 协议 | `Protocol/Fass2Protocol.cs`、`Fass2UdpHub.cs` |
|
||||
| 组包 | `Tasking/Fass2TaskBuilder.cs` |
|
||||
| 状态机 | `Tasking/Fass2TaskStateMachine.cs` |
|
||||
| 动作判定 | `Tasking/Fass2ActionResolver.cs` |
|
||||
| 环线 Mission | `Chained/MagFass2LoopMission.cs` |
|
||||
| 环线 Mission | `Chained/Mag2LoopMission.cs` |
|
||||
| 站点字段常量 | `Tasking/Fass2SiteFields.cs` |
|
||||
|
||||
---
|
||||
@@ -1,7 +1,7 @@
|
||||
# Export MagFass2Car guide Markdown to PDF (requires Chrome or Edge)
|
||||
# Export Mag2Car guide Markdown to PDF (requires Chrome or Edge)
|
||||
param(
|
||||
[string]$InputMd = "$PSScriptRoot\MagFass2Car使用指南.md",
|
||||
[string]$OutputPdf = "$PSScriptRoot\MagFass2Car使用指南.pdf"
|
||||
[string]$InputMd = "$PSScriptRoot\Mag2Car使用指南.md",
|
||||
[string]$OutputPdf = "$PSScriptRoot\Mag2Car使用指南.pdf"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -147,7 +147,7 @@ $html = @"
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>MagFass2Car Guide</title>
|
||||
<title>Mag2Car Guide</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 18mm 16mm; }
|
||||
body { font-family: "Microsoft YaHei", "Segoe UI", sans-serif; color: #222; line-height: 1.55; font-size: 11pt; }
|
||||
|
||||
Reference in New Issue
Block a user