using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Threading; using System.Threading.Tasks; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; using MultiWheelC.TrajectoryPlanning.EMPlanner; using MultiWheelC.TrajectoryPlanning.Mapping; using MultiWheelC.TrajectoryPlanning.PathSmoothing; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; /// 一次观察会话启动的不可变结果,成功时同时携带冻结的粗路径、平滑路径、地图和方向段。 public sealed class TrajectoryObservationBootstrapResult { private readonly VehicleParameters vehicle; private TrajectoryObservationBootstrapResult(CoarsePathPlanningJob job, CoarsePathPlanningJobResult coarse, PathSmoothingResult smoothedPath, IReadOnlyList segments, string failureReason) { Job = job ?? throw new ArgumentNullException(nameof(job)); vehicle = CopyVehicle(job.Vehicle); CoarseResult = coarse; SmoothedPath = smoothedPath; Segments = CopySegments(segments); FailureReason = failureReason ?? string.Empty; Succeeded = coarse != null && smoothedPath != null && Segments.Count > 0 && FailureReason.Length == 0; } public bool Succeeded { get; } public CoarsePathPlanningJob Job { get; } public VehicleParameters Vehicle => CopyVehicle(vehicle); public CoarsePathPlanningJobResult CoarseResult { get; } public PathSmoothingResult SmoothedPath { get; } public IReadOnlyList Segments { get; } public string FailureReason { get; } public PlanningGridMap Map => CoarseResult?.MapResult?.Map; public static TrajectoryObservationBootstrapResult FromFailure(CoarsePathPlanningJob job, CoarsePathPlanningJobResult coarse, PathSmoothingResult smoothedPath, string failureReason) { return new TrajectoryObservationBootstrapResult(job, coarse, smoothedPath, Array.Empty(), string.IsNullOrEmpty(failureReason) ? "Planning bootstrap failed." : failureReason); } public static TrajectoryObservationBootstrapResult Success(CoarsePathPlanningJob job, CoarsePathPlanningJobResult coarse, PathSmoothingResult smoothedPath, IReadOnlyList segments) { if (coarse == null) throw new ArgumentNullException(nameof(coarse)); if (smoothedPath == null) throw new ArgumentNullException(nameof(smoothedPath)); if (segments == null || segments.Count == 0) throw new ArgumentException("A successful bootstrap requires reference segments.", nameof(segments)); return new TrajectoryObservationBootstrapResult(job, coarse, smoothedPath, segments, string.Empty); } private static IReadOnlyList CopySegments(IReadOnlyList source) { var copy = new List(source == null ? 0 : source.Count); if (source != null) { for (int index = 0; index < source.Count; index++) copy.Add(source[index]); } return new ReadOnlyCollection(copy); } private static VehicleParameters CopyVehicle(VehicleParameters source) { if (source == null) return null; return new VehicleParameters { LengthMeters = source.LengthMeters, WidthMeters = source.WidthMeters, SafetyMarginMeters = source.SafetyMarginMeters, MaximumCurvaturePerMeter = source.MaximumCurvaturePerMeter, MinimumTurningRadiusMeters = source.MinimumTurningRadiusMeters, }; } } /// 只在会话开始阶段构造并冻结规划输入;后续观察 tick 不会重新读取或改写该快照。 public sealed class TrajectoryObservationBootstrapper { private readonly CoarsePathPlanningService coarseService; private readonly PathSmoothingService smoothingService; public TrajectoryObservationBootstrapper() : this(new CoarsePathPlanningService(), new PathSmoothingService()) { } public TrajectoryObservationBootstrapper(CoarsePathPlanningService coarseService, PathSmoothingService smoothingService) { this.coarseService = coarseService ?? throw new ArgumentNullException(nameof(coarseService)); this.smoothingService = smoothingService ?? throw new ArgumentNullException(nameof(smoothingService)); } public TrajectoryObservationBootstrapResult Bootstrap(CoarsePathPlanningJob job, CancellationToken cancellationToken = default) { if (job == null) throw new ArgumentNullException(nameof(job)); TimeSpan configuredLimit = job.Configuration == null ? TimeSpan.FromSeconds(5d) : job.Configuration.SearchTimeout; using var deadline = new TrajectoryObservationPlanningDeadline(configuredLimit, cancellationToken); return Bootstrap(job, deadline, cancellationToken); } public TrajectoryObservationBootstrapResult Bootstrap(CoarsePathPlanningJob job, TrajectoryObservationPlanningDeadline deadline, CancellationToken cancellationToken = default) { if (job == null) throw new ArgumentNullException(nameof(job)); if (deadline == null) throw new ArgumentNullException(nameof(deadline)); if (DeadlineExpired(deadline)) return DeadlineFailure(job, null, null, deadline, "coarse"); CoarsePathPlanningJob effectiveJob = CopyJobWithDeadline(job, deadline); bool coarseUsesCycleRemainder = job.Configuration != null && effectiveJob.Configuration.SearchTimeout < job.Configuration.SearchTimeout; using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource( deadline.Token, cancellationToken); CoarsePathPlanningJobResult coarse = coarseService.Plan(effectiveJob, linkedCancellation.Token); if (DeadlineExpired(deadline) || (coarseUsesCycleRemainder && coarse.PlanningResult.Status == PlanningStatus.SearchTimeout)) { return DeadlineFailure(job, coarse, null, deadline, "coarse"); } if (coarse.PlanningResult.Status != PlanningStatus.Success) return TrajectoryObservationBootstrapResult.FromFailure( job, coarse, null, "Coarse planning status: " + coarse.PlanningResult.Status); if (DeadlineExpired(deadline)) return DeadlineFailure(job, coarse, null, deadline, "smoothing"); var smoothingRequest = new PathSmoothingRequest( CopyFiniteClearance(coarse.PlanningResult.Path, coarse.MapResult.Map), coarse.PlanningResult.Segments, coarse.MapResult.Map, job.Vehicle, new PathSmoothingConfiguration()); PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, linkedCancellation.Token); if (DeadlineExpired(deadline)) return DeadlineFailure(job, coarse, smooth, deadline, "smoothing"); if (!IsPublishedSmoothingStatus(smooth.Status)) return TrajectoryObservationBootstrapResult.FromFailure( job, coarse, smooth, smooth.Diagnostics.TerminationReason); return TrajectoryObservationBootstrapResult.Success(job, coarse, smooth, ReferencePathSegmenter.Create(smooth)); } private static bool DeadlineExpired(TrajectoryObservationPlanningDeadline deadline) { return deadline.IsExpired && !deadline.CallerCancellationRequested; } private static TrajectoryObservationBootstrapResult DeadlineFailure(CoarsePathPlanningJob job, CoarsePathPlanningJobResult coarse, PathSmoothingResult smooth, TrajectoryObservationPlanningDeadline deadline, string phase) { string diagnostic = "cycleDeadlineExpired=true;phase=" + phase + ";remainingMs=" + deadline.Remaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture); return TrajectoryObservationBootstrapResult.FromFailure(job, coarse, smooth, diagnostic); } private static CoarsePathPlanningJob CopyJobWithDeadline(CoarsePathPlanningJob source, TrajectoryObservationPlanningDeadline deadline) { return new CoarsePathPlanningJob { MapRequest = source.MapRequest, Start = source.Start, Goal = source.Goal, Vehicle = source.Vehicle, Configuration = CopyConfigurationWithDeadline(source.Configuration, deadline), StartVehicleCurvature = source.StartVehicleCurvature, StartDirection = source.StartDirection, GoalDirection = source.GoalDirection, DebugOptions = source.DebugOptions, }; } private static HybridAStarConfiguration CopyConfigurationWithDeadline(HybridAStarConfiguration source, TrajectoryObservationPlanningDeadline deadline) { if (source == null) return null; return new HybridAStarConfiguration { PrimitiveLengthMeters = source.PrimitiveLengthMeters, IntegrationStepMeters = source.IntegrationStepMeters, MaximumCollisionCheckStepMeters = source.MaximumCollisionCheckStepMeters, HeadingResolutionRadians = source.HeadingResolutionRadians, CurvatureLevelCount = source.CurvatureLevelCount, GoalPositionToleranceMeters = source.GoalPositionToleranceMeters, GoalHeadingToleranceRadians = source.GoalHeadingToleranceRadians, MaximumExpandedNodes = source.MaximumExpandedNodes, SearchTimeout = deadline.Clamp(source.SearchTimeout), HeuristicWeight = source.HeuristicWeight, ReverseCostMultiplier = source.ReverseCostMultiplier, GearSwitchPenaltyMeters = source.GearSwitchPenaltyMeters, CurvatureMagnitudeWeight = source.CurvatureMagnitudeWeight, CurvatureChangePenaltyMetersPerLevel = source.CurvatureChangePenaltyMetersPerLevel, ClearanceCostWeight = source.ClearanceCostWeight, ClearanceCostDistanceMeters = source.ClearanceCostDistanceMeters, AllowReverse = source.AllowReverse, }; } private static IReadOnlyList CopyFiniteClearance(IReadOnlyList path, PlanningGridMap map) { if (path == null) throw new ArgumentNullException(nameof(path)); if (map == null) throw new ArgumentNullException(nameof(map)); double widthMeters = (map.Bounds.XMax - map.Bounds.XMin) / 1000d; double heightMeters = (map.Bounds.YMax - map.Bounds.YMin) / 1000d; double mapDiagonalMeters = Math.Sqrt(widthMeters * widthMeters + heightMeters * heightMeters); var copy = new List(path.Count); for (int index = 0; index < path.Count; index++) { CoarsePathPoint point = path[index] ?? throw new ArgumentException( "The coarse path cannot contain null points.", nameof(path)); double clearance = double.IsPositiveInfinity(point.BodyClearance) ? mapDiagonalMeters : point.BodyClearance; copy.Add(new CoarsePathPoint(point.X, point.Y, point.Heading, point.UnwrappedHeading, point.ArcLength, point.Direction, point.VehicleCurvature, clearance, point.IsGearSwitchPoint, point.Source)); } return new ReadOnlyCollection(copy); } private static bool IsPublishedSmoothingStatus(PathSmoothingStatus status) { return status == PathSmoothingStatus.Complete || status == PathSmoothingStatus.PartialImprovement || status == PathSmoothingStatus.NotNeeded || status == PathSmoothingStatus.Unchanged; } } /// 单个观察 tick 的只读结果,供状态文本、报告和可视化共同消费。 public sealed class TrajectoryObservationObservation { internal TrajectoryObservationObservation(DateTimeOffset observedAtUtc, VehicleMotionState vehicleState, EmTrajectory publishedTrajectory, EmTrajectoryPoint selectedPoint, TrajectoryControlCommand command, TrajectoryExecutionState executorState) { ObservedAtUtc = observedAtUtc; VehicleState = vehicleState ?? throw new ArgumentNullException(nameof(vehicleState)); PublishedTrajectory = publishedTrajectory; SelectedPoint = selectedPoint; Command = command; ExecutorState = executorState; } public DateTimeOffset ObservedAtUtc { get; } public VehicleMotionState VehicleState { get; } public EmTrajectory PublishedTrajectory { get; } public EmTrajectoryPoint SelectedPoint { get; } public TrajectoryControlCommand Command { get; } public TrajectoryExecutionState ExecutorState { get; } } /// 协调一次冻结会话中的 EM 规划、轨迹采样和诊断;从不将预测命令发送给车辆。 public sealed class TrajectoryObservationController { private readonly TrajectoryObservationBootstrapResult bootstrap; private readonly TrajectoryObservationSettings settings; private readonly EmPlannerConfiguration configuration; private readonly IEmPlanningService planningService; private readonly TrajectoryObservationSegmentTracker segmentTracker; private EmPlanningCoordinator coordinator; private TrajectoryExecutor executor; private EmTrajectory previousTrajectoryForVisualization; private EmTrajectory activeFullDirectionTrajectory; private EmPlanningCoordinator pendingFullDirectionCoordinator; private readonly string sessionId; private long cycleId; private int plannedSegmentIndex; private int pendingFullDirectionSegmentIndex = -1; private bool planAttemptedForActiveSegment; private bool pendingFullDirectionPlanAttempted; public TrajectoryObservationController(TrajectoryObservationBootstrapResult bootstrap, TrajectoryObservationSettings settings, IEmPlanningService planningService, string sessionId) { this.bootstrap = bootstrap ?? throw new ArgumentNullException(nameof(bootstrap)); if (!bootstrap.Succeeded) throw new ArgumentException("A successful planning bootstrap is required.", nameof(bootstrap)); if (settings == null) throw new ArgumentNullException(nameof(settings)); if (planningService == null) throw new ArgumentNullException(nameof(planningService)); if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("A session ID is required.", nameof(sessionId)); TrajectoryObservationSettings snapshot = settings.CreateValidatedSnapshot(); this.settings = snapshot; configuration = EmPlannerConfiguration.CreateDefault(); configuration.Scheduling.ReplanPeriodSeconds = snapshot.ReplanPeriodSeconds; configuration.Scheduling.SolverTimeoutSeconds = snapshot.SolverTimeoutSeconds; configuration.Solver.MaximumOsqpIterations = snapshot.MaximumOsqpIterations; configuration.Scheduling.TimeHorizonSeconds = snapshot.TimeHorizonSeconds; configuration.Scheduling.OutputTimeStepSeconds = snapshot.OutputTimeStepSeconds; this.planningService = planningService; coordinator = new EmPlanningCoordinator(planningService); executor = new TrajectoryExecutor(configuration); segmentTracker = new TrajectoryObservationSegmentTracker(bootstrap.Segments, snapshot, configuration.Longitudinal.StopSpeedToleranceMetersPerSecond); this.sessionId = sessionId; } public EmTrajectory PublishedTrajectory => settings.PlanningScope == EmPlanningScope.FullDirectionSegment ? activeFullDirectionTrajectory : coordinator.PublishedTrajectory; public DirectionSegmentView ActiveSegment => bootstrap.Segments[segmentTracker.State.ActiveSegmentIndex]; public TrajectoryObservationSegmentState SegmentState => segmentTracker.State; public EmTrajectory PreviousTrajectoryForVisualization => previousTrajectoryForVisualization; internal EmPlannerConfiguration CreateEffectiveConfigurationSnapshot() { return configuration.Copy(); } public TrajectoryObservationDiagnostic CreateConfigurationDiagnostic() { return TrajectoryObservationDiagnostics.CreateConfiguration(configuration); } public bool ShouldStartCycle(DateTimeOffset now) { if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment) { if (plannedSegmentIndex != ActiveSegment.SegmentIndex) { plannedSegmentIndex = ActiveSegment.SegmentIndex; planAttemptedForActiveSegment = false; } if (TryGetPendingFullDirectionSegmentIndex(out int pendingSegmentIndex)) { if (pendingFullDirectionSegmentIndex != pendingSegmentIndex) return true; return !pendingFullDirectionPlanAttempted; } return !planAttemptedForActiveSegment; } return coordinator.ShouldStartCycle(now); } public async Task StartCycle(DateTimeOffset now, VehicleMotionState state, CancellationToken cancellationToken) { using var deadline = new TrajectoryObservationPlanningDeadline( TimeSpan.FromSeconds(settings.SolverTimeoutSeconds), cancellationToken); return await StartCycle(now, state, deadline, cancellationToken).ConfigureAwait(false); } public async Task StartCycle(DateTimeOffset now, VehicleMotionState state, TrajectoryObservationPlanningDeadline deadline, CancellationToken cancellationToken) { if (state == null) throw new ArgumentNullException(nameof(state)); if (deadline == null) throw new ArgumentNullException(nameof(deadline)); using var callerCancellation = CancellationTokenSource.CreateLinkedTokenSource( deadline.CallerToken, cancellationToken); int targetSegmentIndex = ActiveSegment.SegmentIndex; int pendingSegmentIndex = -1; bool planningPendingFullDirection = settings.PlanningScope == EmPlanningScope.FullDirectionSegment && TryGetPendingFullDirectionSegmentIndex(out pendingSegmentIndex); EmPlanningCoordinator targetCoordinator; EmTrajectory previousTrajectory; if (planningPendingFullDirection) { targetSegmentIndex = pendingSegmentIndex; if (pendingFullDirectionSegmentIndex != targetSegmentIndex || pendingFullDirectionCoordinator == null) { pendingFullDirectionSegmentIndex = targetSegmentIndex; pendingFullDirectionPlanAttempted = false; pendingFullDirectionCoordinator = new EmPlanningCoordinator(planningService); } pendingFullDirectionPlanAttempted = true; targetCoordinator = pendingFullDirectionCoordinator; previousTrajectory = null; } else { planAttemptedForActiveSegment = true; plannedSegmentIndex = ActiveSegment.SegmentIndex; targetCoordinator = coordinator; previousTrajectory = coordinator.PublishedTrajectory; } long currentCycleId = Interlocked.Increment(ref cycleId); EmPlannerConfiguration effectiveConfiguration = configuration.Copy(); TimeSpan cycleRemaining = deadline.Remaining; TimeSpan configuredSolverLimit = TimeSpan.FromSeconds( configuration.Scheduling.SolverTimeoutSeconds); if (cycleRemaining > TimeSpan.Zero && cycleRemaining < configuredSolverLimit - TimeSpan.FromMilliseconds(1d)) { effectiveConfiguration.Scheduling.SolverTimeoutSeconds = cycleRemaining.TotalSeconds; } var request = new EmPlanningRequest( bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Vehicle, state, effectiveConfiguration, targetSegmentIndex, previousTrajectory, now, now, sessionId + "-trajectory-" + currentCycleId, sessionId + "-reference", previousTrajectory?.Metadata.TrajectoryId ?? string.Empty, EmMotionModel.NonholonomicForwardReverse, settings.PlanningScope, cycleRemaining, callerCancellation.Token, deadline.DeadlineToken, deadline.PublicationAuthorization); if (callerCancellation.IsCancellationRequested) { var cancelledResult = new EmPlanningResult(EmPlanningStatus.Cancelled, null, "callerCancellation=true;phase=request"); return new PlanningCycleResult(currentCycleId, PlanningCycleIdentity.FromRequest(request), cancelledResult, false, cancelledResult.FailureReason); } if (deadline.DeadlineToken.IsCancellationRequested) { var expiredResult = new EmPlanningResult(EmPlanningStatus.CycleDeadlineExpired, null, "cycleDeadlineExpired=true;phase=request;remainingMs=" + cycleRemaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); return new PlanningCycleResult(currentCycleId, PlanningCycleIdentity.FromRequest(request), expiredResult, false, expiredResult.FailureReason); } Action fullDirectionCommit = settings.PlanningScope == EmPlanningScope.FullDirectionSegment && !planningPendingFullDirection ? trajectory => activeFullDirectionTrajectory = trajectory : null; PlanningCycleResult result = await targetCoordinator.PlanLatestAsync( new PlanningCycleInput(request, now), callerCancellation.Token, fullDirectionCommit).ConfigureAwait(false); if (!result.Published && deadline.IsExpired && !callerCancellation.IsCancellationRequested && result.Result.Status == EmPlanningStatus.Cancelled) { var expiredResult = new EmPlanningResult(EmPlanningStatus.CycleDeadlineExpired, null, "cycleDeadlineExpired=true;phase=planning;remainingMs=" + deadline.Remaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); result = new PlanningCycleResult(result.Version, result.Identity, expiredResult, false, expiredResult.FailureReason); } return result; } public bool TryAdvanceSegment(DateTimeOffset now, VehicleMotionState state) { if (state == null) throw new ArgumentNullException(nameof(state)); if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment && segmentTracker.State.Phase == TrajectoryObservationSegmentPhase.WaitingForDirection && !HasPendingFullDirectionTrajectory()) { return false; } EmTrajectory activeTrajectory = PublishedTrajectory; TrajectoryObservationSegmentUpdate update = segmentTracker.Update(now, state, activeTrajectory); if (!update.Advanced) return false; if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment) { EmTrajectory pendingTrajectory = pendingFullDirectionCoordinator.PublishedTrajectory; previousTrajectoryForVisualization = activeTrajectory; activeFullDirectionTrajectory = RebaseEffectiveAt(pendingTrajectory, now); coordinator = pendingFullDirectionCoordinator; pendingFullDirectionCoordinator = null; pendingFullDirectionSegmentIndex = -1; pendingFullDirectionPlanAttempted = false; executor = new TrajectoryExecutor(configuration); plannedSegmentIndex = segmentTracker.State.ActiveSegmentIndex; planAttemptedForActiveSegment = true; return true; } previousTrajectoryForVisualization = coordinator.PublishedTrajectory; coordinator = new EmPlanningCoordinator(planningService); executor = new TrajectoryExecutor(configuration); plannedSegmentIndex = segmentTracker.State.ActiveSegmentIndex; planAttemptedForActiveSegment = false; return true; } public TrajectoryObservationObservation Observe(DateTimeOffset now, VehicleMotionState state) { if (state == null) throw new ArgumentNullException(nameof(state)); EmTrajectory trajectory = PublishedTrajectory; if (trajectory == null) return new TrajectoryObservationObservation(now, state, null, null, null, null); TrajectoryObservationSegmentState segmentState = segmentTracker.State; TravelDirection currentDirection = ActiveSegment.Direction; bool waitingForDirection = segmentState.Phase == TrajectoryObservationSegmentPhase.WaitingForStop || segmentState.Phase == TrajectoryObservationSegmentPhase.WaitingForDirection; TravelDirection desiredDirection = waitingForDirection && segmentState.ExpectedDirection.HasValue ? segmentState.ExpectedDirection.Value : currentDirection; TrajectoryControlCommand command = executor.UpdateCommand(now, state, trajectory, desiredDirection, currentDirection, !waitingForDirection); TrajectoryExecutionState executorState = executor.State; return new TrajectoryObservationObservation(now, state, trajectory, executorState.SelectedPoint, command, executorState); } private bool TryGetPendingFullDirectionSegmentIndex(out int pendingSegmentIndex) { pendingSegmentIndex = -1; if (settings.PlanningScope != EmPlanningScope.FullDirectionSegment || segmentTracker.State.Phase != TrajectoryObservationSegmentPhase.WaitingForDirection || segmentTracker.State.ActiveSegmentIndex + 1 >= bootstrap.Segments.Count) { return false; } pendingSegmentIndex = segmentTracker.State.ActiveSegmentIndex + 1; return true; } private bool HasPendingFullDirectionTrajectory() { if (!TryGetPendingFullDirectionSegmentIndex(out int pendingSegmentIndex) || pendingFullDirectionSegmentIndex != pendingSegmentIndex || pendingFullDirectionCoordinator == null) { return false; } EmTrajectory pending = pendingFullDirectionCoordinator.PublishedTrajectory; DirectionSegmentView segment = bootstrap.Segments[pendingSegmentIndex]; return pending != null && pending.Metadata.SegmentIndex == segment.SegmentIndex && pending.Metadata.Direction == segment.Direction; } private static EmTrajectory RebaseEffectiveAt(EmTrajectory source, DateTimeOffset effectiveAtUtc) { if (source == null) throw new ArgumentNullException(nameof(source)); EmTrajectoryMetadata metadata = source.Metadata; return new EmTrajectory(new EmTrajectoryMetadata(metadata.TrajectoryId, metadata.GeneratedAtUtc, effectiveAtUtc, metadata.MapSnapshotId, metadata.ReferencePathId, metadata.VehicleStateSequenceId, metadata.PreviousTrajectoryId, metadata.SegmentIndex, metadata.Direction, metadata.TerminalType, metadata.LongitudinalMode, metadata.PlanningScope), source.Points); } } public sealed class TrajectoryObservationLoopTick { internal TrajectoryObservationLoopTick(TrajectoryObservationObservation observation, PlanningCycleResult latestCycle, TimeSpan latestPlanningElapsed, bool planningInFlight, bool planningStarted, bool planningCompleted, bool segmentAdvanced, TrajectoryObservationSegmentState segmentState) { Observation = observation ?? throw new ArgumentNullException(nameof(observation)); LatestCycle = latestCycle; LatestPlanningElapsed = latestPlanningElapsed; PlanningInFlight = planningInFlight; PlanningStarted = planningStarted; PlanningCompleted = planningCompleted; SegmentAdvanced = segmentAdvanced; SegmentState = segmentState ?? throw new ArgumentNullException(nameof(segmentState)); } public TrajectoryObservationObservation Observation { get; } public PlanningCycleResult LatestCycle { get; } public TimeSpan LatestPlanningElapsed { get; } public bool PlanningInFlight { get; } public bool PlanningStarted { get; } public bool PlanningCompleted { get; } public bool SegmentAdvanced { get; } public TrajectoryObservationSegmentState SegmentState { get; } public bool ShouldLog => true; } /// 按观察周期捕获新鲜只读状态并发布快照的循环;规划耗时不能改变观察安全边界。 public sealed class TrajectoryObservationLoop { private readonly TrajectoryObservationController controller; private Task planningTask; private DateTimeOffset planningStartedAtUtc; private PlanningCycleResult latestCycle; private TimeSpan latestPlanningElapsed; public TrajectoryObservationLoop(TrajectoryObservationController controller) { this.controller = controller ?? throw new ArgumentNullException(nameof(controller)); } public TrajectoryObservationLoopTick Tick(DateTimeOffset now, VehicleMotionState state, CancellationToken cancellationToken) { if (state == null) throw new ArgumentNullException(nameof(state)); cancellationToken.ThrowIfCancellationRequested(); bool planningCompleted = ConsumeCompletedPlanning(now); bool segmentAdvanced = planningTask == null && controller.TryAdvanceSegment(now, state); if (segmentAdvanced) latestCycle = null; bool planningStarted = false; if (planningTask == null && controller.ShouldStartCycle(now)) { planningStarted = true; planningStartedAtUtc = now; planningTask = controller.StartCycle(now, state, cancellationToken); planningCompleted |= ConsumeCompletedPlanning(now); } TrajectoryObservationObservation observation = controller.Observe(now, state); return new TrajectoryObservationLoopTick(observation, latestCycle, latestPlanningElapsed, planningTask != null, planningStarted, planningCompleted, segmentAdvanced, controller.SegmentState); } private bool ConsumeCompletedPlanning(DateTimeOffset observedAtUtc) { Task completed = planningTask; if (completed == null || !completed.IsCompleted) return false; latestCycle = completed.GetAwaiter().GetResult(); TimeSpan elapsed = observedAtUtc - planningStartedAtUtc; latestPlanningElapsed = elapsed < TimeSpan.Zero ? TimeSpan.Zero : elapsed; planningTask = null; return true; } } public enum TrajectoryObservationSessionEndReason { BootstrapFailure, RuntimeFault, Cancellation, } /// 集中定义观察会话的开始、停止和故障清理语义,避免留下旧会话可视化。 public static class TrajectoryObservationSessionLifecycle { public static bool ShouldClearLayers(TrajectoryObservationSessionEndReason reason) { switch (reason) { case TrajectoryObservationSessionEndReason.BootstrapFailure: return false; case TrajectoryObservationSessionEndReason.RuntimeFault: case TrajectoryObservationSessionEndReason.Cancellation: return true; default: throw new ArgumentOutOfRangeException(nameof(reason)); } } } public sealed class TrajectoryObservationRuntimeState { public const string GearSwitchWaitingNotice = "等待真实档位/方向确认;观察模式不会激活下一方向段"; private TrajectoryObservationRuntimeState(bool waitingAtGearSwitch) { WaitingAtGearSwitch = waitingAtGearSwitch; WorldNotice = waitingAtGearSwitch ? GearSwitchWaitingNotice : string.Empty; } public bool WaitingAtGearSwitch { get; } public string WorldNotice { get; } public static TrajectoryObservationRuntimeState Create(TrajectoryObservationSegmentState segmentState) { if (segmentState == null) throw new ArgumentNullException(nameof(segmentState)); bool waiting = segmentState.Phase == TrajectoryObservationSegmentPhase.WaitingForStop || segmentState.Phase == TrajectoryObservationSegmentPhase.WaitingForDirection; return new TrajectoryObservationRuntimeState(waiting); } public static TrajectoryObservationRuntimeState Create(DateTimeOffset now, EmTrajectory trajectory) { if (trajectory == null || trajectory.Metadata.TerminalType != EmTerminalType.GearSwitch) return new TrajectoryObservationRuntimeState(false); EmTrajectoryPoint finalPoint = trajectory.Points[trajectory.Points.Count - 1]; bool waiting = (now - trajectory.Metadata.EffectiveAtUtc).TotalSeconds >= finalPoint.TimeFromStart; return new TrajectoryObservationRuntimeState(waiting); } } public sealed class TrajectoryObservationLsSample { public TrajectoryObservationLsSample(double pathS, double lateralOffset) { PathS = pathS; LateralOffset = lateralOffset; } public double PathS { get; } public double LateralOffset { get; } } public sealed class TrajectoryObservationStSample { public TrajectoryObservationStSample(double timeFromStart, double pathS) { TimeFromStart = timeFromStart; PathS = pathS; } public double TimeFromStart { get; } public double PathS { get; } } public sealed class TrajectoryObservationSpeedSample { public TrajectoryObservationSpeedSample(double timeFromStart, double signedLongitudinalVelocity) { TimeFromStart = timeFromStart; SignedLongitudinalVelocity = signedLongitudinalVelocity; } public double TimeFromStart { get; } public double SignedLongitudinalVelocity { get; } } public sealed class TrajectoryObservationCharts { private TrajectoryObservationCharts(IReadOnlyList lsSamples, IReadOnlyList stSamples, IReadOnlyList speedSamples, int failedProjectionCount) { LsSamples = new ReadOnlyCollection( new List(lsSamples)); StSamples = new ReadOnlyCollection( new List(stSamples)); SpeedSamples = new ReadOnlyCollection( new List(speedSamples)); FailedProjectionCount = failedProjectionCount; } public IReadOnlyList LsSamples { get; } public IReadOnlyList StSamples { get; } public IReadOnlyList SpeedSamples { get; } public int FailedProjectionCount { get; } public static TrajectoryObservationCharts Build(EmTrajectory trajectory, DirectionSegmentView segment, double maximumProjectionDistanceMeters) { if (trajectory == null) throw new ArgumentNullException(nameof(trajectory)); if (segment == null) throw new ArgumentNullException(nameof(segment)); if (double.IsNaN(maximumProjectionDistanceMeters) || double.IsInfinity(maximumProjectionDistanceMeters) || maximumProjectionDistanceMeters < 0d) throw new ArgumentOutOfRangeException(nameof(maximumProjectionDistanceMeters)); var ls = new List(trajectory.Points.Count); var st = new List(trajectory.Points.Count); var speed = new List(trajectory.Points.Count); var projector = new FrenetProjector(); double seedReferenceS = 0d; int failedProjectionCount = 0; for (int index = 0; index < trajectory.Points.Count; index++) { EmTrajectoryPoint point = trajectory.Points[index]; var pose = new Pose2D(point.X, point.Y, point.Yaw); if (projector.TryProject(pose, segment, 0d, segment.LengthMeters, maximumProjectionDistanceMeters, seedReferenceS, out FrenetProjection projection)) { ls.Add(new TrajectoryObservationLsSample( segment.SourceStartArcLength + projection.ReferenceS, projection.LateralOffset)); seedReferenceS = projection.ReferenceS; } else { failedProjectionCount++; } st.Add(new TrajectoryObservationStSample(point.TimeFromStart, point.PathS)); speed.Add(new TrajectoryObservationSpeedSample(point.TimeFromStart, point.SignedLongitudinalVelocity)); } return new TrajectoryObservationCharts(ls, st, speed, failedProjectionCount); } }