Complete robot program¶
This page builds a competition-ready robot program one piece at a time: two thrusters, a servo, an IMU, and both match phases.
Everything below goes in one file: src/main.py in your MARLIN project. Each
step adds to the file from the step before it. Every step runs on its own,
so you can stop at any point, press Start Driver, and see what changed.
What you need¶
- Python 3.10 or newer.
- The MARLIN extension running in the editor. It bundles the MARLIN Python
package, so there is nothing to install with
pip. - A MARLIN project open in VS Code. A project is a folder containing
src/main.pyand aproject.marlinmarker file. The extension runssrc/main.py, so there is no file to select. - A Controller connected through the extension when using real hardware.
The extension launches src/main.py with the MARLIN library and local link
already configured. There is no need to copy the API into your project or open
the Controller's serial port from Python.
If you have not written a MARLIN program at all yet, read Your first MARLIN program first. Step 1 below picks up exactly where it leaves off.
The ports¶
Every step names its ports as constants at the top of the file:
Change these four values to match your robot and the rest of the code works
unaltered. Sensor ports are S1–S6; effector ports are M1–M8.
1. One thruster¶
Start with the whole file. This is the program from
Your first MARLIN program, without the empty
autonomous stub. Autonomous arrives at step 5.
from marlin import Robot, Thruster
LEFT = "M8"
robot = Robot()
robot.configure(sensors={}, effectors={LEFT: Thruster})
def driver(robot):
while robot.running:
robot.thruster(LEFT).set_duty(robot.controller.joystick("ONE").value)
robot.run(driver=driver)
Joystick ONE is the left stick's up-and-down axis. .value gives -100 to 100.
That is exactly the range set_duty() wants, so nothing needs converting.
You should see: pushing the left stick forward spins the thruster, pulling it back reverses it, and centring the stick stops it.
2. A second thruster¶
One thruster cannot turn. Add a second one, and give each stick a side of the boat to drive.
Change configure() to declare both ports:
LEFT = "M8"
RIGHT = "M1"
robot = Robot()
robot.configure(sensors={}, effectors={LEFT: Thruster, RIGHT: Thruster})
Then replace driver with this:
def driver(robot):
while robot.running:
left = robot.controller.joystick("ONE").value # left stick, up/down
right = robot.controller.joystick("THREE").value # right stick, up/down
robot.thruster(LEFT).set_duty(left)
robot.thruster(RIGHT).set_duty(right)
This is tank drive: each stick owns one side of the boat, and nothing is
mixed. A stick reads -100 to 100 and set_duty() takes -100 to 100, so each
value goes straight through without any arithmetic of your own.
You should see: both sticks forward drives straight, both back reverses, and one forward while the other is pulled back spins the robot in place.
3. Stop the drift¶
A stick that does not sit perfectly centred reports a small value when nobody is touching it, and the robot creeps around the Pool between commands. Treat small readings as zero:
def driver(robot):
while robot.running:
left = robot.controller.joystick("ONE").value
right = robot.controller.joystick("THREE").value
# abs() ignores the sign, so -3 and 3 both count as small.
if abs(left) < 5:
left = 0
if abs(right) < 5:
right = 0
robot.thruster(LEFT).set_duty(left)
robot.thruster(RIGHT).set_duty(right)
You should see: the robot holds still with both sticks released, and still
responds normally as soon as you push past a light touch. If it creeps, raise
the 5; if the sticks feel dead near the centre, lower it.
4. An arm on a button¶
Add a servo. Declare its port:
Add Servo to the import at the top of the file:
Then add press detection to driver:
def driver(robot):
previous_a = False
arm_is_up = False
while robot.running:
left = robot.controller.joystick("ONE").value
right = robot.controller.joystick("THREE").value
if abs(left) < 5:
left = 0
if abs(right) < 5:
right = 0
robot.thruster(LEFT).set_duty(left)
robot.thruster(RIGHT).set_duty(right)
current_a = robot.controller.button("A").is_down
# True only on the loop where the button went from up to down.
if current_a and not previous_a:
arm_is_up = not arm_is_up
robot.servo(ARM).angle(90 if arm_is_up else 0)
previous_a = current_a
is_down is a level, not an event. Without the previous_a comparison a single
half-second press would toggle the arm dozens of times. arm_is_up remembers
the position, because the servo cannot tell you where it is.
You should see: each press of A swings the arm once and it stays there. Holding A down does nothing more.
5. An autonomous routine¶
So far the robot only works during the Driver Controlled Period. Add an
autonomous callback for the 20 seconds before it.
Timing needs Python's time module, so add it at the very top of the file:
Then add this function, and pass it to run():
def autonomous(robot):
robot.servo(ARM).angle(0) # stow the arm before moving
turn_start = time.monotonic()
while robot.running:
if time.monotonic() - turn_start < 2.0:
robot.thruster(LEFT).set_duty(30)
robot.thruster(RIGHT).set_duty(-30)
else:
robot.thruster(LEFT).set_duty(0)
robot.thruster(RIGHT).set_duty(0)
robot.run(autonomous=autonomous, driver=driver)
time.monotonic() returns seconds as a decimal. Take one reading before the
loop and subtract it inside the loop to get elapsed time. Do not use
time.sleep() to time the movement. While your code sleeps it cannot read
anything or change what the robot is doing.
The Autonomous Period is 20 seconds, so a routine whose deadlines add up past 20 never finishes. In a Qualification Match your partner's robot is also in the Pool, running its own routine at the same time. See Alliance.
You should see: on Start Auton, the arm stows, the robot spins in place for two seconds, then holds still until the period ends.
stateDiagram-v2
[*] --> Configured: robot.configure()
Configured --> Auton: Autonomous Period starts
Auton --> Pause: Autonomous Period ends
Pause --> Driver: Driver Controlled Period starts
Driver --> MatchOver: Driver Controlled Period ends
Auton --> Estop: E-Stop
Driver --> Estop: E-Stop
Pause --> Estop: E-Stop
Estop --> [*]
MatchOver --> [*]
6. Check the sensor before trusting it¶
Add an IMU, and make autonomous refuse to drive on stale data.
IMU_PORT = "S1"
robot.configure(
sensors={IMU_PORT: IMU},
effectors={LEFT: Thruster, RIGHT: Thruster, ARM: Servo},
)
Add IMU to the import, then guard the loop:
def autonomous(robot):
robot.servo(ARM).angle(0)
turn_start = time.monotonic()
while robot.running:
reading = robot.imu(IMU_PORT).read()
if not reading.ok:
# No fresh reading: hold still rather than drive blind.
robot.thruster(LEFT).set_duty(0)
robot.thruster(RIGHT).set_duty(0)
continue
if time.monotonic() - turn_start < 2.0:
robot.thruster(LEFT).set_duty(30)
robot.thruster(RIGHT).set_duty(-30)
else:
robot.thruster(LEFT).set_duty(0)
robot.thruster(RIGHT).set_duty(0)
Always check a reading's ok field before using it to decide how to move. The
turn here is still timed, not driven to an angle, because MARLIN's IMU has no
heading or compass field. See
Gyro turn estimate if you want to estimate an
angle from gyro_z.
You should see: no change while the IMU is healthy. Unplug it and the robot holds still through autonomous instead of turning.
7. Check the robot before the match¶
initialize runs once before the match starts. Use it for setup and checks:
def initialize(robot):
print("Battery:", robot.battery_voltage, "V")
robot.run(
initialize=initialize,
autonomous=autonomous,
driver=driver,
)
initialize is for setup only. Commanding a thruster or servo there does
nothing, because MARLIN idles every output as soon as initialize returns. That
is why the arm is stowed at the top of autonomous instead. See
Match lifecycle.
You should see: a line like battery 12.4 V in the MARLIN Console before
the match starts.
Complete program¶
All seven steps together:
import time
from marlin import IMU, Robot, Servo, Thruster
LEFT = "M8"
RIGHT = "M1"
ARM = "M2"
IMU_PORT = "S1"
robot = Robot()
robot.configure(
sensors={IMU_PORT: IMU},
effectors={LEFT: Thruster, RIGHT: Thruster, ARM: Servo},
)
def initialize(robot):
print("Battery:", robot.battery_voltage, "V")
def autonomous(robot):
robot.servo(ARM).angle(0) # stow the arm before moving
turn_start = time.monotonic()
while robot.running:
reading = robot.imu(IMU_PORT).read()
if not reading.ok:
robot.thruster(LEFT).set_duty(0)
robot.thruster(RIGHT).set_duty(0)
continue
if time.monotonic() - turn_start < 2.0:
robot.thruster(LEFT).set_duty(30)
robot.thruster(RIGHT).set_duty(-30)
else:
robot.thruster(LEFT).set_duty(0)
robot.thruster(RIGHT).set_duty(0)
def driver(robot):
previous_a = False
arm_is_up = False
while robot.running:
left = robot.controller.joystick("ONE").value # left stick, up/down
right = robot.controller.joystick("THREE").value # right stick, up/down
if abs(left) < 5:
left = 0
if abs(right) < 5:
right = 0
robot.thruster(LEFT).set_duty(left)
robot.thruster(RIGHT).set_duty(right)
current_a = robot.controller.button("A").is_down
if current_a and not previous_a:
arm_is_up = not arm_is_up
robot.servo(ARM).angle(90 if arm_is_up else 0)
previous_a = current_a
robot.run(
initialize=initialize,
autonomous=autonomous,
driver=driver,
)
What to read next¶
Examples has smaller patterns you can drop into this file: trigger and D-pad control, a multi-step auton, and beacon seeking. It also shows how to combine two of them.
For the rules behind what you just built, read Hardware configuration and Match lifecycle.