Skip to content

Fix SystemCommand Command_out getting clobbered same-tick by exec fallback - #30

Open
grahas wants to merge 1 commit into
PCrnjak:mainfrom
grahas:investigate/guard-homed-fastpath-bug
Open

Fix SystemCommand Command_out getting clobbered same-tick by exec fallback#30
grahas wants to merge 1 commit into
PCrnjak:mainfrom
grahas:investigate/guard-homed-fastpath-bug

Conversation

@grahas

@grahas grahas commented Aug 8, 2026

Copy link
Copy Markdown

Summary

After an ESTOP latches PAROL6.disabled at the firmware level, RESET (and any SystemCommand) silently fails to re-enable the robot -- HOME/JOG/MOVE all get accepted, planned, and streamed with correct, smoothly-changing setpoints, and the server reports success throughout, but the arm never actually moves. No error is raised anywhere in the stack.

Root cause

Each 100Hz control-loop tick runs _poll_commands() (dispatches SystemCommands like RESET) before _execute_commands(). _execute_commands()'s "nothing active" fallback unconditionally sets state.Command_out = CommandCode.IDLE -- which clobbers ResetCommand.execute_step()'s state.Command_out = CommandCode.ENABLE before _write_to_firmware() ever observes it, on every single RESET call (RESET never queues an active segment, so it always hits this fallback branch). ENABLE(101) therefore never reaches the firmware, and PAROL6.disabled stays latched forever at the firmware's if (PAROL6.disabled == 0) {...} gate -- while every higher layer (planner, segment player, server) believes the command succeeded.

Found via live per-tick instrumentation of the pipeline (_handle_motion_command -> planner.submit -> subprocess -> segment_queue -> SegmentPlayer.tick/_activate_next -> _write_to_firmware) rather than static reading, which had repeatedly given false confidence at every layer.

Fix

ControllerState gains command_out_locked: bool = False:

  • _poll_commands() resets it to False at the top of every tick.
  • _handle_system_command() sets it True after command.tick(state) if state.Command_out != CommandCode.IDLE.
  • _execute_commands()'s fallback checks the lock first -- if set, it consumes it (= False) instead of stomping Command_out back to IDLE.

Testing

  • home() verified on real hardware: robot genuinely drives to standby, confirmed both via continuous status().angles polling during the move and by physically observing it.
  • Full existing test suite passes unchanged: python3 -m pytest -q -m "unit or integration" (90 tests).

Open item

Whether the same same-tick clobbering affects STOP/ESTOP's own Command_out assignments wasn't independently verified -- their handling additionally calls _segment_player.cancel()/_executor.cancel_active_command(), which may sidestep the issue differently, but I haven't confirmed either way. Flagging for review/follow-up rather than asserting it's fine.

…lback

RESET (and any other one-shot SystemCommand that sets state.Command_out,
e.g. to CommandCode.ENABLE) had its signal silently overwritten before it
ever reached the firmware: _poll_commands() (which dispatches SystemCommands
during the "poll_cmd" phase) runs before _execute_commands() (the "exec"
phase) in the same control-loop tick, and _execute_commands()'s "nothing
active" fallback unconditionally reset state.Command_out = CommandCode.IDLE
whenever no segment/streaming command was active -- which is the case right
after a plain RESET, since RESET itself doesn't queue any motion.

The practical symptom: RESET appeared to succeed (state.enabled is pure
Python state, set unconditionally), but PAROL6.disabled on the firmware
never actually got cleared, because the ENABLE(101) command code set by
ResetCommand.execute_step() never survived to _write_to_firmware(). Once
PAROL6.disabled was latched from an earlier ESTOP, every subsequent
HOME/JOG/MOVE was silently dropped by the firmware's `if (PAROL6.disabled
== 0)` gate -- while the server-side planner/segment-player pipeline
computed and "sent" a perfectly valid trajectory the whole time, believing
it succeeded.

Fixed with a same-tick lock flag (ControllerState.command_out_locked): set
whenever a SystemCommand assigns a non-IDLE Command_out during poll_cmd,
consumed by _execute_commands()'s fallback instead of blindly resetting to
IDLE, and cleared fresh at the top of every _poll_commands() call.

Verified against real hardware: home() on an unhomed-but-referenced robot
now actually drives the arm to standby (confirmed via continuous
status().angles polling during the move, and visually). Full test suite
(90 tests, unit + integration) passes unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant