Skip to content

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.

robot.thruster("M1").set_duty(30)
robot.servo("M2").angle(90)

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

thruster.set_duty(duty)   # -100 to 100

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.overtemp reports 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, like robot.estopped:
if robot.overtemp:
    print("Robot brain thermal protection is active")
  • 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.