Over the past few weeks, I've been developing a base station for Optimus'. (That's the IGVC robot's name.) In order to operate autonomously, Optimus' is outfit with a slew of sensors. In order to keep tabs on Optimus' and his operation, the base station establishes a radio link with the robot. The robot constantly sends telemetry data out to the base station and the base station periodically sends commands to the robot.
The robot transmits values for set of its sensors, depending on the mode it's in. The base station assesses how reliable the radio link is and periodically changes the robot's mode to modulate bandwidth usage. When everything's operational, the base station shows a huge set of information:
- Autoscaling map with the robot's track and waypoints
- Turn rate
- Readings for all 3 range finders
- Current coordinates
- Target coordinates
- Radio rx rate
- Framerate (frequency of individual gauge updates)
- Robot uptime
Okay. Maybe not huge. But it still feels like a lot when you're coding it by hand.
Besides displaying telemetry from the robot, the base station is capable of sending an E-Stop command, switching between teleoperated and autonomous modes, and powering down the onboard laptop. This radio E-Stop is complemented by an hardware radio E-Stop and a physical button. In teleoperated mode, the base station sends joystick commnands out to the robot to drive; in autonomous, the joystick is disabled.
In terms of code, the base station is very modular. It's broken into five sections: frontend, widgets, serial, callbacks, and configuration. I'll speak about each briefly. There will probably be more lists. I'm enjoying my lists.
The frontend pulls all of the code we're about to dive into together. It places all of the widgets onto the canvas, starts the serial listener thread, handles exceptions, and keeps track of events (clicks, joystick, etc). Once everything's running, the frontend refreshes the screen at 30fps, processes queued serial data, and notifies widgets of clicks.
As is visible in the picture, there are five types of widget: dial, textbox, indicator light, image, map, and button. Dials have defined minimum and maximum settings and display readout in three forms: needle position, center color, and numerals. The compass is a special type of dial whose needle swings all the way around. The indicator light turns green for True and red for False, with interpolation through the oranges. An image simply displays an image, given a string bitstream.
The map is the most sophisticated widget. It plots a track, redrawing when the track approaches the map's edge. The recalculation logic is a bit beastly. Every point in the track is examined to find maximum and minimum latitude and longitude. The map's new bounding box is sized such that all of those points fit into the central ½ of the map. The relative ratio of latitude and longitude is recalculated, too. Once bounds are selected, the map draws an accurate scale, just like Google's maps.
Buttons are comparatively simple. When they're clicked (press and release), they fire a callback function defined in their constructor. The buttons are designed to be familiar: they use the same colors and effects (normal, rollover, onclick) as pre-2000 Windows systems.
The widgets are the most portable code in the base station: they could be used to display any data for any project, although they're focused on telemetry.
link.py & rcode.py
Serial communications are the base station's primary concern. (I was going to say first, but primary sounds more robot-ish.) At peak speeds, the robot transmits about 10kilobytes/s of data. pySerial, the serial handling module, provides a 3800byte buffer. If that buffer overflows, data is corrupted and lost.
To prevent loss, the serial listener (link.py) runs in its own thread. It reads data from pySerial into a waiting queue, then transmits any data in the transmission buffer, looping at about 30Hz.
This enhanced buffer is complemented by a pair of data-parsing functions in rcode.py: setState() and setGauges(). The former analyzes the current radio strength and sets the robot's set of transmissions to compensate. (If 95% of messages get through, loss isn't an issue. If only 10% of messages arrive, the robot should send less data with more redundancy.)
The latter parses the buffer for awaiting data. It pulls lines out of the buffer until it either recognizes a data flag or runs out of buffer. (Flags look like "prime,Rxx", where xx is a 2-digit number that represents a type of data.) With a flag is recognized, the associated data is captured. Normally, the lines after a flag are either one or two floating-point numbers, representing the data. If text happens to get mixed in, the function tries to recover the original number. Bounds testing helps pitch corrupt data.
This file contains the callbacks that buttons use. The functions within are mostly glue between the other sections. Buttons, as a class, don't do a single thing. The functions bound to them determine their behavior. One function clears the map's traces. Another switches the robot between teleoperated and autonomous modes.
In some ways, config is the least important file: it doesn't do anything on its own. In others, it's the most important: it stores all global variables and allows easy geometry modification. Either way, config defines a pile of variables ranging from font names to serial timing to geometry constants.
Every other file imports all of the config variables for their use. Modifying a value in config instantly propagates through the entire program.