# controller.rs — Gamepad Input

<span style="white-space: pre-wrap;">Runs a background listener for gamepad input using the </span>`<span class="editor-theme-code">gilrs</span>`<span style="white-space: pre-wrap;"> library, translating button and axis events into UDP packets sent to the rover. All dispatching is gated on the current mode (drive vs. pickup) and whether the relevant manual mode is active.</span>

### Modes

<span style="white-space: pre-wrap;">The controller operates in one of two top-level modes, toggled with the </span>**Start**<span style="white-space: pre-wrap;"> button:</span>

<table id="bkmrk-modeactive-whencontr"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Mode

</th><th>Active when

</th><th>Controls

</th></tr><tr><td>Drive

</td><td>`<span class="editor-theme-code">pickup_mode = false</span>`

</td><td>Left stick (forward/turn), triggers (brake)

</td></tr><tr><td>Pickup

</td><td>`<span class="editor-theme-code">pickup_mode = true</span>`

</td><td>Both sticks (X/Y/rotate/flick), D-pad (Z), triggers (gripper)

</td></tr></tbody></table>

<span style="white-space: pre-wrap;">Within each mode, the </span>**Select**<span style="white-space: pre-wrap;"> button toggles the relevant manual mode (</span>`<span class="editor-theme-code">drive_manual_mode</span>`<span style="white-space: pre-wrap;"> or </span>`<span class="editor-theme-code">arm_manual_mode</span>`). Commands are silently suppressed when the associated manual mode is inactive.

### Threads

Three concurrent threads are spawned on startup:

`<span class="editor-theme-code">start_controller_listener()</span>`<span style="white-space: pre-wrap;"> — Entry point. Spawns all threads and owns the shared </span>`<span class="editor-theme-code">CommandState</span>`.

**Event thread**<span style="white-space: pre-wrap;"> — Polls </span>`<span class="editor-theme-code">gilrs</span>`<span style="white-space: pre-wrap;"> at ~125 Hz (8 ms sleep) and routes each </span>`<span class="editor-theme-code">Event</span>`<span style="white-space: pre-wrap;"> to the appropriate handler. Holds the </span>`<span class="editor-theme-code">shared</span>`<span style="white-space: pre-wrap;"> mutex only for the duration of each state update.</span>

**Heartbeat thread**<span style="white-space: pre-wrap;"> — Wakes every 2 seconds and re-sends the current state (drive axes + brake, or arm + brake). Ensures the rover never silently drifts from its commanded state if packets are dropped.</span>

**Ramp threads**<span style="white-space: pre-wrap;"> — Spawned on demand when a ramped button (D-pad up/down, left/right trigger in pickup mode) is pressed. Ticks at ~60 Hz and increments the axis value from </span>`<span class="editor-theme-code">0.0</span>`<span style="white-space: pre-wrap;"> toward </span>`<span class="editor-theme-code">±1.0</span>`<span style="white-space: pre-wrap;"> over </span>`<span class="editor-theme-code">RAMP_DURATION_SECS</span>`<span style="white-space: pre-wrap;"> (1.0 s). The thread exits when the direction is set back to </span>`<span class="editor-theme-code">0.0</span>`<span style="white-space: pre-wrap;"> on button release.</span>

### Button mapping

<table id="bkmrk-buttondrive-modepick"><colgroup><col style="width: 160px;"></col><col style="width: 286px;"></col><col style="width: 356px;"></col></colgroup><tbody><tr><th>Button

</th><th>Drive mode

</th><th>Pickup mode

</th></tr><tr><td>Start

</td><td>Toggle pickup mode

</td><td>Toggle pickup mode

</td></tr><tr><td>Select

</td><td><span style="white-space: pre-wrap;">Toggle </span>`<span class="editor-theme-code">drive_manual_mode</span>`

</td><td><span style="white-space: pre-wrap;">Toggle </span>`<span class="editor-theme-code">arm_manual_mode</span>`

</td></tr><tr><td>Left trigger (LB)

</td><td>Toggle latching brake

</td><td>Ramp gripper speed → close (−1.0)

</td></tr><tr><td>Right trigger (RB)

</td><td>—

</td><td>Ramp gripper speed → open (+1.0)

</td></tr><tr><td>Left trigger 2 (LT)

</td><td>Toggle latching brake

</td><td>—

</td></tr><tr><td>Right trigger 2 (RT)

</td><td>Momentary brake (hold)

</td><td>—

</td></tr><tr><td>D-pad up

</td><td>—

</td><td>Ramp Z → up (+1.0)

</td></tr><tr><td>D-pad down

</td><td>—

</td><td>Ramp Z → down (−1.0)

</td></tr></tbody></table>

### Axis mapping

<table id="bkmrk-axisdrive-modepickup"><colgroup><col style="width: 117px;"></col><col style="width: 184px;"></col><col></col></colgroup><tbody><tr><th>Axis

</th><th>Drive mode

</th><th>Pickup mode

</th></tr><tr><td>Left stick Y

</td><td>Forward / backward

</td><td>Flick

</td></tr><tr><td>Left stick X

</td><td>Turn

</td><td>Rotate

</td></tr><tr><td>Right stick X

</td><td>—

</td><td>End effector X (left/right)

</td></tr><tr><td>Right stick Y

</td><td>—

</td><td>End effector Y (forward/backward)

</td></tr></tbody></table>

All analog axes pass through a deadzone (`<span class="editor-theme-code">±0.05</span>`<span style="white-space: pre-wrap;">) and are only dispatched when the change from the last-sent value exceeds </span>`<span class="editor-theme-code">AXIS_CHANGE_THRESHOLD</span>`<span style="white-space: pre-wrap;"> (0.05). Z and gripper speed are not axis-driven — they are ramped from button presses.</span>

### Packets sent

<table id="bkmrk-packetwhenbasestatio"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Packet

</th><th>When

</th></tr><tr><td>`<span class="editor-theme-code">BasestationManualDrive</span>`

</td><td>Drive mode, on axis change or heartbeat

</td></tr><tr><td>`<span class="editor-theme-code">BasestationManualBrake</span>`

</td><td>Drive mode, on brake toggle/press/release or heartbeat; also sent continuously (engaged) during pickup heartbeat

</td></tr><tr><td>`<span class="editor-theme-code">BasestationManualArmMovement</span>`

</td><td>Pickup mode, on any arm state change or heartbeat

</td></tr></tbody></table>

<span style="white-space: pre-wrap;">All values are scaled from </span>`<span class="editor-theme-code">[−1.0, 1.0]</span>`<span style="white-space: pre-wrap;"> to the full </span>`<span class="editor-theme-code">sint32</span>`<span style="white-space: pre-wrap;"> range before transmission.</span>

### Constants

<table id="bkmrk-constantvaluepurpose"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Constant

</th><th>Value

</th><th>Purpose

</th></tr><tr><td>`<span class="editor-theme-code">AXIS_CHANGE_THRESHOLD</span>`

</td><td>`<span class="editor-theme-code">0.05</span>`

</td><td>Deadzone boundary and minimum delta before a packet is sent

</td></tr><tr><td>`<span class="editor-theme-code">HEARTBEAT_INTERVAL</span>`

</td><td>`<span class="editor-theme-code">2 s</span>`

</td><td>How often state is re-sent without an input event

</td></tr><tr><td>`<span class="editor-theme-code">RAMP_DURATION_SECS</span>`

</td><td>`<span class="editor-theme-code">1.0 s</span>`

</td><td><span style="white-space: pre-wrap;">Time for a ramped axis to travel from </span>`<span class="editor-theme-code">0.0</span>`<span style="white-space: pre-wrap;"> to </span>`<span class="editor-theme-code">±1.0</span>`

</td></tr><tr><td>`<span class="editor-theme-code">RAMP_TICK_MS</span>`

</td><td>`<span class="editor-theme-code">16 ms</span>`

</td><td>Ramp thread tick interval (~60 Hz)

</td></tr><tr><td>`<span class="editor-theme-code">MOMENTARY_BRAKE_DURATION</span>`

</td><td>`<span class="editor-theme-code">500 ms</span>`

</td><td>Defined but unused — auto-release was commented out

</td></tr></tbody></table>