35 KiB
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 bindIPAddress.Any,0.0.0.0, a LAN address, or a hostname prefix. - Accept only HTTP
GET; cap request headers at16 KiBand request reads at2 s. - Require the per-session random token on the page, assets, bootstrap, and event endpoints.
Publishmay only perform validation plusInterlocked.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
HistoryCycleLimitand stores summaries only. - Default refresh rate is
10 Hz, default history limit is60, default port is0, and maximum clients is fixed at2. - 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, andVisualizationCycleSummary. -
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:
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:
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:
<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:
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:
<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:
<Compile Remove="TrajectoryPlanningVisualization\**\*.cs" />
<Compile Remove="tests\TrajectoryPlanningVisualizationVerificationHost\**\*.cs" />
<ProjectReference Include="TrajectoryPlanningVisualization\TrajectoryPlanningVisualization.csproj" />
Define the stable chart contract exactly as follows:
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:
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:
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:
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
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(), andVisualizationJson.Serialize(...). -
VisualizationFramecontains 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:
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:
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
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, andSessionInfo.Uri. -
This task produces
/api/bootstrapand/api/events; Task 4 adds/,/app.css, and/app.js. Every available route requirestoken=<session-token>. -
Statuses: bad token
403, unknown route404, non-GET405, oversized/malformed request400, third live SSE client503. -
Step 1: Write failing real-socket checks
Create ServerChecks, call it after RuntimeChecks, and use TcpClient rather than mocks:
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:
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:
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:
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:
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:
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
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, andyaw-rate-t. -
The client gets
/api/bootstraponce and consumes/api/eventsthroughEventSource. -
Step 1: Write failing embedded-resource and content checks
Create WebAssetChecks, call it after ServerChecks, read resources through EmbeddedWebAssets, and assert:
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:
<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:
<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-trather than adding a final sample; -
show “数据已过期” when no frame arrives for
2 / refreshRateHzseconds; -
show “会话已结束” only after the explicit terminal
event: end; on an ordinaryEventSourceerror show “连接中断,正在重连” while the independent stale timer may show “数据已过期”; -
install
window.onerrorandunhandledrejectionhandlers that show “页面绘图异常” and stop only that tab's redraw loop; -
never evaluate HTML from snapshot strings; use
textContentfor labels and values. -
Step 5: Run GREEN and build from a non-repository working directory
Run the host from repository root, then:
$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.
if (args.Length == 2 && args[0] == "--smoke-seconds" &&
int.TryParse(args[1], out int seconds) && seconds > 0)
return RunSmoke(seconds);
Verify with:
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
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
localhostscope, 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:
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
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
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.