In July of last year, I joined Yujin Robot for a six month internship with the innovation team. Since 2014, the team has been working on the newest addition to the robots at Yujin, GoCart
, the second version of which was announced late in 2015. My goal for the duration of my internship was to help develop the intermediate layer of GoCart's software system. I worked between the lower level modules like navigation, and the UI from which the robot receives its instructions.
The innovation team uses ROS extensively, and while I had used ROS for several earlier projects, I hadn't yet made any contributions. I was encouraged to do so. I enjoyed the experience, and hope to contribute more in future. My first addition was a small change to the sound_play package which allows the the volume of individual sounds to be set, something which was much requested by one of the team members who was annoyed by the loud (and frequent) sounds coming from the robot while it was being tested.
The most significant contribution I made to ROS was a modification to the diagnostics package. We wanted to ensure that the diagnostic UI could be used by testers with little technical knowledge to see problems and report them to developers. I modified the diagnostic aggregator so that we could make sure only relevant information was displayed to them.
The package allows users to define analysers which listen on a topic for diagnostic messages. These messages are then aggregated into user defined groups, which are displayed in an ordered way in the diagnostic UI. As with many ROS systems, the GoCart software consists of a large number of different modules which are combined to drive the robot. Each module has its own diagnostics, which make up a group in the aggregator.
Previously, the diagnostic aggregator was configured when it was first run, by loading a yaml file in the node launch file. The configuration file contains information about the analysers to be created, and which messages they listen for. Once the node had started, it was not possible to modify the aggregator configuration without shutting it down and changing the configuration. This meant that diagnostic groups which weren't relevant to the currently running modules would still appear in the UI.
The updated diagnostic aggregator allows you to define diagnostics configurations for different modules, and load them into the aggregator when you need them. Diagnostics can be loaded by adding a diagnostic loading node to your launch file. The diagnostics are automatically unloaded when the node shuts down. You can find a tutorial on the new functionality here
I also spent a good deal of time working with behaviour trees, a control structure which is an alternative to state machines. Originally developed in the games industry for controlling AI agents, behaviour trees provide a simple but powerful structure for defining which action is taken under certain conditions. It’s very easy to modify them at runtime, which provides additional flexibility.
Each node in a behaviour tree is a behaviour, which can be in one of four states: success, failure, running, or invalid. The tree operates using time steps. At each timestep, the tree 'ticks' its nodes, starting from the root, descending depth-first into the tree. When a node is ticked, it executes some code, and returns one of three states: success, failure, or running. Running means that a node has not finished whatever task it is supposed to do. For example, a behaviour that asks the controller to turn the robot 0.5 radians would return running until odometry determines that 0.5 radians has been turned, at which point it would return success. The behaviour might also listen to an obstacle detection topic, stopping the turn and returning failure if an obstacle was detected. The return value of a node can change the part of the tree that is ticked.
Beyond the simple behaviour, there are also composites. A composite is a behaviour whose return state depends on the return states of child behaviours. A composite's children can be simple behaviours or further composites. There are two basic types of composites: sequences and selectors. A sequence runs each of its children in order. The sequence returns success if all of its children also return success. If any child fails, the sequence returns failure. A selector runs its children until one returns success, at which point the selector also returns success. If all of the children fail, the selector also fails. While composites are running their children, they are also in the running state. Composites give you more control over which behaviours run, and when.
In late 2015, Daniel Stonier did the groundwork for a behaviour tree package in Python. He and I then spent time improving it. We designed and implemented behaviour trees defining several scenarios for GoCart, and tested them extensively both in simulation and in real-world trials. Using the tf_tree viewer and rqt_bag as a base, I wrote an rqt package which can be used to view the tree as it changes in real time, and to replay recorded bag files.
Here is a screenshot of the rqt viewer in its current state. The colouring of the nodes shows their current state. Green is success, red is failure, and black is running. Grey nodes are those which have not been run yet. Ellipses denote simple behaviours if they are a leaf node, or selectors otherwise. Rectangular nodes are sequences. The timeline at the bottom shows when the tree changed, and can be used to navigate through all received trees. This tree is used by the GoCart when it is performing a parking behaviour. When parking, the GoCart returns to a docking station to charge, or to a predefined parking location where it will wait for the next task to arrive. The viewer is very useful for debugging behaviours, and seeing exactly what state the robot is in at a given time.
We plan to improve the documentation for the package, finalise the structure, improve the UI, and write more tests before we release it to the community sometime later this year. Hopefully we'll have something cool to show at ROSCon!