Files
osmWorkflow/.trellis/tasks/08-24-v2x-realtime-cross-fidelity/implement.md

8.4 KiB
Raw Blame History

Implement — 还原 V2X 实时路口信号灯与车辆展示

参照实现(只读)

/Users/que01/Project/v2x_web/applications/dashboard/src/views/dashboard/components/Content/Map/HologramCross/

改动前先读对应源文件,不要凭记忆写契约。

改动文件

文件 性质
scripts/lib/v2x-cesium-overlay.js 主要改动L1L4 分层重写
scripts/lib/cesium-preview.js 删除重复 lampColorName,调整 setSignalState 入参
scripts/test-v2x-cesium-overlay.js 扩充单测,覆盖 AC2AC7
.trellis/tasks/.../prd.md design.md 如实现中发现偏差需回写

不改:build-area.jsarea-preview.js、包产物生成逻辑。

执行顺序

每步结束跑一次 npm run test:v2x-cesium-preview,保持绿色再进下一步。

步骤 1 — L2 灯色字典 + 宽松解析R2、R9

  1. 在 overlay 加 LAMP_STATUS11/21/22/23/31)与 lampColorName(status),未知返回 "other"
  2. 删除旧 lampColor()Number(status)===3 / ===2 的判断。
  3. parseLoosePayload(text)JSON.parse → 受限规范化重试 → null
  4. 心跳报文 pong 短路判定保留(与源项目一致)。
  5. 二者导出到 .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")===nullAC7

审查点 A:灯色字典必须与 HologramCross/components/utils.tslightStatusColorDict 逐项一致,不得增删码值。

步骤 2 — L1 几何工具(支撑 R3

  1. getAngle(start, end):直接对齐源项目 utils.ts 的实现(返回 0360
  2. haversineMeters(a, b)angleDeltaDegrees(a, b)(后者处理 350° vs 10° 的环绕)。
  3. 导出到 .utils

验证:正北 getAngle([0,0],[0,1])≈0;正东 ≈90 angleDeltaDegrees(350,10)===20haversineMeters 对已知两点误差 < 0.5m。

步骤 3 — 相位映射R3、R4

  1. 实现 buildPhaseSignalMap(links, nativeSignals, options),返回 {byPhase, bound, unbound, diagnostics},算法见 design.md §4.2。
  2. link 末两点先 GCJ-02→WGS84 再算角度与距离(原生信号是 WGS84
  3. 一 link 匹配多 signalMap<phaseNo, Set<nodeKey>> 并集累加。
  4. 支持 options.phaseSignalMap 配置覆盖,命中则跳过几何匹配。
  5. 默认 maxDistanceMeters=30maxHeadingDeltaDegrees=45
  6. 导出到 .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

  1. 实现 createVehicleRegistry({now}),含 ingest / sweep / list / clear
  2. interval 取自推送体,0 或缺失回退 500
  3. sweep|now - timeStamp| >= interval*1.5visible=false
  4. 槽位复用:优先占用同 model 的不可见记录。
  5. modelNameFor(vehicle)OBU → dashboard 最终模板使用的 11.glbtarget → ${type}${subType}.glb。把 dashboard 车辆 GLB 复制到预览包。
  6. normalizeTargetVehicles 补上源项目的 type==1 && isNil(subType) → subType=1
  7. 导出 createVehicleRegistrymodelNameFor

验证AC5

  • 注入一次推送 → list() 一条 visible=true
  • 假时钟推进 interval*1.5sweep() → 该车 visible=false
  • 再注入同 key → 复用原槽位,list().length 不变。
  • clear() 后为空。
  • modelNameFor({kind:"obu"})==="11.glb"{kind:"target",type:1,subType:2}"12.glb"

步骤 5 — WebSocket 连接层R1、R6

  1. 实现 createSocket({url, onOpen, onMessage, onClose, heartbeatMs, reconnect, socketFactory, setIntervalFn, setTimeoutFn})
  2. 心跳 {"heartBeat":"ping"} / 30000ms。
  3. 退避重连 1s→2s→4s→8s 封顶,重连成功重放 onOpen
  4. 主动 close() 不重连;dispose() 清所有计时器。
  5. 三个 socket 的订阅帧按 design.md §4.3 的表格实现。
  6. OBU boundscamera.computeViewRectangle() → WGS84→GCJ-02 → "lng,lat;…" 四角, camera.moveEnd 节流发送;不可用时发空串。
  7. 导出 createSocketbuildBoundsMessage

验证AC1、AC6

  • 假 socket + 假时钟:推进 30s 断言收到一帧 {"heartBeat":"ping"};推进 90s 断言三帧。
  • 模拟 onclose 后推进退避时间,断言重新建连且订阅帧被重放
  • signal 首帧 {"junctionId":"<code>"}target 无配置首帧 {"deviceId":null} 有配置 {"deviceId":"1,2"}obu 首帧为空串。
  • dispose() 后推进时钟不再产生任何发送。

审查点 C:心跳报文与间隔必须逐字节对齐源项目 useObuCars.ts:28-31useTargetCars.ts:27-30CrossTrafficLights3D.vue:122-130)。

步骤 6 — 接线与渲染适配R4、R5、R10

  1. loadLiveData() 移除 FlowTravelRatio/queryListWeek,删 state.metrics 与面板 flow 文案。
  2. buildPhaseSignalMap 结果替换 state.linkPhasesMap<phase, entity> 整体删除)。
  3. updateSignalPhases → 归一化为 {nodeKeys, color, countDown} 列表后调 context.setSignalState(...),同时给 link polyline 上色(同相位多 link 全部上色)。
  4. 车辆渲染改为消费 registryvisibleentity.show,位置用 SampledPositionPropertyduration 插值。
  5. 面板状态行补:已绑定/未绑定相位数、缺失模型数、解析失败数。
  6. dispose() 清 socket、计时器、实体、registry。

步骤 7 — cesium-preview.js 对接R2、R5

  1. 删除 cesium-preview.js 中的 lampColorName()(与 overlay 重复且映射错误)。
  2. createLiveTrafficSignals().update(lamps) 改签名为 update(entries)entries = [{nodeKeys:string[], color:"red"|"yellow"|"green"|"off"|"other", countDown:number}]
  3. nodeKeys 直接点亮 TrafficSignalDynamic_<nodeKey>_<color> 节点, 删除失效的 signalByPhase 回退逻辑。
  4. countDown 驱动 category === "countdown" 资产显示。
  5. 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.enabledfalse 重新生成, 确认生成页无 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)。
  • 步骤 16 全在 overlay 单文件内,git checkout scripts/lib/v2x-cesium-overlay.js 即回到当前状态。
  • v2xPreview.enabled=false 是运行时总开关。

不做(留待后续任务)

  • AMap 底图 / 矢量瓦片图层
  • CrossDevices/Model3D.vue 3D 杆件模型(设备维持点位)
  • useV2XEvents 事件气泡