using SimpleCore; using SimpleCore.PropType; using StandardScene.Magnetic.Protocol; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace StandardScene.Magnetic.Tasking { public sealed class Fass2TaskCallbacks { public Func BuildPlan { get; set; } public Action SendNodes { get; set; } public Action SendAction { get; set; } public Action SendControl { get; set; } public Func ResolveNodeId { get; set; } public Func ResolveSite { get; set; } public Action Log { get; set; } public Action Persist { get; set; } public Action ClearPersisted { get; set; } public Func AllocateTaskId { get; set; } public Func AllocateActionId { get; set; } } public sealed class Fass2TaskTickResult { public Fass2TaskPhase Phase { get; set; } public bool Dispatched { get; set; } public bool Rebuilt { get; set; } public bool ActionSent { get; set; } public bool Advanced { get; set; } public bool Completed { get; set; } public bool Faulted { get; set; } public string Message { get; set; } } /// /// FASS 2.0 任务状态机:规划 → 滑动窗口下发 → UDP/TCP 状态驱动推进 → 动作闭环。 /// public sealed class Fass2TaskStateMachine { private readonly Fass2TaskCallbacks _callbacks; private readonly object _syncRoot = new object(); private TaskCompletionSource _completionSource; public Fass2TaskStateMachine(Fass2TaskCallbacks callbacks) { _callbacks = callbacks ?? throw new ArgumentNullException(nameof(callbacks)); Context = new Fass2TaskContext(); } public Fass2TaskContext Context { get; } public int LockCount { get; set; } = 4; public int ResendIntervalMs { get; set; } = 500; public int ActionRetryIntervalMs { get; set; } = 1000; public bool StartBeforeMove { get; set; } = true; public ushort HeadingAngle { get; set; } public bool IsIdle { get { lock (_syncRoot) { return Context.Phase is Fass2TaskPhase.Idle or Fass2TaskPhase.Complete or Fass2TaskPhase.Cancelled; } } } public bool IsRunning { get { lock (_syncRoot) { return Context.Phase is Fass2TaskPhase.Planning or Fass2TaskPhase.Dispatching or Fass2TaskPhase.Moving or Fass2TaskPhase.AtStation or Fass2TaskPhase.SegmentDone; } } } public event Action Completed; public event Action Faulted; public void Begin(int startSiteId, int goalSiteId, double defaultSpeed = -1) { lock (_syncRoot) { if (!IsIdle) { throw new InvalidOperationException($"task state machine busy, phase={Context.Phase}"); } ResetCompletionSource(); Context.Phase = Fass2TaskPhase.Planning; Context.StartSiteId = startSiteId; Context.GoalSiteId = goalSiteId; Context.CurrentIndex = 0; Context.DefaultSpeed = defaultSpeed; Context.StartedAt = DateTime.Now; Context.FaultReason = null; Context.Plan = null; Context.FieldsSignature = string.Empty; Context.LastDispatchAt = DateTime.MinValue; Context.LastActionSentAt = DateTime.MinValue; Context.LastActionId = 0; Log($"task begin start={startSiteId}, goal={goalSiteId}, speed={defaultSpeed}"); Persist(); } } public bool TryRestore(Fass2TaskContext saved) { if (saved == null) { return false; } lock (_syncRoot) { if (IsRunning) { return false; } ResetCompletionSource(); Context.Phase = saved.Phase; Context.StartSiteId = saved.StartSiteId; Context.GoalSiteId = saved.GoalSiteId; Context.CurrentIndex = saved.CurrentIndex; Context.TaskId = saved.TaskId; Context.DefaultSpeed = saved.DefaultSpeed; Context.FieldsSignature = saved.FieldsSignature ?? string.Empty; Context.StartedAt = DateTime.Now; Context.LastDispatchAt = DateTime.MinValue; Context.LastActionSentAt = DateTime.MinValue; Context.Plan = null; Context.FaultReason = null; if (Context.Phase == Fass2TaskPhase.Planning) { Log($"task restored phase={Context.Phase}, goal={Context.GoalSiteId}, index={Context.CurrentIndex}"); Persist(); return true; } Context.Phase = Fass2TaskPhase.Planning; Log($"task restored and normalized to Planning, goal={Context.GoalSiteId}, index={Context.CurrentIndex}"); Persist(); return true; } } public Fass2TaskTickResult Tick(Fass2StateReport report) { lock (_syncRoot) { if (Context.Phase is Fass2TaskPhase.Idle or Fass2TaskPhase.Complete or Fass2TaskPhase.Cancelled or Fass2TaskPhase.Fault) { return new Fass2TaskTickResult { Phase = Context.Phase }; } 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); } if (report?.State == 4) { return Fault("vehicle fault", result); } switch (Context.Phase) { case Fass2TaskPhase.Planning: return PlanAndDispatch(report, result); case Fass2TaskPhase.Dispatching: Context.Phase = Fass2TaskPhase.Moving; result.Phase = Context.Phase; return result; case Fass2TaskPhase.Moving: return HandleMoving(report, result); case Fass2TaskPhase.AtStation: return HandleAtStation(report, result); case Fass2TaskPhase.SegmentDone: return HandleSegmentDone(report, result); default: return result; } } } public void Cancel(string reason) { lock (_syncRoot) { if (IsIdle) { return; } Context.Phase = Fass2TaskPhase.Cancelled; Context.FaultReason = reason; Log($"task cancelled: {reason}"); ClearPersisted(); _completionSource?.TrySetException(new OperationCanceledException(reason)); } } public async Task WaitAsync(int timeoutMs, Func poll, int pollIntervalMs, CancellationToken cancellationToken = default) { if (poll == null) { throw new ArgumentNullException(nameof(poll)); } TaskCompletionSource waiter; lock (_syncRoot) { waiter = _completionSource ?? ResetCompletionSource(); } var timeoutTask = Task.Delay(timeoutMs, cancellationToken); var pollTask = Task.Run(async () => { while (!cancellationToken.IsCancellationRequested) { Fass2StateReport report; lock (_syncRoot) { if (!IsRunning) { return; } } report = poll(); Tick(report); lock (_syncRoot) { if (!IsRunning) { return; } } await Task.Delay(Math.Max(20, pollIntervalMs), cancellationToken); } }, cancellationToken); var finished = await Task.WhenAny(waiter.Task, timeoutTask); if (finished == timeoutTask) { Cancel("wait timeout"); throw new TimeoutException( $"FASS2 task timeout after {timeoutMs}ms, phase={Context.Phase}, index={Context.CurrentIndex}, goal={Context.GoalSiteId}"); } await waiter.Task; } /// /// UDP 模式由 Hub 回调驱动 Tick,此处只等待完成信号,避免与 OnUdpStateReceived 争用锁导致死锁。 /// public async Task WaitForCompletionAsync(int timeoutMs, CancellationToken cancellationToken = default) { TaskCompletionSource waiter; lock (_syncRoot) { if (Context.Phase is Fass2TaskPhase.Complete or Fass2TaskPhase.Cancelled or Fass2TaskPhase.Fault) { return; } waiter = _completionSource ?? ResetCompletionSource(); } var timeoutTask = Task.Delay(timeoutMs, cancellationToken); var finished = await Task.WhenAny(waiter.Task, timeoutTask); if (finished == timeoutTask) { Cancel("wait timeout"); throw new TimeoutException( $"FASS2 task timeout after {timeoutMs}ms, phase={Context.Phase}, index={Context.CurrentIndex}, goal={Context.GoalSiteId}"); } await waiter.Task; } private Fass2TaskTickResult PlanAndDispatch(Fass2StateReport report, Fass2TaskTickResult result) { var startSiteId = ResolveCurrentSiteId(report); Context.Plan = _callbacks.BuildPlan(startSiteId, Context.GoalSiteId, Context.DefaultSpeed); if (Context.Plan == null || Context.Plan.Nodes.Count == 0) { return Fault($"empty task plan from {startSiteId} to {Context.GoalSiteId}", result); } if (Context.CurrentIndex >= Context.Plan.Nodes.Count) { Context.CurrentIndex = Math.Max(0, Context.Plan.Nodes.Count - 1); } Context.FieldsSignature = Context.Plan.FieldsSignature; Context.TaskId = _callbacks.AllocateTaskId(); if (StartBeforeMove && Context.CurrentIndex == 0) { _callbacks.SendControl(Fass2Protocol.CmdStart, HeadingAngle); } DispatchCurrentWindow(); Context.Phase = Fass2TaskPhase.Moving; result.Phase = Context.Phase; result.Dispatched = true; result.Message = $"planned nodes={Context.Plan.Nodes.Count}, task={Context.TaskId}"; Log(result.Message); Persist(); return result; } private Fass2TaskTickResult HandleMoving(Fass2StateReport report, Fass2TaskTickResult result) { if (TryRebuildForFieldsChange(result)) { return result; } if (ShouldResend()) { DispatchCurrentWindow(); result.Dispatched = true; } var expected = GetExpectedNode(); if (!Fass2ActionResolver.IsAtNode(report, expected.Node)) { result.Message = $"moving node={report.Node.Node}, expect={expected.Node}"; return result; } if (Fass2ActionResolver.IsVehicleMoving(report.State)) { result.Message = $"at node={expected.Node}, still running"; return result; } Context.Phase = Fass2TaskPhase.AtStation; result.Phase = Context.Phase; result.Message = $"arrived site index={Context.CurrentIndex}, node={expected.Node}"; Log(result.Message); Persist(); return result; } private Fass2TaskTickResult HandleAtStation(Fass2StateReport report, Fass2TaskTickResult result) { if (TryRebuildForFieldsChange(result)) { return result; } if (ShouldResend()) { DispatchCurrentWindow(); result.Dispatched = true; } var expected = GetExpectedNode(); if (!Fass2ActionResolver.IsAtNode(report, expected.Node)) { Context.Phase = Fass2TaskPhase.Moving; result.Phase = Context.Phase; result.Message = $"left station node={report.Node.Node}, expect={expected.Node}"; return result; } if (!Fass2ActionResolver.IsStationActionComplete(expected, report.Node, report.State)) { if (ShouldSendActionPatch()) { var patch = Fass2ActionResolver.BuildActionPatch(expected, report.Node); if (patch != null) { Context.LastActionId = _callbacks.AllocateActionId(); Context.LastActionSentAt = DateTime.Now; _callbacks.SendAction(patch, Context.LastActionId); result.ActionSent = true; result.Message = $"action pending [{Fass2ActionResolver.DescribePending(expected, report.Node)}], sent 0xA1={Context.LastActionId}"; Log(result.Message); } } return result; } Context.Phase = Fass2TaskPhase.SegmentDone; result.Phase = Context.Phase; result.Message = $"station done index={Context.CurrentIndex}, node={expected.Node}"; Log(result.Message); Persist(); return result; } private Fass2TaskTickResult HandleSegmentDone(Fass2StateReport report, Fass2TaskTickResult result) { Context.CurrentIndex++; result.Advanced = true; if (Context.Plan == null || Context.CurrentIndex >= Context.Plan.Nodes.Count) { Context.Phase = Fass2TaskPhase.Complete; result.Phase = Context.Phase; result.Completed = true; result.Message = $"task complete goal={Context.GoalSiteId}"; Log(result.Message); ClearPersisted(); Completed?.Invoke(Context); _completionSource?.TrySetResult(1); return result; } Context.Phase = Fass2TaskPhase.Dispatching; DispatchCurrentWindow(); Context.Phase = Fass2TaskPhase.Moving; result.Phase = Context.Phase; result.Dispatched = true; result.Message = $"segment advanced index={Context.CurrentIndex}"; Log(result.Message); Persist(); return result; } private bool TryRebuildForFieldsChange(Fass2TaskTickResult result) { if (Context.Plan?.SiteIds == null || Context.Plan.SiteIds.Count == 0) { return false; } var newSignature = Fass2SiteFieldReader.BuildFieldsSignature( Context.Plan.SiteIds, Context.CurrentIndex, SimpleLib.GetSite); if (string.Equals(newSignature, Context.FieldsSignature, StringComparison.Ordinal)) { return false; } var currentSiteId = Context.Plan.SiteIds[Context.CurrentIndex]; Log($"fields changed at index={Context.CurrentIndex}, rebuild from site={currentSiteId}"); Context.Plan = _callbacks.BuildPlan(currentSiteId, Context.GoalSiteId, Context.DefaultSpeed); if (Context.Plan == null || Context.Plan.Nodes.Count == 0) { Fault($"rebuild failed from {currentSiteId} to {Context.GoalSiteId}", result); return true; } Context.CurrentIndex = 0; Context.FieldsSignature = Context.Plan.FieldsSignature; Context.TaskId = _callbacks.AllocateTaskId(); DispatchCurrentWindow(); Context.Phase = Fass2TaskPhase.Moving; result.Phase = Context.Phase; result.Rebuilt = true; result.Dispatched = true; result.Message = $"rebuilt nodes={Context.Plan.Nodes.Count}, task={Context.TaskId}"; Persist(); return true; } private void DispatchCurrentWindow() { var window = BuildDispatchWindow(); if (window.Length == 0) { throw new InvalidOperationException("dispatch window is empty"); } _callbacks.SendNodes(window, Context.TaskId); Context.LastDispatchAt = DateTime.Now; Log($"dispatch window count={window.Length}, fromIndex={Context.CurrentIndex}, task={Context.TaskId}"); } private Fass2NodeMessage[] BuildDispatchWindow() { var nodes = Context.Plan.Nodes; var start = Context.CurrentIndex; var remaining = nodes.Count - start; var windowSize = Math.Max(1, Math.Min(LockCount, remaining)); windowSize = Math.Min(windowSize, 10); var window = new Fass2NodeMessage[windowSize]; for (var i = 0; i < windowSize; i++) { window[i] = nodes[start + i]; } return window; } private Fass2NodeMessage GetExpectedNode() { return Context.Plan.Nodes[Context.CurrentIndex]; } private int ResolveCurrentSiteId(Fass2StateReport report) { if (Context.CurrentIndex > 0 && Context.Plan?.SiteIds != null && Context.CurrentIndex < Context.Plan.SiteIds.Count) { return Context.Plan.SiteIds[Context.CurrentIndex]; } if (report?.Node != null && report.Node.Node != 0) { var site = _callbacks.ResolveSite(report.Node.Node); if (site != null) { return site.id; } } return Context.StartSiteId; } private bool ShouldResend() { return Context.LastDispatchAt == DateTime.MinValue || (DateTime.Now - Context.LastDispatchAt).TotalMilliseconds >= ResendIntervalMs; } private bool ShouldSendActionPatch() { return Context.LastActionSentAt == DateTime.MinValue || (DateTime.Now - Context.LastActionSentAt).TotalMilliseconds >= ActionRetryIntervalMs; } private Fass2TaskTickResult Fault(string reason, Fass2TaskTickResult result) { Context.Phase = Fass2TaskPhase.Fault; Context.FaultReason = reason; result.Phase = Context.Phase; result.Faulted = true; result.Message = reason; Log($"task fault: {reason}"); Persist(); Faulted?.Invoke(Context, reason); _completionSource?.TrySetException(new InvalidOperationException(reason)); return result; } private void Persist() { _callbacks.Persist?.Invoke(Context); } private void ClearPersisted() { _callbacks.ClearPersisted?.Invoke(); } private void Log(string message) { _callbacks.Log?.Invoke(message); } private TaskCompletionSource ResetCompletionSource() { _completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); return _completionSource; } } }