SourceBots runs an annual summer school at the University of Southampton to engage Year 12 students in STEM through robotics. We pretty much take a Student Robotics or SourceBots competition and squeeze it from six months into a week.
Just a week to design, build and program a functional robot is pretty tight timing, so we provide the students with all of the materials to build their robots, rather than just the basic electronics kits like we for for SR.
After the thresholding issues that we encountered at the SourceBots competition in April and realising that we had yet to reliably fix this, we decided to design the game this year to not use fiducial markers and instead challenge competitors to rely on other sensors. You can build a surprisingly good robot with some ultrasonic distance sensors in a known environment.
The game design chosen was based off a game that has been played several times before, Tin Can Rally". This previously involved racing around an arena and picking up cans. We decided that this was a little too boring though and have added an additional super (soup-er) can in the centre, which has the effect of halving the points of your competitor.
One issue that was recognised but not diagnosed at SourceBots 2018, was robots turning off too early during the matches. We initially dismissed this as students’ code crashing unexpectedly as the error seemed to be particularly intermittent. We left it for a while, theorised about what it could be and moved on to other problems without fixing it.
Old Timeout Code
This is the code that we were running to kill the code at the end of the match.
def kill_after_delay(timeout_seconds): """ Interrupts main process after the given delay. """ end_time = time.time() + timeout_seconds def worker(): while time.time() < end_time: remaining = end_time - time.time() time.sleep(max(remaining, 0.01)) LOGGER.info("Timeout %rs expired: Game over!", timeout_seconds) # Interrupt the main thread to kill the user code _thread.interrupt_main() # type: ignore worker_thread = Thread(target=worker, daemon=True) worker_thread.start() return worker_thread
As you can see, this code spawns a second thread which then kills the parent thread on timeout. Whilst this seems okay in principle, the
_thread.interrupt_main function is nasty. It sends the parent thread a
KeyboardInterrupt. This interrupt would show on the team logs, and did cause some confusion and comments during SB2018.
The actual problem is quite interesting when it was suggested to us what it could be. The timeout functionality in our software stack was using Python’s
time.sleep function, which is pretty standard. This would start running early in the program, when not much else was going on. Modern processors have a nice feature that let’s them change their clock speed when usage is low, usually to save power. As the Pi began processing more information (computer vision), the clock speed of the Pi would increase. Strangely, this meant that the Pi would experience CPU time faster, and as this is what we were using to activate the robot timeout, it would cut out early.
The solution to this actually resulted in significantly nicer code for the timeout.
We decided to find a neater way to do this and the obvious place to look is in the Linux Kernel. Signals are pretty useful:
def timeout_handler(signum, stack): """ Handle the `SIGALRM` to kill the current process. """ raise SystemExit("Timeout expired: Game Over!") def kill_after_delay(timeout_seconds): """ Interrupts main process after the given delay. """ signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout_seconds)
Not only is this a smaller, neater solution, but it also removes the nasty
KeyboardInterrupt from the logs. There is a disadvantage to this method though. In theory, teams could use a
try catch statement and avoid the timeout. However, this is clearly cheating and would result in disqualification!
All code snippets on this page are from sourcebots/robot-api.