Skip to content

Hardware configuration

Before MARLIN moves anything, you have to tell it where each piece of hardware is plugged in. This page is about getting that declaration right, and what MARLIN does when it disagrees with the robot in front of it.

Robot.configure() declares which sensors and effectors are connected to the robot. Call it exactly once, before Robot.run().

robot.configure(
    sensors={"S1": IMU, "S6": RadioBeacon},
    effectors={"M1": Thruster, "M2": Servo},
)

Port declarations

A port is a labelled socket on the robot brain. The label printed next to the socket is the string you use as the key:

flowchart TB
    brain["Robot brain"]

    subgraph sen["Sensor ports S1 to S6, declared in sensors"]
        senlist["IMU, at most 2<br/>RadioBeacon, at most 1"]
    end

    subgraph eff["Effector ports M1 to M8, declared in effectors"]
        efflist["Thruster<br/>Servo"]
    end

    brain --> sen
    brain --> eff

Both mappings are dictionaries. The keys are port names and the values are MARLIN classes:

Mapping Valid ports Supported types
sensors S1 through S6 IMU, RadioBeacon
effectors M1 through M8 Thruster, Servo

The Python library validates these port names before sending configuration to the robot. The robot still verifies the complete declaration and can reject unsupported physical ports or hardware.

Pass the classes themselves, not instances:

# Correct
sensors={"S1": IMU}

# Incorrect
sensors={"S1": IMU("S1")}

Sensor limits

Each robot may declare at most:

  • 2 IMU sensors
  • 1 RadioBeacon sensor

The beacon limit exists because every radio beacon receiver on a robot listens on the same LoRa channel: the one set by the Beacon channel dropdown in the extension's Controller card. A second RadioBeacon would report the same data as the first, so it would tell you nothing new.

The game manual describes the Robot Brain as having six Sensor Ports. That is the connector count, not a licence to declare six of one sensor. The per-type limits above are what configure() enforces, and the spare ports are there for the additional sensor types coming in future MARLIN seasons.

Exceeding either limit raises ValueError and names the offending ports:

robot.configure(
    sensors={"S2": IMU, "S3": IMU, "S4": IMU},
    effectors={},
)
# ValueError: At most 2 IMU may be configured; got 3 (S2, S3, S4).

Configuration verification

During configuration, the library:

  1. validates the local declarations;
  2. opens its local connection to the MARLIN extension;
  3. sends the configuration to the robot through the extension;
  4. waits for the parsed configuration to be echoed back;
  5. raises an error if the echo does not match.

In other words, a mismatch raises an error immediately, during configure(), instead of failing silently and showing up later as confusing behaviour on the field. This prevents a program from running against a different hardware layout than the one you declared.

Accessing configured hardware

configure() only declares the hardware. To command or read a piece of hardware, ask the robot for it by port name:

front_sensor = robot.sensor("S6")
left_thruster = robot.thruster("M8")
gripper_servo = robot.servo("M2")

robot.effector(port) returns whatever effector is on that port. The specific ones, robot.thruster() and robot.servo(), raise TypeError if the port holds the other kind. That catches typos early.

Typed sensor accessors

robot.imu(port) and robot.radiobeacon(port) are methods, not properties. Call them, and always name the port:

robot.imu("S1").read()
robot.radiobeacon("S6").read()

The port must be one you declared in configure(). If it holds a different kind of sensor, the call raises TypeError, which is the point: the typo stops your program now rather than during a match.

Naming the port is what lets a second sensor of the same type just work:

robot.configure(
    sensors={"S2": IMU, "S3": IMU},
    effectors={},
)

front = robot.imu("S2").read()
rear = robot.imu("S3").read()