8. My two robots at once
From one robot to a swarm
The problem. drive() returns only once its robot has stopped, so a script that drives two robots moves them one after the other instead of together.
There is no docs section for this step: it is the workshop’s own. The trick is in goto(), from step 7: it returns at once, so you can give every robot its target first, and only then wait for all of them.
Two robots, one after the other
Save two.py (below) next to academy.py. Change the address in academy.connect(...) as in step 5, and the two labels to your robots. Check that the floor in front of both robots is free, then run it as it is:
python two.pytwo.py
import academy
academy.connect("http://192.168.1.10:8000") # as in step 5
robot_a, robot_b = academy.swarm(["A1B2C3", "D4E5F6"]) # your two labels
robot_a.led(255, 0, 0) # A is red
robot_b.led(0, 0, 255) # B is blue
robot_a.drive(speed=100, seconds=2) # A drives about 200 mm forward...
robot_b.drive(speed=100, seconds=2) # ...then B, once A has stoppedYou should see: robot A turns red and robot B blue; A drives about 200 mm forward, and only once it has stopped does B do the same.
Core at the same time
Your turn. Make both robots leave at the same moment. Replace the two drive() lines: read each robot’s position(), give each one a goto() 200 mm up the map, and only then wait, in one loop, until both have arrived().
You should see: your red robot and your blue robot leave together, and stop about 200 mm up the map.
while not (robot_a.arrived() and robot_b.arrived()): keeps waiting while at least one of them is still on its way. Put a time.sleep(0.5) inside, and import time at the top.
two.py
import time
import academy
academy.connect("http://192.168.1.10:8000") # as in step 5
robot_a, robot_b = academy.swarm(["A1B2C3", "D4E5F6"]) # your two labels
robot_a.led(255, 0, 0) # A is red
robot_b.led(0, 0, 255) # B is blue
xa, ya = robot_a.position()
xb, yb = robot_b.position()
robot_a.goto(xa, ya - 200) # goto() returns at once...
robot_b.goto(xb, yb - 200) # ...so B leaves while A is still on its way
while not (robot_a.arrived() and robot_b.arrived()):
time.sleep(0.5)Optional: drive() and spin() at the same time
Want both robots to drive and spin together, without goto()? academy_parallel.py adds wait=False to drive(), spin() and wheels(): the call returns at once, the robot keeps moving in the background, and wait() waits until the moves are over. A new move replaces the running one, stop() cancels it, and when your script ends, or you stop it with Ctrl+C, both robots stop.
Save it next to academy.py, then try parallel.py (below) with your controller address and labels.
You should see: both robots drive forward together, spin a full turn together, one each way, then back up to where they started.
parallel.py
"""Step 8, optional tool: make two robots drive at the same time.
Needs academy_parallel.py next to academy.py. With wait=False a move returns
at once, so both robots move together; wait() waits for both.
You should see: both robots drive forward together, spin a full turn together,
then back up to where they started, together.
"""
import academy
import academy_parallel
FULL_TURN = 4.8 # seconds of spin(speed=50) for about 360 degrees
academy.connect("http://192.168.1.10:8000") # as in step 5
robot_a, robot_b = academy_parallel.swarm(["A1B2C3", "D4E5F6"]) # your two labels
robot_a.led(255, 0, 0) # A is red
robot_b.led(0, 0, 255) # B is blue
# Both forward, at the same time: each call returns at once.
robot_a.drive(speed=100, seconds=2, wait=False)
robot_b.drive(speed=100, seconds=2, wait=False)
academy_parallel.wait(robot_a, robot_b)
# Both spin a full turn, one each way.
robot_a.spin(speed=50, seconds=FULL_TURN, wait=False)
robot_b.spin(speed=-50, seconds=FULL_TURN, wait=False)
academy_parallel.wait(robot_a, robot_b)
# Both back to where they started.
robot_a.drive(speed=-100, seconds=2, wait=False)
robot_b.drive(speed=-100, seconds=2, wait=False)
academy_parallel.wait(robot_a, robot_b)
robot_a.led(0, 255, 0) # green: done
robot_b.led(0, 255, 0)academy_parallel.py
"""Drive several robots at the same time, on top of academy.py.
academy.py's moves block: robot_a.drive() returns once robot_a has stopped, so
robot_b can only start after it. Here, wait=False makes drive(), spin() and
wheels() return at once while a background thread keeps the robot moving, and
wait() blocks until the move is over. Without wait=False they block, as in
academy.py.
import academy
import academy_parallel
academy.connect("http://localhost:8000")
robot_a, robot_b = academy_parallel.swarm(["000000", "000001"])
robot_a.drive(speed=100, seconds=2, wait=False)
robot_b.spin(speed=50, seconds=2, wait=False)
academy_parallel.wait(robot_a, robot_b)
"""
import atexit
import threading
import time
import requests
import academy
PERIOD = 0.1 # s between resends; a robot stops by itself ~0.5 s after the last one
_bots = []
class Bot(academy.Bot):
"""An academy robot whose drive(), spin() and wheels() can run in the background.
One daemon thread per robot resends the running move every PERIOD; a new
move replaces the running one, and stop() cancels it.
"""
def __init__(self, address):
super().__init__(address)
self._lock = threading.RLock()
self._move = None # (wheel_velocity body, end time) of the running move
self._done = threading.Event()
self._done.set()
self._thread = None
_bots.append(self)
def drive(self, speed=100, seconds=1, wait=True):
"""Drive straight at `speed` mm/s (negative backs up) for `seconds`, then stop."""
self.wheels(speed, speed, seconds, wait)
def spin(self, speed=100, seconds=1, wait=True):
"""Turn on the spot, clockwise on the map (negative: anticlockwise), then stop."""
self.wheels(speed, -speed, seconds, wait)
def wheels(self, left, right, seconds, wait=True):
"""Each wheel's speed in mm/s for `seconds`, then stop. Replaces a running move."""
body = {"left_mm_s": left, "right_mm_s": right}
with self._lock:
self._move = (body, time.time() + seconds)
self._done.clear()
self._put("wheel_velocity", body)
if self._thread is None:
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
if wait:
self.wait()
def wait(self):
"""Block until the running move is over."""
while not self._done.wait(PERIOD):
pass
def stop(self):
"""Cancel the running move and set both wheels to 0."""
with self._lock:
self._move = None
try:
super().stop()
finally:
self._done.set()
def goto(self, x, y, threshold=60):
"""As academy's goto(), after cancelling a running move."""
if self._move is not None:
self.stop()
super().goto(x, y, threshold)
def _run(self):
while True:
with self._lock:
if self._move is not None:
body, end = self._move
try:
if time.time() < end:
self._put("wheel_velocity", body)
else:
self.stop()
except requests.RequestException:
pass # try again next period; the robot stops by itself meanwhile
if time.time() >= end:
self._move = None
self._done.set()
time.sleep(PERIOD)
def bot(label):
"""Your robot, from its label, with non-blocking moves."""
return Bot(academy.bot(label).address)
def swarm(labels):
"""Your robots, from their labels, with non-blocking moves."""
return [bot(label) for label in labels]
def wait(*robots):
"""Block until every robot's running move is over."""
for robot in robots:
robot.wait()
@atexit.register
def _stop_all():
# Runs on a normal exit and after Ctrl+C, so no robot is left driving
for robot in _bots:
if robot._thread is not None:
try:
robot.stop()
except requests.RequestException:
pass