Skip to content

Controller input

Controller input is exposed through robot.controller. The Controller is the handheld device on your desk, separate from the robot, and its readings come from its own USB packets. That means robot.controller keeps working even when the Bluetooth (BLE) link to the robot is down. The data is cached: reading a property does not block waiting for a new packet.

The controller

MARLIN controller driver inputs: four joystick axes labelled one to four, D-pad, buttons X/Y/A/B, and left and right triggers

Everything in the diagram is read the same way:

robot.controller.joystick("ONE").value   # a stick axis, -100 to 100
robot.controller.button("A").is_down     # a button, True or False

Controller

Controller(state_mirror=None)

You normally use the instance owned by Robot, rather than constructing a Controller directly.

joystick

controller.joystick(name: str) -> Joystick

Input: an axis name, as a string. Output: the Joystick object for that axis.

The controller has two physical sticks. MARLIN treats each stick's two directions as separate, single-axis joysticks, so there are four names:

Name Physical axis
"ONE" Left stick, up and down
"TWO" Left stick, left and right
"THREE" Right stick, up and down
"FOUR" Right stick, left and right

Names are case-insensitive, so joystick("four") and joystick("FOUR") are the same object.

Raises ValueError for any other name. If Python throws a ValueError, check your spelling.

robot.controller.joystick("FIVE")   # ValueError: Unknown joystick 'FIVE'. Expected one of ONE, TWO, THREE, FOUR.

button

controller.button(name: str) -> Button

Input: a button name, as a string. Output: the Button object for that name.

Names are converted to uppercase, so button("a") and button("A") are the same button.

The names are the ones printed on the Controller, so you write in code what you read on the panel. There are ten:

  • "UP", "DOWN", "LEFT", "RIGHT": the D-pad
  • "X", "Y", "A", "B": the face buttons
  • "LEFT_TRIGGER", "RIGHT_TRIGGER": the two triggers

A name outside this list raises ValueError, so a typo stops your program. Always check your variable spelling.

robot.controller.button("TRIGGER_LEFT")   # wrong spelling: raises ValueError
Unknown button 'TRIGGER_LEFT'. Expected one of UP, DOWN, LEFT, RIGHT, X, Y, A, B, LEFT_TRIGGER, RIGHT_TRIGGER.

Joystick

Joystick(name: str, axis: str, state_mirror=None)

You normally obtain joysticks from Controller.joystick().

Properties

Property Type Meaning Value before data arrives
name str The axis name, always the panel number: "ONE", "TWO", "THREE", or "FOUR" n/a
value float Scaled deflection, -100 to 100 0.0
raw int Raw 12-bit ADC reading, 0 to 4095, centre 2048. Advanced use 2048

An ADC turns the voltage from the stick into a number. Most programs use value and never touch raw.

value is scaled from the project's stick calibration: the measured resting position reads 0, and the measured ends of travel read -100 and 100. Until you run Calibrate Sticks, that scaling assumes a perfect stick: centre 2048 and the full 0–4095 range. A released stick may then read a little off 0, and a stick at the stop may not quite reach 100. See Calibrate Sticks. raw is never scaled or deadzoned.

Which way is positive?

The firmware sets the sign. This page cannot tell you the direction.

Print the value while you move the stick, and match your code to what you observe:

def driver(robot):
    while robot.running:
        print(robot.controller.joystick("ONE").value)

If the robot drives opposite to the stick, negate the value in your code. Do not rewire the stick.

Worked example: one stick, one thruster

def driver(robot):
    while robot.running:
        power = robot.controller.joystick("ONE").value   # -100 to 100
        robot.thruster("M1").set_duty(power)             # same range, so no conversion

Worked example: a dead zone

A stick that is not perfectly centred reports a small non-zero value, so the robot creeps when nobody is touching it. Ignore small values:

def driver(robot):
    while robot.running:
        power = robot.controller.joystick("ONE").value

        if abs(power) < 5:      # abs() ignores the sign: -3 and 3 both count as small
            power = 0

        robot.thruster("M1").set_duty(power)

Raise the 5 if the robot still creeps. Lower it if the stick feels unresponsive near the centre.

Button

Button(name: str, state_mirror=None)

You normally obtain buttons through Controller.button().

is_down: bool

True while the button is currently pressed. This is a level, not a press/release event. MARLIN does not provide a "just pressed" helper. Compare the current value against the previous value, as the example shows.

previous = False
while robot.running:
    current = robot.controller.button("A").is_down
    if current and not previous:
        toggle_arm()
    previous = current

Worked example: hold a trigger to run a thruster

The triggers are read exactly like any other button. They report on or off, not how far you pulled them:

def driver(robot):
    while robot.running:
        if robot.controller.button("RIGHT_TRIGGER").is_down:
            robot.thruster("M8").set_duty(60)
        else:
            robot.thruster("M8").set_duty(0)

Worked example: D-pad nudges

The D-pad is four separate buttons, so you can read more than one at a time:

def driver(robot):
    while robot.running:
        forward = 0
        if robot.controller.button("UP").is_down:
            forward = 30
        if robot.controller.button("DOWN").is_down:
            forward = -30

        robot.thruster("M1").set_duty(forward)
        robot.thruster("M8").set_duty(forward)