Effector commands and safety¶
This page is about making the robot move. It also covers the things MARLIN does on its own, so that a mistake in your code does not become a mistake in the Pool.
An effector is anything on the robot that moves: a thruster or a servo.
Calling an effector method does not send a message by itself. It records the value you want, and MARLIN repeats that value to the robot many times a second until you change it.
The value you set last keeps being sent until you replace it, or until MARLIN
idles the effectors. MARLIN does that automatically when a phase callback
returns, when a phase ends, and when run() exits.
Idling a servo moves it to 90°
Idling means something different for each kind of effector. A thruster idles by stopping. A servo has no "stopped", because it is always holding some angle. Idling sends it to the centre of its travel, 90 degrees.
So a gripper you closed at 0°, or an arm you raised to 180°, returns to 90° the moment the phase ends. It is not a fault, and your program cannot prevent it. Design the mechanism so that 90° is a safe resting position, or expect the movement and plan around it.
Thruster modes¶
Duty is how hard the thruster runs, as a percentage of full power. 100 is
full forward, -100 is full reverse, and 0 is stopped. Positive is forward:
MARLIN handles the wiring direction, so you never negate a value just to make a
thruster drive forwards.
set_duty() is the only thruster command. There is no speed or distance
control: MARLIN cannot tell you how fast the robot is actually moving, so pick
a duty, watch what the robot does, and adjust the number.
Servo modes¶
servo.angle(90) # position, 0 to 180 degrees
servo.micros(1500) # raw pulse width, 500 to 2500 microseconds
flowchart LR
angle["angle(deg)<br/>0 to 180 degrees"]
micros["micros(us)<br/>sets the pulse directly"]
pulse["Pulse width sent to the servo<br/>500 to 2500 microseconds<br/>1500 = centre"]
angle --> pulse
micros --> pulse
A servo is told what to do by a pulse width: the length, in microseconds, of
an electrical pulse repeated many times a second. Roughly 1500 µs means
"centre", and shorter or longer moves it one way or the other. angle() does
that conversion for you, so you only need micros() when you want to set the
pulse yourself. The usual reason is that your servo does not reach both ends of
its travel at 0 and 180 degrees.
Range clamping¶
Every effector command has a range. A value past either end is clamped, not rejected. MARLIN pins it to the nearest end and sends it:
| Call | Range | 3000 becomes | -200 becomes |
|---|---|---|---|
thruster.set_duty(duty) |
-100 to 100 | 100 |
-100 |
servo.angle(deg) |
0 to 180 | 180 |
0 |
servo.micros(us) |
500 to 2500 | 2500 |
500 |
A stick already reads -100 to 100, so passing one straight to set_duty() never
needs clamping. It matters as soon as you calculate a duty, and then you still
need no bounds checking of your own:
left = robot.controller.joystick("ONE").value
right = robot.controller.joystick("THREE").value
boost = 1.5 if robot.controller.button("RIGHT_TRIGGER").is_down else 1.0
robot.thruster("M8").set_duty(left * boost) # 150 pushes at 100
robot.thruster("M1").set_duty(right * boost)
Be aware of what clamping does to two thrusters scaled together, though. Once
one side saturates, the ratio between them stops matching what you asked for:
at left = 90, right = 60 with the boost held, the left side clamps to 100
while the right reaches 90, so the robot turns less sharply than the sticks
suggest. If you need that ratio to hold at full throttle, scale both values down
together instead of letting them clamp separately.
The one value MARLIN will not take is nan, which no clamp can place. nan
compares False against everything, so clamping it would quietly hand back a
limit, and a nan duty would read as full throttle. The setpoint is left as it
was instead, and MARLIN prints a line naming the port and the setter, once per
setter:
[API] M1: set_duty(nan) is not a usable number, so the setpoint is left as it was.
Check the maths feeding it.
The robot brain still applies its own hardware limits on top; the clamp bounds what you can ask for, not what the hardware will tolerate.
Safety ownership¶
- The Python library clamps out-of-range effector commands at the call site (see above).
- The robot brain enforces final per-effector hardware protection and may clamp internally.
robot.overtempreports whether the robot brain's board thermal protection is currently tripped. The robot brain stops itself at 50 °C and will not accept commands again until the board is back down to 45 °C; the MARLIN extension brings the robot back when that happens. It is read-only, likerobot.estopped:
- The MARLIN extension and the pool dashboard own E-Stop.
- The Python runtime idles outputs when callbacks finish or the match ends.
- Your code should react safely to stale sensors, lost links, and unexpected status values.
The Python API does not make match-safety decisions on your behalf and does not provide an E-Stop command.