Skip to content

[ROB-1464] fix(linear_rail): keep brake engaged after a single shutdown#56

Open
nu-jliu wants to merge 1 commit into
mainfrom
fix/linear-rail-brake-engaged-on-shutdown
Open

[ROB-1464] fix(linear_rail): keep brake engaged after a single shutdown#56
nu-jliu wants to merge 1 commit into
mainfrom
fix/linear-rail-brake-engaged-on-shutdown

Conversation

@nu-jliu

@nu-jliu nu-jliu commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

On the consolidated single-host LinearBot (x86; brake driven through the bestep USB-to-GPIO converter, not a Raspberry Pi), stopping the controller left the vertical rail brake RELEASED, so the rail could drift/drop. The standalone runner only appeared to work because it closes the vehicle twice; any caller that closes once (e.g. lab42) ended up with the brake released. A single LinearRailController.cleanup() / LinearRailVehicle.close() now leaves the brake ENGAGED (GPIO LOW) on both x86 and Raspberry Pi, with no dependence on a second call and no serial-port reopen.

Root cause: cleanup() engaged the brake (drove it LOW), then GPIO.cleanup() undid it — on x86 UsbGpioBackend.cleanup() deliberately drove every configured OUTPUT channel HIGH (released), and on the Pi the global RPi.GPIO.cleanup() floats all pins. Net result of one cleanup = brake released.

Safety note: brake polarity is LOW = engaged/held (unchanged — set_brake(engaged=True) already commanded LOW; this PR only stops cleanup() from overriding it back to HIGH). Steady state after a single close = the same latched LOW the standalone double-close already holds the rail in. Software cannot guarantee the level survives a hard kill / power loss / USB unplug — that depends on the brake being spring-engaged (power-to-release). Scope is brake-only; rail-motor / CAN-loop shutdown behavior is intentionally unchanged.

Changes

  • i2rt/utils/usb_gpio_driver.py: UsbGpioBackend.cleanup() no longer drives configured OUTPUT channels HIGH (released) on shutdown. The converter latches its last-driven level across a port close, so the brake stays at its last commanded level (LOW = engaged) and the rail stays held. Rewrote the misleading comment.
  • i2rt/flow_base/linear_rail_controller.py: LinearRailController.cleanup() is now idempotent (a 2nd call is a no-op, so the standalone double-close no longer reopens the serial port and resets the converter); set_velocity(0.0) is isolated in its own try/except so a CAN error can't skip the safety-critical brake engage; replaced global GPIO.cleanup() with GPIO.cleanup((UPPER_LIMIT_GPIO, LOWER_LIMIT_GPIO)) so on the Pi the brake OUTPUT pin stays driven LOW instead of being floated. Added a _cleaned_up guard flag in __init__.
  • i2rt/utils/tests/test_usb_gpio_driver.py: inverted the cleanup test (test_cleanup_keeps_brake_latched_low_not_released) to assert the brake stays LOW and cleanup issues no HIGH to the brake channel; added test_cleanup_accepts_pin_tuple_and_tears_down for the limit-pin tuple the controller now passes.

Test Plan

Unit tests (x86, no hardware — runs in CI)

  • uv run pytest i2rt/utils/tests/test_usb_gpio_driver.py -q — 21 pass, incl. test_cleanup_keeps_brake_latched_low_not_released and test_cleanup_accepts_pin_tuple_and_tears_down.
  • uv run pytest -n auto — full suite green (expected: 281 passed, 2 skipped).
  • uv run ruff check . — clean.

Manual on x86 (the reported failure)

  • Bring up LinearRailVehicle(enable_linear_rail=True, usb_gpio_device="/dev/ttyUSB0", ...) and call vehicle.close() exactly once.
  • Confirm converter channel 3 reads LOW and the brake physically holds — the rail does not drift/drop.

Standalone runner

  • python i2rt/flow_base/flow_base_controller.py --linear-rail --device /dev/ttyUSB0, then Ctrl-C.
  • Confirm the brake stays engaged and there is no port-reopen / board-reset glitch on shutdown.

Regression / parity

  • Run with enable_linear_rail=False — base-motor behavior unchanged.
  • (If a Pi is available) confirm pin 12 stays driven LOW after a single close (note: subject to the installed GPIO library's process-exit semantics; the x86 converter is the primary target).

Related to ROB-1464

🤖 Generated with Claude Code

- i2rt/utils/usb_gpio_driver.py: stop UsbGpioBackend.cleanup() from driving configured OUTPUT channels HIGH (released) on shutdown. The converter latches its last-driven level, so the brake now stays at its last commanded level (LOW = engaged) and the rail stays held. Rewrote the misleading comment.
- i2rt/flow_base/linear_rail_controller.py: make LinearRailController.cleanup() idempotent (a 2nd call is a no-op, so the standalone double-close no longer reopens the serial port and resets the converter); isolate set_velocity(0.0) in its own try/except so a CAN error can't skip the brake engage; replace global GPIO.cleanup() with GPIO.cleanup((UPPER_LIMIT_GPIO, LOWER_LIMIT_GPIO)) so on the Pi the brake OUTPUT pin stays driven LOW instead of being floated.
- i2rt/utils/tests/test_usb_gpio_driver.py: invert the cleanup test to assert the brake stays LOW and cleanup issues no HIGH to the brake channel; add a test that cleanup() accepts and ignores the limit-pin tuple the controller now passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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