Robot¶
Robot is the top-level handle for configuration, the match, hardware,
controller input, and robot status.
Constructor¶
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.
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:
S1throughS6. - Effector ports:
M1throughM8. - Supported sensor types:
IMU,RadioBeacon. - Supported effector types:
Thruster,Servo. - Limit: at most 2
IMUsensors and at most 1RadioBeaconper robot.
Raises:
RuntimeErrorifconfigure()is called more than once, or if the robot brain's echo does not match the request.ValueErrorfor a bad port name or too many sensors of one type.TypeErrorfor a class that is not aSensororEffectorsubclass, or for an unsupported sensor or effector type.
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
autonomousduring the Autonomous Period anddriverduring 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:
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: bool¶
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:
- 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.