Skip to content

Implement switches - #34

Open
CatYang3 wants to merge 11 commits into
mainfrom
tension
Open

Implement switches#34
CatYang3 wants to merge 11 commits into
mainfrom
tension

Conversation

@CatYang3

@CatYang3 CatYang3 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Describe the main changes in this PR:

  • Added exosuit states. Main thread monitors switches to change exosuit status accordingly.
  • Added a 3-way switch for inclination modes (the configuration of which can be changed in Definitions.)
  • Added pre-tensioning to the loop thread
  • Added mocks for switches and motors, allowing testing w/o hardware
  • Added tests for exosuit state changes

Testing

Describe how you verified functionality:

  • Unit tests added
  • Existing tests pass
  • Manual testing performed

Steps to reproduce/test:
1.
2.
3.


Code Quality Checklist

Before requesting review, ensure:

  • I ran make format
  • I ran make test
  • All CI checks pass
  • Code follows project style & conventions
  • New functions/classes include docstrings
  • Public APIs are typed (type hints)

Documentation

  • Code comments updated
  • README updated (if needed)

Additional Notes

  • Anything else reviewers should know?
  • Attach outputs, plots, logs, or GIFs here.

@CatYang3
CatYang3 marked this pull request as ready for review June 8, 2026 15:28
@CatYang3 CatYang3 self-assigned this Jun 8, 2026
@CatYang3
CatYang3 requested a review from NGierden June 8, 2026 15:30
@NGierden

NGierden commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

@claude-review

@claude

claude Bot commented Jun 8, 2026

Copy link
Copy Markdown

Claude finished @NGierden's task in 2m 37s —— View job


PR Review: Implement switches

  • Gather context from PR description and changed files list
  • Read changed source files (exosuit.py, gpio.py, motor.py, definitions.py, __main__.py)
  • Read test file changes
  • Analyze for bugs, correctness, style, and safety issues
  • Post detailed review feedback

Good structural direction — the state machine and mock infrastructure are the right foundations. However there are several bugs that need fixing before this merges.


🔴 Critical Bugs

1. Motor copy-paste: motor_right never receives a command

exosuit.py lines 290–296:

self.motor_left.set_velocity(convert_rad_per_sec_to_rpm(command_left))

self.motor_left.set_velocity(   # ← should be motor_right
    convert_rad_per_sec_to_rpm(command_right)
)

The second call should be self.motor_right.set_velocity(...). The original copy-paste bug from signal_left was fixed, but a new one was introduced here for the motor output. Fix this →

2. motor_right.close() missing from _cleanup()

exosuit.py line 251 only calls self.motor_left.close(). The right motor is never closed, which will leave the serial/CAN connection open. Fix this →

3. PRETENSIONING loop has no sleep when torque is below limit — CPU spin

exosuit.py lines 304–314: if left_motor_torque < motor_torque_limit, there is no time.sleep() and the thread spins at 100% CPU. With the placeholder value 0.85 >= 0.85 this always triggers so it sleeps, but as soon as real torque reading is wired in and reads below the limit, this becomes a busy loop. Add a time.sleep() in the else branch (or unconditionally at the end of the loop body).

4. _pretension() races with the PRETENSIONING block in _loop()

_switch_event_handler calls _pretension() which sets velocity → 3. Then, within the same SWITCH_EVENT_HANDLER_INTERVAL window (0.5 s), the PRETENSIONING inner loop in _loop() detects torque >= limit (always true with placeholder) and immediately calls set_velocity(0). The motor is stopped before it can tension. These two threads are working against each other. The velocity-setting responsibility should live in one place — either _loop or _switch_event_handler, not both.


🟡 Design / Correctness Issues

5. _loop() outer while True never exits for STOPPED state

exosuit.py line 263:

def _loop(self) -> None:
    while True:
        while self._status == ExosuitStates.RUNNING: ...
        while self._status == ExosuitStates.PRETENSIONING: ...
        time.sleep(EXOSUIT_STANDBY_INTERVAL)  # spins here forever when STOPPED

When status becomes STOPPED, neither inner loop matches and the outer loop sleeps 0.1 s indefinitely. _cleanup() joins this thread with a 2 s timeout and will always time out silently. Change while True: to while self._status != ExosuitStates.STOPPED:. Fix this →

6. Registering two add_event_detect calls on the same GPIO channel

exosuit.py lines 138–163 call add_event_detect twice for POWER_SWITCH (once for FALLING, once for RISING) and twice for TENSION_SWITCH. On real Jetson GPIO, a second call on an already-registered channel raises a RuntimeError. This will work with MockGPIO (each call just spawns a new thread) but will fail on hardware. Verify against the Jetson.GPIO API and restructure if needed (e.g., use BOTH edge if available, or use event_detected() in a polling thread).

7. No transition from PRETENSIONING → RUNNING

_switch_event_handler only handles:

  • STANDBYRUNNING (power on)
  • STANDBYPRETENSIONING (tension on)
  • RUNNINGSTANDBY (power off)
  • PRETENSIONINGSTANDBY (tension off)

If a user finishes tensioning (tension switch OFF) and then turns power ON, they go STANDBY → RUNNING correctly. But if they somehow end up in PRETENSIONING with both switches, or complete tensioning and immediately power on, there's no PRETENSIONING → RUNNING path. Consider whether this gap matches the intended hardware workflow.

8. _initialize_imus() ignores mock flag

With mock=True, IMUFactory.detect_and_create() is still called and will fail without hardware. The tests only pass if the IMU mock is already set up at the imu-python package level. If the package doesn't provide an in-process mock, tests on CI will fail or silently skip initialization. Consider adding a mock IMU path analogous to the mock motor.


🔵 Minor / Style

9. MockMotor.get_torque() docstring is below the TODO (motor.py line 27) — docstrings must be the first statement in a function, before comments:

def get_torque(self) -> int:
    """Get the torque of the motor."""  # move above TODO
    # TODO: make this function name/param identical to the motor implementation
    return 0

10. MockGPIO.cleanup() missing return type hint (gpio.py line 56) — def cleanup(self):def cleanup(self) -> None:

11. _power_on_callback and friends missing type hint for channel (exosuit.py lines 205–214) — channel should be typed as int.

12. SWITCH_ON / SWITCH_OFF are strings used as getattr keys (definitions.py lines 87–88) — this is fragile; the attribute names in MockGPIO must match exactly. Consider defining these as the integer constants directly in definitions.py (e.g., SWITCH_ON = MockGPIO.FALLING) or at least adding a comment explaining the coupling.


Summary

Severity Count
🔴 Critical (must fix) 4
🟡 Design/Correctness 4
🔵 Minor/Style 4

The mock infrastructure (MockGPIO, MockMotor) and the state enum are solid additions. The main things to fix are the motor routing bug (#1, #2), the pretensioning thread conflict (#3, #4), and the loop exit condition (#5) before this is ready to merge.

@NGierden NGierden left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See claude comments

@CatYang3

CatYang3 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Every Claude comment has been addressed in the last commits, except point 7:
It is intended that the RUNNING state and the PRETENSIONING state cannot transition to each other without transitioning to STANDBY first.

@CatYang3
CatYang3 requested a review from NGierden June 9, 2026 13:44
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.

2 participants