fix: FASS2 状态机与 UDP 枢纽审查项

等锁超时、全路径签名、无锁窗口不下发、Fault 终态可等待、Pass+机构不再短路、Alarm 不杀单、catch-up 停在未完成站;VehicleCode=0/重复车号拒绝注册,Hub 已运行时不抢监听口;FileLogger 空目录可写。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
黄兆尉
2026-08-26 23:04:58 +08:00
co-authored by Cursor
parent 29a06f004f
commit e4a245e644
13 changed files with 465 additions and 48 deletions
@@ -80,6 +80,9 @@ namespace StandardScene.Magnetic.Tasking
public int ActionRetryIntervalMs { get; set; } = 1000;
/// <summary>交管等锁 / 管控等放行上限。0 表示不限制。</summary>
public int TrafficWaitTimeoutMs { get; set; } = 180_000;
public bool StartBeforeMove { get; set; } = true;
public ushort HeadingAngle { get; set; }
@@ -90,7 +93,8 @@ namespace StandardScene.Magnetic.Tasking
{
lock (_syncRoot)
{
return Context.Phase is Fass2TaskPhase.Idle or Fass2TaskPhase.Complete or Fass2TaskPhase.Cancelled;
return Context.Phase is Fass2TaskPhase.Idle or Fass2TaskPhase.Complete
or Fass2TaskPhase.Cancelled or Fass2TaskPhase.Fault;
}
}
}
@@ -327,11 +331,6 @@ namespace StandardScene.Magnetic.Tasking
var result = new Fass2TaskTickResult { Phase = Context.Phase };
if (report?.Alarm != 0)
{
return Fault($"alarm=0x{report.Alarm:X}", result);
}
if (report?.State == 3)
{
return Fault("vehicle emergency stop", result);
@@ -432,14 +431,25 @@ namespace StandardScene.Magnetic.Tasking
public async Task WaitForCompletionAsync(int timeoutMs, CancellationToken cancellationToken = default)
{
TaskCompletionSource<int> waiter;
string failReason;
lock (_syncRoot)
{
if (Context.Phase is Fass2TaskPhase.Complete or Fass2TaskPhase.Cancelled or Fass2TaskPhase.Fault)
if (Context.Phase == Fass2TaskPhase.Complete)
{
return;
}
waiter = _completionSource ?? ResetCompletionSource();
waiter = _completionSource;
failReason = Context.FaultReason;
if (Context.Phase is not Fass2TaskPhase.Cancelled and not Fass2TaskPhase.Fault)
{
waiter = waiter ?? ResetCompletionSource();
}
}
if (waiter == null)
{
throw new InvalidOperationException(failReason ?? "task ended without completion source");
}
await WaitWithTrafficPauseAsync(waiter, timeoutMs, cancellationToken);
@@ -459,6 +469,7 @@ namespace StandardScene.Magnetic.Tasking
var remainingMs = timeoutMs <= 0 ? -1 : timeoutMs;
const int sliceMs = 500;
var lastPauseLog = DateTime.MinValue;
var pauseElapsedMs = 0;
while (true)
{
@@ -500,6 +511,17 @@ namespace StandardScene.Magnetic.Tasking
if (waitingPaused)
{
if (TrafficWaitTimeoutMs > 0)
{
pauseElapsedMs += delayMs;
if (pauseElapsedMs >= TrafficWaitTimeoutMs)
{
Cancel($"{pauseKind} timeout after {TrafficWaitTimeoutMs}ms");
throw new TimeoutException(
$"FASS2 {pauseKind} timeout after {TrafficWaitTimeoutMs}ms, phase={Context.Phase}, index={Context.CurrentIndex}, goal={Context.GoalSiteId}");
}
}
if ((DateTime.Now - lastPauseLog).TotalSeconds >= 5)
{
lastPauseLog = DateTime.Now;
@@ -509,6 +531,8 @@ namespace StandardScene.Magnetic.Tasking
continue;
}
pauseElapsedMs = 0;
if (remainingMs < 0)
{
continue;
@@ -552,7 +576,13 @@ namespace StandardScene.Magnetic.Tasking
}
catch (Exception ex)
{
return Fault($"traffic prepare failed: {ex.Message}", result);
Context.Phase = Fass2TaskPhase.Moving;
Context.WaitingForTraffic = true;
result.Phase = Context.Phase;
result.Message = $"traffic prepare wait: {ex.Message}";
Log(result.Message);
Persist();
return result;
}
if (StartBeforeMove && Context.CurrentIndex == 0)
@@ -884,7 +914,7 @@ namespace StandardScene.Magnetic.Tasking
}
var newSignature = Fass2SiteFieldReader.BuildFieldsSignature(
Context.Plan.SiteIds, Context.CurrentIndex, SimpleLib.GetSite);
Context.Plan.SiteIds, 0, SimpleLib.GetSite);
if (string.Equals(newSignature, Context.FieldsSignature, StringComparison.Ordinal))
{
return false;
@@ -1005,7 +1035,12 @@ namespace StandardScene.Magnetic.Tasking
lockedCount = Fass2ControlAreaGate.LimitWindowCount(
nodes, start, lockedCount, Context.ControlReleasedIndex);
lockedCount = Math.Max(1, Math.Min(lockedCount, wantWindow));
lockedCount = Math.Max(0, Math.Min(lockedCount, wantWindow));
if (lockedCount <= 0)
{
return Array.Empty<Fass2NodeMessage>();
}
var window = new Fass2NodeMessage[lockedCount];
for (var i = 0; i < lockedCount; i++)
{
@@ -1039,8 +1074,12 @@ namespace StandardScene.Magnetic.Tasking
var catchTo = reportIndex;
for (var i = Context.CurrentIndex; i < reportIndex; i++)
{
if (Fass2ControlAreaGate.IsControlStop(Context.Plan.Nodes[i].StartStop) &&
Context.ControlReleasedIndex != i)
var node = Context.Plan.Nodes[i];
var unreleasedControl = Fass2ControlAreaGate.IsControlStop(node.StartStop) &&
Context.ControlReleasedIndex != i;
var actionStation = Fass2ActionResolver.RequiresActionWait(node) &&
node.StartStop != Fass2TaskBuilder.StartStopControlStart;
if (unreleasedControl || actionStation)
{
catchTo = i;
break;