# dsh-ros2 架构 > 版本 0.14.1(51 工具 + 4 skills)· 配套 `README.md`(使用)· `compatibility.md`(兼容基线)· `safety.md`(安全边界)· `safety-handover.md`(本体适配交接) --- ## 1. 设计概览 dsh-ros2 让 DSH agent 以 **CLI + ROS2 话题/service 双通道**调试机器人系统。工具按安全边界分四层: | 层 | 范围 | 工具数 | 安全模型 | | --- | --- | --- | --- | | **L1 只读诊断** | 包/工作区/依赖/节点/话题/服务/动作/参数/接口/TF/拓扑/健康/包/MoveIt 发现与状态/运动校验(`motion_validate`)/档案读取/安全状态 | 25 | 纯只读,无审批 | | **L2 审批管理** | 构建/依赖安装/消息骨架/参数设置/录制 + 任务查询/MoveIt 运动(规划→校验→审批→执行→验证)/机器人注册与拓扑/安全监视启动与人工门锁存 | 15 | 写操作一律 `ctx.approval.request`,fail-closed | | **L3 可视化与交互** | GUI 生命周期/截图/多模态描述/xdotool 交互 | 7 | 本地会话良性操作,无审批 | | **L4 实时视觉** | 并行 VLM / 图像话题取帧 / 视觉链路自动建立 | 4 | 图像来自话题(无头),无审批 | **核心设计原则** 1. **文本/结构化优先**:L1 输出 JSON,LLM 直接消费;GUI 给人看、多模态给 agent 看; 2. **执行缝隙注入**:所有外部依赖(run/approval/jobs/gui/vision)经 `ToolDeps` 注入,测试全用 fake; 3. **无头优先**:视觉图像一律来自 `sensor_msgs/Image` 话题或离屏渲染内核,不依赖 X11 截图。 --- ## 2. 工具分层 ### L1 只读诊断(25) `ros2_pkg_list` / `ros2_colcon_list` / `ros2_rosdep_check` / `ros2_node_list` / `ros2_node_info` / `ros2_topic_list` / `ros2_topic_info` / `ros2_topic_echo` / `ros2_service_list` / `ros2_action_list` / `ros2_param_list` / `ros2_interface_show` / `ros2_graph` / `ros2_tf_list` / `ros2_tf_echo` / `ros2_doctor` / `ros2_bag_info` / `ros2_jobs_list` / `ros2_job_status`(任务查询)/ `moveit_discover` / `moveit_status`(MoveIt 发现与在线探测)/ `motion_validate` (确定性运动校验)/ `robot_load`(档案读取)/ `robot_safety_state` / `robot_safety_arbitrate`(VLM 语义仲裁,非 safe 升级人工)。 ### L2 审批管理(15) `ros2_colcon_build`(后台任务)/ `ros2_rosdep_install`(dry-run 预览)/ `ros2_interface_create`(msg/srv/action 骨架,不覆盖既有文件)/ `ros2_param_set`(JSON 值类型化)/ `ros2_bag_record`(限时录制)/ `ros2_bag_play`(回放)/ `ros2_launch`(launch 后台任务)/ `ros2_install` (FishROS 一键安装)/ `ros2_zero_pose_semantics`(零位语义校准)/ `robot_register` / `robot_topology`(档案与拓扑)/ `moveit_move`(运动,单一路径 规划→校验→审批→执行→验证)/ `robot_safety_start` / `robot_safety_lock` / `robot_safety_unlock`(安全监视启动与人工门锁存)。 ### L3 可视化与交互(7) `ros2_gui_start` / `ros2_gui_list` / `ros2_gui_close` / `ros2_screenshot` / `ros2_vision_describe` / `ros2_gui_observe` / `ros2_gui_interact` (统一 xdotool 交互:click/drag/key,RViz2 视点操控、显示配置重载)。 ### L4 实时视觉(4) `ros2_image_snapshot`(图像话题取帧,raw/compressed)/ `ros2_vlm_analyze` (文件或 bridge 最新帧 → 并行 VLM)/ `ros2_vision_topics`(图像话题 + 桥接 service 清单)/ `ros2_vision_analyze`(按话题路由到对应 bridge 分析最新帧)。 --- ## 3. 执行缝隙(seams) ``` DSH 会话 → tools/skills 服务 │ createRos2Tools({run, approval, jobs, gui, vision}) ▼ 工具层 tools.ts ──▶ runner.ts(CLI 执行)│ approval(L2 审批) ──▶ jobs(后台任务) │ gui.ts(GUI 生命周期/截图/xdotool) ──▶ vision(L3 多模态) │ vlm/ offscreen/(L4 ROS2 包) ``` | 缝隙 | 生产实现 | 测试 | | --- | --- | --- | | `run` | `runCommand`(超时/杀进程/自动 ROS_LOG_DIR 回退) | fake 返回夹具 | | `approval` | `ctx.approval.request`(仅 `allowed-once` 放行) | allowed-once/rejected | | `jobs` | `ctx.jobs.start` + `spawnJob` | fake registry | | `gui` | `GuiManager`(spawn/wmctrl/Pillow/xdotool,全可注入) | fake 各原语 | | `vision` | `VisionProvider`(gemini/openai/mock,key 用户自备) | Mock | --- ## 4. 实时视觉架构(L4) L4 解决具身 agent 的**实时性与无头部署**:VLM 分析在独立进程运行(与机器人控制栈一致的 ROS2 通信),图像全部来自话题,杜绝 X11 截图依赖与窗口层级问题。 ``` ┌─ ROS2 图(多进程)─────────────────────────────────────────────┐ │ │ │ /camera/image (相机/仿真) /rviz/scene (RViz2 离屏渲染, 可选) │ │ └──────────┬───────────┘ │ │ ▼ │ │ [vlm_bridge_node] 常驻桥接:缓存最新帧,仅被触发时转发 │ │ (每话题一路,service/trigger/result 按话题唯一化) │ │ │ image_bytes_b64(内存直传,无磁盘/重编码) │ │ ▼ │ │ [vlm_node] 独立进程(MultiThreadedExecutor) │ │ service /vlm/describe · topic /vlm/description(latest 缓存)│ │ ▼ │ │ 本地 VLM 网关(OpenAI 兼容)→ 描述 │ └───────────────────────────────────────────────────────────────┘ ``` ### 4.1 并行 VLM 节点(`vlm_node`) - service `/vlm/describe`(图像路径或 base64 字节 + prompt → 描述), `MultiThreadedExecutor` 并发处理; - `/vlm/description`(transient-local)缓存最近结果,新订阅者 0 等待; - API key 经参数或 `VLM_API_KEY` 注入,不落盘明文。 ### 4.2 常驻桥接(`vlm_bridge_node`) - 订阅一路图像话题(raw / CompressedImage,jpeg 保持原始字节免重编码), **仅缓存最新帧,未被触发时零开销**; - 被触发时把帧经 `image_bytes_b64` **内存直传** `vlm_node`(无磁盘中转); - service `/vlm_bridge/analyze_latest`(同步)+ topic trigger→result(异步); - 并发设计:VLM client 由**专用 spin 线程**服务——executor 回调线程等待自己的 client 响应会死锁(rclpy 实测排除 coroutine 方案),此为唯一可靠模式。 ### 4.3 视觉链路自动建立(`vision_bringup`) - 自动发现全部 `sensor_msgs/Image` / `CompressedImage` 话题,每路拉起一个 参数化 bridge(`id` = 话题规范化名,service/trigger/result 按话题唯一化); - LLM/harness 统一入口:`ros2_vision_topics`(话题+桥接清单)、 `ros2_vision_analyze {topic, prompt}`(路由到该话题 bridge 分析最新帧)。 ### 4.4 RViz2 离屏渲染(`dsh_ros2_rviz_offscreen`) - 驱动真实 rviz 渲染栈(`rviz_common` + OGRE + `rviz_default_plugins`),在 **Xvfb(无物理屏、无窗口层级)**上加载 `.rviz` 场景离屏渲染,输出 `/rviz/scene` 图像话题(Grid/TF/RobotModel/PointCloud2/Marker 等,读取渲染内核而非 X 截图); - 相机图直接经 `ros2_image_snapshot` 从话题取帧(rviz 的 Image 面板无头下不适用)。 **机器人本体 mesh 渲染要点**(v0.8.0/v0.8.1 实测): 1. **Jazzy RobotModel 属性格式**:`.rviz` 中须用 `Description Source: Topic` + `Description Topic: <话题名>`(旧版 `Robot Description:` 字段被忽略,导致 RobotModel 未订阅任何话题、`Links` 为空); 2. **mesh 路径**:URDF 内 mesh 用**绝对路径或 `file://` 前缀**(裸相对/绝对路径会被 `resource_retriever` fopen 失败——`Could not load resource ... Unable to open file`); 可用 sed 将 `../meshes/` 替换为 `file://<绝对路径>/` 生成绝对版 URDF,常驻发布到 独立话题(transient-local,发布者须保持运行,否则新订阅者收不到); 3. **URDF 与 TF 必须同名绑定**:发布给 RobotModel 的 URDF **link 名必须与实时 TF frame 名完全一致**(如 `left_shoulder_pitch`,不带 `_link` 后缀)。直接发布机器人 实际描述(`/robot_description`,mesh 路径改写为 `file://`)即可。**若 URDF 与 TF 不匹配,所有 link 的变换查找失败,mesh 全部渲染到固定坐标系原点——即"零件堆叠在 原点"**(v0.8.0 曾因发布了一套 `_link` 后缀的旧 URDF 而踩坑); 4. **视距与视角**:Orbit `Distance` ≈ 1.5–2.0 m 可得到 RViz 式近景全身视角;Distance ≳ 5 m 时机器人缩成画面中心小点。**相机焦点高度**(`Focal Point Z`)应对准机器人主体高度 (如双臂展开在 Z≈0.8 m 时设 Z≈0.55–0.8),否则高支架/柱状底座会遮挡或裁切主体; 5. **材质颜色**:URDF 内 `` 会被 RobotModel 应用—— **带材质的 URDF 渲染出真实配色**(如 lite_urdf:白基座/躯干 + 橙上臂 + 红前臂 + 黑关节,v0.8.2 实测);无 `` 的 STL 才渲染为默认白模。 mesh 尺寸正常(米制,如 world_root 0.39×0.39×1.03 m);渲染大 mesh 时首次加载 需数十秒(RSS 从 ~300 MB 升至 ~1 GB,CPU 持续 = 加载中),期间画面静止属正常; 6. 已实测渲染出机器人实体几何(白/灰色连杆外壳,STL 无材质渲染为默认白模; inertia 警告 `unrealistic inertia` 为 rviz 提示,不影响 mesh);节点启动 ~3 s 后 在日志打印 `FM: ... frames=N` 与 `transformHasProblems(...)=0` 表明 TF 已解析, 可用作"mesh 是否正确绑定 TF"的判定信号。 --- ## 5. 架构演进与性能对比 | 阶段 | 取帧 | 分析 | 三路/单帧性能 | | --- | --- | --- | --- | | ① X11 截图链路(已废弃) | 窗口截图 → PNG 落盘 | 进程内 HTTP,串行 | 依赖显示器/窗口层级,无头不可用 | | ② 话题取帧 + 并行 VLM | `image_snapshot`(冷启动 + 磁盘中转) | `vlm_analyze` 并行 | 三路 ~6.6s(≈2.6× vs 串行);链路开销 ~2s/帧 | | ③ **bridge 常驻**(当前) | 桥接常驻缓存(0 冷启动) | 内存直传并行 VLM | 链路开销 **~0.7s/帧**;单路 service 3.0~5.5s | **关键结论** - VLM HTTP(3~6s)为主导成本,由网关/模型决定,非插件瓶颈; - 并行调用消除串行等待;常驻桥接消除进程冷启动与磁盘中转; - 高频/持续观察场景桥接收益最大,重复调用稳定。 ### 5.1 离屏渲染性能优化(v0.9.0,实测) "渲染机器人动作"提速的两个关键手段(完整链路 1.9 → 10.2 Hz,**5.4×**): | 手段 | 做法 | 效果 | | --- | --- | --- | | **① 渲染低模 mesh** | `scripts/simplify_visual_meshes.py` 用 **open3d** quadric decimation 把大 STL 降到 25k/15k 面(实测 276 万 → 38.7 万面,7.1×) | 稳定帧率 1.9 → 7.1 Hz;内存 962 → 386 MB;mesh 加载 ~90s → ~40s;渲染内容保留 99.7% | | **② 直接读像素(跳过 PNG)** | `rviz_offscreen_node` 用 `Ogre::RenderSystem::getRenderTargetIterator` + `copyContentsToMemory` 直读帧缓冲(不再 `captureScreenShot` 写 PNG + libpng 解码) | capture 38ms → 1-2ms/帧;每帧 74ms → 31ms;帧率 7.1 → 10.2 Hz | **注意**: - **不要用 fast_simplification 生成渲染低模**:实测其输出在 OGRE 中渲染丢失 ~70% 内容(面统计/法线/流形均正常但光栅化空洞),open3d 的 quadric decimation 输出完整(工具脚本已内置此结论); - OGRE include 用 rviz vendor 头(`` 不带 `OGRE/` 前缀,include 路径为 `.../include/OGRE`;带前缀会回退系统 OGRE 1.9 报类型冲突);CMake 需 `find_package(rviz_ogre_vendor)` + `include_directories(BEFORE ${OGRE_INCLUDE_DIRS})`; - 节点每 100 帧打印一行 `loop-timing: loop/onupdate/events/spin/frame/sleep`,可观测循环预算。 ### 5.2 30 Hz 请求:双重渲染消除(v0.9.2,实测) rate=30 实测发现真正瓶颈是**双重渲染**:`VisualizationManager::onUpdate()` 内部 `ogre_root_->renderOneFrame()` 已渲染场景,而主循环又调 `win->render()`—— **每帧渲染两次**(+31ms/帧)。消除后帧率翻倍: | 优化 | 做法 | 效果 | | --- | --- | --- | | **去掉冗余 win->render()** | `onUpdate()` 已渲染(renderOneFrame,受 `render_requested_`/10ms 门控),主循环直接读像素 | 每帧 66ms → ~33ms;30Hz 请求 11.1 → **~22 Hz**(2×);TF 全帧率刷新 | | **events 节流** | `app.processEvents()` 每 5 帧(headless 下 Qt 事件少) | processEvents 触发 Qt paint → OGRE 额外渲染(~30ms/帧),节流后 0ms | 实测(1000×750,38.7 万面低模):30Hz 请求静止 22.9 Hz / 运动 21.5–24.2 Hz (400+ 帧稳态);800×600 仅 17.1 Hz(render 由**三角形数**决定,分辨率影响小); 10Hz 请求仍达上限(10.3 Hz)。 **GPU 直通(v0.9.3,已验证)**:NVIDIA Xorg GLX + `disableAntiAliasing()` 后 30 Hz 请求 **满帧 30.0 Hz**(onUpdate 30 → 9 ms,链路开支不增)。详见 `docs/test-gpu-passthrough.md`。 --- ## 6. 安全模型 - **L1**:纯只读,无审批; - **L2**:写操作一律审批(fail-closed:无审批服务/无 agent/非 `allowed-once` 均拒绝), reason 含完整命令预览;后台任务有输出截断; - **L3**:GUI/截图/xdotool 交互限定本地会话,无审批; - **L4**:图像来自话题与离屏渲染,无审批;视觉 API key 用户自备,不落盘明文; - **通用**:`~/.ros/log` 不可写时自动回退可写目录(`runCommand` 与 ROS2 节点均内置)。 --- ## 6.5 Skills 插件注册两个运行时 skill(`ctx.skills.register`): | Skill | 用途 | | --- | --- | | `ros2-diagnostics` | 调试工作流:先广后窄、无数据/消息不匹配/TF 排障 | | `robot-state-vision-analysis` | **状态 → 离屏渲染 → VLM → 交叉验证** 的无头机器人状态分析流水线(L1 状态读取 → `rviz_offscreen_node` → `vision_bringup`/`ros2_vision_analyze` → 数值交叉验证,如零位构型下 TF 轴共线重叠属预期而非异常) | **测试结果(2026-08-20 真机)** - 单测:79 用例全绿(含 skill 结构/内容断言); - 端到端·零位场景:19 节点链路——关节零位读取 ✅、离屏渲染 800×600 `/rviz/scene` ✅、 bridge VLM 分析 5.7s ✅(识别直立姿态/TF 树,提示坐标系重叠;与零位数据交叉验证一致)。 - 端到端·非零构型场景(重启后):`ros2_vision_analyze`(插件工具本体)分析 `/rviz/scene` 5.6s ✅; **交叉验证发现视觉/数值分歧**——VLM 判"零位/中性姿态",而 `joint_states` 显示双臂 shoulder_roll 外展 ±77°(-1.353/+1.384 rad)、wrist_yaw 0.81/1.575 rad(非零构型): 视觉对 TF 骨架的关节角判断有限,**姿态应以 `joint_states` 数值为准**,视觉用于结构与粗粒度姿态。 --- ## 7. 兼容性与环境注意 详见 `compatibility.md`。要点:Jazzy 实测(Humble 预期可用);X11 仅 L3 需要 (L4 无头);`dsh_ros2_vlm` / `dsh_ros2_rviz_offscreen` 需 colcon 构建并 source (web profile 经 `rosSetup` 注入);FastDDS SHM stderr 噪音默认丢弃。