Files
ParkingRobot/docs/superpowers/plans/2026-08-05-em-observation-web-integration.md
T

37 KiB
Raw Blame History

EM Observation Web Visualization Integration Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Upgrade TrajectoryObservationMovementTest into a complete multi-segment, observe-only EM planning experiment that publishes effective configuration, global/active path geometry, LS/ST and kinematic evidence to the reusable local scientific dashboard.

Architecture: A pure segment tracker advances only after a real stop and stable signed-speed confirmation. The controller owns one coordinator per active direction segment, while pure snapshot builders convert existing EM/map/smoothing data into the generic visualization contracts. A thin MovementTest host conditionally owns web and Painter sessions and isolates every visualization failure from planning.

Tech Stack: C# 10, netstandard2.0, existing EMPlanner/TrajectoryExecution/Map/PathSmoothing modules, TrajectoryPlanningVisualization, OSQP, MDCS read interfaces, existing console verification host.

Prerequisite

Complete and review docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md first. This plan consumes its exact PlanningVisualizationSession, snapshot, chart, geometry, and configuration contracts.

Global Constraints

  • Continue OBSERVE_ONLY; do not call chassis, motor, steering, brake, wheel, gear, or controller write APIs.
  • Use actual MDCS pose and signed longitudinal speed only as read inputs.
  • Preserve user defaults exactly: solver timeout 5.0 s, OSQP maximum iterations 100000, ST horizon 2.0 s, output step 0.10 s.
  • Default web visualization is off; default native Painter visualization is off.
  • Web refresh defaults to 10 Hz; history defaults to 60; port defaults to 0.
  • Segment transitions are strictly N -> N+1; never skip, infer at zero speed, or reuse a previous-direction trajectory as an EM seed.
  • Current and previous cycles may compare world position and shared direction-segment ReferenceS; never compare their independent local PathS origins.
  • j[i] represents [t_i, t_i+1); publish exactly trajectory.Points.Count - 1 jerk samples and no synthetic terminal sample.
  • Static geometry/configuration is built once after successful bootstrap. Dynamic snapshots are built at most at WebRefreshRateHz.
  • Visualization exceptions are logged once and disable only the web output.
  • Preserve unrelated worktree changes and stage only task files.

File Structure

File Responsibility
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs Runtime, web and direction-confirmation settings with frozen validation.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs Pure stop/direction/projection state machine.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs Active-segment controller and asynchronous loop integration.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs Map/global paths/direction segments/effective configuration.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs LS/ST/curvature/v/a/j/yaw-rate generic charts.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs Absolute-time interpolation and shared-segment handoff deltas.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs World overlays, status, charts and cycle summary.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs 10 Hz gating and exception fuse.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs Public fields, lifecycle, browser launch and conditional Painter/web ownership.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs Lazily created optional native Painter fallback.
ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md Operator workflow and chart interpretation.
ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs Defaults, validation and frozen-setting checks.
ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs Pure multi-segment transition checks.
ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs Static/dynamic adapter and web isolation checks.
ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs Existing regression entry; calls the new focused groups.
ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs New managed DLL packaging contract.
ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1 Copies the visualization class library beside ClumsyPilot.dll.
ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md Updated plugin tree and observation link.

Task 1: Synchronize effective settings and visualization controls

Files:

  • Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs
  • Create: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md

Interfaces:

  • Extends TrajectoryObservationSettings with web/Painter and direction-confirmation properties.

  • CreateValidatedSnapshot() freezes every new property.

  • MovementTest public fields map one-to-one into settings before background work begins.

  • Step 1: Write failing defaults, copy, and validation checks

Add TrajectoryObservationSettingsChecks.Run() and invoke it from TrajectoryObservationChecks.Run().

var defaults = new TrajectoryObservationSettings();
Verification.NearlyEqual(5d, defaults.SolverTimeoutSeconds, "observer solver timeout default");
Verification.Equal(100000, defaults.MaximumOsqpIterations, "observer OSQP default");
Verification.NearlyEqual(2d, defaults.TimeHorizonSeconds, "observer ST horizon default");
Verification.True(!defaults.EnableWebVisualization, "web defaults off");
Verification.True(!defaults.EnableNativePainterVisualization, "Painter defaults off");
Verification.Equal(0, defaults.WebVisualizationPort, "dynamic port default");
Verification.NearlyEqual(10d, defaults.WebRefreshRateHz, "web refresh default");
Verification.Equal(60, defaults.VisualizationHistoryCycleLimit, "history default");
Verification.NearlyEqual(0.02d, defaults.DirectionConfirmationSpeedMetersPerSecond,
    "direction speed threshold");
Verification.Equal(3, defaults.DirectionConfirmationSamples, "direction sample default");
Verification.NearlyEqual(0.50d, defaults.GearSwitchProjectionToleranceMeters,
    "gear projection default");
Verification.NearlyEqual(0.20d, defaults.GearSwitchStopHoldSeconds, "gear stop hold default");

Mutate all source values after CreateValidatedSnapshot() and assert the snapshot retains the originals. Add invalid cases for port -1/1/1023/65536, refresh 0, history 0, speed threshold 0, sample count 0, projection tolerance 0, and hold 0.

  • Step 2: Run RED
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation

Expected: failure because settings still use 0.50 / 12000 / 6.0 and the new properties do not exist.

  • Step 3: Implement settings and MovementTest field mapping

Add exact get/set defaults to settings and matching public fields to MovementTest:

public bool EnableWebVisualization { get; set; } = false;
public bool AutoOpenWebVisualization { get; set; } = true;
public int WebVisualizationPort { get; set; } = 0;
public double WebRefreshRateHz { get; set; } = 10d;
public int VisualizationHistoryCycleLimit { get; set; } = 60;
public bool EnableNativePainterVisualization { get; set; } = false;
public double DirectionConfirmationSpeedMetersPerSecond { get; set; } = 0.02d;
public int DirectionConfirmationSamples { get; set; } = 3;
public double GearSwitchProjectionToleranceMeters { get; set; } = 0.50d;
public double GearSwitchStopHoldSeconds { get; set; } = 0.20d;

Use inclusive validation for port 0 and 1024..65535. Update the Chinese XML summaries so the ST default says 2 s, not 6 s. Update the README configuration table with all solver, ST, web, Painter and direction-confirmation fields.

  • Step 4: Run GREEN

Run trajectory-observation. Expected: PASS and no assertion still expects the superseded defaults.

  • Step 5: Commit settings
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: configure observation visualization"

Task 2: Pure sequential direction-segment tracker

Files:

  • Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs
  • Create: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs

Interfaces:

  • Produces: TrajectoryObservationSegmentPhase, TrajectoryObservationSegmentState, and TrajectoryObservationSegmentTracker.Update(...).

  • Consumes: frozen settings, ordered DirectionSegmentView list, EM stop tolerance, caller time, measured VehicleMotionState, and current published trajectory.

  • Step 1: Write failing state-machine checks

Build a two-segment forward/reverse fixture with a duplicated world pose at the gear switch. Exercise this exact sequence:

var tracker = new TrajectoryObservationSegmentTracker(segments, settings, stopSpeedTolerance: 0.01d);
Verification.Equal(0, tracker.State.ActiveSegmentIndex, "tracker begins on segment zero");

tracker.Update(t0, StateAtSwitch(0d, t0, 1L), GearTerminal(t0));
Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForStop, tracker.State.Phase,
    "first zero sample begins stop hold");

tracker.Update(t0.AddSeconds(0.21d), StateAtSwitch(0d, t0.AddSeconds(0.21d), 2L), GearTerminal(t0));
Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForDirection, tracker.State.Phase,
    "continuous stop arms next direction");

tracker.Update(t0.AddSeconds(0.25d), StateAtSwitch(-0.03d, t0.AddSeconds(0.25d), 3L), GearTerminal(t0));
tracker.Update(t0.AddSeconds(0.30d), StateAtSwitch(-0.03d, t0.AddSeconds(0.30d), 4L), GearTerminal(t0));
TrajectoryObservationSegmentUpdate advanced = tracker.Update(
    t0.AddSeconds(0.35d), StateAtSwitch(-0.03d, t0.AddSeconds(0.35d), 5L), GearTerminal(t0));
Verification.True(advanced.Advanced && tracker.State.ActiveSegmentIndex == 1,
    "three stable reverse samples advance exactly one segment");

Separate checks prove that wrong sign, a zero sample, a sequence-id repeat, excessive switch distance, a non-gear terminal, and a discontinuous timestamp reset confirmation. A one-segment path reaches Completed rather than indexing past the end.

  • Step 2: Run RED

Run trajectory-observation. Expected: compile failure because tracker types do not exist.

  • Step 3: Implement the tracker

Use these stable phases:

public enum TrajectoryObservationSegmentPhase
{
    Planning,
    WaitingForStop,
    WaitingForDirection,
    Completed,
}

Update first validates strictly increasing SequenceId and nondecreasing caller time. It only leaves Planning when the current published trajectory has matching segment/direction, TerminalType.GearSwitch, and the caller time is at or beyond trajectory.Metadata.EffectiveAtUtc + trajectory.Points[^1].TimeFromStart. Use FrenetProjector to confirm the measured pose is within the configured tolerance of both the current-segment end and next-segment start.

The expected speed sign is positive for Forward and negative for Reverse. A successful update changes the active index once, resets counters/timers, and returns a Chinese transition diagnostic. It must not write to coordinator, browser, UI, or hardware.

  • Step 4: Run GREEN

Run trajectory-observation twice. Expected: PASS both times with deterministic state transitions.

  • Step 5: Commit tracker
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: track observed direction segments"

Task 3: Active-segment rolling controller and loop

Files:

  • Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs

Interfaces:

  • TrajectoryObservationController.ActiveSegment, SegmentState, PreviousTrajectoryForVisualization, and TryAdvanceSegment(...) become the single active-segment source.

  • TrajectoryObservationController.CreateEffectiveConfigurationSnapshot() returns configuration.Copy() so adapters never read mutable MovementTest fields or retain the controller's private configuration object.

  • StartCycle builds a request for ActiveSegment.SegmentIndex.

  • TrajectoryObservationLoop.Tick may call TryAdvanceSegment only when no planning task is in flight.

  • Step 1: Write failing controller integration checks

Use a recording IEmPlanningService and two-segment bootstrap:

controller.StartCycle(t0, forwardState, CancellationToken.None).GetAwaiter().GetResult();
Verification.Equal(0, service.Requests[0].SegmentIndex, "first request uses segment zero");

AdvanceTrackerThroughRealStopAndReverse(controller, gearTrajectory, t0);
controller.StartCycle(t0.AddSeconds(1d), reverseState, CancellationToken.None).GetAwaiter().GetResult();
Verification.Equal(1, service.Requests[1].SegmentIndex, "next request uses segment one");
Verification.True(service.Requests[1].PreviousTrajectory == null,
    "new direction does not reuse old segment trajectory");

Also prove two cycles on the same segment retain the exact published previous trajectory reference and ID, and prove an in-flight task prevents a segment reset.

  • Step 2: Run RED

Expected: checks fail because the controller hardcodes segmentIndex = 0.

  • Step 3: Refactor controller ownership

Store the planning service and make coordinator/executor replaceable per segment:

private readonly IEmPlanningService planningService;
private EmPlanningCoordinator coordinator;
private TrajectoryExecutor executor;
private readonly TrajectoryObservationSegmentTracker segmentTracker;
private EmTrajectory previousTrajectoryForVisualization;

Add:

internal EmPlannerConfiguration CreateEffectiveConfigurationSnapshot()
{
    return configuration.Copy();
}

On a confirmed transition, preserve the old published trajectory only in previousTrajectoryForVisualization, then construct a new coordinator and executor. Same-segment StartCycle uses coordinator.PublishedTrajectory; cross-segment starts with null. Continue monotonic session cycle IDs across coordinator replacement.

Observe supplies the tracker-confirmed current direction to TrajectoryExecutor. While waiting at a gear switch, desired direction is the next segment direction and directionConfirmed=false; after transition both current and desired are the new direction.

  • Step 4: Update the async loop

In Tick, after consuming a completed task and before starting a new task:

bool segmentAdvanced = planningTask == null && controller.TryAdvanceSegment(now, state);
if (segmentAdvanced)
    latestCycle = null;

Expose SegmentAdvanced and the immutable segment state in TrajectoryObservationLoopTick. Never cancel a running planner solely to advance a segment.

  • Step 5: Run GREEN and existing coordinator/executor regressions
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor

Expected: all three PASS.

  • Step 6: Commit controller integration
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: observe EM planning across gear segments"

Task 4: Static world geometry and effective configuration adapter

Files:

  • Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs
  • Create: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs

Interfaces:

  • Produces: PlanningVisualizationStaticSnapshot Build(bootstrap, effectiveConfiguration, settings, obstacleSnapshotVersion).

  • Static polylines use metres; map source bounds/resolution remain named with their original millimetre units in configuration values.

  • Step 1: Write failing static snapshot checks

Assert a forward/reverse bootstrap produces every segment and switch marker:

PlanningVisualizationStaticSnapshot snapshot = builder.Build(bootstrap, configuration, settings, 17L);
Verification.Equal(bootstrap.Segments.Count, snapshot.DirectionSegments.Count,
    "all direction segments exported");
Verification.Equal("Forward", snapshot.DirectionSegments[0].Direction, "forward direction exported");
Verification.Equal("Reverse", snapshot.DirectionSegments[1].Direction, "reverse direction exported");
Verification.True(snapshot.StaticMarkers.Any(x => x.Kind == "gear-switch"),
    "gear switch marker exported");
Verification.Equal((bootstrap.GridMap.Rows * bootstrap.GridMap.Cols + 7) / 8,
    Convert.FromBase64String(snapshot.OccupancyGrid.OccupancyBitsBase64).Length,
    "occupancy bitset has exact compact length");
Verification.True(IsOccupiedBitSet(snapshot.OccupancyGrid, occupiedRow, occupiedColumn),
    "occupied map cell uses row-major least-significant-bit-first encoding");

Flatten configuration entries by raw field name and assert exact values for TimeHorizonSeconds=2, DistanceHorizonMeters=5, MaximumOsqpIterations=100000, all solver tolerances, all LS/ST weights, vehicle fields, map snapshot/resolution/bounds, web settings, and direction-confirmation settings.

  • Step 2: Run RED

Expected: compile failure because builder does not exist.

  • Step 3: Implement static conversion

Build configuration groups in this stable order: 调度, OSQP, 车辆与安全, 纵向限制, ST 权重, 横向限制, LS 权重, 走廊与投影, 地图, 可视化与换向确认.

Use raw field names exactly as C# properties and invariant values. Encode PlanningGridMap.IsOccupied(row, column) into the generic row-major occupancy bitset using bit index row * map.Cols + column; do not create one DTO, SVG element, or polyline per occupied cell. Build global coarse and Local G2 polylines once. Direction segments must retain segment index, direction and switch topology; dynamic state decides completed/current/future highlighting.

  • Step 4: Run GREEN

Run trajectory-observation. Expected: PASS with all effective values read from the controller's frozen EmPlannerConfiguration, not editable MovementTest fields.

  • Step 5: Commit static adapter
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: export EM observation configuration"

Task 5: Kinematic charts, rolling semantics, and handoff evidence

Files:

  • Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs
  • Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs
  • Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs

Interfaces:

  • Produces chart IDs ls, st, curvature-s, curvature-t, velocity-t, acceleration-t, jerk-t, yaw-rate-t.

  • Produces TrajectoryObservationHandoffMetrics with availability, DeltaPositionMeters, DeltaReferenceSMeters, DeltaVelocityMetersPerSecond, and DeltaAccelerationMetersPerSecondSquared.

  • Produces one PlanningVisualizationDynamicSnapshot per accepted publication tick.

  • Step 1: Write failing jerk and chart checks

For a 21-point rolling trajectory:

IReadOnlyList<VisualizationChart> charts = chartBuilder.Build(trajectory, segment, configuration);
VisualizationChart jerk = FindChart(charts, "jerk-t");
Verification.Equal(20, jerk.Series[0].Points.Count, "jerk has one real sample per interval");
Verification.True(jerk.NoteChinese.Contains("末点后无时间区间"), "jerk explains terminal interval");
Verification.Equal(21, FindChart(charts, "velocity-t").Series[0].Points.Count,
    "velocity remains knot based");
Verification.Equal("ReferenceS (m)", FindChart(charts, "ls").XAxisLabel,
    "LS uses shared segment station");
Verification.Equal("PathS (m)", FindChart(charts, "st").YAxisLabel,
    "ST uses local planned PathS");

Assert acceleration, jerk, curvature and signed-speed limit series are present with VisualizationLineStyle.Limit. Do not invent a yaw-rate limit: the effective configuration has no independent maximum yaw-rate field.

  • Step 2: Write failing rolling/exact-stop semantic checks

Build dynamic snapshots for RollingContinuation and ExactStopAtBoundary. Assert Chinese status values respectively contain 滚动末端停车硬约束:未启用 and 精确停车锚点, and that the displayed terminal speed/acceleration equal the actual final point.

  • Step 3: Write failing handoff coordinate checks

Create two trajectories whose local PathS both start at zero but whose world points project to different positions on the same full segment. Align at current.Metadata.EffectiveAtUtc; assert:

Verification.NearlyEqual(expectedWorldDelta, metrics.DeltaPositionMeters, "handoff world delta");
Verification.NearlyEqual(expectedReferenceDelta, metrics.DeltaReferenceSMeters,
    "handoff compares shared ReferenceS");
Verification.True(expectedReferenceDelta != current.Points[0].PathS - previous.Points[0].PathS,
    "handoff does not compare local PathS origins");

Projection failure must return unavailable DeltaReferenceS while preserving world/velocity/acceleration deltas.

  • Step 4: Run RED

Expected: compilation failure because builders and metrics do not exist.

  • Step 5: Implement chart and handoff builders

For jerk, iterate only while index + 1 < trajectory.Points.Count and use the interval start time/value. For LS, project every world pose to the complete active DirectionSegmentView and use segment.SourceStartArcLength + projection.ReferenceS. For ST, retain point.TimeFromStart and the plan-local point.PathS.

Use TrajectorySampler.TrySample at:

double previousTime = (current.Metadata.EffectiveAtUtc - previous.Metadata.EffectiveAtUtc).TotalSeconds;

Compare the sampled previous point with current point zero. Project both to the same full segment for DeltaReferenceS; never subtract local PathS.

  • Step 6: Implement dynamic snapshot composition

Dynamic world overlays contain real pose, current trajectory, previous trajectory, active-segment highlight and current projected horizon. Status values include active segment/direction, planning status, mode, terminal type, elapsed, trajectory age, projection failures, rolling constraint semantics, terminal v/a, handoff deltas, and unmet gear-confirmation condition. Cycle summary contains no full point arrays.

  • Step 7: Run GREEN
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all

Expected: both commands PASS; the existing rolling service regression still has 21 knots and nonzero rolling terminal speed.

  • Step 8: Commit the visualization adapter
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs
git commit -m "feat: visualize EM rolling kinematics"

Task 6: MovementTest web lifecycle and optional Painter fallback

Files:

  • Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs

Interfaces:

  • TrajectoryObservationVisualizationPublisher.Start(...) creates the generic session and returns the URI.

  • Internal ITrajectoryObservationVisualizationSink defines Start, Publish, and Stop; the production adapter wraps PlanningVisualizationSession, while tests inject a throwing sink without mocking the library.

  • TryPublish(...) enforces the configured cadence and fuses on the first adapter/server fault.

  • Stop() is idempotent.

  • Native Painter objects are constructed only when EnableNativePainterVisualization=true.

  • Step 1: Write failing lifecycle/isolation checks

Add a fake visualization sink whose Publish throws. Verify the wrapper logs/disables once, later calls are no-ops, and the TrajectoryObservationLoop continues producing ticks. Add a source audit asserting the MovementTest has no static eager new TrajectoryObservationPresentation() and conditionally creates it only under the native Painter flag.

Add a cadence check: observer ticks at 20 Hz with web refresh at 10 Hz produce no more than 11 snapshots over one second, including the initial snapshot.

  • Step 2: Run RED

Expected: failures because the publisher does not exist and Painter creation is eager.

  • Step 3: Implement publisher cadence and fuse

Use this internal seam and facade surface:

internal interface ITrajectoryObservationVisualizationSink
{
    PlanningVisualizationSessionInfo Start(PlanningVisualizationOptions options,
        PlanningVisualizationStaticSnapshot snapshot);
    void Publish(PlanningVisualizationDynamicSnapshot snapshot);
    void Stop();
}

internal sealed class TrajectoryObservationVisualizationPublisher
{
    internal TrajectoryObservationVisualizationPublisher(TrajectoryObservationSettings settings,
        ITrajectoryObservationVisualizationSink sink, Action<string> log);
    internal PlanningVisualizationSessionInfo Start(PlanningVisualizationStaticSnapshot snapshot);
    internal bool TryPublish(DateTimeOffset now,
        Func<PlanningVisualizationDynamicSnapshot> snapshotFactory);
    internal void Stop();
    internal string FaultReason { get; }
}

The production sink constructs and disposes one PlanningVisualizationSession; the fake sink records calls or throws.

The publisher stores nextPublishAtUtc. When enabled and healthy:

if (now < nextPublishAtUtc) return false;
nextPublishAtUtc = now + TimeSpan.FromSeconds(1d / settings.WebRefreshRateHz);
session.Publish(dynamicBuilder.Build(...));
return true;

Catch any exception from build/start/publish, call session.Stop(), set one immutable Chinese fault reason, invoke the supplied log callback once, and return false thereafter.

  • Step 4: Integrate successful bootstrap and browser launch

After bootstrap succeeds and before the loop begins, build the static snapshot and start the web session only when enabled. Log the full tokenized URI. If auto-open is enabled, call:

Process.Start(new ProcessStartInfo
{
    FileName = sessionInfo.Uri.AbsoluteUri,
    UseShellExecute = true,
});

Catch browser-launch exceptions separately and keep the web session running.

  • Step 5: Make Painter lazy and conditional

Remove static eager Presentation. Create a session-local presentation only when enabled, pass null otherwise, and guard all DrawWorld/DrawLs/DrawSt/ClearAll calls. Stopping or replacing a session stops its web publisher and clears only an existing Painter. Bootstrap failure still logs through UI/console even when both visualization modes are off.

  • Step 6: Publish dynamic snapshots from real ticks

Use controller.ActiveSegment for LS/ST building and world highlight. Pass the completed cycle, elapsed time, in-flight flag, segment state, current/previous trajectories, live state and current sample into TryPublish. Do not serialize or wait in the observer loop.

  • Step 7: Run GREEN and source audit
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest

Expected: check PASS and rg finds no actuator call in runtime observer sources.

  • Step 8: Commit MovementTest lifecycle
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: host EM observation dashboard"

Task 7: Plugin packaging and operator documentation

Files:

  • Modify: ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1
  • Modify: ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md
  • Modify: ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md

Interfaces:

  • Plugin output adds plugins/TrajectoryPlanningVisualization.dll beside ClumsyPilot.dll.

  • The publisher resolves the visualization DLL only as a sibling of the explicit ManagedDll; it does not search PATH or current directory.

  • Step 1: Write failing package-tree check

Change the expected sorted tree to:

plugins/ClumsyPilot.dll|
plugins/TrajectoryPlanningVisualization.dll|
plugins/licenses/OSQP-LICENSE.txt|
plugins/licenses/OSQP-NOTICE.txt|
plugins/licenses/OSQP-VERSION.txt|
plugins/osqp.dll

Also use AssemblyName.GetAssemblyName to verify the new file is a managed assembly named TrajectoryPlanningVisualization.

  • Step 2: Run RED
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package

Expected: failure because the publish script omits the visualization DLL.

  • Step 3: Update the atomic publisher

Resolve:

$visualizationDll = Join-Path ([System.IO.Path]::GetDirectoryName($managedDllPath)) 'TrajectoryPlanningVisualization.dll'
if (-not [System.IO.File]::Exists($visualizationDll)) {
    throw "Visualization DLL does not exist beside managed DLL: $visualizationDll"
}

Copy it into staging before the atomic directory swap. Keep all existing drive-root/workspace-root protections, OSQP hash validation and recovery behavior unchanged.

  • Step 4: Run package GREEN

Run plugin-package twice. Expected: PASS both times and no stale staging/backup directories.

  • Step 5: Update both READMEs

The MovementTest README must document:

  • how to enable web/Painter modes;
  • localhost/token URL behavior;
  • Chinese scientific chart conventions;
  • ReferenceS versus local PathS;
  • jerk interval semantics;
  • rolling/approach/exact-stop labels;
  • active/completed/future segment colors;
  • direction-confirmation conditions;
  • browser closure and TestStop behavior.

The EM README plugin tree must include TrajectoryPlanningVisualization.dll and link to the observation README.

  • Step 6: Commit packaging and docs
git add -- ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1 ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md
git commit -m "docs: package EM observation dashboard"

Task 8: Full automated and manual acceptance

Files:

  • No planned source changes. If a command fails, return to the owning task, add a focused failing regression there, implement the minimal correction, and rerun this acceptance task from Step 1.

Interfaces:

  • Consumes: both verification hosts, the packaged plugin output, the deterministic smoke mode, and the deployed MovementTest entry.

  • Produces: fresh automated evidence plus an explicit completed-or-pending vehicle checklist; it does not introduce a new runtime API.

  • Step 1: Run the complete automated suite

dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all
dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true
git diff --check

Expected: visualization host PASS; every em-all component PASS; main build exits 0; no new whitespace errors. Record any pre-existing warnings separately rather than claiming they were introduced here.

  • Step 2: Run a local dashboard smoke session

Run the deterministic smoke mode delivered by Plan 1:

dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 30

Open the printed tokenized URI during the 30-second window and verify:

  • Chinese titles/status and English/scientific axes;
  • thin white-background scientific plots;
  • all four tabs and all required charts;
  • active segment/world horizon highlight;
  • effective configuration values;
  • stale-state display after sample publication stops;
  • port release after the host exits.

This smoke command must publish synthetic snapshots only and must not reference MDCS or hardware.

  • Step 3: Perform the deployed vehicle observation checklist

In a supervised safe environment:

  1. Enable web and leave native Painter disabled.
  2. Confirm OBSERVE_ONLY appears in UI/console/page.
  3. Observe rolling, approach and exact-stop modes; confirm no forced rolling parking label.
  4. Confirm jerk has N-1 interval samples and no point-21 successor interval or JerkLimitExceeded.
  5. Confirm current/previous handoff uses Δposition/ΔReferenceS/Δv/Δa.
  6. At a real gear switch, confirm stop hold and three signed-speed samples precede N -> N+1 highlight.
  7. Close the browser and confirm planning cadence continues.
  8. Stop MovementTest and confirm server/port/Painter cleanup and no actuator output.
  • Step 4: Commit only acceptance-driven fixes

If Step 13 required code changes, commit each regression and fix with exact file paths. If no changes were required, do not create an empty commit.

Plan 2 is complete only after automated evidence is fresh and the vehicle-only checklist is explicitly reported as completed or, if the vehicle is unavailable, explicitly reported as pending rather than silently treated as passed.