oomwoo_recovery_safety: recovery twist is published once, not held — maneuvers longer than the base cmd_vel watchdog are silently truncated
Opening this as a conversation-first bug report (per CONTRIBUTING.md) rather than a code PR, since the module lives in a contributor's folder (contributions/recovery-safety/xbattlax/). Flagging it for the maintainer / module owner to decide the fix.
Where
contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/recovery_node.py
_execute (~L110–121) — publishes a twist step once
_timer_cb (~L97–103) — 20 Hz timer, only stops/escalates once the deadline passes; never re-publishes
What happens
A twist recovery step (back_up 0.8s, rotate_in_place 1.2s, rotate_to_collect_scans 1.5s, etc.) sets:
twist.linear.x = step.linear_x
twist.angular.z = step.angular_z
self._cmd_pub.publish(twist) # published ONCE
...
self._active_deadline = monotonic() + step.duration_sec
The 20 Hz _timer_cb then does nothing until the deadline — it only stops + escalates. But a velocity command has to be re-sent continuously to keep a base moving: ROS 2 diff-drive controllers (ros2_control DiffDriveController, and most real bases) enforce a cmd_vel_timeout (default 0.5s) and halt if no fresh command arrives.
So any step with duration_sec > cmd_vel_timeout stops after ~0.5s while the node holds the deadline open for the full duration and then reports the maneuver as attempted.
Reproduce
On a base with the default 0.5s cmd_vel_timeout: trigger WEDGED → rotate_in_place (1.2s, angular_z=0.7). The base spins ~0.5s, watchdog-halts, then the node escalates at 1.2s. The robot performed ~40% of the intended rotation. back_up and the rotate_* steps are all affected.
Impact
Recovery maneuvers are silently truncated for exactly the stuck situations this node exists to handle — reduced recovery effectiveness, no error surfaced. Severity is minor and depends on the base's cmd_vel_timeout; a base with no watchdog is unaffected.
Suggested direction (owner's call)
Re-publish the active twist on each 20 Hz tick until the deadline — the timer already runs at the right rate. Roughly: hold self._active_twist when starting a twist step, re-publish it each tick while monotonic() < self._active_deadline, and clear it wherever _active_deadline is cleared (stop/command steps, _stop_motion sites).
Also worth a look (not the main bug)
External-command steps (e.g. clear_costmap) reuse duration_sec (0.1s) as the behavior_result callback timeout (recovery_node.py ~L121). 100ms may be too tight for an external service round-trip, so delegated steps could routinely time out into RECOVERY_EXHAUSTED. A separate, longer timeout for delegated commands vs. self-driven twist durations may be warranted.
Happy to help however is useful. Thanks for the project.
oomwoo_recovery_safety: recovery twist is published once, not held — maneuvers longer than the basecmd_velwatchdog are silently truncatedOpening this as a conversation-first bug report (per
CONTRIBUTING.md) rather than a code PR, since the module lives in a contributor's folder (contributions/recovery-safety/xbattlax/). Flagging it for the maintainer / module owner to decide the fix.Where
contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/recovery_node.py_execute(~L110–121) — publishes a twist step once_timer_cb(~L97–103) — 20 Hz timer, only stops/escalates once the deadline passes; never re-publishesWhat happens
A twist recovery step (
back_up0.8s,rotate_in_place1.2s,rotate_to_collect_scans1.5s, etc.) sets:The 20 Hz
_timer_cbthen does nothing until the deadline — it only stops + escalates. But a velocity command has to be re-sent continuously to keep a base moving: ROS 2 diff-drive controllers (ros2_controlDiffDriveController, and most real bases) enforce acmd_vel_timeout(default 0.5s) and halt if no fresh command arrives.So any step with
duration_sec > cmd_vel_timeoutstops after ~0.5s while the node holds the deadline open for the full duration and then reports the maneuver as attempted.Reproduce
On a base with the default 0.5s
cmd_vel_timeout: triggerWEDGED→rotate_in_place(1.2s,angular_z=0.7). The base spins ~0.5s, watchdog-halts, then the node escalates at 1.2s. The robot performed ~40% of the intended rotation.back_upand therotate_*steps are all affected.Impact
Recovery maneuvers are silently truncated for exactly the stuck situations this node exists to handle — reduced recovery effectiveness, no error surfaced. Severity is minor and depends on the base's
cmd_vel_timeout; a base with no watchdog is unaffected.Suggested direction (owner's call)
Re-publish the active twist on each 20 Hz tick until the deadline — the timer already runs at the right rate. Roughly: hold
self._active_twistwhen starting a twist step, re-publishit each tick whilemonotonic() < self._active_deadline, and clear it wherever_active_deadlineis cleared (stop/command steps,_stop_motionsites).Also worth a look (not the main bug)
External-command steps (e.g.
clear_costmap) reuseduration_sec(0.1s) as thebehavior_resultcallback timeout (recovery_node.py~L121). 100ms may be too tight for an external service round-trip, so delegated steps could routinely time out intoRECOVERY_EXHAUSTED. A separate, longer timeout for delegated commands vs. self-driven twist durations may be warranted.Happy to help however is useful. Thanks for the project.