diff --git a/ClumsyPilot/TrajectoryPlanningVisualization/README.md b/ClumsyPilot/TrajectoryPlanningVisualization/README.md new file mode 100644 index 0000000..03faf93 --- /dev/null +++ b/ClumsyPilot/TrajectoryPlanningVisualization/README.md @@ -0,0 +1,44 @@ +# Trajectory Planning Visualization + +`TrajectoryPlanningVisualization` 是一个独立的 `netstandard2.0` 轨迹观察类库。它只接受通用、不可变的可视化快照,并在本机浏览器中显示中文科研风图表;它不引用 EMPlanner、MDCS、Painter、ClumsyCore 或 MovementTest。 + +## 安全与运行边界 + +此库固定为 `OBSERVE_ONLY`:浏览器没有控制路由,任何网页事件都不能回流到规划、车辆或执行器。服务仅使用 `TcpListener` 绑定 `127.0.0.1`,不是 `localhost` 的局域网别名,也不会监听外网地址。它只处理受限的 HTTP/1.1 `GET`;请求头最大为 16 KiB、读取总时限为 2 秒。 + +`Start` 为每个会话生成随机 256-bit 小写十六进制 token,并在完整 URI 中返回它。页面、`/app.css`、`/app.js`、`/api/bootstrap` 与 `/api/events` 都要求该 token。服务不使用 Cookie,最多服务两个 SSE 客户端。网页地址只适合运行类库的本机用户;不要把带 token 的地址转发给其他人。 + +## 最小用法 + +调用方负责把自己的领域数据转换为通用的静态和动态快照;类库不理解任何规划器专有类型。 + +```csharp +using var visualization = new PlanningVisualizationSession( + new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 10d, HistoryCycleLimit = 60 }); +PlanningVisualizationSessionInfo info = visualization.Start(staticSnapshot); +visualization.Publish(dynamicSnapshot); +visualization.Stop(); +``` + +`Start` 只传输一次静态快照。`Publish` 只校验并以原子交换写入最新动态帧,绝不序列化、等待 socket、浏览器或绘图;旧帧会被丢弃,因此最新帧槽的 **capacity 1** 是有意的。默认刷新率为 **10 Hz**,周期摘要默认保留 60 条;完整轨迹只属于当前最新帧和调用方快照,不进入历史。 + +重复 `Start` 会返回已有会话信息;重复 `Stop` 安全。正常 `Stop` 最多尝试 100 ms 发送 `event: end`(“会话已结束”),之后关闭 SSE 和监听端口。 + +## 路由与性能模型 + +- `/?token=…`:嵌入式 `index.html`,并将同一 token 放入 CSS 和 JavaScript 的 URL。 +- `/app.css?token=…`、`/app.js?token=…`:程序集嵌入资源,无 CDN、Node.js、包管理器或运行时网页文件查找。 +- `/api/bootstrap?token=…`:一次返回静态快照和有效刷新率。 +- `/api/events?token=…`:以 SSE 推送最新动态帧及有界周期摘要。 + +慢客户端使用容量为 1 的发送槽,因此不会对 `Publish` 形成反压;没有 SSE 客户端时服务不会重复序列化动态帧。占用栅格是行优先 LSB-first bitset,浏览器用单个 Canvas 解码和绘制,SVG 只覆盖路径、车辆和标记。 + +## 图表约定 + +页面包含“路径总览”、“LS / ST”、“曲率与运动学”和“周期历史与生效配置”四个页签。八个稳定 chart ID 为 `ls`、`st`、`curvature-s`、`curvature-t`、`velocity-t`、`acceleration-t`、`jerk-t`、`yaw-rate-t`。说明文本为中文;轴标签保留输入快照提供的科学变量、数学符号和 SI 单位。 + +`ls` 的横轴是活动方向段的 `ReferenceS (m)`,`st` 的横轴是每轮局部 `PathS (m)`;两者不混用。jerk 的 `j[i]` 仅表示真实区间 `[tᵢ,tᵢ₊₁)`,数量为轨迹点数减一,最后一个轨迹点不补造 `j=0`,页面明确标注“末点后无时间区间”。 + +## 适配器责任 + +适配器应在外部项目中冻结配置、构造 `PlanningVisualizationStaticSnapshot`,并在每个观察周期构造 `PlanningVisualizationDynamicSnapshot`。它可以在启动后使用 `info.Uri` 由宿主决定是否打开浏览器;本类库不会读取桌面环境或启动进程。快照、服务或前端异常应被宿主隔离为一次中文诊断,不得改变规划周期、求解结果或任何车辆控制行为。 diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs index f969d55..c3a63b7 100644 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs +++ b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using TrajectoryPlanningVisualization; @@ -12,6 +13,7 @@ internal static class ContractChecks CopiesCollectionsAndRejectsNonFiniteNumbers(); StoresOccupancyAsDefensiveCompactBits(); ExposesImmutableSnapshotContracts(); + DocumentsPublicSafetyAndLifecycleBoundary(); } private static void CopiesCollectionsAndRejectsNonFiniteNumbers() @@ -84,4 +86,23 @@ internal static class ContractChecks Verification.True(property.SetMethod == null, type.Name + " exposes no property setters"); } } + + private static void DocumentsPublicSafetyAndLifecycleBoundary() + { + string readmePath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, + "..", "..", "..", "..", "..", "TrajectoryPlanningVisualization", "README.md")); + string readme = File.ReadAllText(readmePath); + + Verification.True(readme.Contains("OBSERVE_ONLY"), "README documents observe-only boundary"); + Verification.True(readme.Contains("127.0.0.1"), "README documents loopback boundary"); + Verification.True(readme.Contains("capacity 1"), "README documents latest-frame capacity"); + Verification.True(readme.Contains("60"), "README documents default history limit"); + Verification.True(readme.Contains("10 Hz"), "README documents default refresh rate"); + Verification.True(readme.Contains("j[i]"), "README documents jerk interval semantics"); + Verification.True(readme.Contains("末点后无时间区间"), "README documents jerk terminal explanation"); + Verification.True(readme.Contains("PlanningVisualizationSessionInfo info = visualization.Start(staticSnapshot);"), + "README documents Start example"); + Verification.True(readme.Contains("visualization.Publish(dynamicSnapshot);"), "README documents Publish example"); + Verification.True(readme.Contains("visualization.Stop();"), "README documents Stop example"); + } }