310 lines
10 KiB
Markdown
310 lines
10 KiB
Markdown
# RM75 关节命令提前制动实施计划
|
||
|
||
> **供智能体执行者:** 必须使用 `superpowers:subagent-driven-development`
|
||
>(推荐)或 `superpowers:executing-plans` 逐项实施;所有步骤使用复选框跟踪。
|
||
|
||
**目标:** 修复 90 Hz 关节命令在稳定目标附近反复越界的问题,使 RM75 在保留
|
||
现有速度、加速度限制和低跟随模式的前提下提前制动并稳定停止。
|
||
|
||
**架构:** 保留当前 QP、反馈和故障恢复链路,只替换
|
||
`SingleArmVelocityTeleop._limit_joint_command_step()` 内部的关节命令生成规则。
|
||
每个关节根据离散制动距离决定继续加速或开始减速,最终命令仍由现有
|
||
`_send_joint_target()` 发送。
|
||
|
||
**技术栈:** Python 3.10、ROS2 Humble、NumPy、pytest、ament/colcon。
|
||
|
||
---
|
||
|
||
## 执行约束
|
||
|
||
- 设计文档:
|
||
`docs/superpowers/specs/2026-07-30-rm75-joint-command-braking-design.md`。
|
||
- 构建、测试和启动命令在 `/home/robot/WS_xr` 执行,并先运行
|
||
`source /opt/ros/humble/setup.bash`。
|
||
- 不连接真机,不发送真实 CANFD,不移动机械臂,不操作夹爪。
|
||
- 启动验证只使用
|
||
`xr_rm_bringup/launch/arm_debug.launch.py arm:=right use_mock:=true`。
|
||
- 不修改 QP、YAML、RealMan 适配器、launch、UI、依赖和公开 API。
|
||
- 保留工作空间、圆柱、TCP 速度、姿态速度、关节速度、关节加速度、超时保持、
|
||
CANFD 恢复、Grip 重新使能和安全停止逻辑。
|
||
- 用户未要求 Git 提交,因此本计划不执行 `git commit` 或 `git push`。
|
||
- 保留工作区中已有的其他修改,不回退、不覆盖:
|
||
`placo_ik_solver.py`、`test_placo_transforms.py` 及现有 Superpowers 文档。
|
||
|
||
## 文件范围
|
||
|
||
- 修改 `xr_rm_teleop/test/test_joint_control.py`
|
||
- 增加固定目标提前制动回归测试。
|
||
- 修改 `xr_rm_teleop/xr_rm_teleop/single_arm_velocity_teleop.py`
|
||
- 在现有关节限幅入口实现离散制动距离判断。
|
||
- 不创建新的运行时代码文件或配置项。
|
||
|
||
### 任务一:增加持续振荡回归测试
|
||
|
||
**文件:**
|
||
|
||
- 修改:`xr_rm_teleop/test/test_joint_control.py:206`
|
||
- 测试:`xr_rm_teleop/test/test_joint_control.py`
|
||
|
||
- [x] **步骤 1:在现有首周期加速度测试后增加固定目标测试**
|
||
|
||
增加以下测试:
|
||
|
||
```python
|
||
def test_joint_command_step_brakes_before_fixed_target_without_overshoot() -> None:
|
||
dt = 1.0 / 90.0
|
||
max_speed = math.radians(180.0)
|
||
max_acceleration = math.radians(300.0)
|
||
target = np.radians(
|
||
[10.0, -10.0, 3.0, -3.0, 1.0, -1.0, 0.1]
|
||
).tolist()
|
||
command = [0.0] * 7
|
||
velocity = [0.0] * 7
|
||
|
||
for _ in range(180):
|
||
previous_velocity = list(velocity)
|
||
command, velocity = (
|
||
SingleArmVelocityTeleop._limit_joint_command_step(
|
||
target=target,
|
||
previous_target=command,
|
||
previous_velocity=velocity,
|
||
max_speed=max_speed,
|
||
max_acceleration=max_acceleration,
|
||
dt=dt,
|
||
)
|
||
)
|
||
|
||
for index in range(7):
|
||
assert min(0.0, target[index]) - 1e-12 <= command[index]
|
||
assert command[index] <= max(0.0, target[index]) + 1e-12
|
||
assert abs(velocity[index]) <= max_speed + 1e-12
|
||
assert (
|
||
abs(velocity[index] - previous_velocity[index])
|
||
<= max_acceleration * dt + 1e-12
|
||
)
|
||
|
||
assert command == pytest.approx(target, abs=1e-12)
|
||
assert velocity == pytest.approx([0.0] * 7, abs=1e-12)
|
||
```
|
||
|
||
该测试同时覆盖正负方向、不同目标距离、最大速度、最大加速度、禁止越过固定目标
|
||
和最终停止。
|
||
|
||
- [x] **步骤 2:运行新增测试并确认失败**
|
||
|
||
运行:
|
||
|
||
```bash
|
||
source /opt/ros/humble/setup.bash
|
||
PYTHONPATH="/home/robot/WS_xr/src/xr_rm_teleop:${PYTHONPATH:-}" \
|
||
python3 -m pytest \
|
||
src/xr_rm_teleop/test/test_joint_control.py::test_joint_command_step_brakes_before_fixed_target_without_overshoot \
|
||
-v
|
||
```
|
||
|
||
预期:`FAIL`,现有实现会让至少一个关节命令越过固定目标。失败原因必须来自新增
|
||
越界断言,不能是导入或环境错误。
|
||
|
||
### 任务二:实现离散提前制动
|
||
|
||
**文件:**
|
||
|
||
- 修改:
|
||
`xr_rm_teleop/xr_rm_teleop/single_arm_velocity_teleop.py:1232-1263`
|
||
- 测试:`xr_rm_teleop/test/test_joint_control.py`
|
||
|
||
- [x] **步骤 1:用离散制动逻辑替换现有限幅计算**
|
||
|
||
保留方法签名和现有长度、参数校验,将
|
||
`desired_velocity = np.clip(...)` 到返回值的部分替换为:
|
||
|
||
```python
|
||
values = np.asarray(
|
||
[target, previous_target, previous_velocity],
|
||
dtype=float,
|
||
)
|
||
if not np.isfinite(values).all():
|
||
raise ValueError("joint command contains NaN/Inf")
|
||
|
||
velocity_step = max_acceleration * dt
|
||
arrival_distance = velocity_step * dt
|
||
limited_target = []
|
||
limited_velocity = []
|
||
for desired_target, last_target, last_velocity in zip(
|
||
target,
|
||
previous_target,
|
||
previous_velocity,
|
||
):
|
||
error = desired_target - last_target
|
||
if (
|
||
abs(last_velocity) <= 1e-12
|
||
and abs(error) <= arrival_distance
|
||
):
|
||
velocity = error / dt
|
||
position = desired_target
|
||
else:
|
||
direction = (
|
||
math.copysign(1.0, error)
|
||
if abs(error) > 1e-12
|
||
else 0.0
|
||
)
|
||
accelerated_speed = min(
|
||
abs(last_velocity) + velocity_step,
|
||
max_speed,
|
||
)
|
||
braking_steps = max(
|
||
0,
|
||
math.ceil(accelerated_speed / velocity_step) - 1,
|
||
)
|
||
braking_distance = accelerated_speed * dt + dt * (
|
||
braking_steps * accelerated_speed
|
||
- velocity_step
|
||
* braking_steps
|
||
* (braking_steps + 1)
|
||
/ 2.0
|
||
)
|
||
desired_velocity = direction * max_speed
|
||
if (
|
||
last_velocity * error > 0.0
|
||
and abs(error) <= braking_distance
|
||
):
|
||
desired_velocity = 0.0
|
||
velocity = _clamp(
|
||
desired_velocity,
|
||
last_velocity - velocity_step,
|
||
last_velocity + velocity_step,
|
||
)
|
||
velocity = _clamp(velocity, -max_speed, max_speed)
|
||
position = last_target + velocity * dt
|
||
|
||
limited_target.append(position)
|
||
limited_velocity.append(velocity)
|
||
|
||
if not np.isfinite(limited_target).all():
|
||
raise ValueError("joint command contains NaN/Inf")
|
||
return limited_target, limited_velocity
|
||
```
|
||
|
||
不要增加 ROS 参数或辅助类。制动距离直接使用当前方法已有的
|
||
`max_acceleration`、`max_speed` 和 `dt`。
|
||
|
||
- [x] **步骤 2:运行新增测试并确认通过**
|
||
|
||
运行:
|
||
|
||
```bash
|
||
source /opt/ros/humble/setup.bash
|
||
PYTHONPATH="/home/robot/WS_xr/src/xr_rm_teleop:${PYTHONPATH:-}" \
|
||
python3 -m pytest \
|
||
src/xr_rm_teleop/test/test_joint_control.py::test_joint_command_step_brakes_before_fixed_target_without_overshoot \
|
||
-v
|
||
```
|
||
|
||
预期:`PASS`。
|
||
|
||
- [x] **步骤 3:运行关节控制测试文件**
|
||
|
||
运行:
|
||
|
||
```bash
|
||
source /opt/ros/humble/setup.bash
|
||
PYTHONPATH="/home/robot/WS_xr/src/xr_rm_teleop:${PYTHONPATH:-}" \
|
||
python3 -m pytest src/xr_rm_teleop/test/test_joint_control.py -v
|
||
```
|
||
|
||
预期:全部通过;现有
|
||
`test_joint_command_step_limits_acceleration_from_rest` 继续通过,证明首周期
|
||
加速度行为没有回归。
|
||
|
||
### 任务三:完整验证
|
||
|
||
**文件:**
|
||
|
||
- 不修改文件。
|
||
|
||
- [x] **步骤 1:运行 `xr_rm_teleop` 全部测试**
|
||
|
||
运行:
|
||
|
||
```bash
|
||
source /opt/ros/humble/setup.bash
|
||
export RM75_TEST_PYTHONPATH="/home/robot/WS_xr/src/xr_rm_teleop:/home/robot/miniconda3/envs/xr/lib/python3.10/site-packages:/home/robot/miniconda3/envs/xr/lib/python3.10/site-packages/cmeel.prefix/lib/python3.10/site-packages"
|
||
PYTHONPATH="${RM75_TEST_PYTHONPATH}:${PYTHONPATH:-}" \
|
||
python3 -m pytest \
|
||
src/xr_rm_teleop/test -v
|
||
```
|
||
|
||
预期:全部测试通过,无失败;真实 Placo 回归测试必须执行,不能因缺少模块而跳过。
|
||
|
||
- [x] **步骤 2:单独运行姿态控制测试**
|
||
|
||
运行:
|
||
|
||
```bash
|
||
source /opt/ros/humble/setup.bash
|
||
PYTHONPATH="/home/robot/WS_xr/src/xr_rm_teleop:${PYTHONPATH:-}" \
|
||
python3 -m pytest \
|
||
src/xr_rm_teleop/test/test_orientation_control.py -v
|
||
```
|
||
|
||
预期:全部通过。
|
||
|
||
- [x] **步骤 3:构建工作空间**
|
||
|
||
运行:
|
||
|
||
```bash
|
||
source /opt/ros/humble/setup.bash
|
||
colcon build --symlink-install
|
||
```
|
||
|
||
预期:`xr_rm_input`、`xr_rm_interfaces`、`xr_rm_teleop` 和 `xr_rm_bringup`
|
||
全部构建成功。
|
||
|
||
- [x] **步骤 4:使用 mock 启动右臂统一 launch**
|
||
|
||
运行:
|
||
|
||
```bash
|
||
source /opt/ros/humble/setup.bash
|
||
source install/setup.bash
|
||
timeout 10s ros2 launch xr_rm_bringup arm_debug.launch.py \
|
||
arm:=right use_mock:=true
|
||
```
|
||
|
||
预期:
|
||
|
||
- 节点日志显示控制周期约 `dt=0.0111s`;
|
||
- 日志显示 `follow=False`;
|
||
- 不连接厂商 SDK,不发送真实 CANFD;
|
||
- 除 `timeout` 主动结束产生的退出状态外,没有 Python 异常或 ROS 错误。
|
||
|
||
- [x] **步骤 5:检查最终差异**
|
||
|
||
运行:
|
||
|
||
```bash
|
||
git -C /home/robot/WS_xr/src diff --check
|
||
git -C /home/robot/WS_xr/src status --short
|
||
git -C /home/robot/WS_xr/src diff -- \
|
||
xr_rm_teleop/xr_rm_teleop/single_arm_velocity_teleop.py \
|
||
xr_rm_teleop/test/test_joint_control.py
|
||
```
|
||
|
||
预期:
|
||
|
||
- `diff --check` 无输出;
|
||
- 本次运行时代码改动只涉及上述两个文件;
|
||
- 原有工作区修改仍保留;
|
||
- 不存在提交或远程推送。
|
||
|
||
## 用户真机验证边界
|
||
|
||
自动验证完成后,只提供手动验证步骤,不由 Codex 操作真机:
|
||
|
||
1. 保持低跟随,从安全姿态和小于 5 mm 的上下位移开始;
|
||
2. 手柄停止后观察机械臂是否立即减振并稳定;
|
||
3. 确认无持续 QP、UDP、CANFD 或故障锁存错误后,再测试 10 mm;
|
||
4. 若不再振荡但仍有不可接受的整臂大幅构型变化,停止扩大位移,转入独立的奇异点
|
||
处理设计。
|