Two ways to handle control loops in Python
In robot control systems, it’s common to have an “inner loop” that controls basic motion and an “outer loop” that does higher level control, such as navigation. (You can have even higher level control, such as mission planning, above that but we’ll concentrate on the first two for now). In the case of drones, for example, the inner loop runs very fast (400 times a second, or 400 Hz, is typical) for precise control of basic stabilization. The outer navigation loop, on the other hand, can run more slowly — typically 10 times a second (10 Hz), which is the speed at which standard GPS modules update their position.
Ideally, you have both these loops running at the same time, using multithreading on Linux or a real-time operating system. But that can be somewhat intimidating to program and there are some gotchas that you have to watch out for when you’re running multiple threads, such as race conditions.
Until recently, Python didn’t have such “concurrancy” built-in, and you had to use special libraries to do this. But starting with 3.5, this sort of asynchronous code execution was vastly improved with the native “asyncIO” module, which is well explained here. This should allow you to run multiple loops running at different speed simultaneously by using “coroutines“, without much programming overhead and risky clashes. To try this out, I did the following experiment.
Let’s say you want to run and inner loop at 10Hz and an outer loop at 1Hz. In regular Python you’d code it like this:
import time def tenhz(): time1 = time.time() print ("Ten Hz") while True: if time.time() > (time1 + 0.1): # check to see if a tenth of a second has passed break def onehz(): time1 = time.time() print ("One Hz") while True: tenhz() if time.time() > (time1 + 1): # check to see if a second has passed break while True: print("root") onehz()
That works — the “tenhz()” function will run ten times a second, and the “onehz()” function will run once a second — but the problem is that the two loops won’t run simultaneously. While the tenhz() function is running, the onehz() function is not, and vice versa. One blocks the other.
With Python’s new AsyncIO concurrency feature, you can effectively have the two running at the same time — no blocking — in separate threads without a lot of fuss. Here’s how those same loops look programmed for asynchronous operation (thanks to this guide as a starter).
import time import asyncio start = time.time() def tic(): return 'at %1.1f seconds' % (time.time() - start) async def gr1(): # Busy waits for a tenth of a second, but we don't want to stick around... print('10Hz loop started work: {}'.format(tic())) time1 = time.time() while True: # do some work here await asyncio.sleep(0) if time.time() > time1 + 0.1: print('10Hz loop ended work: {}'.format(tic())) time1 = time.time() async def gr2(): # Busy waits for a second, but we don't want to stick around... print('1 Hz loop started work: {}'.format(tic())) time2 = time.time() while True: # do some work here await asyncio.sleep(0) if time.time() > time2 + 1: print('1 Hz loop ended work: {}'.format(tic())) time2 = time.time() async def gr3(): print("Let's do some stuff while the coroutines are blocked, {}".format(tic())) time3 = time.time() while True: # do some work here if time.time() > time3 + 20: print("Done!") await asyncio.sleep(0) ioloop = asyncio.get_event_loop() tasks = [ ioloop.create_task(gr1()), ioloop.create_task(gr2()), ioloop.create_task(gr3()) ] ioloop.run_until_complete(asyncio.wait(tasks)) ioloop.close()
Much better! Now you can insert your own code in those loop and not worry about one blocking the other. Yay Python 3.5!