Our Stack

This is a brief overview of our codebase. Since the codebase is constantly changing, expect that some or many parts of these docs will be out-of-date. The most up-to-date reference on our codebase is our code itself.


To see when any documentation page was last updated, click “Edit on GitHub” in the top right, then see the timestamp on the last commit.


Gameplay is where our strategy is produced. It takes in the current state and assigns all robots a task based on some coordination. Our current system has a play selector that chooses a play based on world state. After that, a play assigns tactics (multi-robot behavior), each tactic assigns a role (complex single-robot behavior) based on their progress, and the role select an atomic skill (kick, move, etc.) to perform. The gameplay loop is set to tick at 60Hz. In general, these layers each use a state machine to transition between behaviors.

Below is the tick process. Each time the timer ends, the callback method is entered:

timer_period = 1 / 60  # seconds
self.timer = self.create_timer(timer_period, self.gameplay_tick)

Motion Control

See the control directory, motion_control.cpp uses a PID controller to correct robot position error that may come from deviations in hardware. The generic PID controller implementation is not in software, but it is in the fshare repository.

Correction for error in robot velocity is done by robot firmware (the code that runs on each robot’s microcontroller) in the separate firmware repository using a Kalman Filter. It uses IMU data and motor feedback, in conjunction with previous state, with an understanding of the various noises that could contribute to error to calculate a duty cycle to send to each motor. At a high level, a duty cycle is the ratio between high and low signals. We need this ratio since our motors are controlled entirely electrically. Again, more detail can be found in our firmware repository.

Motion Planning

Motion planning is about generating a path from a robot’s current position and orientation (pose) to a goal supplied by gameplay. Starting at the lowest level, we use RRT, or Rapidly exploring Random Trees, for path generation. When the straight line path to a goal fails, we utilize the position path generated by RRT (if there are no obstacles in that straight line why bother with RRT). We have our own RRT implementation in a separate repository which we use as a submodule. This way others can easily use our RRT implementation if they want.

But we can’t send a path to a robot and expect it to figure it out. We need to give our firmware a single velocity command for that instant of planning. The file soccer/src/soccer/planning/primitives/velocity_profiling.cpp finds that velocity trajectory which we can then return as the current trajectory as the robot taking constraints such as max acceleration into account.

At the highest level of planning, we have several different planners which the planning_node selects based on the skill that gameplay requested. They have different behaviors and based on the their names which give you insight on their purpose. They are all located in soccer/src/soccer/planning/planner.

Although this is not an extensive explanation of everything planning does, it is a good start, and looking through the planning folder will give you more information about this topic.

main.cpp & Processor

main.cpp is located in soccer/src/soccer/. It is the entry point of soccer and starts the processor and the ui. It also takes in configuration values that may have been provided by the launch file (such as initial team color) and shares them with the rest of the program.

Processor is defined in soccer/src/soccer/processor.cpp and starts multiple temp nodes. It then updates the context in an infinite loop. Context is used to update the ui. It is good that most other nodes are not dependent on this loop.


See the /radio directory. We have two different radio implementations that inherit from a base class: a sim radio for virtual matches or testing locally, and a network radio for when we run on real hardware.

Sim radio sends commands to our robots using the league designated ports for yellow and blue. We can take advantage of this to simulate matches against ourselves (see the sim2play launch file for example) or play other teams virtually without hardware. The simulation packets are standardized by the league and are located in the rj_protos directory (the ones with the ssl prefix).

The network radio expects each robot to know the ip address of where to send to (the field computer or comp laptop). When soccer eventually receives a robot status message from a robot, it adds the robot id to an ip map. Now our software knows how to reach that robot id! If that robot doesn’t send any robot status in a while, network radio will disconnect that robot and that robot id will disappear from the UI (when running with real robots).


The referee node captures the match state and distributes it to the rest of the system. We have two referee nodes: external referee, which takes commands from the SSL Game Controller, and internal referee, which takes commands from our internal UI, so we don’t always have to launch the game controller. The specific ports and message formats for referee messages are designated by the league.


We use Qt for UI development. Code for it is located in soccer/src/soccer/ui. The qt directory contains the view as .ui files and images that are placed on the view. You could directly edit these .ui files based on their proper syntax to change our view, but I wouldn’t recommend it. Instead install Qt designer and open .ui files through it to save yourself a lot of time.

After the view, all the C++ files act as the view-model for each view. Some of them are quite messy and do not follow our naming conventions; however, if you follow the common method patterns, you should be able to add new functionality to any part of our ui.


There are two parts to vision: a receiver and filter. First, receiver gets frames from the simulator or camera. The ports and protocol used for vision data is standardized for all teams in the Small Size League. It updates our geometry data if the received packet has geometry information (see rj_protos/protos/ssl_vision_geometry.proto for what that packet contains), then sends the raw image to the filter. Vision Filter uses a kalman filter to estimate the current world state (information like ball and robot position) and publishes that as a built world state message for the rest of codebase to use.