8.4 KiB
Implement — 还原 V2X 实时路口信号灯与车辆展示
参照实现(只读)
/Users/que01/Project/v2x_web/applications/dashboard/src/views/dashboard/components/Content/Map/HologramCross/
改动前先读对应源文件,不要凭记忆写契约。
改动文件
| 文件 | 性质 |
|---|---|
scripts/lib/v2x-cesium-overlay.js |
主要改动,L1–L4 分层重写 |
scripts/lib/cesium-preview.js |
删除重复 lampColorName,调整 setSignalState 入参 |
scripts/test-v2x-cesium-overlay.js |
扩充单测,覆盖 AC2–AC7 |
.trellis/tasks/.../prd.md design.md |
如实现中发现偏差需回写 |
不改:build-area.js、area-preview.js、包产物生成逻辑。
执行顺序
每步结束跑一次 npm run test:v2x-cesium-preview,保持绿色再进下一步。
步骤 1 — L2 灯色字典 + 宽松解析(R2、R9)
- 在 overlay 加
LAMP_STATUS(11/21/22/23/31)与lampColorName(status),未知返回"other"。 - 删除旧
lampColor()中Number(status)===3 / ===2的判断。 - 加
parseLoosePayload(text):JSON.parse→ 受限规范化重试 →null。 - 心跳报文
pong短路判定保留(与源项目一致)。 - 二者导出到
.utils。
验证:node scripts/test-v2x-cesium-overlay.js
lampColorName(21)==="red",(22)==="yellow",(23)==="green",(11)==="off",(31)==="other"lampColorName(2)!=="yellow"且lampColorName(3)!=="green"(防回归,AC2)parseLoosePayload("{'a':1,}")解析成功;parseLoosePayload("{bad")===null(AC7)
审查点 A:灯色字典必须与 HologramCross/components/utils.ts 的
lightStatusColorDict 逐项一致,不得增删码值。
步骤 2 — L1 几何工具(支撑 R3)
- 加
getAngle(start, end):直接对齐源项目utils.ts的实现(返回 0–360)。 - 加
haversineMeters(a, b)、angleDeltaDegrees(a, b)(后者处理 350° vs 10° 的环绕)。 - 导出到
.utils。
验证:正北 getAngle([0,0],[0,1])≈0;正东 ≈90;
angleDeltaDegrees(350,10)===20;haversineMeters 对已知两点误差 < 0.5m。
步骤 3 — 相位映射(R3、R4)
- 实现
buildPhaseSignalMap(links, nativeSignals, options),返回{byPhase, bound, unbound, diagnostics},算法见design.md§4.2。 - link 末两点先 GCJ-02→WGS84 再算角度与距离(原生信号是 WGS84)。
- 一 link 匹配多 signal;
Map<phaseNo, Set<nodeKey>>并集累加。 - 支持
options.phaseSignalMap配置覆盖,命中则跳过几何匹配。 - 默认
maxDistanceMeters=30、maxHeadingDeltaDegrees=45。 - 导出到
.utils。
验证(AC3、AC4):
- 用
outputs/fengshu-er-road/package/runtime/traffic-signals.json的真实 7 条信号做夹具, 配一组构造的 link(末点取自stopLongitude/stopLatitude反算 GCJ-02、 航向取自headingDegrees),断言bound > 0。 - 构造两条不同 link 共用同一
phaseNo,断言byPhase.get(phaseNo)含两条 link 的全部 nodeKey。 - 构造一条远离所有信号的 link,断言其相位进入
unbound且带reason。 - 配置覆盖生效:
phaseSignalMap指定的 phaseNo 取配置值。
审查点 B:这是全任务最易做错的一步。合入前必须用真实 traffic-signals.json 跑出
非零绑定数,若为 0 则说明容差或坐标方向反了,不得靠放大容差蒙混过关——先打印
diagnostics 的实际距离/角差,确认量级合理(距离应是米级而非千米级)。
步骤 4 — 车辆注册表(R7、R8)
- 实现
createVehicleRegistry({now}),含ingest / sweep / list / clear。 interval取自推送体,0或缺失回退500。sweep:|now - timeStamp| >= interval*1.5→visible=false。- 槽位复用:优先占用同
model的不可见记录。 modelNameFor(vehicle):OBU → dashboard 最终模板使用的11.glb;target →${type}${subType}.glb。把 dashboard 车辆 GLB 复制到预览包。normalizeTargetVehicles补上源项目的type==1 && isNil(subType) → subType=1。- 导出
createVehicleRegistry、modelNameFor。
验证(AC5):
- 注入一次推送 →
list()一条visible=true。 - 假时钟推进
interval*1.5后sweep()→ 该车visible=false。 - 再注入同 key → 复用原槽位,
list().length不变。 clear()后为空。modelNameFor({kind:"obu"})==="11.glb";{kind:"target",type:1,subType:2}→"12.glb"。
步骤 5 — WebSocket 连接层(R1、R6)
- 实现
createSocket({url, onOpen, onMessage, onClose, heartbeatMs, reconnect, socketFactory, setIntervalFn, setTimeoutFn})。 - 心跳
{"heartBeat":"ping"}/ 30000ms。 - 退避重连
1s→2s→4s→8s封顶,重连成功重放onOpen。 - 主动
close()不重连;dispose()清所有计时器。 - 三个 socket 的订阅帧按
design.md§4.3 的表格实现。 - OBU bounds:
camera.computeViewRectangle()→ WGS84→GCJ-02 →"lng,lat;…"四角,camera.moveEnd节流发送;不可用时发空串。 - 导出
createSocket、buildBoundsMessage。
验证(AC1、AC6):
- 假 socket + 假时钟:推进 30s 断言收到一帧
{"heartBeat":"ping"};推进 90s 断言三帧。 - 模拟
onclose后推进退避时间,断言重新建连且订阅帧被重放。 - signal 首帧
{"junctionId":"<code>"};target 无配置首帧{"deviceId":null}, 有配置{"deviceId":"1,2"};obu 首帧为空串。 dispose()后推进时钟不再产生任何发送。
审查点 C:心跳报文与间隔必须逐字节对齐源项目
(useObuCars.ts:28-31、useTargetCars.ts:27-30、CrossTrafficLights3D.vue:122-130)。
步骤 6 — 接线与渲染适配(R4、R5、R10)
loadLiveData()移除FlowTravelRatio/queryListWeek,删state.metrics与面板 flow 文案。- 用
buildPhaseSignalMap结果替换state.linkPhases(Map<phase, entity>整体删除)。 updateSignalPhases→ 归一化为{nodeKeys, color, countDown}列表后调context.setSignalState(...),同时给 link polyline 上色(同相位多 link 全部上色)。- 车辆渲染改为消费
registry:visible切entity.show,位置用SampledPositionProperty按duration插值。 - 面板状态行补:已绑定/未绑定相位数、缺失模型数、解析失败数。
dispose()清 socket、计时器、实体、registry。
步骤 7 — cesium-preview.js 对接(R2、R5)
- 删除
cesium-preview.js中的lampColorName()(与 overlay 重复且映射错误)。 createLiveTrafficSignals().update(lamps)改签名为update(entries),entries = [{nodeKeys:string[], color:"red"|"yellow"|"green"|"off"|"other", countDown:number}]。- 按
nodeKeys直接点亮TrafficSignalDynamic_<nodeKey>_<color>节点, 删除失效的signalByPhase回退逻辑。 countDown驱动category === "countdown"资产显示。color === "off" | "other"时三色节点全灭。
验证:npm run test:preview-assets
步骤 8 — 全量校验
npm run test:v2x-cesium-preview
npm run test:v2x-preview-server
npm run test:preview-assets
npm run test:traffic-signals
npm run test:native-preview-traffic
npm run test:package-contract
AC10 校验:将区域配置 v2xPreview.enabled 置 false 重新生成,
确认生成页无 V2X 面板且 package/ 下产物字节不变。
人工验收(AC9,需真实上游)
V2X_UPSTREAM=http://<host>:<port> \
npm run serve:v2x-preview -- --root outputs/fengshu-er-road --host 0.0.0.0 --port 7862
登录后逐项确认:
- 信号灯随相位变色,颜色与 dashboard 实时路口一致
- 倒计时数字走动
- 车辆出现、移动平滑、离开视野后消失(无幽灵车堆积)
- 连接持续 5 分钟以上不掉线
- 车辆与路口位置无明显偏移(坐标未二次转换)
- 面板绑定数、失败数符合预期
回滚点
- 步骤 7 独立可 revert(只动
cesium-preview.js)。 - 步骤 1–6 全在 overlay 单文件内,
git checkout scripts/lib/v2x-cesium-overlay.js即回到当前状态。 v2xPreview.enabled=false是运行时总开关。
不做(留待后续任务)
- AMap 底图 / 矢量瓦片图层
CrossDevices/Model3D.vue3D 杆件模型(设备维持点位)useV2XEvents事件气泡