# 可拔插移动操作环境(mobile-manipulator-v2) 统一训练入口:主工作台「控制台 → 强化学习任务」。独立 `/mobile.html` 产品页已移除;主工作台另有「LeKiwi 智能抓放」,见 [智能任务](lekiwi-agent.md)。物理/ONNX/训练底层与 `/physics/mobile.html` 夹具保留。服务配置及完整点击流程见 [训练服务](../training_server/README.md) 和 [统一训练面板](../web_platform/TRAINING.md)。 任务**复用现有加载器、MuJoCo WASM 与 Three.js 查看器**,不替换 Go2 任务、不授予外部控制桥训练 RPC。v2 控制、分阶段训练与评估说明见 [训练课程](mobile-training-curriculum.md);旧 v1 策略不能继续使用。 ## 1. 分层与文件规划 | 层 | 文件 | 职责 | | ------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | A:描述与加载 | `contracts/mobile-robots-v1.json`、`web_platform/src/mobile/RobotDescriptor.ts` | 强类型 `RobotConfig`;名称绑定、限位、默认姿态、底座混合矩阵、夹爪与末端配置 | | A:场景 | `web_platform/src/mobile/SceneComposer.ts` | URDF bundle 独立适配;已展开 MJCF 中加入物体、目标、地面和 EEF site | | A:生命周期 | `web_platform/src/mobile/RobotManager.ts` | ZIP 导入、双槽 VFS、候选验证、查看器提交、失败回滚、策略失效及资源释放 | | B:任务契约 | `contracts/mobile-manipulator-v2.json`、`web_platform/src/mobile/TaskKernel.ts` | 归一化、动作映射、阶段奖励、成功/超时;不依赖渲染和 ONNX | | B:WASM 环境 | `web_platform/src/mobile/MobileManipulatorWasmEnv.ts` | `reset/observe/step`、编译后地址缓存、物理积分与交互状态重置;另导出别名 `InteractiveManipulatorEnv` | | C:推理 | `web_platform/src/mobile/ONNXPolicyRunner.ts` | 单飞 ONNX、元数据和 SHA-256 校验、过期结果隔离、延迟指标 | | C:交互 | `app/components/WorkspaceToolsPanel.tsx`、`mobile/agent/`、`TaskDragController.ts` | 主工作台 RL / 智能任务;拖动模块供保留的物理夹具使用 | | Python | `training_server/mobile_manipulator/{kernel,env}.py` | 同公式的 Gymnasium 环境,加载浏览器导出的同一份 MJCF | | Python | `training_server/mobile_manipulator/{train,export_onnx,validate_rollout}.py` | 可选 SB3 PPO 入口、PyTorch 导出及 ORT 校验、跨引擎短轨迹校验 | 现有组件仅增加小型接入点: - `PhysicsAdapter.load()` 的 `sceneComposer` 在 URDF 转换/地图组合后展开 include,再组合任务场景;`configureRobotRuntime: false` 允许使用模型配方而不安装旧外控运行时。任务增加了第二个 freejoint,不能调用要求“唯一自由基座”的旧 LeKiwi 外控绑定。 - ZIP **继续复用现有 `fflate`**(功能等价于此处使用 JSZip),保留路径穿越/文件数/解压大小检查;`MemfsWorkspace` 负责 `FS.mkdirTree/writeFile/unlink/rmdir`。不增加第二套 VFS 或依赖。 - `MuJoCoViewer` 的可选 `advance(now)` 回调提供唯一物理时钟。设置它之后查看器**不再调用** `SimulationSession.advance()`,避免双重步进。 ## 2. 机器人与适配边界 | 描述符 | 本次实际验证的 ZIP | 机械臂 | | --------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `lekiwi-v1` | `build/lekiwi/lekiwi-v1.zip` | 原始五臂轴 + `arm_gripper`;复用已有来源校验、全臂凸包和被动滚子配方 | | `lekiwi-bundle` | `../Reference_Projects/LeKiwi/New_urdf/robot_urdf_bundle (1).zip` | `Link1…Link4` + `arm_wrist_roll` + `arm_gripper`;独立配方,不冒充旧外控 profile | 验证资产 SHA-256: - 原始 ZIP:`a10ac577ea49cdf87f324f3f6e9a7a887a1e3c0254ae638059711fef9e385b04` - bundle ZIP:`f35734d3b4e3d6ef8399491987f50974476bfdd9d6c18506b915bcb9497f5752` 文件名不能证明机械结构相同;历史上同目录其他 ZIP/同名导出可能有不同活动轴。本次支持以上具体结构,**不自动把未知活动轴按顺序重命名**。 新 bundle 配方保留新臂的关节框架、惯性及四个有界关节的限位,给连续腕轴/夹爪补充仿真限位和伺服;替换超过 MuJoCo 面数上限的轮视觉,并构建理想化三全向轮/被动滚子。重复碰撞仅在同网格、同局部变换时去重。新臂接触使用 MuJoCo 原生凸包近似,**没有套用原臂的离线凸分解数据**。底盘质量、驱动增益、夹爪行程和 EEF 偏移是仿真估计,未实机标定。 扩展其他机器人:提供预先带执行器的 MJCF ZIP 和 `recipe: "mjcf"` 的 `RobotConfig`,通过 `RobotManager.loadZip()` 开发接口载入;独立页的自定义 JSON 上传控件不再提供。底层支持: - 任意数量轮子,`baseMix[nwheel][3]` 定义车体坐标 `vx, vy, wz → wheel rad/s`;差速轮可令第二列为零。 - 1–8 个标量机械臂关节;每个 `mode` 可为 `position` 或 `velocity`。目前要求移动根为 freejoint,不支持固定底座或任意浮动关节参数化的自动推断。 - 多个夹爪执行器可指定各自 `joint` 和 `closed/open`;观测使用 `gripperJoint` 的行程比,主关节必须由匹配行程的执行器控制。 - 指定 `eefSiteName` 时,组合器在 `eefBodyName` 上按 `eefOffset` 加 site;未指定时直接使用 body 原点/姿态。 - 编译后校验关节类型/限位、执行器 transmission、gear、控制类型和 ctrlrange;不兼容则回滚。 固定接口只保证任务/UI 不随 DOF 改写,**不保证同一策略能跨机器人泛化**。8 轴以外或需力矩动作时应创建新版本契约,不能静默截断。 ## 3. 数学契约 世界坐标为右手 Z-up;米、秒、弧度;四元数一律 **wxyz**。姿态四元数归一化并采用 `w >= 0` 的符号。所有观测限制在 `[-1,1]`。区间均为左闭右开。 ### Observation:`Float32Array(92)` / ONNX `[1,92]` | 区间 | 内容 | | --------- | ------------------------------------------------------------ | | `[0,3)` | 底座世界位置 / 2 m | | `[3,7)` | 底座四元数 | | `[7,10)` | 底座世界线速度 / 2 m/s | | `[10,13)` | 底座世界角速度 / 4 rad/s(freejoint 局部角速度先转世界坐标) | | `[13,21)` | 8 个臂角槽:`2*(q-min)/(max-min)-1`;无效槽为 0 | | `[21,29)` | 臂关节速度 / 4 rad/s;无效槽为 0 | | `[29,37)` | 臂槽 mask(有效为 1) | | `37` | 夹爪行程比 `g` 映射为 `2*g-1` | | `[38,45)` | EEF 世界位置 / 2 m + 四元数 | | `[45,52)` | 物体相对 EEF 的位置 / 2 m + 四元数 | | `[52,59)` | 物体相对目标的位姿(目标坐标系) | | `[59,66)` | 目标世界位置 / 2 m + 四元数 | | `66,67` | 已抓取抬升标记、当前阶段稳定计数 / 10 | | `[68,80)` | 上次实际施加的归一化动作 | | `[80,92)` | 积分控制目标状态:臂位置与夹爪开度归一化,其余槽为 0 | 相对位姿严格使用 `p_rel = R_parent^T*(p_object-p_parent)`、`q_rel = inverse(q_parent)*q_object`,不是仅减世界 XYZ。 ### Action:`Float32Array(12)` / ONNX `[1,12]` - `[0,3)`:归一化车体 `vx,vy,wz`,先经过共享契约速度/加速度限制再经 `baseMix` 转轮速;超过 `wheelLimit` 时**同比缩放所有轮速**。 - `[3,11)`:8 个臂槽。位置模式是积分目标的归一化增量速度,目标每步最多移动 `min(velocityLimit,0.5)*dt`,并限制跟踪误差;速度模式同样限幅。不存在的槽忽略。零动作保持目标,不再映射到全行程中点。 - `11`:归一化开合速度,+1 张开、-1 闭合、0 保持;开度每秒最多变化 0.5。 - 输入完整校验有限值后才写 ctrl;越界有限数裁剪;NaN/Infinity/尺寸错误拒绝,不部分写入。 控制步长 `0.02 s`(50 Hz);复用源模型物理 timestep(旧全凸包配方 1 ms,新 bundle 2 ms),必须整除控制步长。每个 step 在积分后调用 `mj_forward`,使观测不是上一个积分边界的派生位置。 ### 奖励、终止 - `reach = dt * 0.1 * exp(-8 * distance(eef, object))` - `lift = dt * 0.3 * clip((object.z - 0.019) / 0.08, 0, 1)` - 当抬升达到 8 cm、末端距物体小于 9 cm、夹爪开度小于 0.4 时锁存 `hasLifted`。 - `transport = hasLifted ? dt * 0.5 * exp(-4 * distance(object, goal)) : 0` - 以上抓放密集项按仿真时间积分(`dt=0.02`),上界为 18。v2 另有导航进展/距离/朝向项、动作变化率惩罚及关节速度惩罚,见共享契约与训练课程。 - 已抬升后,物体距目标小于 4 cm、线速度小于 0.05 m/s、夹爪开度大于 0.65、EEF 已撤离 9 cm,连续稳定 10 步才成功:一次性 `success=20`,`terminated=true`。 - 1000 步未成功则 `truncated=true`;结束后必须 reset。不存在自动焊接/吸附物体,搬运依赖实际物理接触。奖励中的抓取是近距/开度/抬升启发式,不是双指接触传感器的证明。 - 导航/末端接近阶段分别使用接近位驻留、末端驻留成功条件,不以抓放成功判断导航。任务训练顺序为 `navigate → reach → pick-place`。 - `info` 包含 `reward_components,is_success,stage,safety_stop,navigation_distance,max_joint_velocity`。物理子步实测关节超速、底盘倾倒/越界均失败终止。 TS `step()` 返回借用的同一对象,obs、raw-state、ctrl、action 缓冲全部复用;需要历史时由调用者复制。Python 返回独立 obs/info,避免 replay buffer 存到后续被覆盖的引用。两端奖励/观测/动作数学相同;引擎浮点求解不承诺长时间逐位一致。 ## 4. 浏览器操作与集成 主工作台训练使用 `SimulationSession` + `MobilePolicyController`,在同一视口中组合任务场景并加载策略,不创建第二个物理步进所有者。 1. 在主工作台导入对应 ZIP,进入「控制台 → 强化学习任务」选择已注册变体,按训练服务流程同步场景、训练并导入 ONNX。 2. 「LeKiwi 智能抓放」仅支持固定 A,使用另一套具名 SI 契约和独立接触评估,不改 RL 92/12 契约。进入 RL 时会重新载入 RL 场景,不能把智能任务双支撑台直接冒充训练快照。 3. 地图草稿保留;智能任务只使用隔离的空旷平地预设,编辑地图使旧模型请求失效。任意机器人/训练后台作业不会因切换智能任务而被远程取消。 4. `RobotManager.loadZip(file, config, entryPath)`、`TaskDragController` 与 `moveTaskEntity()` 仍供开发/物理夹具使用,包括 Shift 拖动、状态重置和两变体资源回归;不再维护第二个产品网页。 `ONNXPolicyRunner` 要求固定 float32 `observation:[1,92] → action:[1,12]`、匹配 v2 task/robot/controlDt/动作语义/训练阶段,以及模型与 RobotConfig SHA-256。**不要格式化训练包里的 `robot.json`**,它的原始字节是导出指纹来源。它校验配置而不是所有网格字节;更改几何/动力学后必须从新场景重新训练和导出,不能只改标签绕过。 推理是单飞异步、固定仿真步长锁步:等待 ORT 时继续渲染,但不积分物理;下一次 rAF 消费动作后才生成下一观测。这避免“持续施加上次动作”造成训练端没有的动作延迟;代价是慢策略导致仿真时钟落后墙钟。计时累计有界,不在恢复后狂追补步。reset、拖动、模式切换、重载和 dispose 都使迟到推理结果失效;ORT 等待在途 run 结束后才 release。 ## 5. 后台训练与自动导出 用户只需在统一面板点击开始训练。浏览器内部上传完整资产、组合后的 `*.training.xml`、`robot.json`、`task.json`、`environment.json`;服务器验证并保存快照,作业仅引用服务器 ID。Python **不再次转换 URDF**。内部 ZIP 只是传输格式,不要求用户下载、解压或运行 Python。 使用独立 MuJoCo 3.11.0 + SB3 解释器,通过训练服务 `--mobile-python` 配置;不要升级已有 Go2/LeRobot 环境。`allow_version_mismatch=True` 仅供底层诊断,服务训练不会启用此绕过。 任务设置包括阶段、接续作业、示教步数、随机化、独立评估回合,以及采样步数、迭代数、环境数、设备和种子。训练按种子扰动物体/目标 XY,部署保留名义初态;评估用不同种子。日志输出共用迭代/损失/平均奖励协议,取消覆盖训练、评估与导出整个进程组。自动导出固定 float32 `[1,92] → [1,12]` 并用原生 ORT 对5组输入比对 PyTorch;元数据包含控制语义、阶段、实际评估、权重/RobotConfig/场景 SHA-256。 阶段门槛和实测导航结果见 [训练课程](mobile-training-curriculum.md)。短程冒烟不是已收敛的抓取策略;尚未验证抓放收敛或跨机器人泛化。底层 `export_policy()`、`--smoke` 及 `RobotManager.exportTrainingBundle()` 仅保留给开发测试,不是用户训练流程。 ## 6. 验证与性能边界 ```bash npm run typecheck npm run lint npm test -- web_platform/src/mobile build/venvs/mobile/bin/python -m unittest training_server.tests.test_mobile_manipulator -v npm run test:e2e:mobile ``` 浏览器测试需要两个上述 ZIP;也可用 `MOBILE_BUNDLE_ZIP=/path/to/file.zip` 指定新包。缺少本地资产会明确 skip。测试包括真实模型动作、8 次交替切换、模型/data `isDeleted()`、VFS 数量、候选绑定失败回滚、真正的鼠标拖动;主工作台 UI 另由 `test:e2e:agent` 验收。产物在 `build/mobile-validation/`;测试用 `/physics/mobile.html` 不进入生产构建。 跨引擎回归与真实 ONNX 测试: ```bash # 先运行浏览器测试产生 bundle-training.zip 和 rollout.json python -m zipfile -e build/mobile-validation/bundle-training.zip build/mobile-validation/package build/venvs/mobile/bin/python -m training_server.mobile_manipulator.validate_rollout \ --package build/mobile-validation/package --rollout build/mobile-validation/rollout.json build/venvs/mobile/bin/python -m training_server.mobile_manipulator.export_onnx \ --package build/mobile-validation/package --smoke --output build/mobile-validation/smoke.onnx npm run test:e2e:mobile ``` 单元对齐另有 Python 生成的 40 组随机/旋转/饱和动作 golden,覆盖两个机器人;成功、重置、超时、速度模式、NaN 原子拒绝、生命周期和过期 ONNX 结果分别测试。重新生成:`python -m training_server.tests.generate_mobile_golden`,之后 Prettier 格式化 fixture。 内存注意:官方 JS 绑定通过 `MjData.delete()`、`MjModel.delete()` 执行原生析构;查看器的 mjvScene/GPU 资源必须先 detach,VFS 随后清理。共享 WASM 线性内存不会缩小,**不能用 heap 不下降判定泄漏**。仅释放 model/data 还不够:新路径会扩大原生资产缓存。管理器串行化加载,并交替复用两个独占 VFS 根目录;编译使共享 heap 增长后,旧环境在回滚/恢复屏障刷新 typed-array views。 以下为 v1 的历史性能基线,不作为 v2 策略验收结果:本机无头 Chromium 短测,8 次交替切换后 heap 在约 862 MiB 稳定(完整 CAD 配方开销很高),VFS 仅保留活动槽,旧 model/data 已删除。记录的是短期稳态检查,不是无限次泄漏证明。原生/WASM 3.11.0 的 12 步对照:原 LeKiwi 最大 qpos 误差约 `1.3e-15`、obs 无差异;新 bundle 最大 qpos 误差约 `6.1e-6`、obs `1.2e-6`;smoke ORT 最近推理约 `0.1 ms`、冷启动约 `4.5 ms`,不代表真实大策略延迟。精确本次数据以 `traces.json`、`onnx-metrics.json` 为准。 60 FPS 是渲染目标而非保证:策略推理、旧臂上千凸包接触、软件 WebGL、加载峰值都会影响墙钟速度。请在目标 GPU 上录制至少 60 秒 FPS/物理耗时/推理 P95/P99 与长时堆曲线;若需硬实时或低内存部署,应先降面/优化碰撞和将推理放 Worker,而不是跳过物理子步或放宽对齐公式。