docs: plan EM observation web visualization
This commit is contained in:
@@ -0,0 +1,694 @@
|
||||
# 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()`.
|
||||
|
||||
```csharp
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
private readonly IEmPlanningService planningService;
|
||||
private EmPlanningCoordinator coordinator;
|
||||
private TrajectoryExecutor executor;
|
||||
private readonly TrajectoryObservationSegmentTracker segmentTracker;
|
||||
private EmTrajectory previousTrajectoryForVisualization;
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```text
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```powershell
|
||||
$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**
|
||||
|
||||
```powershell
|
||||
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**
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```powershell
|
||||
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 1–3 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.
|
||||
@@ -0,0 +1,660 @@
|
||||
# Trajectory Planning Visualization Library 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:** Build a reusable `netstandard2.0` library that publishes immutable planning snapshots through a bounded, non-blocking loopback HTTP/SSE service and renders a self-contained Chinese scientific-style dashboard.
|
||||
|
||||
**Architecture:** The library owns generic visualization contracts, an atomic latest-frame exchange, bounded cycle summaries, a minimal `TcpListener` HTTP/1.1 server, embedded web assets, and one `PlanningVisualizationSession` facade. It has no dependency on EMPlanner, MDCS, MovementTest, ClumsyCore, Painter, ASP.NET Core, Node.js, or an external CDN.
|
||||
|
||||
**Tech Stack:** C# 10, `netstandard2.0`, `TcpListener`, Server-Sent Events, Newtonsoft.Json 13.0.4, embedded HTML/CSS/JavaScript, a `net10.0-windows` console verification host.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Bind only `IPAddress.Loopback`; never bind `IPAddress.Any`, `0.0.0.0`, a LAN address, or a hostname prefix.
|
||||
- Accept only HTTP `GET`; cap request headers at `16 KiB` and request reads at `2 s`.
|
||||
- Require the per-session random token on the page, assets, bootstrap, and event endpoints.
|
||||
- `Publish` may only perform validation plus `Interlocked.Exchange`; it must not serialize, write a socket, wait for a client, or mutate caller-owned collections.
|
||||
- Latest dynamic-frame capacity is exactly `1`; full trajectory data exists only in that latest frame.
|
||||
- Cycle history is bounded by `HistoryCycleLimit` and stores summaries only.
|
||||
- Default refresh rate is `10 Hz`, default history limit is `60`, default port is `0`, and maximum clients is fixed at `2`.
|
||||
- Web assets are embedded and self-contained; no CDN, package manager, web build, or runtime file lookup.
|
||||
- Chinese is used for descriptions and state text; axis variables, SI units, and mathematical symbols retain scientific notation.
|
||||
- Preserve all unrelated dirty-worktree changes and stage only files named by each task.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` | Standalone class library, Newtonsoft dependency, and (from Task 4 onward) embedded assets. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationPrimitives.cs` | Points, poses, bounds, key/value data and defensive collection-copy helpers. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs` | World polylines, markers, segments and static world snapshot. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` | Charts, axes, series and line-style contracts. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/PlanningVisualizationSnapshots.cs` | Static snapshot, dynamic snapshot, cycle summary and session status. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/PlanningVisualizationOptions.cs` | Validated transport and retention options. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LatestVisualizationFrameStore.cs` | Atomic capacity-one dynamic-frame exchange. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/BoundedCycleHistory.cs` | Service-side deduplicated cycle-summary ring. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/VisualizationJson.cs` | Stable camel-case invariant JSON serialization. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs` | Bounded GET request parser. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs` | Per-client capacity-one outbound payload and bounded writer. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs` | TCP accept loop, route authorization and SSE clients. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs` | Public `Start/Publish/Stop` facade and fault isolation. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` | Chinese dashboard structure. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` | Thin-line scientific visual system. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` | Snapshot consumption, SVG charts, tabs and stale-state handling. |
|
||||
| `ClumsyPilot/TrajectoryPlanningVisualization/README.md` | Public API, endpoints, resource guarantees and integration example. |
|
||||
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj` | Independent executable verification host. |
|
||||
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` | Adds each check group as its task lands and provides the final timed smoke mode. |
|
||||
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` | Deterministic synthetic dashboard sample for browser smoke testing. |
|
||||
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs` | Minimal assertion helpers. |
|
||||
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` | Contract immutability and validation checks. |
|
||||
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs` | Latest-frame, history and JSON checks. |
|
||||
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs` | Real loopback HTTP/SSE lifecycle checks. |
|
||||
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` | Embedded Chinese scientific dashboard checks. |
|
||||
| `ClumsyPilot/ClumsyPilot.csproj` | Excludes the new library/test sources from default recursive compilation and references the library project. |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Standalone project and immutable visualization contracts
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationPrimitives.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/PlanningVisualizationSnapshots.cs`
|
||||
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj`
|
||||
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs`
|
||||
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs`
|
||||
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs`
|
||||
- Modify: `ClumsyPilot/ClumsyPilot.csproj`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Produces: `VisualizationPoint`, `VisualizationPose`, `VisualizationBounds`, `VisualizationValue`, `VisualizationConfigurationGroup`, `VisualizationPolyline`, `VisualizationMarker`, `VisualizationDirectionSegment`, `VisualizationOccupancyGrid`, `VisualizationChart`, `VisualizationSeries`, `PlanningVisualizationStaticSnapshot`, `PlanningVisualizationDynamicSnapshot`, and `VisualizationCycleSummary`.
|
||||
- Collection-bearing constructors must reject null elements and copy source collections into `ReadOnlyCollection<T>`.
|
||||
- Numeric constructors must reject NaN/infinity. Timestamps must be carried as `DateTimeOffset`.
|
||||
|
||||
- [ ] **Step 1: Create the independent verification host and failing contract checks**
|
||||
|
||||
Add a host that initially calls only the contract group and returns `1` on exceptions:
|
||||
|
||||
```csharp
|
||||
internal static class Program
|
||||
{
|
||||
private static int Main()
|
||||
{
|
||||
try
|
||||
{
|
||||
ContractChecks.Run();
|
||||
Console.WriteLine("PASS trajectory-planning-visualization");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(exception);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Start `ContractChecks.Run()` with a defensive-copy test:
|
||||
|
||||
```csharp
|
||||
var points = new List<VisualizationPoint> { new VisualizationPoint(1d, 2d) };
|
||||
var series = new VisualizationSeries("current", "当前轨迹", VisualizationLineStyle.Solid, points);
|
||||
points[0] = new VisualizationPoint(9d, 9d);
|
||||
Verification.NearlyEqual(1d, series.Points[0].X, "series copies points");
|
||||
Verification.Throws<ArgumentOutOfRangeException>(
|
||||
() => new VisualizationPoint(double.NaN, 0d), "point rejects NaN");
|
||||
|
||||
byte[] bits = { 0x01 };
|
||||
var grid = new VisualizationOccupancyGrid(new VisualizationBounds(0d, 1d, 0d, 1d),
|
||||
0.5d, rows: 2, columns: 2, bits);
|
||||
bits[0] = 0x00;
|
||||
Verification.Equal("AQ==", grid.OccupancyBitsBase64, "occupancy grid copies compact bits");
|
||||
```
|
||||
|
||||
The verification host project must reference only the standalone visualization library:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\TrajectoryPlanningVisualization\TrajectoryPlanningVisualization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the host to verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
|
||||
```
|
||||
|
||||
Expected: compilation fails because `TrajectoryPlanningVisualization` contracts do not exist.
|
||||
|
||||
- [ ] **Step 3: Add project boundaries and contract implementations**
|
||||
|
||||
The library project must contain:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
Update the main project so nested project/test sources are not compiled twice:
|
||||
|
||||
```xml
|
||||
<Compile Remove="TrajectoryPlanningVisualization\**\*.cs" />
|
||||
<Compile Remove="tests\TrajectoryPlanningVisualizationVerificationHost\**\*.cs" />
|
||||
<ProjectReference Include="TrajectoryPlanningVisualization\TrajectoryPlanningVisualization.csproj" />
|
||||
```
|
||||
|
||||
Define the stable chart contract exactly as follows:
|
||||
|
||||
```csharp
|
||||
public enum VisualizationLineStyle { Solid, Dashed, Limit }
|
||||
|
||||
public sealed class VisualizationSeries
|
||||
{
|
||||
public VisualizationSeries(string id, string legend, VisualizationLineStyle lineStyle,
|
||||
IReadOnlyList<VisualizationPoint> points);
|
||||
public string Id { get; }
|
||||
public string Legend { get; }
|
||||
public VisualizationLineStyle LineStyle { get; }
|
||||
public IReadOnlyList<VisualizationPoint> Points { get; }
|
||||
}
|
||||
|
||||
public sealed class VisualizationChart
|
||||
{
|
||||
public VisualizationChart(string id, string chineseTitle, string xAxisLabel, string yAxisLabel,
|
||||
IReadOnlyList<VisualizationSeries> series, string noteChinese = "");
|
||||
public string Id { get; }
|
||||
public string ChineseTitle { get; }
|
||||
public string XAxisLabel { get; }
|
||||
public string YAxisLabel { get; }
|
||||
public IReadOnlyList<VisualizationSeries> Series { get; }
|
||||
public string NoteChinese { get; }
|
||||
}
|
||||
```
|
||||
|
||||
The two top-level snapshots must expose no setters:
|
||||
|
||||
```csharp
|
||||
public sealed class PlanningVisualizationStaticSnapshot
|
||||
{
|
||||
public PlanningVisualizationStaticSnapshot(string sessionNameChinese, VisualizationBounds worldBounds,
|
||||
VisualizationOccupancyGrid occupancyGrid,
|
||||
IReadOnlyList<VisualizationPolyline> staticPolylines,
|
||||
IReadOnlyList<VisualizationMarker> staticMarkers,
|
||||
IReadOnlyList<VisualizationDirectionSegment> directionSegments,
|
||||
IReadOnlyList<VisualizationConfigurationGroup> configurationGroups);
|
||||
public string SessionNameChinese { get; }
|
||||
public VisualizationBounds WorldBounds { get; }
|
||||
public VisualizationOccupancyGrid OccupancyGrid { get; }
|
||||
public IReadOnlyList<VisualizationPolyline> StaticPolylines { get; }
|
||||
public IReadOnlyList<VisualizationMarker> StaticMarkers { get; }
|
||||
public IReadOnlyList<VisualizationDirectionSegment> DirectionSegments { get; }
|
||||
public IReadOnlyList<VisualizationConfigurationGroup> ConfigurationGroups { get; }
|
||||
}
|
||||
|
||||
public sealed class PlanningVisualizationDynamicSnapshot
|
||||
{
|
||||
public PlanningVisualizationDynamicSnapshot(long sequence, DateTimeOffset observedAtUtc,
|
||||
string sessionStateChinese, int activeSegmentIndex, string activeDirection,
|
||||
VisualizationPose vehiclePose, IReadOnlyList<VisualizationPolyline> dynamicPolylines,
|
||||
IReadOnlyList<VisualizationMarker> dynamicMarkers, IReadOnlyList<VisualizationChart> charts,
|
||||
IReadOnlyList<VisualizationValue> statusValues, VisualizationCycleSummary cycleSummary);
|
||||
public long Sequence { get; }
|
||||
public DateTimeOffset ObservedAtUtc { get; }
|
||||
public string SessionStateChinese { get; }
|
||||
public int ActiveSegmentIndex { get; }
|
||||
public string ActiveDirection { get; }
|
||||
public VisualizationPose VehiclePose { get; }
|
||||
public IReadOnlyList<VisualizationPolyline> DynamicPolylines { get; }
|
||||
public IReadOnlyList<VisualizationMarker> DynamicMarkers { get; }
|
||||
public IReadOnlyList<VisualizationChart> Charts { get; }
|
||||
public IReadOnlyList<VisualizationValue> StatusValues { get; }
|
||||
public VisualizationCycleSummary CycleSummary { get; }
|
||||
}
|
||||
```
|
||||
|
||||
Use these exact primitive/geometry fields so later adapters do not invent parallel DTOs:
|
||||
|
||||
```text
|
||||
VisualizationPoint: X, Y
|
||||
VisualizationPose: X, Y, HeadingRadians
|
||||
VisualizationBounds: XMin, XMax, YMin, YMax
|
||||
VisualizationValue: ChineseName, RawName, Value, Unit, Severity
|
||||
VisualizationConfigurationGroup: ChineseTitle, Entries
|
||||
VisualizationPolyline: Id, LegendChinese, Kind, LineStyle, Points
|
||||
VisualizationMarker: Id, Kind, LabelChinese, Position
|
||||
VisualizationDirectionSegment: SegmentIndex, Direction, StartsAtGearSwitch, EndsAtGearSwitch, Points
|
||||
VisualizationOccupancyGrid: Bounds, ResolutionMeters, Rows, Columns, OccupancyBitsBase64
|
||||
VisualizationCycleSummary: CycleVersion, OccurredAtUtc, Status, Published,
|
||||
PlanningElapsedMilliseconds, SegmentIndex, Direction, LongitudinalMode,
|
||||
TerminalType, TerminalVelocity, TerminalAcceleration, FailureReason
|
||||
```
|
||||
|
||||
`TerminalVelocity` and `TerminalAcceleration` are nullable doubles; all other numeric fields are non-null. `Severity` is one of `normal`, `notice`, or `failure`, validated as an exact string so the generic library does not depend on an EM enum.
|
||||
|
||||
`VisualizationOccupancyGrid` accepts a row-major bit array of exactly `ceil(Rows * Columns / 8)` bytes, makes a defensive copy, and exposes it as Base64 JSON through `OccupancyBitsBase64`. Bit index `row * Columns + column` uses the least-significant bit first within each byte. A null occupancy grid is allowed for non-map visualizations.
|
||||
|
||||
- [ ] **Step 4: Run the focused contract host GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
|
||||
dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true
|
||||
```
|
||||
|
||||
Expected: both exit `0`; the host prints `PASS trajectory-planning-visualization` and the main build has no duplicate-type errors.
|
||||
|
||||
- [ ] **Step 5: Commit the project and contracts**
|
||||
|
||||
```powershell
|
||||
git add -- ClumsyPilot/TrajectoryPlanningVisualization ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost ClumsyPilot/ClumsyPilot.csproj
|
||||
git commit -m "feat: add planning visualization contracts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Capacity-one frame exchange, bounded history, and JSON
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/PlanningVisualizationOptions.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LatestVisualizationFrameStore.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/BoundedCycleHistory.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/VisualizationJson.cs`
|
||||
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs`
|
||||
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Produces: `PlanningVisualizationOptions.CreateValidatedSnapshot()`, `LatestVisualizationFrameStore.Publish(...)`, `TryReadAfter(...)`, `BoundedCycleHistory.Add(...)`, `Snapshot()`, and `VisualizationJson.Serialize(...)`.
|
||||
- `VisualizationFrame` contains a monotonically increasing store version and one dynamic snapshot.
|
||||
|
||||
- [ ] **Step 1: Write failing store, history, and JSON checks**
|
||||
|
||||
Create `RuntimeChecks`, add `RuntimeChecks.Run()` immediately after `ContractChecks.Run()` in `Program`, and add these behaviors:
|
||||
|
||||
```csharp
|
||||
var store = new LatestVisualizationFrameStore();
|
||||
store.Publish(Snap(1));
|
||||
store.Publish(Snap(2));
|
||||
Verification.True(store.TryReadAfter(0, out VisualizationFrame frame), "latest frame exists");
|
||||
Verification.Equal(2L, frame.Snapshot.Sequence, "latest frame replaces old frame");
|
||||
Verification.True(!store.TryReadAfter(frame.Version, out _), "same frame is not replayed");
|
||||
|
||||
var history = new BoundedCycleHistory(2);
|
||||
history.Add(Cycle(1)); history.Add(Cycle(2)); history.Add(Cycle(3)); history.Add(Cycle(3));
|
||||
Verification.Equal("2|3", string.Join("|", history.Snapshot().Select(x => x.CycleVersion)),
|
||||
"history is bounded and deduplicated");
|
||||
|
||||
string json = VisualizationJson.Serialize(Snap(2));
|
||||
Verification.True(json.Contains("\"sessionStateChinese\"") && json.Contains("\"observedAtUtc\""),
|
||||
"JSON uses stable camel case names");
|
||||
```
|
||||
|
||||
Also verify invalid options: negative port, refresh rate `0`, history `0`, and port `65536` throw.
|
||||
|
||||
- [ ] **Step 2: Run to verify RED**
|
||||
|
||||
Run the visualization host. Expected: compilation fails because runtime types do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the non-blocking stores and options**
|
||||
|
||||
`LatestVisualizationFrameStore.Publish` must contain no lock or callback:
|
||||
|
||||
```csharp
|
||||
public void Publish(PlanningVisualizationDynamicSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null) throw new ArgumentNullException(nameof(snapshot));
|
||||
long version = Interlocked.Increment(ref nextVersion);
|
||||
Interlocked.Exchange(ref latest, new VisualizationFrame(version, snapshot));
|
||||
}
|
||||
```
|
||||
|
||||
`TryReadAfter` reads with `Volatile.Read`, and the history uses a private lock only on the service-consumer thread. `BoundedCycleHistory.Add` ignores a repeated `CycleVersion`, removes from the head while count exceeds capacity, and `Snapshot` returns a fresh read-only copy.
|
||||
|
||||
Use Newtonsoft settings with `CamelCasePropertyNamesContractResolver`, `InvariantCulture`, `DateFormatHandling.IsoDateFormat`, and `Formatting.None`.
|
||||
|
||||
- [ ] **Step 4: Run to verify GREEN**
|
||||
|
||||
Run the visualization host twice. Expected: both runs print the PASS line and never depend on working-directory files.
|
||||
|
||||
- [ ] **Step 5: Commit runtime data handling**
|
||||
|
||||
```powershell
|
||||
git add -- ClumsyPilot/TrajectoryPlanningVisualization/Runtime ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs
|
||||
git commit -m "feat: add bounded visualization snapshots"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Restricted loopback HTTP/SSE server and public facade
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs`
|
||||
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs`
|
||||
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Produces: `PlanningVisualizationSession.Start(PlanningVisualizationStaticSnapshot)`, `Publish(PlanningVisualizationDynamicSnapshot)`, `Stop()`, `IsRunning`, and `SessionInfo.Uri`.
|
||||
- This task produces `/api/bootstrap` and `/api/events`; Task 4 adds `/`, `/app.css`, and `/app.js`. Every available route requires `token=<session-token>`.
|
||||
- Statuses: bad token `403`, unknown route `404`, non-GET `405`, oversized/malformed request `400`, third live SSE client `503`.
|
||||
|
||||
- [ ] **Step 1: Write failing real-socket checks**
|
||||
|
||||
Create `ServerChecks`, call it after `RuntimeChecks`, and use `TcpClient` rather than mocks:
|
||||
|
||||
```csharp
|
||||
using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0 }))
|
||||
{
|
||||
PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot());
|
||||
Verification.Equal("127.0.0.1", info.Uri.Host, "server binds loopback");
|
||||
Verification.Equal(403, SendStatus(info.Uri.Port, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"),
|
||||
"missing token is forbidden");
|
||||
Verification.Equal(405, SendStatus(info.Uri.Port,
|
||||
"POST /?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"),
|
||||
"POST is rejected");
|
||||
Verification.Equal(200, SendStatus(info.Uri.Port,
|
||||
"GET /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"),
|
||||
"authorized bootstrap succeeds");
|
||||
session.Stop();
|
||||
}
|
||||
Verification.True(CanBindReleasedPort(port), "stop releases port");
|
||||
```
|
||||
|
||||
Also send an unknown authorized route, a malformed request line, and a header larger than `16 KiB`; assert `404`, `400`, and `400`. Hold two authorized SSE connections open and assert a third receives `503`. Read one normal JSON response as bytes and assert its `Content-Length` equals the UTF-8 body length.
|
||||
|
||||
Add a slow SSE client that stops reading, publish 10,000 tiny snapshots on a task, and assert the publish task completes within one second. The assertion is about `Publish`, not socket delivery.
|
||||
|
||||
Before connecting any SSE client, publish one distinct cycle summary, wait two refresh periods for the dispatcher, and repeat for three summaries. Then connect and assert the first event contains all three summaries. This respects the capacity-one latest-frame contract while proving history collection is service-owned and does not depend on an open browser.
|
||||
|
||||
- [ ] **Step 2: Run to verify RED**
|
||||
|
||||
Run the visualization host. Expected: compilation fails because the server and facade do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement bounded request parsing and routing**
|
||||
|
||||
Use `TcpListener(IPAddress.Loopback, validated.Port)`. For port `0`, read the assigned port from `LocalEndpoint`. The request reader must:
|
||||
|
||||
```csharp
|
||||
const int MaximumHeaderBytes = 16 * 1024;
|
||||
static readonly TimeSpan RequestReadTimeout = TimeSpan.FromSeconds(2d);
|
||||
```
|
||||
|
||||
Read until `\r\n\r\n`, reject a buffer that reaches the cap, split the first line into exactly method/target/version, allow only origin-form paths, and URL-decode only the `token` query value. Never accept a caller-provided filesystem path.
|
||||
|
||||
The server owns one dispatcher task in addition to the accept loop. At `1 / RefreshRateHz`, the dispatcher reads `LatestVisualizationFrameStore.TryReadAfter`, appends a new non-null cycle summary to `BoundedCycleHistory` even when no browser is connected, and serializes only when at least one SSE client exists. It offers the resulting payload to each client's capacity-one outbound slot with `Interlocked.Exchange`.
|
||||
|
||||
Each `SseClientConnection` owns its socket writer. A slow socket can block only its own writer; newer dispatcher payloads overwrite that client's unsent slot. Apply a one-second write timeout and disconnect the client on timeout. The writer emits:
|
||||
|
||||
```text
|
||||
event: frame
|
||||
data: {camelCase JSON containing snapshot and bounded history}
|
||||
|
||||
```
|
||||
|
||||
The SSE response uses `Content-Type: text/event-stream; charset=utf-8`, `Cache-Control: no-cache`, and `Connection: keep-alive`. Normal JSON responses use `application/json; charset=utf-8`; HTML/CSS/JavaScript use explicit UTF-8 content types and byte-accurate `Content-Length`.
|
||||
|
||||
All socket exceptions are caught inside the client task. A server-fatal exception sets `FaultReason`, cancels the server, and never escapes through `Publish`.
|
||||
|
||||
On normal `Stop`, offer one `event: end` payload with Chinese reason `会话已结束` to every client, allow at most `100 ms` for best-effort flush, then close sockets and the listener. Shutdown must never wait indefinitely for a browser.
|
||||
|
||||
- [ ] **Step 4: Implement idempotent facade lifecycle**
|
||||
|
||||
`PlanningVisualizationSession.Start` validates options and creates a 256-bit token using APIs available in `netstandard2.0`:
|
||||
|
||||
```csharp
|
||||
byte[] tokenBytes = new byte[32];
|
||||
using (RandomNumberGenerator random = RandomNumberGenerator.Create())
|
||||
random.GetBytes(tokenBytes);
|
||||
string token = BitConverter.ToString(tokenBytes).Replace("-", string.Empty).ToLowerInvariant();
|
||||
```
|
||||
|
||||
It then starts the server and returns:
|
||||
|
||||
```csharp
|
||||
new PlanningVisualizationSessionInfo(
|
||||
new Uri("http://127.0.0.1:" + port + "/?token=" + token), token);
|
||||
```
|
||||
|
||||
`Stop` swaps the server field to null under a lifecycle lock, then cancels and disposes outside the lock. `Dispose` calls `Stop`. Calling `Publish` before start or after stop is a no-op; passing null still throws.
|
||||
|
||||
- [ ] **Step 5: Run server checks GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
|
||||
```
|
||||
|
||||
Expected: exit `0`, PASS line, loopback ports released, no unobserved task exception.
|
||||
|
||||
- [ ] **Step 6: Commit the transport**
|
||||
|
||||
```powershell
|
||||
git add -- ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs
|
||||
git commit -m "feat: serve planning snapshots on loopback"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Embedded Chinese scientific dashboard
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js`
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/EmbeddedWebAssets.cs`
|
||||
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs`
|
||||
- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj`
|
||||
- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs`
|
||||
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs`
|
||||
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Produces: four tabs with stable IDs `overview`, `ls-st`, `kinematics`, `history-config`.
|
||||
- Required chart IDs: `ls`, `st`, `curvature-s`, `curvature-t`, `velocity-t`, `acceleration-t`, `jerk-t`, and `yaw-rate-t`.
|
||||
- The client gets `/api/bootstrap` once and consumes `/api/events` through `EventSource`.
|
||||
|
||||
- [ ] **Step 1: Write failing embedded-resource and content checks**
|
||||
|
||||
Create `WebAssetChecks`, call it after `ServerChecks`, read resources through `EmbeddedWebAssets`, and assert:
|
||||
|
||||
```csharp
|
||||
Verification.Contains(html, "路径总览", "overview Chinese title");
|
||||
Verification.Contains(html, "LS / ST", "LS/ST tab");
|
||||
Verification.Contains(html, "曲率与运动学", "kinematics tab");
|
||||
Verification.Contains(html, "周期历史", "history tab");
|
||||
Verification.Contains(html, "生效配置", "configuration panel");
|
||||
Verification.Contains(css, "--current-trajectory: #1769aa", "scientific current color");
|
||||
Verification.Contains(css, "stroke-width: 1.1", "thin scientific line");
|
||||
Verification.Contains(js, "末点后无时间区间", "jerk terminal explanation");
|
||||
Verification.Contains(html, "occupancy-grid", "single occupancy canvas exists");
|
||||
Verification.Contains(html, "world-overlay", "SVG trajectory overlay exists");
|
||||
Verification.Contains(js, "atob", "compact occupancy bitset is decoded in browser");
|
||||
Verification.True(!html.Contains("http://") && !html.Contains("https://"), "page has no CDN URL");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify RED**
|
||||
|
||||
Run the visualization host. Expected: failure because resources are absent.
|
||||
|
||||
- [ ] **Step 3: Implement page structure and scientific CSS**
|
||||
|
||||
Add the three web files to the library as embedded resources:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Web\index.html" LogicalName="TrajectoryPlanningVisualization.Web.index.html" />
|
||||
<EmbeddedResource Include="Web\app.css" LogicalName="TrajectoryPlanningVisualization.Web.app.css" />
|
||||
<EmbeddedResource Include="Web\app.js" LogicalName="TrajectoryPlanningVisualization.Web.app.js" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
The page must include:
|
||||
|
||||
```html
|
||||
<header><h1>EM 轨迹规划观察台</h1><div id="live-state">等待数据</div></header>
|
||||
<nav aria-label="图表页签">
|
||||
<button data-tab="overview">路径总览</button>
|
||||
<button data-tab="ls-st">LS / ST</button>
|
||||
<button data-tab="kinematics">曲率与运动学</button>
|
||||
<button data-tab="history-config">周期历史与生效配置</button>
|
||||
</nav>
|
||||
<main>
|
||||
<section id="overview">
|
||||
<div class="world-stack">
|
||||
<canvas id="occupancy-grid" aria-label="占用栅格底图"></canvas>
|
||||
<svg id="world-overlay" role="img" aria-label="全局路径与当前规划段"></svg>
|
||||
</div>
|
||||
</section>
|
||||
<section id="ls-st" hidden></section>
|
||||
<section id="kinematics" hidden></section>
|
||||
<section id="history-config" hidden></section>
|
||||
</main>
|
||||
```
|
||||
|
||||
The HTML references `/app.css?token=__SESSION_TOKEN__` and `/app.js?token=__SESSION_TOKEN__`. When serving only `index.html`, replace that exact placeholder with the lowercase hexadecimal session token; the token alphabet requires no HTML escaping. `app.js` reads the token from `window.location.search` and appends `encodeURIComponent(token)` to `/api/bootstrap` and `/api/events`. This keeps every route authorized without cookies or custom EventSource headers.
|
||||
|
||||
CSS uses a white canvas, `#20252b` text, `#d9dde1` grid, `#1769aa` current line, `#8d959d` previous dashed line, `#d87918` handoff/gear marker, and `#b42318` only for failures/limits. SVG data lines are `1.1px`; axes are `0.8px`; grids are `0.55px`.
|
||||
|
||||
- [ ] **Step 4: Implement SVG rendering and stale-state behavior**
|
||||
|
||||
`app.js` must:
|
||||
|
||||
- preserve equal X/Y scale in the overhead plot;
|
||||
- decode the row-major occupancy bitset once and draw occupied cells on one Canvas below the SVG overlay, without one DOM node per grid cell;
|
||||
- derive plot bounds from finite values and add a 5% pad;
|
||||
- render series as SVG paths without thick strokes or point-per-sample DOM nodes;
|
||||
- label axes exactly from snapshot `xAxisLabel/yAxisLabel`;
|
||||
- render configuration as Chinese name, raw field, invariant value, unit;
|
||||
- render the active segment and current horizon above faded global segments;
|
||||
- show “末点后无时间区间” beside `jerk-t` rather than adding a final sample;
|
||||
- show “数据已过期” when no frame arrives for `2 / refreshRateHz` seconds;
|
||||
- show “会话已结束” only after the explicit terminal `event: end`; on an ordinary `EventSource` error show “连接中断,正在重连” while the independent stale timer may show “数据已过期”;
|
||||
- install `window.onerror` and `unhandledrejection` handlers that show “页面绘图异常” and stop only that tab's redraw loop;
|
||||
- never evaluate HTML from snapshot strings; use `textContent` for labels and values.
|
||||
|
||||
- [ ] **Step 5: Run GREEN and build from a non-repository working directory**
|
||||
|
||||
Run the host from repository root, then:
|
||||
|
||||
```powershell
|
||||
$visualizationHost = (Resolve-Path 'ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj').Path
|
||||
Push-Location $env:TEMP
|
||||
try {
|
||||
dotnet run --project $visualizationHost
|
||||
} finally { Pop-Location }
|
||||
```
|
||||
|
||||
Expected: both invocations print PASS, proving assets come from the assembly rather than current directory.
|
||||
|
||||
- [ ] **Step 6: Add a deterministic timed smoke mode**
|
||||
|
||||
Add `SampleSnapshotFactory` that creates two direction segments, a gear marker, effective configuration groups and all required charts without referencing EMPlanner. Extend `Program` so no arguments runs checks, while `--smoke-seconds N` starts a session, publishes the sample at 10 Hz for exactly `N` seconds, prints the full URI once, then stops.
|
||||
|
||||
```csharp
|
||||
if (args.Length == 2 && args[0] == "--smoke-seconds" &&
|
||||
int.TryParse(args[1], out int seconds) && seconds > 0)
|
||||
return RunSmoke(seconds);
|
||||
```
|
||||
|
||||
Verify with:
|
||||
|
||||
```powershell
|
||||
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 1
|
||||
```
|
||||
|
||||
Expected: exit `0`, one `http://127.0.0.1:<port>/?token=<token>` line, and clean shutdown after one second.
|
||||
|
||||
- [ ] **Step 7: Commit the dashboard**
|
||||
|
||||
```powershell
|
||||
git add -- ClumsyPilot/TrajectoryPlanningVisualization/Web ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj ClumsyPilot/TrajectoryPlanningVisualization/Runtime/EmbeddedWebAssets.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs
|
||||
git commit -m "feat: add scientific planning dashboard"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Library documentation and final standalone verification
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/README.md`
|
||||
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Documents the exact facade, route/security boundary, performance model, known `localhost` scope, chart conventions, and adapter example.
|
||||
|
||||
- [ ] **Step 1: Add a failing documentation check**
|
||||
|
||||
Assert the README contains `OBSERVE_ONLY`, `127.0.0.1`, `capacity 1`, `60`, `10 Hz`, `j[i]`, `末点后无时间区间`, and the exact `Start/Publish/Stop` example.
|
||||
|
||||
- [ ] **Step 2: Run to verify RED**
|
||||
|
||||
Expected: host fails because README is absent.
|
||||
|
||||
- [ ] **Step 3: Write the README**
|
||||
|
||||
Include this minimal use sequence:
|
||||
|
||||
```csharp
|
||||
using var visualization = new PlanningVisualizationSession(
|
||||
new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 10d, HistoryCycleLimit = 60 });
|
||||
PlanningVisualizationSessionInfo info = visualization.Start(staticSnapshot);
|
||||
visualization.Publish(dynamicSnapshot);
|
||||
visualization.Stop();
|
||||
```
|
||||
|
||||
Explicitly state that callers build all domain-specific charts, the server is read-only, `Publish` drops old frames, and no browser event can flow back into planning.
|
||||
|
||||
- [ ] **Step 4: Run complete standalone verification**
|
||||
|
||||
```powershell
|
||||
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
|
||||
dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj
|
||||
dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all commands exit `0`; host prints one PASS line; builds report zero errors; `git diff --check` reports no whitespace errors in task files.
|
||||
|
||||
- [ ] **Step 5: Commit documentation**
|
||||
|
||||
```powershell
|
||||
git add -- ClumsyPilot/TrajectoryPlanningVisualization/README.md ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs
|
||||
git commit -m "docs: document planning visualization library"
|
||||
```
|
||||
|
||||
Plan 1 is complete when this standalone verification is green. Do not begin EM-specific data conversion before this boundary is reviewed.
|
||||
@@ -103,6 +103,7 @@ visualization.Stop();
|
||||
- 只保留当前轮和上一轮的完整轨迹点。
|
||||
- 周期历史默认保留最近 `60` 轮,历史项只含摘要,不含完整轨迹。
|
||||
- 地图占用信息、全局路径、方向段和配置不进入重复动态帧。
|
||||
- 地图占用栅格编码为行优先 bitset(每格 `1 bit`)并由浏览器单个 Canvas 绘制;不得为每个占用格创建 JSON 对象、SVG 节点或 Painter 对象。
|
||||
- 没有浏览器客户端时不重复序列化相同动态帧。
|
||||
- 网页关闭或变慢不能改变重规划周期、求解器超时或规划结果。
|
||||
- `EnableWebVisualization=false` 时不启动服务、不创建网页快照或历史缓冲。
|
||||
@@ -225,7 +226,7 @@ GearSwitchStopHoldSeconds = 0.20
|
||||
- 浏览器超过两个预期刷新周期没有收到新帧:显示“数据已过期”及最后时间戳,不把旧值伪装成实时状态。
|
||||
- 换向确认失败:保持当前段,显示位置、停车保持、速度方向或连续样本中未满足的条件。
|
||||
- EM 新周期失败:保留上一条成功轨迹,醒目显示本轮状态与未改写的原始失败原因。
|
||||
- 服务或前端异常:本次网页输出熔断关闭,不自动循环重启。
|
||||
- 服务或快照转换异常:本次网页输出熔断关闭,不自动循环重启。浏览器前端异常只在该标签页显示“页面绘图异常”并停止该页重绘;由于服务只接受 GET 且页面没有反向控制通道,前端异常不通知或关闭主进程服务,规划继续且服务器最终由 `TestStop()` 回收。
|
||||
- `TestStop()`:取消规划观察、停止发布、关闭 HTTP/SSE、释放端口;仍连接的页面显示“会话已结束”。
|
||||
|
||||
## 11. 测试策略
|
||||
|
||||
Reference in New Issue
Block a user