RC_CHANNELS_OVERRIDE. Operator stick input is
converted into SET_POSITION_TARGET_LOCAL_NED body-frame velocity setpoints and sent to
ArduPilot only when the vehicle is in GUIDED mode and control has been explicitly enabled.
This is a deliberate safety decision: with RC override, a zero value means minimum throttle
(a crash); with a GUIDED velocity setpoint, zero means hold position. Every part of this
subsystem is built around that one property.
The whole path lives in the on-drone gamepad / ws_proxy service:
| File | Responsibility |
|---|---|
docker/gamepad/src/modules/guided_control/controller.py | GuidedVelocityController — builds and sends the MAVLink setpoint, owns the state machine, mode-guards, timeout |
docker/gamepad/src/modules/guided_control/module.py | GuidedControlModule — lifecycle, config → limits, 20 Hz rate limit, 10 Hz watchdog thread |
docker/gamepad/src/modules/guided_control/mapper.py | GamepadMapper — axes → body-frame VelocityCommand with dead-zone + scaling |
docker/gamepad/src/core/message_router.py | routes guided_control / velocity_command / raw gamepad frames |
docker/gamepad/src/shared/config.py | GUIDED_* limits + ROVER_KEYBINDS button map |
This page is the safety model. For how a frame reaches the drone (Dashboard →
{ws_proxy}/redispad/{droneId} → Redis {ip}:gamepad_input → MessageRouter) see
/dashboard/vehicle-commands and
/ecosystem/ws-proxy. For the module framework this plugs into, see
/drone-os/module-system.Command path
The gamepad service connects to the flight controller over MAVLink UDPudpout:127.0.0.1:14777
through the single mavp2p router — see /drone-os/mavlink-topology.
Why velocity setpoints, not RC override
RC_CHANNELS_OVERRIDE (removed)
Zero value = minimum throttle = crash. Fights the RC transmitter. No mode awareness.
GUIDED velocity (current)
Zero velocity = hold position. Ignored outside GUIDED. Loss of input → vehicle stops and holds.
SET_POSITION_TARGET_LOCAL_NED message with MAV_FRAME_BODY_NED (frame 8)
so forward/right/down are relative to the vehicle’s heading. The type mask ignores position,
acceleration, force and absolute yaw, and uses only velocity + yaw-rate:
docker/gamepad/src/modules/guided_control/controller.py
vx = forward(+)/back(-), vy = right(+)/left(-), vz = down(+)/up(-) (NED),
yaw_rate = CW(+)/CCW(-) in rad/s (_send_velocity_raw, controller.py:279).
Enable / disable protocol
Movement is a two-step handshake. The UI must explicitly enable control, and it only succeeds in GUIDED mode.Put the vehicle in GUIDED
Press GUIDED (gamepad Triangle → button
3) or send a set_mode command. enable() reads
the live mode via the injected mode_getter (mavlink_module.get_flight_mode) and refuses
otherwise.Enable velocity control
Send
{"type": "guided_control", "command": "enable"}. On success the module sets
_enabled_by_user = True, sends one zero-velocity setpoint for a clean start, and broadcasts
a guided_control_state message to WebSocket clients.Stream axes frames
Now
axes[] frames are accepted. The Dashboard only sends axes while control is enabled, and
the drone-side process_gamepad_input() also drops them unless _enabled_by_user is set —
a double gate.module.py:131) and publish it to the
Redis output channel as guided_control_status. Send {"type":"guided_control","command":"status"}
to query current state without changing it.
Control state machine
GuidedVelocityController (controller.py:56) has three states. The on_state_change callback
bubbles every transition up to GuidedControlModule._handle_state_change, which auto-clears
_enabled_by_user on any drop to DISABLED and broadcasts the new state.
- DISABLED — no setpoints sent; incoming velocity commands are ignored.
- ENABLED — actively sending clamped setpoints.
- STOPPED — was enabled but input stalled; a zero-velocity hold is sent and the vehicle holds. Resumes to ENABLED automatically when frames arrive again.
Velocity limits & dead-zone
Limits come fromGamepadConfig (config.py:113) and are read into VelocityLimits /
GamepadMapper in module.py:60. Values clamp on the drone side regardless of what the UI sends.
| Env var | Default | Meaning |
|---|---|---|
GUIDED_MAX_HORIZONTAL_SPEED | 3.0 | Max forward/back/strafe (m/s) |
GUIDED_MAX_VERTICAL_SPEED | 2.0 | Max up/down (m/s) |
GUIDED_MAX_YAW_RATE | 0.5 | Max rotation (rad/s) |
GUIDED_COMMAND_TIMEOUT | 0.5 | Seconds of no input before STOPPED |
GUIDED_DEAD_ZONE | 0.15 | Stick dead-zone (0–1) |
The
VelocityLimits dataclass defaults (5.0 / 2.5 / 1.0) are looser than the config, but
they are never used in production — set_connection() always overrides them with the GUIDED_*
config values (3.0 / 2.0 / 0.5). Edit the env vars / config, not the dataclass defaults.mapper.py:165): input below the threshold returns 0.0,
and the remaining dead_zone..1.0 range is stretched back to 0..1 so there is no output step at
the edge of the dead-zone.
Stick → velocity mapping (drone)
The gamepad service hardwiresVEHICLE_TYPE = 2 (quadrotor) in config.py:110, so
GamepadMapper.map_from_existing_format uses the drone mapping (is_rover = False). The
production frame layout:
axes[] | Stick | Mapped to |
|---|---|---|
axes[0] | Left stick X | Yaw rate (× GUIDED_MAX_YAW_RATE) |
axes[1] | Left stick Y | Altitude / down (× GUIDED_MAX_VERTICAL_SPEED) |
axes[2] | Right stick X | Strafe right (× GUIDED_MAX_HORIZONTAL_SPEED) |
axes[3] | Right stick Y | Forward (inverted, × GUIDED_MAX_HORIZONTAL_SPEED) |
20 Hz send + 10 Hz watchdog
Two independent timers protect the link:- 20 Hz send rate limit —
GuidedControlModule(module.py:56) drops frames faster than1/20 s, so an over-eager 60 Hz UI stream is throttled to a steady 20 Hz of setpoints. - 10 Hz timeout watchdog — a daemon thread (
_timeout_loop,module.py:280) callscheck_timeout()every 100 ms. If no command has arrived forcommand_timeout(0.5 s), it sends a zero-velocity setpoint and moves to STOPPED. This is what actually protects against a dropped uplink or a hung UI.
set_velocity_body() accepts a dead_man_active flag that sends zero velocity when False, but
the current gamepad/velocity routes always pass it True. In practice the timeout watchdog,
not the dead-man flag, is the continuous-input safety net. Preserve the watchdog if you touch
this loop.The three safety guards a refactor MUST preserve
Underpinning all three is the design invariant that zero velocity = hold, not throttle-to-zero. Keep the message type (SET_POSITION_TARGET_LOCAL_NED), the body frame (8) and the type mask
(0x07C7) intact — do not reintroduce RC_CHANNELS_OVERRIDE.
Gamepad button / axis map
Buttons are dispatched throughROVER_KEYBINDS (config.py:123) in
MessageRouter._handle_gamepad_input. Unlike axes, button commands are always processed
regardless of guided-control state.
| Button | Action | Physical input |
|---|---|---|
0 | Set mode RTL | Cross (X) |
1 | Set mode AUTO | Circle (O) |
2 | Set mode POSHOLD | Square |
3 | Set mode GUIDED | Triangle |
4 | Toggle gimbal mode | L1 (300 ms debounce) |
5 | Take photo | R1 |
6 | Zoom out (proportional) | L2 trigger / axes[4] |
7 | Zoom in (proportional) | R2 trigger / axes[5] |
8 | Center gimbal + autofocus | Share |
9 | Autofocus | Options |
10 | Toggle recording | L3 |
11 | Track person (Isaac SLAM) | R3 |
12–15 | Gimbal pitch up / down, yaw left / right | D-pad |
0–3) go straight to mavlink.set_mode(). Buttons 10/11 and dock
are advisory here and handled by the Isaac SLAM /
detection & docking services.
Gimbal-modifier mode (L1)
L1 (button4) toggles a modifier that repurposes inputs — a non-obvious dual mapping worth
preserving:
| Input | Normal | Gimbal mode (L1 on) |
|---|---|---|
| Right stick | Forward/back + strafe (movement) | Gimbal yaw + pitch (proportional) |
| D-pad | Gimbal pitch/yaw | Forward/back + strafe (synthetic axes → movement) |
| Left stick | Yaw + altitude | Yaw + altitude (unchanged) |
process_gamepad_input() so movement still works (message_router.py:706). Gimbal control is
additionally locked during ArUco precision landing — the aruco_landing source sets
_gimbal_locked and operator gimbal commands are silently dropped until dock_stop or disarm
(see /drone-os/robotics/detection-and-landing).
Related
Vehicle Commands & Gamepad
The UI side: 60 Hz sampling, redispad WebSocket, guided-enable state sync.
MAVLink Routing (mavp2p)
How port 14777 setpoints reach the FCU through the single MAVLink hub.
Redis Message Bus
The
{ip}:gamepad_input / output channels that carry these messages.Core & Gamepad Module Systems
The
ModuleBase lifecycle GuidedControlModule plugs into.
