Skip to content

Robot

Robot is the top-level handle for configuration, the match, hardware, controller input, and robot status.

Constructor

Robot(endpoint: str | None = None)

Write robot = Robot(). The endpoint argument is for advanced use; the extension sets it for you.

Advanced

The rest of this section explains where that default comes from. You can skip it.

If you omit endpoint, MARLIN uses the MARLIN_LINK_ENDPOINT environment variable. If that variable is not set, MARLIN falls back to tcp://127.0.0.1:5172. The extension normally sets the environment variable automatically when it launches a script.

Robot() does not open the connection. configure() opens the connection.

configure

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

Inputs: sensors and effectors, both dictionaries mapping a port name to a hardware class: IMU, not IMU("S1"). Both are keyword-only, so you must write sensors= and effectors=. Output: none; the hardware objects are stored on the Robot and reached through the accessors below.

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

configure():

  • Declares the robot's hardware.
  • Opens the extension connection.
  • Sends the configuration to the robot brain.
  • Checks the echoed result against the request.

Port and type constraints:

  • Sensor ports: S1 through S6.
  • Effector ports: M1 through M8.
  • Supported sensor types: IMU, RadioBeacon.
  • Supported effector types: Thruster, Servo.
  • Limit: at most 2 IMU sensors and at most 1 RadioBeacon per robot.

Raises:

  • RuntimeError if configure() is called more than once, or if the robot brain's echo does not match the request.
  • ValueError for a bad port name or too many sensors of one type.
  • TypeError for a class that is not a Sensor or Effector subclass, or for an unsupported sensor or effector type.

See Hardware configuration.

run

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

Inputs: up to three functions, each taking one argument. MARLIN passes the Robot into that argument when it calls them, so define them as def driver(robot):. All three are keyword-only and optional. Output: none; run() returns when the session ends.

def driver(robot):          # MARLIN supplies the robot argument
    while robot.running:
        ...


robot.run(driver=driver)    # pass the function itself, with no ()

Passing driver() instead of driver calls your function immediately and hands run() the result, which is not what you want.

run():

  • Calls initialize(robot) once, before the first phase starts, then idles every effector.
  • Runs autonomous during the Autonomous Period and driver during the Driver Controlled Period.
  • Idles every effector at the end of the match.

Idling a servo moves it to 90°

Idle does not mean the same thing for both kinds of effector. A thruster idles by stopping. A servo has no "stopped", because it always holds some angle. Idling sends it to the centre of its travel, 90 degrees.

A servo you left at 0° or 180° will therefore swing back to 90° when the phase ends, when your callback returns, and when the match finishes. If that would drop something the robot is carrying, or swing an arm into something, plan for it.

initialize is for setup and checks only: reading sensors, printing, and working out values the other callbacks need. Effector commands there have no effect, because MARLIN idles the effectors as soon as initialize returns. Put anything that positions the robot at the top of autonomous or driver.

run() blocks until the match ends. Each callback is optional. Every callback receives the same Robot instance.

There is no callback for the pause phase between the two periods, or for the phase after the match ends. MARLIN idles every effector during those.

run() returns when the match ends or when an E-Stop is triggered.

If a callback returns before its phase ends, MARLIN idles every effector and does not call that callback again during the same phase.

State properties

running: bool

robot.running is scoped to the phase your callback was called for.

  • It is True only while that callback is still active and the phase has not changed.
  • It becomes False when the phase ends.

A while robot.running: loop exits by itself when the phase ends.

robot.running also paces your loop. Reading it waits until the next 20 ms tick, so a while robot.running: loop runs at fifty times a second. That is the rate MARLIN sends commands to the robot, and you do not have to do anything:

while robot.running:
    ...

Do not add your own time.sleep()

A time.sleep(0.02) at the end of the loop sleeps on top of the wait robot.running already does, so the loop runs at 25 Hz instead of 50 Hz.

time.sleep() is also the wrong tool for timing a movement, for a separate reason: while your code is sleeping it cannot read a sensor, react to the controller, or change what the robot is doing. Measure elapsed time with time.monotonic() instead. See Timed autonomous movement.

Neither mistake is a safety problem. MARLIN stops every effector when the phase ends, whatever your code happens to be doing at the time.

link_ok reports whether the Bluetooth (BLE) link between the Controller and the robot is healthy, per the robot brain's status telemetry. It does not describe the Python-to-extension connection.

estopped: bool

estopped reports whether the robot is e-stopped. This property is read-only. The MARLIN extension and the pool dashboard hold E-Stop authority. This library cannot trigger or clear E-Stop.

overtemp: bool

overtemp reports whether the robot brain's board thermal protection is tripped. This property is read-only, like estopped.

  • The robot brain stops itself at 50 °C.
  • It refuses to accept commands again until the board cools to 45 °C.
  • The MARLIN extension brings the robot back once the board has cooled.

battery_voltage: float

battery_voltage returns the latest battery voltage in volts. Before a usable status value arrives, it returns 0.0.

controller: Controller

controller returns the controller input object. It is available immediately after construction.

Hardware accessors

robot.sensor(port: str) -> Sensor
robot.effector(port: str) -> Effector
robot.thruster(port: str) -> Thruster
robot.servo(port: str) -> Servo
robot.imu(port: str | None = None) -> IMU
robot.radiobeacon(port: str | None = None) -> RadioBeacon

Input: a port name, as a string. Output: the object configured on that port. These are methods, not properties, so the parentheses are required:

left = robot.thruster("M1")     # fetch once
left.set_duty(30)               # then command it

robot.thruster("M1").set_duty(30)   # or do both in one line

sensor() and effector() raise KeyError for a port that was not declared in configure().

thruster() and servo() raise TypeError when the declared effector at that port has a different type.

imu() and radiobeacon() take a port argument. Always pass it:

reading = robot.imu("S1").read()
beacon = robot.radiobeacon("S6").read()
  • If no sensor of that type is configured on that port, the call raises KeyError.
  • If the sensor at that port has a different type, the call raises TypeError.

The port default exists so a robot with a single sensor of that type can omit it. Code that names its port keeps working when a second one is added, so name it. Omitting the port raises LookupError when zero or more than one sensor of that type is configured.