Skip to content

Effectors

Effector objects are created by Robot.configure(). Calling an effector method records a setpoint; MARLIN keeps sending that setpoint to the robot until you change it, so a value you set once stays applied even when your loop does not repeat the call.

Every effector method returns None. You call them for their effect on the robot, not for a value.

Effector

Advanced

You do not need Effector to program a robot. Skip to Thruster or Servo.

Effector(port: str, state_mirror=None)

Base class for effectors. It is public for extension and advanced use, but you normally configure Thruster or Servo.

Attributes

effector.port: str
effector.type: str
Attribute Type Meaning
port str The port this effector was declared on, such as "M1"
type str The lowercase type name, "thruster" or "servo"

Thruster

set_duty

thruster.set_duty(duty: float) -> None

Input: duty, a number from -100 to 100. Output: none.

duty Result
100 Full forward
30 Forward, roughly a third of full power
0 Stopped
-30 Reverse, roughly a third of full power
-100 Full reverse

Positive is forward. MARLIN handles the wiring direction for you, so you do not need to negate anything to make a thruster drive forwards.

Duty is open-loop. Open-loop means MARLIN sends the power level you ask for. MARLIN does not measure the real speed. MARLIN does not correct the speed. Two thrusters set to the same duty can turn at different speeds.

The library rounds the value to an integer. A value past either end is pinned to that end rather than raising, so mixing two stick readings is safe to write directly.

robot.thruster("M1").set_duty(40)     # forward at 40%
robot.thruster("M1").set_duty(-40)    # reverse at 40%
robot.thruster("M1").set_duty(0)      # stop
robot.thruster("M1").set_duty(140)    # runs at 100, the same as full forward

The one value it will not take is nan, which no clamp can place. The setpoint is left as it was and MARLIN prints a line naming the port and the setter, once per setter, so a 50 Hz loop cannot bury the console.

Worked example: mirrored thrusters

Two thrusters mounted facing each other need opposite duties to push the robot the same way:

def drive_forward(robot, power):
    robot.thruster("M1").set_duty(power)
    robot.thruster("M8").set_duty(-power)


def autonomous(robot):
    while robot.running:
        drive_forward(robot, 30)

Servo

Servo drives a positional hobby servo: a servo with a limited range of travel, 0 to 180 degrees.

Use angle() for normal work. micros() is a manual override that sets the underlying pulse directly.

angle

servo.angle(deg: float) -> None

Input: deg, a number from 0 to 180. Output: none.

Sets servo position in degrees. An angle past either end is pinned to that end.

robot.servo("M2").angle(0)      # one end of travel
robot.servo("M2").angle(90)     # centre
robot.servo("M2").angle(180)    # the other end
robot.servo("M2").angle(200)    # goes to 180, the end of travel

micros

Advanced

Use angle() unless it does not reach the positions you need.

servo.micros(us: int) -> None

Input: us, a whole number of microseconds from 500 to 2500. Output: none.

Sets the raw pulse width. A servo is told what to do by a pulse width: the length, in microseconds, of an electrical pulse repeated many times a second. angle() does this conversion for you, so reach for micros() only when your servo does not reach both ends of its travel at 0 and 180 degrees.

A pulse past either end is pinned to that end.

Worked example: sweeping an arm between two positions

Press A to raise the arm, press it again to lower it. raised remembers which position the arm is in, because the servo cannot tell you.

def driver(robot):
    previous = False
    raised = False

    while robot.running:
        current = robot.controller.button("A").is_down

        if current and not previous:      # a new press, not a held button
            if raised:
                raised = False
                robot.servo("M2").angle(0)
            else:
                raised = True
                robot.servo("M2").angle(120)

        previous = current

Errors shared by effectors

  • Commands on an unattached, directly constructed effector raise RuntimeError.
  • Typed robot accessors raise TypeError when the port has the wrong effector type. For example, robot.thruster("M1") raises TypeError if port M1 is configured as a Servo.