Skip to content

MARLIN API

To use the MARLIN API, import the marlin library in your Python code.

from marlin import Robot, IMU, RadioBeacon, Thruster, Servo, Phase

The API has four parts:

  1. Robot (configuration, match state)
  2. Controller (joysticks and buttons)
  3. Effectors (thrusters and servos)
  4. Sensors (IMU and Beacon Receiver)

Robot is a class. Import it from MARLIN and create one object robot at the start of your code.

Signature

Robot(endpoint: str | None = None)

Example

from marlin import Robot
robot = Robot()

configure() declares the type of hardware connected to each port to the Robot Brain. Only list the ports you use. A wrong declaration will throw an error.

Signature

robot.configure(
*,
sensors: dict[str, type[Sensor]],
effectors: dict[str, type[Effector]],
) -> None

Example

from marlin import Robot, IMU, Thruster, Servo
robot = Robot()
robot.configure(
sensors={"S1": IMU},
effectors={"M1": Thruster, "M8": Thruster, "M2": Servo},
)

Parameters

ParameterPortsTypesLimit
sensorsS1–S6IMU, RadioBeaconAt most 2 IMUs, 1 RadioBeacon
effectorsM1–M8Thruster, Servo—

Errors

ErrorWhen
ValueErrorA bad port name, or too many sensors of one type
TypeErrorA type that is not a supported sensor or effector
RuntimeErrorCalled twice, or the brain reports different hardware than you declared

run() watches robot.phase and calls the respective match phase function once.

You write one function per phase and pass them in here — each is optional, keyword-only, and receives the robot. Each function is called once, the moment its phase starts, so it must keep looping until the phase ends — that is what while robot.running: is for. run() blocks until the match is over or an E-Stop fires.

Whenever a phase ends — or your function returns early — MARLIN idles every effector. A function that returns early is not called again that phase; one that loops past the end is stopped and its commands ignored. An exception in your function stops the robot, then comes out of run() with the usual Python traceback.

Signature

robot.run(
*,
initialize: Callable[[Robot], None] | None = None,
autonomous: Callable[[Robot], None] | None = None,
driver: Callable[[Robot], None] | None = None,
) -> None

Example

def autonomous(robot):
while robot.running:
...
def driver(robot):
while robot.running:
...
robot.run(autonomous=autonomous, driver=driver)

Parameters

ParameterDescription
initializeRuns before the first phase. Setup only: read sensors, print, work out numbers. Effector commands here are thrown away. Put anything that moves hardware in autonomous() or driver().
autonomousRuns when the Autonomous Period starts.
driverRuns when the Driver Controlled Period starts.

Seven read-only properties describing the robot and the match. The Pool Server dictates the match phase. In its absence, MARLIN can specify the match phase from the control station.

PropertyTypeMeaning
robot.runningboolTrue while the phase your function was called for is still going
robot.phasePhaseWhich part of the match the robot is in right now
robot.link_okboolTrue while the wireless link to the robot is healthy. False means your commands are not reaching the Robot.
robot.estoppedboolTrue while the robot is emergency-stopped. E-Stop can be activated and cleared by the Pool Server and MARLIN control station.
robot.overtempboolTrue while the brain is overheating. The Robot Brain’s thermal protection trips at 50°C. Full functionality is restored once it has cooled to 45°C.
robot.battery_voltagefloatBattery’s voltage in volts (V). Reads 0.0 before the Robot Brain reports anything.
robot.controllerControllerThe handheld control panel — see Controller

True while the phase your function was called for is still going. False when the phase ends, causing a while robot.running: loop to exit at the end of a match phase.

robot.running runs at a rate of 50 Hz, the same rate MARLIN sends commands. Adding time.sleep() decreases the frequency.

Example

def driver(robot):
while robot.running:
... # runs 50 times a second without time.sleep()

Reads which part of the match the robot is in.

Example

from marlin import Phase
if robot.phase == Phase.AUTON:
...
PhaseMeaning
CONFIGURINGProgram started, configure() not done yet
CONFIGUREDHardware declared and confirmed
AUTONAutonomous Period
PAUSEBetween phases
DRIVERDriver Controlled Period
ESTOPEmergency Stop
POSTMATCHAfter the match

The Controller has four joystick axes and ten independent buttons. Follow the naming conventions strictly. A wrong button or joystick axis name will throw an error.

Controller.

Returns one of the four joystick axes. There are two types of values that can be returned, scaled and raw values.

Signature

robot.controller.joystick(name: str) -> Joystick

Parameters

ParameterDescription
name"ONE", "TWO", "THREE" or "FOUR". Any other name raises ValueError.

How far the stick is pushed: −100 to 100, and 0 when centred. The joystick axis range matches the thruster’s duty, so a stick can drive a thruster directly.

power = robot.controller.joystick("ONE").value

The unscaled reading behind value: 0–4095, with centre at 2048. raw is used for checking a stick’s true centre or writing your own scaling.

joystick_value = robot.controller.joystick("ONE").raw

Returns the button state. True while the button is held down, False when released.

Signature

robot.controller.button(name: str).is_down -> bool

Example

Reading the current state of the button:

if robot.controller.button("A").is_down:
...

Recording a button press event:

while robot.running:
was_down = False
down = robot.controller.button("A").is_down
if down and not was_down:
...
was_down = down

Parameters

ParameterDescription
nameOne of UP, DOWN, LEFT, RIGHT, X, Y, A, B, LEFT_TRIGGER, RIGHT_TRIGGER. An unknown name raises ValueError.

Declare the type of effector in configure() and fetch it with robot.thruster() or robot.servo(). These are the generic behaviours of effectors.

  1. An effector will follow the last command given in the current match phase.
  2. Effector inputs are clamped.
  3. Idle state differs by effector type.

Set the duty cycle of the thruster from a range of -100 to 100, with 0 being the idle state. Note that this does not represent the exact speed or power of the thruster.

Signature

robot.thruster(port: str).set_duty(duty: float) -> None

Example

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

Parameters

ParameterDescription
duty−100 to 100, as a percentage of duty cycle. Values past either end are pinned to that end.

Turns a positional servo to an angle from 0° to 180° and holds it there. Due to the nature of positional servos, it will always hold a position within its working range. The idle state of the servos is 90°. Keep that in mind when writing your code.

Signature

robot.servo(port: str).angle(deg: float) -> None

Example

robot.servo("M2").angle(120)

Parameters

ParameterDescription
deg0° to 180°. An angle past either end is pinned to that end.

Set the movement speed of the servo. The speed range is 100 deg/s to 360 deg/s. If the speed isn’t defined, the servo will default to 180 deg/s.

Signature

robot.servo(port: str).set_speed(speed: float) -> None

Example

robot.servo("M2").set_speed(180)

Parameters

ParameterDescription
speed100 deg/s to 360 deg/s. Default is 180 deg/s

:::

::::

Like effectors, sensors are declared in configure() and fetched with robot.imu() or robot.radiobeacon(). Every sensor has one method: read().

read() returns the latest data measured by the sensor. Every sensor reading carries these three fields:

FieldMeaning
timestampTime in milliseconds when the Robot measures the reading. Robot Brain’s clock starts from boot.
ageHow long ago it was measured, in milliseconds. The age is calculated based on the difference in time between the current and previous reading.
okTrue if the reading exists and is less than 250 milliseconds old. Reject the reading if False.

IMU sensor: Acceleration, Turn Rate and Magnetic field.

Signature

robot.imu(port: str).read() -> ImuReading

Example

reading = robot.imu("S1").read()
if reading.ok:
print(reading)

Returns: ImuReading with ok, age, timestamp and:

FieldsUnitMeaning
accel_x, accel_y, accel_zgAcceleration per axis; the axis pointing down reads ≈ -1g at rest
gyro_x, gyro_y, gyro_z°/sTurn rate per axis; 0 when not turning
mag_x, mag_y, mag_z—Magnetic field, raw uncalibrated integers

Long-range radio receiver. It reports how strong a beacon’s signal is and which beacon sent it — not a direction or a distance, though strength does tend to rise as you get closer.

Signature

robot.radiobeacon(port: str).read() -> RadioBeaconReading

Example

reading = robot.radiobeacon("S6").read()
if reading.ok:
print(reading)

Returns: RadioBeaconReading with ok, age, timestamp and:

FieldUnitMeaning
rssidBmSignal strength, negative, and closer to zero is stronger.
snrdBHow far the signal stands above the noise. Negative means buried.
beacon_id—Which beacon sent the message.
rx_count—Total beacon messages received since the start of the code.

This page lists the errors raised by the MARLIN Python library. In the MARLIN Console they appear under the [OUT] tag, in two forms:

  1. Exceptions — the program stops with a Python traceback. If the last line of the traceback names one of the types below, MARLIN raised it and the tables explain the fix. ZeroDivisionError, NameError, SyntaxError, etc. are ordinary Python errors in your own code.
  2. [API] warnings — the program keeps running. MARLIN absorbed the mistake and printed what it did about it.

Messages tagged [EXT], [CON], or [POOL] come from the extension, the Controller, or the Pool Server, not from your code. See the MARLIN Console.

MARLIN idles every effector when a match phase ends, regardless of any error.

Error typeMeaningRaised by
ValueErrorA value outside the accepted setconfigure(), joystick(), button()
TypeErrorThe wrong kind of object in that placeconfigure(), typed accessors
KeyErrorA port that was not declaredHardware accessors
LookupErrorSensor absent or ambiguous with port omittedimu(), radiobeacon()
RuntimeErrorCalls in the wrong order, or the robot disagreesconfigure(), run()
LinkErrorThe connection to the extension or robot failedAny call that talks to the robot
[API] warningsNon-fatal mistakes MARLIN worked aroundrun(), effector commands
MessageCause and fix
Invalid sensor port 'S9'. Expected one of S1, S2, ...Sensor ports are S1–S6.
Invalid effector port 'M9'. Expected one of M1, M2, ...Effector ports are M1–M8.
At most 2 IMU may be configured; got 3 (S1, S2, S3).A robot supports at most 2 IMUs and 1 RadioBeacon. Remove the extras.
Unknown joystick 'FIVE'. Expected one of ONE, TWO, THREE, FOUR.Joysticks are named by the numbers printed on the panel.
Unknown button 'START'. Expected one of UP, DOWN, ...Buttons are named by their panel labels — the message lists all ten.
MessageCause and fix
S2: sensor declaration must be a Sensor subclass.Write the bare type in configure(): IMU, not IMU() and not "IMU".
S2: unsupported sensor type Servo. Known types: IMU, RadioBeacon.A sensor port received an effector type, or vice versa — sensors= and effectors= are swapped or mixed up.
M1 is configured as servo, not a Thruster.Wrong accessor for the port’s type — this port holds a servo, so use robot.servo("M1").
MessageCause and fix
KeyError: 'M3'The port was not declared in configure(). Add it there, or use a port you did declare.
MessageCause and fix
No IMU is configured.robot.imu() was called with no IMU declared. Add one to sensors=.
2 IMUs are configured (S1, S2); specify the port, e.g. robot.imu('S1').More than one sensor of that type is configured — name the port.
MessageCause and fix
Robot.configure() is only legal before the robot is armed.configure() runs once, at the top of the program. Remove the second call.
Robot.arm() requires a successful Robot.configure() first.run() was called before configure(). Swap them.
Robot configuration mismatch. Expected ..., got ...The declared hardware differs from what the Robot Brain reports. Ensure that your code is consistent with the wiring.

Raised when the connection between the program, the extension, and the robot fails. These are not code problems — check the control station: Controller connected, Robot Brain paired, battery in. (“C3” in these messages means the Controller.) LinkError is a subclass of RuntimeError.

MessageCause and fix
Extension not reachable at tcp://...The program was started outside MARLIN — VS Code’s own Run button does this. Start it from Match Control instead.
Timed out waiting for the robot to confirm its configuration.Nothing answered: Controller not connected, or the Robot Brain unpaired or off.
Timed out waiting for arm acknowledgement ...As above, detected at the start of run().
The robot rejected its configuration.The Robot Brain refused the declared configuration.
The robot rejected arm.The Robot Brain refused to enable motors — it is e-stopped or overheated. Check the control station dials and E-Stop state.
Lost connection to the MARLIN extension ...The connection dropped mid-run — usually the Controller USB cable.

Printed to the MARLIN Console; the program keeps running. Each warning states what MARLIN did about the mistake.

MessageCause and fix
driver() has no loop. ...The function ran once and returned immediately. Loop on while robot.running:.
driver() loops but never checks robot.running, ...The loop cannot end on its own; MARLIN stops it at the phase change. Add robot.running to the loop condition.
the auton phase started, but run() was not given autonomous=; ...No function was passed for that phase. The robot idles — intended for a driver-only program.
driver() returned before the driver phase ended; ...The function finished early — often a return inside the loop. It is not called again that phase.
driver() ran past the end of the driver phase; MARLIN stopped it. — or ... is still running ...; its outputs are ignored.The phase ended while the code was still going. The overrunning code cannot move the robot.
M1: set_duty(nan) is not a usable number, ...A calculation produced nan — usually a division by zero. The effector keeps its previous setpoint.