diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fb74510f17..89f73899ac 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,8 @@ +This file outlines a list of common things that should be addressed when opening a PR. It's built from previous issues we've seen in a lot of pull requests. If you notice something that's being noted in a lot of PRs, it should probably be added here to help save people time in the future. -## Please fill out the following before requesting review on this PR +Please fill out the following before requesting review on this PR! +--> ### Description @@ -19,13 +19,14 @@ This file outlines a list of common things that should be addressed when opening ### Resolved Issues ### Length Justification and Key Files to Review - + ### Review Checklist diff --git a/README.md b/README.md index 023354ceef..7e9e2cbf0c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,19 @@ -# Software + + + Thunderbots Logo + + +--- [![Tbots CI](https://github.com/UBC-Thunderbots/Software/actions/workflows/main.yml/badge.svg)](https://github.com/UBC-Thunderbots/Software/actions?query=workflow%3A%22Tbots+CI%22+branch%3Amaster) -Our main software and firmware repository. To get started, please see [Getting Started](docs/getting-started.md). Please thoroughly read this guide, along with our [style guide](docs/code-style-guide.md), before making *any* contributions. +**Welcome to our main software and firmware repository!** +Our team is building software that controls our fleet of autonomous soccer-playing robots competing in the [RoboCup Small Size League](https://ssl.robocup.org/). + +- To get started with building and setting up our software, please see [Getting Started](docs/getting-started.md). Please thoroughly read this guide, along with our [style guide](docs/code-style-guide.md), before making *any* contributions. -For an explanation of our software layout and architecture, check out [software architecture and design](docs/software-architecture-and-design.md). +- Check out our [software architecture and design](docs/software-architecture-and-design.md) docs for an overview of how our software works and explanations of key systems and components. -Want to learn more about the RoboCup Small Size League? (teams, rules, etc.) Check out the [Official RoboCup SSL Website](https://ssl.robocup.org/). +- Want to learn more about the RoboCup Small Size League? (teams, rules, etc.) Check out the [Official RoboCup SSL Website](https://ssl.robocup.org/). -Want to edit these docs? If you're planning on editing diagrams, read the guide on [editing the docs](docs/editing-the-docs.md). +- Want to edit these docs? If you're planning on editing diagrams, read the guide on [editing the docs](docs/editing-the-docs.md). diff --git a/docs/code-style-guide.md b/docs/code-style-guide.md index 4385fef6f7..c26f594e7b 100644 --- a/docs/code-style-guide.md +++ b/docs/code-style-guide.md @@ -1,19 +1,24 @@ # Code Style Guide -## Table of Contents -* [Names and Variables](#names-and-variables) -* [Comments](#comments) -* [Headers](#headers) -* [Includes](#includes) -* [Namespaces](#namespaces) -* [Exceptions](#exceptions) -* [Tests](#tests) -* [Getter And Setter Functions](#getter-and-setter-functions) -* [Static Creators](#static-creators) -* [Spelling](#spelling) -* [Miscellaneous](#miscellaneous) -* [Protobuf](#protobuf) - +### Table of Contents + + + +- [Table of Contents](#table-of-contents) +- [Names and Variables](#names-and-variables) +- [Comments](#comments) +- [Headers](#headers) +- [Includes](#includes) +- [Namespaces](#namespaces) +- [Exceptions](#exceptions) +- [Tests](#tests) +- [Getter And Setter Functions](#getter-and-setter-functions) +- [Static Creators](#static-creators) +- [Spelling](#spelling) +- [Miscellaneous](#miscellaneous) +- [Protobuf](#protobuf) + + Our C++ coding style is based off of [Google's C++ Style Guide](https://google.github.io/styleguide/cppguide.html). We use [clang-format](https://clang.llvm.org/docs/ClangFormat.html) to enforce most of the nit-picky parts of the style, such as brackets and alignment, so this document highlights the important rules to follow that clang-format cannot enforce. diff --git a/docs/getting-started-wsl.md b/docs/getting-started-wsl.md index 1e48c769b8..bacf96489d 100644 --- a/docs/getting-started-wsl.md +++ b/docs/getting-started-wsl.md @@ -2,39 +2,42 @@ ## Table of Contents -* [Table Of Contents](#table-of-contents) -* [Introduction](#introduction) -* [WSLg Setup (Windows 11 - Recommended)](#wslg-setup-(windows-11---recommended)) -* [WSL2 Setup (windows 10)](#wsl2-setup-(windows-10)) - * [X Server Setup](#x-server-setup) + +- [Table of Contents](#table-of-contents) +- [Introduction](#introduction) +- [WSLg Setup (Windows 11/10 - Recommended)](#wslg-setup-windows-1110---recommended) +- [WSL2 Setup (Windows 10)](#wsl2-setup-windows-10) + - [X Server Setup](#x-server-setup) +- [Networking Issues](#networking-issues) +- [USB Issues](#usb-issues) + + ## Introduction Windows has a Windows Subsystem for Linux component that can be used to develop and run code for Linux on Windows. WSL1 was a Windows component that implemented Linux kernel interfaces, and didn't work great with Thunderbots software. WSL2 runs a full-fledged Linux kernel in a VM, and works great with Thunderbots software with the exception that we need to use software rendering instead of GPU-accelerated rendering for our AI. WSLg is WSL2 but with built-in support for running GUI applications (e.g. Thunderscope). -**Support for WSL is experimental. Because we use software rendering, the experience will also be degraded on computers with weak or old CPUs.** - -**Note that this will not work with legacy robots. Due to the lack of USB support in WSL2, we are unable to use the USB dongle used to communicate with them.** +> [!WARNING] +> **Support for WSL is experimental. Performance will be degraded, features may not work properly, and the developer experience will be worse overall.** -## WSLg Setup (Windows 11 - Recommended) -1. Installing WSLg is more straight forward than WSL2. For up to date documentation, please follow the [official documentation for setting up WSLg](https://github.com/microsoft/wslg#installing-wslg). +## WSLg Setup (Windows 11/10 - Recommended) +1. Installing WSLg is more straight forward than WSL2 and is the recommended way to run Linux GUI applications in Windows. For up to date documentation, please follow the [official documentation for setting up WSLg](https://github.com/microsoft/wslg#installing-wslg). 2. Once you have completed all of the above, complete the [Software Setup](./getting-started.md). ## WSL2 Setup (Windows 10) -If you are not using Windows 11 and would prefer not to upgrade, you can follow the following steps. Note that this setup is more complex than the [WSLg](#wslg-setup-(windows-11---recommended)) setup as it does not support GUI applications out of the box. +If you are not using Windows 11 or the latest version of Windows 10 and would prefer not to upgrade, you can follow the following steps. Note that this setup is more complex than the [WSLg](#wslg-setup-(windows-11---recommended)) setup as it does not support GUI applications out of the box. 1. You'll need to be on build 19041 or later to use WSL2. If you have updated to Windows 10 version 2004 or newer, you will be able to use WSL2. 2. When you have ensured that your Windows version supports WSL2, do the following to enable it. - Enable WSL by opening an Administrator PowerShell window and running command - ``` - dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart - ``` + ``` + dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart + ``` - Enable the 'Virtual Machine Platform' component by Administrator PowerShell window and running command - ``` - dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart - - ``` + ``` + dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart + ``` - Reboot your machine. 3. Now, let's install Ubuntu. - Download the WSL2 kernel from [here](https://docs.microsoft.com/en-us/windows/wsl/wsl2-kernel). @@ -62,3 +65,26 @@ If you are not using Windows 11 and would prefer not to upgrade, you can follow 7. Verify that your system is configured correctly by running `glxgears -info` on the Linux command line. You should see a window pop up with spinning gears and the line `GL_RENDERER = llvmpipe (LLVM 9.0, 256 bits)` at the top of the output. Once you have completed all of the above, complete the [Software Setup](./getting-started.md). + +## Networking Issues + +Networking compatibility with WSL is limited but it can be improved by enabling [mirrored mode](https://learn.microsoft.com/en-us/windows/wsl/networking#mirrored-mode-networking). There are still many unresolved issues with mirrored mode enabled (no vision, no robot status, etc.) but robot diagnostics should work and you should be able to control robots on the network. + +Create a `.wslconfig` file in your `%UserProfile%` directory (typically your home directory, `cd ~`) and copy the following into the config file: + +``` +[wsl2] +networkingMode = mirrored +``` + +This will enable [mirrored mode networking for WSL](https://learn.microsoft.com/en-us/windows/wsl/networking#mirrored-mode-networking). This mode “mirrors” the networking interfaces you have on Windows onto Linux, which improves networking capabilities and compatibility. + +When selecting a network interface to use, choose `eth...`/`en...` or similar. There probably won’t be a `wlan` interface since WSL only sees the virtual network interface `eth...`. + +## USB Issues + +WSL does not natively support connecting USB devices, which is necessary for some tasks like flashing firmware onto our robots or using a physical e-stop. You will need to install a piece of open-source software called `usbipd-win` to support USB connectivity. + +Please follow the [official documentation on installing `usbipd-win`](https://github.com/dorssel/usbipd-win?tab=readme-ov-file#how-to-install). + +Note that connected devices are not automatically shared with `usbipd`, so you will have to manually share the device with `usbipd` and attach/detach the device to a `usbipd` client whenever you plug/unplug it (or between reboots). See the [official documentation for details on usage](https://github.com/dorssel/usbipd-win?tab=readme-ov-file#how-to-install). diff --git a/docs/getting-started.md b/docs/getting-started.md index 4c6d3bbd54..c26005b666 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,66 +1,64 @@ -Table of Contents -================= - -* [Software Setup](#software-setup) - * [Introduction](#introduction) - * [Installation and Setup](#installation-and-setup) - * [Operating systems](#operating-systems) - * [Getting the Code](#getting-the-code) - * [Installing Software Dependencies](#installing-software-dependencies) - * [Installing an IDE](#installing-an-ide) - * [Installing an IDE: CLion](#installing-an-ide-clion) - * [Getting your Student License](#getting-your-student-license) - * [Installing CLion](#installing-clion) - * [Installing an IDE: VSCode](#installing-an-ide-vscode) - * [Editing with Vim or NeoVim](#editing-with-vim-or-neovim) - * [Building and Running the Code](#building-and-running-the-code) - * [Building from the command-line](#building-from-the-command-line) - * [Building from the command-line using the fuzzy finder](#building-from-the-command-line-using-the-fuzzy-finder) - * [Building with CLion](#building-with-clion) - * [Building with VSCode](#building-with-vscode) - * [Running our AI, Simulator, SimulatedTests or Robot Diagnostics](#running-our-ai-simulator-simulatedtests-or-robot-diagnostics) - * [Debugging](#debugging) - * [Debugging with CLion](#debugging-with-clion) - * [Debugging from the Command line](#debugging-from-the-command-line) - * [Profiling](#profiling) - * [Callgrind](#callgrind) - * [Tracy](#tracy) - * [Building for the robot](#building-for-the-robot) - * [Deploying Robot Software to the robot](#deploying-robot-software-to-the-robot) - * [Setting up Virtual Robocup 2021](#setting-up-virtual-robocup-2021) - * [Setting up the SSL Simulation Environment](#setting-up-the-ssl-simulation-environment) - * [Pushing a Dockerfile to dockerhub](#pushing-a-dockerfile-to-dockerhub) -* [Workflow](#workflow) - * [Issue and Project Tracking](#issue-and-project-tracking) - * [Issues](#issues) - * [Git Workflow](#git-workflow) - * [Forking and Branching](#forking-and-branching) - * [Creating a new Branch](#creating-a-new-branch) - * [Making Commits](#making-commits) - * [Updating Your Branch and Resolving Conflicts](#updating-your-branch-and-resolving-conflicts) - * [Formatting Your Code](#formatting-your-code) - * [Pull Requests](#pull-requests) - * [Reviewing Pull Requests](#reviewing-pull-requests) - * [Example Workflow](#example-workflow) - * [Testing](#testing) - - +# Getting Started + +# Table of Contents + + + +- [Table of Contents](#table-of-contents) +- [Software Setup](#software-setup) + - [Introduction](#introduction) + - [Installation and Setup](#installation-and-setup) + - [Operating systems](#operating-systems) + - [Getting the Code](#getting-the-code) + - [Installing Software Dependencies](#installing-software-dependencies) + - [Installing an IDE](#installing-an-ide) + - [Installing an IDE: CLion](#installing-an-ide-clion) + - [Getting your Student License](#getting-your-student-license) + - [Installing CLion](#installing-clion) + - [Installing an IDE: VS Code](#installing-an-ide-vs-code) + - [Editing with Vim or NeoVim](#editing-with-vim-or-neovim) + - [Building and Running the Code](#building-and-running-the-code) + - [Building from the command line](#building-from-the-command-line) + - [Building from the command line using the fuzzy finder](#building-from-the-command-line-using-the-fuzzy-finder) + - [Building with CLion](#building-with-clion) + - [Building with VS Code](#building-with-vs-code) + - [Running our AI, Simulator, SimulatedTests or Robot Diagnostics](#running-our-ai-simulator-simulatedtests-or-robot-diagnostics) + - [Debugging](#debugging) + - [Debugging with CLion](#debugging-with-clion) + - [Debugging from the Command Line](#debugging-from-the-command-line) + - [Profiling](#profiling) + - [Callgrind](#callgrind) + - [Tracy](#tracy) + - [Building for the robot](#building-for-the-robot) + - [Deploying Robot Software to the robot](#deploying-robot-software-to-the-robot) + - [Setting up Virtual Robocup 2021](#setting-up-virtual-robocup-2021) + - [Setting up the SSL Simulation Environment](#setting-up-the-ssl-simulation-environment) + - [Pushing a Dockerfile to dockerhub](#pushing-a-dockerfile-to-dockerhub) +- [Workflow](#workflow) + - [Issue and Project Tracking](#issue-and-project-tracking) + - [Issues](#issues) + - [Git Workflow](#git-workflow) + - [Forking and Branching](#forking-and-branching) + - [Creating a new Branch](#creating-a-new-branch) + - [Making Commits](#making-commits) + - [Updating Your Branch and Resolving Conflicts](#updating-your-branch-and-resolving-conflicts) + - [Formatting Your Code](#formatting-your-code) + - [Pull Requests](#pull-requests) + - [Reviewing Pull Requests](#reviewing-pull-requests) + - [Example Workflow](#example-workflow) + - [Testing](#testing) + + # Software Setup ## Introduction These instructions assume that you have the following accounts setup: -- [Github](https://github.com/login) +- [GitHub](https://github.com/login) - [Discord](https://discord.com). Please contact a Thunderbots lead to receive the invite link. -These instructions assume you have a basic understanding of Linux and the command-line. There are many great tutorials online, such as [LinuxCommand](http://linuxcommand.org/). The most important things you'll need to know are how to move around the filesystem, and how to run programs or scripts. +These instructions assume you have a basic understanding of Linux and the command line. There are many great tutorials online, such as [LinuxCommand](http://linuxcommand.org/). The most important things you'll need to know are how to move around the filesystem and how to run programs or scripts. ## Installation and Setup @@ -84,18 +82,23 @@ You can use Ubuntu 20.04 LTS, Ubuntu 22.04 LTS or Ubuntu 24.04 LTS inside Window 4. Click the `Fork` button in the top-right to fork the repository ([click here to learn about Forks](https://help.github.com/en/articles/fork-a-repo)) 1. Click on your user when prompted 2. You should be automatically redirected to your new fork -5. Clone your fork of the repository. As GitHub is forcing users to stop using usernames and passwords, we will be using the SSH link. Returning members who are migrating to using SSH after cloning from a previous method can use the following instructions to set up a new local repository using SSH. - 1. To connect to GitHub using SSH, if not setup prior, you will need to add an SSH key to your GitHub account. Instructions can be found [here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent). For each computer you contribute to GitHub with, you will need an additional SSH Key pair linked to your account. - 2. After you have successfully set up a SSH key for your device and added it to GitHub, you can clone the repository using the following command (you can put it wherever you like): - 1. Eg. `git clone git@github.com:/Software.git` - 2. You can find this link under the green `Clone or Download` button on the main page of the Software repository, under the SSH tab. (This should now be available after adding your SSH key to GitHub successfully.) +5. Clone your fork of the repository. As GitHub is forcing users to stop using usernames and passwords for authorization, we will be using the SSH link. + + To clone using SSH: + + 1. If not setup prior, you will need to add an SSH key to your GitHub account. Instructions can be found [here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent). For each computer you contribute to GitHub with, you will need an additional SSH Key pair linked to your account. + 2. After you have successfully set up a SSH key for your device and added it to GitHub, you can clone the repository using the following command: + 1. e.g. `git clone git@github.com:/Software.git` + 2. You can find this link under the green `Code` button on the main page of your fork on GitHub, under the SSH tab. (This should now be available after adding your SSH key to GitHub successfully.) + + Alternatively, you can clone using HTTPS. You'll need to either use a credential helper (Git Credential Manager, GitHub CLI, etc.) or a personal access token ([details here](https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls)). 6. Set up your git remotes ([what is a remote and how does it work?](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)) 1. You should have a remote named `origin` that points to your fork of the repository. Git will have set this up automatically when you cloned your fork in the previous step. 2. You will need to add a second remote, named `upstream`, that points to our main Software repository, which is where you created your fork from. (**Note:** This is _not_ your fork) 1. Open a terminal and navigate to the folder you cloned (your fork): `cd path/to/the/repository/Software` - 2. Navigate to our main Software repository in your browser and copy the url from the "Clone or Download" button. Copy the HTTPS url if you originally cloned with HTTPS, and use the SSH url if you previously cloned with SSH + 2. Navigate to our main Software repository in your browser and copy the url from the green `Code` button. Copy the SSH url if you originally cloned with SSH, or use the HTTPS url if you previously cloned with HTTPS 3. From your terminal, add the new remote by running `git remote add upstream ` (without the angle brackets) - 1. Eg. `git remote add upstream https://github.com/UBC-Thunderbots/Software.git` + 1. e.g. `git remote add upstream git@github.com:UBC-Thunderbots/Software.git` 4. That's it. If you want to double check your remotes are set up correctly, run `git remote -v` from your terminal (at the base of the repository folder again). You should see two entries: `origin` with the url for your fork of the repository, and `upstream` with the url for the main repository *See our [workflow](#workflow) for how to use git to make branches, submit Pull Requests, and track issues* @@ -104,14 +107,14 @@ You can use Ubuntu 20.04 LTS, Ubuntu 22.04 LTS or Ubuntu 24.04 LTS inside Window We have several setup scripts to help you easily install the necessary dependencies in order to build and run our code. You will want to run the following scripts, which can all be found in `Software/environment_setup` -* Inside a terminal, navigate to the environment_setup folder. Eg. `cd path/to/the/repository/Software/environment_setup` +* Inside a terminal, navigate to the environment_setup folder. e.g. `cd path/to/the/repository/Software/environment_setup` * Run `./setup_software.sh` * You will be prompted for your admin password - * This script will install everything necessary in order to build and run our main `AI` software + * This script will install everything necessary in order to build and run our software ### Installing an IDE -For those who prefer working on C/C++ with an IDE, we provide two options: CLion for an integrated experience and VSCode for a more lightweight setup. Both support our build system `bazel`. +For those who prefer working on C/C++ with an IDE, we provide two options: CLion for an integrated experience and VS Code for a more lightweight setup. Both support our build system `bazel`. #### Installing an IDE: CLion @@ -127,20 +130,18 @@ CLion is free for students, and you can use your UBC alumni email address to cre ##### Installing CLion -* Inside a terminal, navigate to the environment_setup folder. Eg. `cd path/to/the/repository/Software/environment_setup` -* Run `./install_clion.sh` (* **DO NOT** download CLion yourself unless you know what you're doing. The `install_clion.sh` script will grab the correct version of CLion and the Bazel plugin to ensure everything is compatible *). -* When you run CLion for the first time you will be prompted to enter your JetBrains account or License credentials. Use your student account. +1. Follow [the latest instructions from JetBrains](https://www.jetbrains.com/help/clion/installation-guide.html#toolbox) on installing CLion. We recommend installing CLion through the JetBrains Toolbox App which makes it easy to upgrade/downgrade your version of CLion if necessary. +2. When you run CLion for the first time, you will be prompted to enter your JetBrains account or License credentials. Use your student account. +3. Install the [Bazel plugin for CLion](https://plugins.jetbrains.com/plugin/9554-bazel-for-clion). -### Installing an IDE: VSCode +### Installing an IDE: VS Code -VSCode is the more lightweight IDE, with support for code navigation, code completion, and integrated building and testing. However, debugging isn't integrated into this IDE. +VS Code is a more lightweight "IDE", with support for code navigation, code completion, and integrated building and testing. However, debugging isn't integrated by default into VS Code. -1. Inside a terminal, navigate to the environment_setup folder. Eg. `cd path/to/the/repository/Software/environment_setup` -2. Run `./install_vscode.sh` (* **DO NOT** download VSCode yourself unless you know what you're doing. The `install_vscode.sh` script will grab the most stable version of VSCode *) -3. Open `vscode`. You can type `vscode` in the terminal, or click the icon on your Desktop. -&. Click `Open Folder` and navigate to where you cloned software. So if I cloned the repo to `/home/my_username/Downloads/Software`, I would select `/home/my_username/Downloads/Software`. -4. VSCode will prompt you to install recommended extensions, click `Install`, this installs necessary plugins to work on the codebase. (Bazel, C++, Python, etc..) -5. Navigate to File -> Preferences -> Settings -> Workspace -> Extensions -> Bazel and select the `Bazel: Enable Code Lens` option. +1. Follow the [latest instructions from the VS Code documentation](https://code.visualstudio.com/docs/setup/linux) on installing VS Code. +2. Open VS Code. Go to File -> Open Folder and navigate to where you cloned the software repo. So if I cloned the repo to `/home/my_username/Downloads/Software`, I would select `/home/my_username/Downloads/Software`. +3. VS Code will prompt you to install recommended extensions. Click `Install` — this installs necessary plugins to work on the codebase. (Bazel, C++, Python, etc.) +4. Navigate to File -> Preferences -> Settings -> Workspace -> Extensions -> Bazel and select the `Bazel: Enable Code Lens` option. ### Editing with Vim or NeoVim @@ -152,7 +153,7 @@ These tools require a `compile_commands.json` file, which can be generated by fo ## Building and Running the Code -### Building from the command-line +### Building from the command line 1. Navigate to the root of this repository (wherever you have it cloned on your computer) 2. Navigate to `src`. @@ -165,9 +166,9 @@ These tools require a `compile_commands.json` file, which can be generated by fo *See the Bazel [command-line docs](https://bazel.build/reference/command-line-reference) for more info.* *Note: the targets are defined in the BUILD files in our repo* -### Building from the command-line using the fuzzy finder +### Building from the command line using the fuzzy finder -We have a ./tbots.py test runner script in the src folder that will fuzzy find for targets. For example, +We have a `tbots.py` test runner script in the src folder that will fuzzy find for targets and call Bazel. For example, 1. Build a specific target for running (for example): `./tbots.py build angletest` 2. Run a specific target by running (for example): `./tbots.py run goalietactictest -t` @@ -177,7 +178,7 @@ where the `-t` flag indicates whether Thunderscope should be launched. Run `./tb ### Building with CLion -First we need to setup CLion +First, we need to setup CLion: 1. Open CLion 2. Select `Import Bazel Project` 3. Set `Workspace` to wherever you cloned the repository + `/src`. So if I cloned the repo to `/home/my_username/Downloads/Software`, my workspace would be `/home/my_username/Downloads/Software/src`. @@ -187,7 +188,7 @@ First we need to setup CLion 7. Click `Finish` and you're good to go! Give CLion some time to find everything in your repo. Now that you're setup, if you can run it on the command line, you can run it in CLion. There are two main ways of doing so. -1. Open any `BUILD` file and right clight in a `cc_library()` call. This will give you the option to `Run` or `Debug` that specific target. Try it by opening `Software/src/software/geom/BUILD` and right-clicking on the `cc_library` for `angle_test`! +1. Open any `BUILD` file and right click on a `cc_library()` call. This will give you the option to `Run` or `Debug` that specific target. Try it by opening `Software/src/software/geom/BUILD` and right-clicking on the `cc_library` for `angle_test`! 2. Add a custom build configuration (more powerful, so make sure you understand this!) 1. Select `Add Configuration` from the drop-down in the top-right of CLion 2. Click on `+`, choose `Bazel Command`. @@ -195,17 +196,17 @@ Now that you're setup, if you can run it on the command line, you can run it in 4. For `Bazel Command` you can put any Bazel command, like `build`, `run`, `test`, etc. 5. Click `Ok`, then there should be a green arrow in the top right corner by the drop-down menu. Click it and the test will run! -### Building with VSCode +### Building with VS Code -1. Open VSCode +1. Open VS Code 2. Navigate to `Software/src/software/geom/BUILD` 3. On top of every `cc_test`, `cc_library` and `cc_binary` there should be a `Test ...`, `Build ...` or `Run ...` for the respective target. 4. Click `Test //software/geom:angle_test` to run the `angle_test` ### Running our AI, Simulator, SimulatedTests or Robot Diagnostics -1. Run our AI on Thunderscope: - - Thunderscope is the software that coordinates our AI, Simulator, Visualizer and RobotDiagnostics +1. Run our AI on [Thunderscope](./software-architecture-and-design.md#thunderscope-gui): + - [Thunderscope](./software-architecture-and-design.md#thunderscope-gui) is the software that coordinates and visualizes our AI, Simulator, and RobotDiagnostics. - After launching Thunderscope, we can see what the AI is currently "seeing" and interact with it through dynamic parameters. - If we want to run with simulated AI vs AI: - `./tbots.py run thunderscope_main --enable_autoref` will start Thunderscope with a Simulator, a blue FullSystem, yellow FullSystem and a headless Autoref. @@ -301,13 +302,13 @@ Debugging from the command line is certainly possible, but debugging in a full I Debugging in CLion is as simple as running the above instructions for building CLion, but clicking the little green bug in the top right corner instead of the little green arrow! -### Debugging from the Command line +### Debugging from the Command Line To debug from the command line, first you need to build your target with the debugging flag - `bazel build -c dbg //some/target:here`. When the target builds, you should see a path `bazel-bin/`. Copy that path, and run `gdb `. Please see [here](https://www.cs.cmu.edu/~gilpin/tutorial/) for a tutorial on how to use `gdb` if you're not familiar with it. Alternatively, you could do `bazel run -c dbg --run_under="gdb" //some/target:here`, which will run the target in `gdb`. While this is taken directly from the Bazel docs, gdb may sometimes hang when using `--run_under`, so building the target first with debugging flags and running afterwards is preferred. -## Profiling +## Profiling -Profiling is an optimization tool used to identify the time and space used by code, with a detailed breakdown to help identify areas of potential performance improvements. Unfortunately profiling for Bazel targets is not supported in CLion at this time. Hence the only way is via command line. Use the following command: +Profiling is an optimization tool used to identify the time and space used by code, with a detailed breakdown to help identify areas of potential performance improvements. Unfortunately profiling for Bazel targets is not supported in CLion at this time. Hence, the only way to profile our software is via the command line. ### Callgrind @@ -383,7 +384,7 @@ After editing the dockerfile, build the image and push it to dockerhub with the ## Issue and Project Tracking -We try keep our issue and project tracking fairly simple, to reduce the overhead associated with tracking all the information and to make it easier to follow. If you are unfamiliar with GitHub issues, [this article](https://guides.github.com/features/issues/) gives a good overview. +We try keep our issue and project tracking fairly simple to reduce the overhead associated with tracking all the information and to make it easier to follow. If you are unfamiliar with GitHub issues, [this article](https://guides.github.com/features/issues/) gives a good overview. ### Issues @@ -407,20 +408,22 @@ In general, we follow the Forking Workflow ### Creating a new Branch -For each Issue of project you are working on, you should have a separate branch. This helps keep work organized and separate. +For each issue that you work on, you should have a separate branch. This helps keep work organized and separate. **Branches should always be created from the latest code on the `master` branch of our main Software repository**. If you followed the steps in [Installation and Setup](#installation-and-setup), this will be `upstream/master`. Once this branch is created, you can push it to your fork and update it with commits until it is ready to merge. 1. Navigate to the base folder of your Software repository: `cd path/to/the/repository/Software` 2. Make git aware of any new changes to `upstream` by running `git fetch upstream` 3. Create a new branch from `upstream/master` by running `git checkout upstream/master` then `git checkout -b your-branch-name` - 1. Our branch naming convention is: `your_name/branch_name` (all lowercase, words separated by underscores). The branch name should be short and descriptive of the work being done on the branch. -**Example:** if you were working on a new navigation system using RRT and your name was "Bob" your branch name might look like: `bob/new_rrt_navigator` -4. You can now commit changes to this branch, and push them to your fork with `git push origin your_branch_name` or `git push -u` + Our branch naming convention is: `your_name/branch_name` (all lowercase, words separated by underscores). The branch name should be short and descriptive of the work being done on the branch. + + **Example:** if you were working on a new navigation system using RRT and your name was "Bob" your branch name might look like: `bob/new_rrt_navigator` + +4. You can now commit changes to this branch and push them to your fork with `git push origin your_branch_name` or `git push -u`
-Aside: Why should you only create branches from "upstream/master"? +Aside: Why should you only create branches from upstream/master? Because we squash our commits when we merge Pull Requests, a new commit with a new hash will be created, containing the multiple commits from the PR branch. Because the hashes are different, git will not recognize that the squashed commit and the series of commits that are inside the squashed commit contain the same changes, which can result in conflicts. @@ -432,7 +435,7 @@ tl;dr Always create new branches from upstream/master. Do not create branches fr ### Making Commits -We don't impose any rules for how you should be committing code, just keep the following general rules in mind: +We don't impose any rules for how you should be committing code, just keep the following general guidelines in mind: 1. Commits should represent logical steps in your workflow. Avoid making commits too large, and try keep related changes together 2. Commit messages should give a good idea of the changes made. You don't have to go in-depth with technical details, but no one will know what you've done if your commit message is "fixed broken stuff" @@ -446,13 +449,13 @@ To do this, you have 2 options: rebase or merge. [What's the difference?](https: Merging is generally recommended, because it is easier to handle conflicts and get stuff working. To merge, simply run `git pull upstream master`. -Rebasing requires more knowledge of git and can cause crazy merge conflicts, so it isn't recommended. You can simply `git pull --rebase upstream master` to rebase your branch onto the latest `upstream/master`. +Rebasing requires more knowledge of git and can cause crazy merge conflicts, so it isn't recommended. You can simply `git pull --rebase upstream master` to rebase your branch onto the latest `upstream/master`. The main benefit of rebasing is that you get a clean, linear commit history; however, we squash all the commits in each PR into a single commit before merging into master, so the extra effort involved in rebasing is somewhat pointless. -If you do rebase or merge and get conflicts, you'll need to resolve them manually. [See here for a quick tutorials on what conflicts are and how to resolve them](https://www.atlassian.com/git/tutorials/using-branches/merge-conflicts). Feel free to do this in your IDE or with whatever tool you are most comfortable with. Updating your branch often helps keep conflicts to a minimum, and when they do appear they are usually smaller. Ask for help if you're really stuck! +If you do rebase or merge and get conflicts, you'll need to resolve them manually. [See here for a quick tutorial on what conflicts are and how to resolve them](https://www.atlassian.com/git/tutorials/using-branches/merge-conflicts). Feel free to do this in your IDE or with whatever tool you are most comfortable with. Updating your branch often helps keep conflicts to a minimum, and when they do appear they are usually smaller. Ask for help if you're really stuck! ### Formatting Your Code -We use [clang-format](https://electronjs.org/docs/development/clang-format) to automatically format our code. Using an automatic tool helps keep things consistent across the codebase without developers having to change their personal style as they write. See the [code style guide](code-style-guide.md) for more information on exactly what it does. +We use a variety of code formatters and linters to automatically format our code. Using automatic tools helps keep things consistent across the codebase without developers having to change their personal style as they write. See the [code style guide](code-style-guide.md) for more information on exactly what these tools enforce. To format the code, from the `Software` directory run `./scripts/lint_and_format.sh`. @@ -485,14 +488,14 @@ The Pull Request process usually looks like the following: 5. Mark the Pull Request as "Approved" when you think it looks good 2. **If you are the recipient of the review (the PR creator):** 1. **Make sure to reply to the PR comments as you address / fix issues**. This helps the reviewers know you have made a change without having to go check the code diffs to see if you made a change. - 1. Eg. Reply with "done" or "fixed" to comments as you address them + 1. e.g. Reply with "done" or "fixed" to comments as you address them 2. Leave comments unresolved, let the reviewer resolve them. 2. Don't be afraid to ask for clarification regarding changes or suggest alternatives if you don't agree with what was suggested. The reviewers and reviewee should work together to come up with the best solution. 3. **Do not resolve conversations as you address them** (but make sure to leave a comment as mentioned above). That is the responsibility of the reviewers. 4. Once you have addressed all the comments, re-request review from reviewers. -11. Make sure our automated tests with Github Actions are passing. There will be an indicator near the bottom of the Pull Request. If something fails, you can click on the links provided to get more information and debug the problems. More than likely, you'll just need to re-run clang-format on the code. +11. Make sure our automated tests with Github Actions are passing. There will be an indicator near the bottom of the Pull Request. If something fails, you can click on the links provided to get more information and debug the problems. 12. Once your Pull Request has been approved and the automated tests pass, you can merge the code. There will be a big 'merge" button at the bottom of the Pull Request with several options to choose from - 1. We only allow "Squash and merge". This is because it keep the commit history on `upstream/master` shorter and cleaner, without losing any context from the commit messages (since they are combined in the squashed commit. A squashed commit also makes it easier to revert and entire change/feature, rather than having to "know" the range of commits to revert. + 1. We only allow "Squash and merge". This is because it keeps the commit history on `upstream/master` shorter and cleaner, without losing any context from the commit messages (since they are combined in the squashed commit. A squashed commit also makes it easier to revert and entire change/feature, rather than having to "know" the range of commits to revert. 13. That's it, your changes have been merged! You will be given the option to delete your remote branch. but are not required to do so. We recommend it since it will keep your fork cleaner, but you can do whatever you like. *Remember, code reviews can be tough. As a reviewer, it can be very tricky to give useful constructive criticism without coming off as condescending or degrading (emotions are hard to express through text!). As the recipient of a code review, it might feel like you are being criticized too harshly and that your hard work is being attacked. Remember that these are your teammates, who are not trying to arbitrarily devalue your contributions but are trying to help make the code as good as possible, for the good of the team.* diff --git a/docs/images/tbots_logo_dark.png b/docs/images/tbots_logo_dark.png new file mode 100644 index 0000000000..f03f1c692b Binary files /dev/null and b/docs/images/tbots_logo_dark.png differ diff --git a/docs/images/tbots_logo_light.png b/docs/images/tbots_logo_light.png new file mode 100644 index 0000000000..e053733afb Binary files /dev/null and b/docs/images/tbots_logo_light.png differ diff --git a/docs/images/thunderscope.png b/docs/images/thunderscope.png new file mode 100644 index 0000000000..633c33e1a1 Binary files /dev/null and b/docs/images/thunderscope.png differ diff --git a/docs/robot-software-architecture.md b/docs/robot-software-architecture.md index 9f6059524b..cc69d1560c 100644 --- a/docs/robot-software-architecture.md +++ b/docs/robot-software-architecture.md @@ -1,14 +1,20 @@ # Robot Software Architecture # Table of Contents -* [Tools](#tools) - * [Ansible](#ansible) - * [Systemd](#systemd) - * [Redis](#redis) -* [Redis](#redis) -* [Thunderloop](#thunderloop) -* [Announcements](#announcements) -* [Display](#display) + + + +- [Table of Contents](#table-of-contents) +- [Robot Software Diagram](#robot-software-diagram) +- [Tools](#tools) + - [Ansible](#ansible) + - [Systemd](#systemd) + - [Redis](#redis) +- [Thunderloop](#thunderloop) + + + +# Robot Software Diagram ![Robot Software Diagram](images/robot_software_diagram.svg) diff --git a/docs/software-architecture-and-design.md b/docs/software-architecture-and-design.md index a3ff3259f5..1e800ee90c 100644 --- a/docs/software-architecture-and-design.md +++ b/docs/software-architecture-and-design.md @@ -1,139 +1,120 @@ # Architecture and Design Rationales # Table of Contents -* [Tools](#tools) - * [SSL-Vision](#ssl-vision) - * [SSL-Gamecontroller](#ssl-gamecontroller) -* [Important Classes](#important-classes) - * [World](#world) - * [Team](#team) - * [Robot](#robot) - * [Ball](#ball) - * [Field](#field) - * [GameState](#gamestate) - * [Intents](#intents) - * [Dynamic Parameters](#dynamic-parameters) -* [Protobuf](#protobuf) - * [Important Protobuf Messages](#important-protobuf-messages) - * [Primitives](#primitives) - * [Robot Status](#robot-status) -* [Design Patterns](#design-patterns) - * [Abstract Classes and Inheritance](#abstract-classes-and-inheritance) - * [Singleton Pattern](#singleton-pattern) - * [Factory Pattern](#factory-pattern) - * [Visitor Pattern](#visitor-pattern) - * [Observer Pattern](#observer-pattern) - * [Threaded Observer](#threaded-observer) - * [Publisher-Subscriber Pattern](#publisher-subscriber-pattern) - * [C++ Templating](#c-templating) -* [Coroutines](#coroutines) - * [What Are Coroutines?](#what-are-coroutines) - * [What Coroutines Do We Use?](#what-coroutines-do-we-use) - * [How Do We Use Coroutines?](#how-do-we-use-coroutines) - * [Coroutine Best Practices](#coroutine-best-practices) -* [Finite State Machines](#finite-state-machines) - * [What Are Finite State Machines?](#what-are-finite-state-machines) - * [Boost-ext SML Library](#boost-ext-sml-library) - * [How Do We Use SML?](#how-do-we-use-sml) - * [SML Best Practices](#sml-best-practices) -* [Conventions](#conventions) - * [Coordinates](#coordinates) - * [Angles](#angles) - * [Convention Diagram](#convention-diagram) -* [Architecture Overview](#architecture-overview) - * [Fullsystem](#fullsystem) - * [Backend](#backend) - * [Backend Diagram](#backend-diagram) - * [Sensor Fusion](#sensor-fusion) - * [Filters](#filters) - * [AI](#ai) - * [Strategy](#strategy) - * [STP Diagram](#stp-diagram) - * [Tactics](#tactics) - * [Plays](#plays) - * [Navigation](#navigation) - * [Path Manager](#path-manager) - * [Path Objective](#path-objective) - * [Path Planner](#path-planner) - * [AI Diagram](#ai-diagram) - * [Thunderscope](#thunderscope) - * [Thunderscope GUI](#thunderscope-gui) - * [3D Visualizer](#3d-visualizer) - * [Layers](#layers) - * [Simulator](#simulator) - * [Standalone Simulator](#standalone-simulator) - * [Simulated Tests](#simulated-tests) - * [Simulated Tests Architecture](#simulated-tests-architecture) - * [Validation Functions](#validation-functions) - * [Component Connections and Determinism](#component-connections-and-determinism) - * [Simulated Tests Diagram](#simulated-tests-diagram) - * [Inter-process Communication](#inter-process-communication) - * [Estop](#estop) - -# Tools -A few commonly-used terms and tools to be familiar with: -#### SSL-Vision - * This is the shared vision system used by the Small Size League. It is what connects to the cameras above the field, does the vision processing, and transmits the positional data of everything on the field to our [AI](#ai) computers. - * The GitHub repository can be found [here](https://github.com/RoboCup-SSL/ssl-vision) -#### SSL-Gamecontroller - * Sometimes referred to as the "Referee", this is another shared piece of Small Size League software that is used to send gamecontroller and referee commands to the teams. A human controls this application during the games to send the appropriate commands to the robots. For example, some of these commands are what stage the gameplay is in, such as `HALT`, `STOP`, `READY`, or `PLAY`. - * The GitHub repository can be found [here](https://github.com/RoboCup-SSL/ssl-game-controller) - - -# Important Classes -These are classes that are either heavily used in our code, or are very important for understanding how the AI works, but are _not_ core components of the AI or other major modules. To learn more about these core modules and their corresponding classes, check out the sections on the [Backend](#backend), [Sensor Fusion](#sensor-fusion), [AI](#ai), and [Thunderscope](#thunderscope). - -## World -The `World` class is what we use to represent the state of the world at any given time. In this context, the world includes the positions and orientations of all robots on the field, the position and velocity of the ball, the dimensions of the field being played on, and the current referee commands. Altogether, it's the information we have at any given time that we can use to make decisions. - -### Team -A team is a collection of [Robots](#robot). -### Robot -A Robot class represents the state of a single robot on the field. This includes its position, orientation, velocity, angular velocity, and any other information about its current state. + + +- [Table of Contents](#table-of-contents) +- [Architecture Overview](#architecture-overview) + - [League-Maintained Software](#league-maintained-software) + - [SSL Vision](#ssl-vision) + - [SSL Gamecontroller](#ssl-gamecontroller) +- [Protobuf](#protobuf) + - [Important Protobuf Messages](#important-protobuf-messages) + - [Primitives](#primitives) + - [Robot Status](#robot-status) +- [Conventions](#conventions) + - [Coordinates](#coordinates) + - [Angles](#angles) + - [Convention Diagram](#convention-diagram) +- [Fullsystem](#fullsystem) + - [Backend](#backend) + - [Backend Diagram](#backend-diagram) + - [Sensor Fusion](#sensor-fusion) + - [World](#world) + - [Team](#team) + - [Robot](#robot) + - [Ball](#ball) + - [Field](#field) + - [Game State](#game-state) + - [Filters](#filters) +- [AI](#ai) + - [Strategy](#strategy) + - [STP Diagram](#stp-diagram) + - [Skills](#skills) + - [Tactics](#tactics) + - [Tactic Assignment](#tactic-assignment) + - [Hierarchical Tactic FSMs](#hierarchical-tactic-fsms) + - [Control Parameters](#control-parameters) + - [Plays](#plays) + - [Finite State Machines](#finite-state-machines) + - [What Are Finite State Machines?](#what-are-finite-state-machines) + - [Boost-ext SML Library](#boost-ext-sml-library) + - [How Do We Use SML?](#how-do-we-use-sml) + - [SML Best Practices](#sml-best-practices) + - [Coroutines](#coroutines) + - [What Are Coroutines?](#what-are-coroutines) + - [What Coroutines Do We Use?](#what-coroutines-do-we-use) + - [How Do We Use Coroutines?](#how-do-we-use-coroutines) + - [Coroutine Best Practices](#coroutine-best-practices) + - [Motion Planning](#motion-planning) + - [Trajectory Planner](#trajectory-planner) + - [Trajectory Generation and Obstacle Avoidance](#trajectory-generation-and-obstacle-avoidance) +- [Thunderscope](#thunderscope) + - [Thunderscope GUI](#thunderscope-gui) + - [3D Visualizer](#3d-visualizer) + - [Layers](#layers) + - [Inter-Process Communication](#inter-process-communication) + - [Proto Log Replay](#proto-log-replay) + - [Dynamic Parameters](#dynamic-parameters) +- [Simulator](#simulator) + - [Simulated Tests](#simulated-tests) + - [Simulated Tests Architecture](#simulated-tests-architecture) + - [Validation Functions](#validation-functions) + - [Component Connections and Determinism](#component-connections-and-determinism) + - [Simulated Tests Diagram](#simulated-tests-diagram) +- [E-Stop](#e-stop) +- [Design Patterns](#design-patterns) + - [Abstract Classes and Inheritance](#abstract-classes-and-inheritance) + - [Singleton Pattern](#singleton-pattern) + - [Factory Pattern](#factory-pattern) + - [Visitor Pattern](#visitor-pattern) + - [Observer Pattern](#observer-pattern) + - [Threaded Observer](#threaded-observer) + - [Publisher-Subscriber Pattern](#publisher-subscriber-pattern) + - [C++ Templating](#c-templating) + + -### Ball -The Ball class represents the state of the ball. This includes its position and velocity, and any other information about its current state. +# Architecture Overview -### Field -The Field class represents the state of the physical field being played on, which is primarily its physical dimensions. The Field class provides many functions that make it easy to get points of interest on the field, such as the enemy net, friendly corner, or center circle. Also see the [coordinate convention](#coordinates) we use for the field (and all things on it). +At a high-level, our system is split into several independent processes that [communicate with each other](#inter-process-communication). Our architecture is designed in this manner to promote decoupling of different features, making our system easier to expand, maintain, and test. -### GameState -These represent the current state of the game as dictated by the Gamecontroller. These provide functions like `isPlaying()`, `isHalted()` which tell the rest of the system what game state we are in, and make decisions accordingly. We need to obey the rules! +- [**Fullsystem**](#fullsystem) is the program that processes data and makes decisions for a [team](#team) of [robots](#robot). It manages [**Sensor Fusion**](#sensor-fusion), which is responsible for processing and filtering raw data, and the [**AI**](#ai) that makes gameplay decisions. -## Intents -An `Intent` represents a simple thing the [AI](#ai) wants (or intends for) a robot to do, but is at a level that requires knowledge of the state of the game and the field (e.g. Referee state, location of the other robots). It does not represent or include _how_ these things are achieved. Some examples are: -* Moving to a position without colliding with anything on its way and while following all rules -* Pivoting around a point -* Kicking the ball at a certain direction or at a target +- [**Thunderscope**](#thunderscope) is an application that provides a GUI for visualizing and interacting with our software. -There are two types of `Intent`s: `DirectPrimitiveIntent`s and `NavigatingIntent`s. `DirectPrimitiveIntent`s directly represent the [Primitives](#primitives) that the AI is trying to send to the robots. `NavigatingIntent`s are intents that require moving while avoiding obstacles, so they contain extra parameters to help with [Navigation](#navigation). +- The [**Simulator**](#simulator) provides a physics simulation of the world (robots, ball, and field), enabling testing of our gameplay when we don't have access to a real field. This process is optional and used only for development and testing purposes; in a real match, our system will receive data from [SSL-Vision](#ssl-vision). -## Dynamic Parameters -`Dynamic Parameters` are the system we use to change values in our code at runtime. The reason we want to change values at runtime is primarily because we may want to tweak our strategy or aspects of our gameplay very quickly. During games we are only allowed to touch our computers and make changes during halftime or a timeout, so every second counts! Using `Dynamic Parameters` saves us from having to stop the [AI](#ai), change a constant, recompile the code, and restart the [AI](#ai). +- [**Thunderloop**](/docs/robot-software-architecture.md#thunderloop) is the software that runs onboard our robots. It is responsible for coordinating communication between our [AI](#ai) computer and the motor and power boards in our robots. It is part our robot software architecture, which is documented [here](/docs/robot-software-architecture.md). -Additionally, we can use `Dynamic Parameters` to communicate between [Thunderscope](#thunderscope) and the rest of our system. [Thunderscope](#thunderscope) can change the values of `DynamicParameters` when buttons or menu items are clicked, and these new values will be picked up by the rest of the code. For example, we can define a `Dynamic Parameter` called `run_ai` that is a boolean value. Then when the `Start [AI](#ai)` button is clicked in [Thunderscope](#thunderscope), it sets the value of `run_ai` to `true`. In the "main loop" for the [AI](#ai), it will check if the value of `run_ai` is true before running its logic. +## League-Maintained Software -Here's a slightly more relevant example of how we used `Dynamic Parameters` during a game in RoboCup 2019. We had a parameter called `enemy_team_can_pass`, which indicates whether or not we think the enemy team can pass. This parameter was used in several places in our defensive logic, and specifically affected how we would shadow enemy robots when we were defending them. If we assumed the enemy team could pass, we would shadow between the robots and the ball to block any passes, otherwise we would shadow between the enemy robot and our net to block shots. During the start of a game, we had `enemy_team_can_pass` set to `false` but the enemy did start to attempt some passes during the game. However, we didn't want to use one of our timeouts to change the value. Luckily later during the half, the enemy team took a time out. Because `Dynamic Parameters` can be changed quick without stopping [AI](#ai), we were quickly able to change `enemy_team_can_pass` to `true` while the enemy team took their timeout. This made our defence much better against that team and didn't take so much time that we had to burn our own timeout. Altogether this is an example of how we use `Dynamic Parameters` to control our [AI](#ai) and other parts of the code. +Our software is designed to interact with the following software developed and maintained by the RoboCup Small Size League: -It is worth noting that constants are still useful, and should still be used whenever possible. If a value realistically doesn't need to be changed, it should be a constant (with a nice descriptive name) rather than a `Dynamic Parameter`. Having too many `Dynamic Parameters` is overwhelming because there are too many values to understand and change, and this can make it hard to tune values to get the desired behaviour while under pressure during a game. +### SSL Vision +* This is the shared vision system used by the Small Size League. It is what connects to the cameras above the field, does the vision processing, and transmits the positional data of everything on the field to our [AI](#ai) computers. +* The GitHub repository can be found [here](https://github.com/RoboCup-SSL/ssl-vision) +### SSL Gamecontroller +* Sometimes referred to as the "Referee", this is another shared piece of Small Size League software that is used to send gamecontroller and referee commands to the teams. A human and/or [computer auto-referee](#https://ssl.robocup.org/league-software/#auto-referees) controls this application during the games to send the appropriate commands to the robots. For example, some of these commands are what stage the gameplay is in, such as `HALT`, `STOP`, `READY`, or `PLAY`. +* The GitHub repository can be found [here](https://github.com/RoboCup-SSL/ssl-game-controller) # Protobuf [Protobufs or protocol buffers](https://protobuf.dev/) are used to pass messages between components in our system. After building using Bazel, the `.proto` files are generated into `.pb.h` and `.pb.cc` files, which are found in `bazel-out/k8-fastbuild/bin/proto`. -To include these files in our code, we simply include `proto/.pb.h` +To include these files in our code, we simply include `proto/.pb.h`. ## Important Protobuf Messages These are [protobuf](https://developers.google.com/protocol-buffers/docs/cpptutorial) messages that we define and that are important for understanding how the [AI](#ai) works. ### Primitives -`TbotsProto::Primitive`s represent simple actions that robots blindly execute (e.g. send signals to motor drivers), so it's up to the [AI](#ai) to send `Primitives` that follow all the rules and avoid collisions with obstacles. Some examples are: -* Moving in a straight line to a position -* Pivoting around a point -* Kicking the ball at a certain direction -`Primitives` act as the abstraction between our [AI](#ai) and our robot firmware. It splits the responsibility such that the [AI](#ai) is responsible for sending a `Primitive` to a robot telling it what it wants it to do, and the robot is responsible for making sure it does what it's told. For every `Primitive` protobuf message, there is an equivalent `Primitive` implementation in our robot firmware. When robots receive a `Primitive` command, they perform their own logic and control in order to perform the task specified by the `Primitive`. +**Primitives** represent the low-level actions that a robot can execute. They are sent to the robots and can be "blindly" executed without knowledge of the [World](#world) and/or the high-level gameplay [strategy](#strategy). Primitives are understood directly by our robot software, which will translate the primitive into motor and power board inputs to make the robot move, dribble, kick, and/or chip as instructed. + +Each Primitive is a C++ class that generates an associated `TbotsProto::Primitive` protobuf message that can be sent to the robots. + +Primitives act as the abstraction between our [AI](#ai) and our robot software. It splits the responsibility such that the [AI](#ai) is responsible for sending a Primitive to a robot telling it what it wants it to do, and the robot is responsible for making sure it does what it's told. ### Robot Status The `TbotsProto::RobotStatus` protobuf message contains information about the status of a single robot. Examples of the information they include are: @@ -142,92 +123,332 @@ The `TbotsProto::RobotStatus` protobuf message contains information about the st * The capacitor charge on the robot * The temperature of the dribbler motor -Information about the robot status is communicated and stored as `RobotStatus` protobuf messages. [Thunderscope](#thunderscope) displays warnings from incoming `RobotStatus`es so we can take appropriate action. For example, during a game we may get a "Low battery warning" for a certain robot, and then we know to substitute it and replace the battery before it dies on the field. +Information about the robot status is communicated and stored as `RobotStatus` protobuf messages. [Thunderscope](#thunderscope) displays warnings from incoming `RobotStatus` messages so we can take appropriate action. For example, during a game we may get a "low battery warning" for a certain robot, so we know to substitute it and replace the battery before it dies on the field. -# Design Patterns -Below are the main design patterns we use in our code, and what they are used for. -## Abstract Classes and Inheritance -Abstract classes let us define interfaces for various components of our code. Then we can implement different objects that obey the interface, and use them interchangeably, with the guarantee that as long as they follow the same interface we can use them in the same way. +# Conventions -Read https://www.geeksforgeeks.org/inheritance-in-c/ for more information. +Below documents various conventions we use and follow in our software. -Examples of this can be found in many places, including: -* [Plays](#plays) -* [Tactics](#tactics) -* [Intents](#intents) -* Different implementations of the [Backend](#backend) +## Coordinates +We use a slightly custom coordinate convention to make it easier to write our code in a consistent and understandable way. This is particularly important for any code handling gameplay logic and positions on the field. +The coordinate system is a simple 2D x-y plane. The x-dimension runs between the friendly and enemy goals, along the longer dimension of the field. The y-dimension runs perpendicular to the x-dimension, along the short dimension of the field. -## Singleton Pattern -The Singleton pattern is useful for having a single, global instance of an object that can be accessed from anywhere. Though it's generally considered an anti-pattern (aka _bad_), it is useful in specific scenarios. +Because we have to be able to play on either side of a field during a game, this means the "friendly half of the field" will not always be in the positive or negative x part of the coordinate plane. This inconsistency is a problem when we want to specify points like "the friendly net", or "the enemy corner". We can't simple say the friendly net is `(-4.5, 0)` all the time, because this would not be the case if we were defending the other side of the field where the friendly net would be `(4.5, 0)`. -Read https://refactoring.guru/design-patterns/singleton for more information. +In order to overcome this, our convention is that: +* The **friendly half** of the field is **always negative x**, and the **enemy half** of the field is **always positive x** +* `y` is positive to the "left" of someone looking at the enemy goal from the friendly goal +* The center of the field (inside the center-circle) is the origin / `(0, 0)` -We use the Singleton pattern for our logger. This allows us to create a single logger for the entire system, and code can make calls to the logger from anywhere, rather than us having to pass a `logger` object literally everywhere. +This is easiest to understand in the [diagram](#convention-diagram) below. +Based on what side we are defending, [Sensor Fusion](#sensor-fusion) will transform all the coordinates of incoming data so that it will match our convention. This means that from the perspective of the rest of the system, the friendly half of the field is always negative x and the enemy half is always positive x. Now when we want to tell a robot to move to the friendly goal, we can simply tell it so move to `(-4.5, 0)` and we know this will _always_ be the friendly side. All of our code is written with the assumption in mind. -## Factory Pattern -The Factory pattern is useful for hiding or abstracting how certain objects are created. +## Angles +Going along with our coordinate convention, we have a convention for angles as well. An Angle of `0` is along the positive x-axis (facing the enemy goal), and positive rotation is counter-clockwise (from a perspective above the field, looking at it like a regular x-y plane where +y is "up"). See the [diagram](#convention-diagram) below. -Read the Refactoring Guru articles on the [Factory Method pattern](https://refactoring.guru/design-patterns/factory-method) and the [Abstract Factory pattern](https://refactoring.guru/design-patterns/abstract-factory) for more information. +Because of our [Coordinate Conventions](#coordinates), this means that an angle of `0` will always face the enemy net regardless of which side of the field we are actually defending. -Because the Factory needs to know about what objects are available to be created, it can be taken one step further to auto-register these object types. Rather than a developer having to remember to add code to the Factory every time they create a new class, this can be done "automatically" with some clever code. This helps reduce mistakes and saves developers work. +## Convention Diagram +![Coordinate Convention Diagram](images/coordinate_and_angle_convention_diagram.svg) -Read http://derydoca.com/2019/03/c-tutorial-auto-registering-factory/ for more information. +# Fullsystem -The auto-registering factory is particularly useful for our `PlayFactory`, which is responsible for creating [Plays](#plays). Every time we run our [AI](#ai) we want to know what [Plays](#plays) are available to choose from. The Factory pattern makes this really easy, and saves us having to remember to update some list of "available Plays" each time we add or remove one. +**Fullsystem** processes data and makes decisions for a [team](#team) of [robots](#robot). It manages [Sensor Fusion](#sensor-fusion), which is responsible for processing and filtering raw data, and the [AI](#ai) that makes gameplay decisions. -The Factory pattern is also used to create different [Backends](#backend) +Data within Fullsystem is shared between components using the [observer pattern](#observer-pattern); for instance, [Sensor Fusion](#sensor-fusion) and the [Backend](#backend) are `Subject`s that the [AI](#ai) observes. +## Backend +Fullsystem contains a `Backend` responsible for all communication with the "outside world". The responsibilities of the `Backend` can be broken down into communication using `SensorProto` and [Primitives](#primitives) messages: -## Visitor Pattern -The Visitor pattern is useful when we need to perform different operations on a group of "similar" objects, like objects that inherit from the same parent class (e.g. [Tactic](#tactics)). We might only know all these objects are a [Tactic](#tactic), but we don't know specifically which type each one is (eg. `AttackerTactic` vs `ReceiverTactic`). The Visitor Pattern helps us "recover" that type information so we can perform different operations on the different types of objects. It is generally preferred to a big `if-block` with a case for each type, because the compiler can help warn you when you've forgotten to handle a certain type, and therefore helps prevent mistakes. +* Upon receiving the following messages from the network, the `Backend` will store it in a `SensorProto` message and send it to [Sensor Fusion](sensor-fusion): + * Robot status messages + * Vision data about where the robots and ball are (typically from [SSL-Vision](#ssl-vision)) + * Referee commands (typically from the [SSL-Gamecontroller](#ssl-gamecontroller) -Read https://refactoring.guru/design-patterns/visitor for more information. +* Upon receiving [Primitives](#primitives) from the [AI](#ai), `Backend` will send the primitives to the robots or the [Simulator](#simulator). -An example of where we use the Visitor pattern is in our `MotionConstraintVisitor`. This visitor allows us to update the current set of motion constraints based on the types of tactics that are currently assigned. +The `Backend` was designed to be a simple interface that handles all communication with the "outside world", allowing for different implementations that can be swapped out in order to communicate with different hardware/ protocols/programs. -## Observer Pattern -The Observer pattern is useful for letting components of a system "notify" each other when something happens. Read https://refactoring.guru/design-patterns/observer for a general introduction to the pattern. +#### Backend Diagram +![Backend Diagram](images/backend_diagram.svg) -Our implementation of this pattern consists of two classes, `Observer` and `Subject`. `Observer`s can be registered with a `Subject`, after which new values will be sent from each `Subject` to all of it's registered `Observer`s. Please see the headers of both classes for details. Note that a class can extend both `Observer` and `Subject`, thus receiving and sending out data. In this way we can "chain" multiple classes. +## Sensor Fusion +`Sensor Fusion` is responsible for processing the raw data contained in SensorProto into a coherent snapshot of the [World](#world) that the [AI](#ai) can use. It invokes filters to update components of [World](#world), and then combines the components to send out the most up-to-date version. -### Threaded Observer -In our system, we need to be able to do multiple things (receive camera data, run the [AI](#ai), send commands to the robots) at the same time. In order to facilitate this, we extend the `Observer` to the `ThreadedObserver` class. The `ThreadedObserver` starts a thread with an infinite loop that waits for new data from `Subject` and performs some operation with it. +### World +The `World` class is what we use to represent the state of the world at any given time. In this context, the world includes the positions and orientations of all robots on the field, the position and velocity of the ball, the dimensions of the field being played on, and the current referee commands. Altogether, it's the information we have at any given time that we can use to make decisions. -**WARNING:** If a class extends multiple `ThreadedObserver`s (for example, [AI](#ai) could extend `ThreadedObserver` and `ThreadedObserver`), then there will be two threads running, one for each observer. We **do not check** for data race conditions between observers, so it's entirely possible that one `ThreadedObserver` thread could read/write from data at the same time as the other `ThreadedObserver` is reading/writing the same data. Please make sure any data read/written to/from multiple `ThreadedObserver`s is thread-safe. +A `World` is composed of the following classes: -One example of this is [SensorFusion](#sensor-fusion), which extends `Subject` and the [AI](#ai), which extends `ThreadedObserver`. [SensorFusion](#sensor-fusion) runs in one thread and sends data to the [AI](#ai), which receives and processes it another thread. +#### Team +A `Team` class represents a collection of [Robots](#robot). -## Publisher-Subscriber Pattern +#### Robot +A `Robot` class represents the state of a single robot on the field. This includes its position, orientation, velocity, angular velocity, and any other information about its current state. -The publisher-subscriber pattern ("pub-sub") is a messaging pattern for facilitating communication between different components. It is closely related to the [message queue](https://en.wikipedia.org/wiki/Message_queue) design pattern. +#### Ball +The `Ball` class represents the state of the ball. This includes its position and velocity, and any other information about its current state. -In this pattern, `Publisher`s send messages without knowing who the recipients (`Subscriber`s) are. `Subscriber`s express interest in specific types of messages by subscribing to relevant topics; when a `Publisher` sends a message of a topic, the messaging system ensures that all interested subscribers receive the message. +#### Field +The `Field` class represents the state of the physical field being played on, which is primarily its physical dimensions. The `Field` class provides many functions that make it easy to get points of interest on the field, such as the enemy net, friendly corner, or center circle. Also see the [coordinate convention](#coordinates) we use for the field (and all things on it). -Read https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern for an introduction to the pub-sub pattern. +#### Game State +The `GameState` class represents the current state of the game as dictated by the Gamecontroller. These provide functions like `isPlaying()`, `isHalted()` which tell the rest of the system what game state we are in, and make decisions accordingly. We need to obey the rules! -We use the pub-sub pattern to facilitate [inter-process communication](#inter-process-communication) in our system. Through a class called [`ProtoUnixIO`](../src/software/thunderscope/proto_unix_io.py), components can subscribe to receive certain [Protobuf](#protobuf) message types sent out by other processes or system components. +### Filters +Filters take the raw data from SensorProto and returns an updated version of a component of the [World](#world). For example, the `BallFilter` takes `BallDetection`s and returns an updated `Ball`. -## C++ Templating -While debatably not a design pattern depending on who you ask, templating in C++ is a powerful tool that is very useful to understand. [https://www.geeksforgeeks.org/templates-cpp/] gives a great explanantion and example. +> **Why we need to do this:** Programs that provide data like [SSL-Vision](#ssl-vision) only provide raw data. This means that if there are several orange blobs on the field, [SSL-Vision](#ssl-vision) will tell us the ball is in several different locations. It is up to us to filter this data to determine the "correct" position of the ball. The same idea applies to robot positions and other data we receive. + +Filters provide a flexible way to modularize the processing of raw data, making it easy to update filters and add new ones. Filters are sometimes stateful. For example, the `BallFilter` "remembers" previous locations of the ball in order to estimate the ball's current velocity. + + +# AI +The **AI** is the part of the [Fullsystem](#fullsystem) where all of our gameplay logic takes place, and it is the main "brain" of our system. It uses the information received from [Sensor Fusion](#sensor-fusion) to make decisions, and then sends [Primitives](#primitives) to the [Backend](#backend) for the robots to execute. Altogether, this feedback loop is what allows us to react to what's happening on the field and play soccer in real-time. + +The two main components of the AI are [strategy](#strategy) and [motion planning](#motion-planning). + +## Strategy +We use a framework called **STP (Skills, Tactics, Plays)** to organize and implement our gameplay strategy. The STP framework was originally proposed by Carnegie Mellon University back in 2004. The original paper can be found [here](https://kilthub.cmu.edu/articles/STP_Skills_Tactics_and_Plays_for_Multi-Robot_Control_in_Adversarial_Environments/6561002/1). + +STP is a way of breaking down roles and responsibilities into a simple hierarchy, making it easier to build up more complex strategies from simpler pieces. This is the core of where our strategy is implemented. + +### STP Diagram +The STP diagram shows how this works. Functions to assign tactics to robots and build motion constraints are passed into a `Play`'s `get` function, which the `Play` uses to generate tactics with assigned robots and with updated motion constraints. + +![STP Diagram](images/STP.svg) -We use templating in a few places around the codebase, with the most notable examples being our [Factory Design Patterns](#factory-pattern), and our `Gradient Descent` optimizer. +### Skills +The S in STP stands for **Skills**. A Skill represents a lower-level behaviour that a robot can execute. Examples include: +- Moving to a position (without colliding with anything) +- Shooting the ball at a target +- Chipping the ball towards a target -# Coroutines -## What Are Coroutines? +When we refer to Skills in our AI, we typically are talking about [Primitives](#primitives), which are messages representing low-level actions that can be sent to and interpreted by our robots directly. We may also sometimes use the term Skill to describe simpler, skill-like [Tactics](#tactics) that other Tactics are composed from (see [Hierarchical Tactic FSMs](#hierarchical-tactic-fsms)). + +### Tactics +The T in STP stands for **Tactics**. A Tactic represents a single robot's role on a team. Examples include: + +- Being a goalie +- Being an attacker +- Being a pass receiver +- Being a defender that shadows enemy robots +- Being a defender that tries to steal the ball from enemies + +Tactics return [Primitives](#primitives) describing the low-level actions the robot should take as the Tactic. Every Tactic has an associated Tactic FSM (see [Finite State Machines](#finite-state-machines)) that it uses to decide what Primitive to execute based on the state of the game; the FSM is updated with the current [World](#world) and returns a Primitive to execute. + +#### Tactic Assignment + +A Tactic by itself is not directly assigned to any single robot. Instead, `Tactic::get` returns a map associating each and every friendly robot with the [Primitive](#primitives) they would execute *if* that robot was assigned that Tactic. We then say that every Primitive has some *cost* quantifying how easy it is for the robot to perform the action. For example, a `MovePrimitive` may incur a larger cost depending on the distance between the robot and the `MovePrimitive`'s target position, reason being that it is in some sense "harder" for a robot to reach a faraway target successfully. With all this information, the [Play](#plays) assigns one robot to every Tactic based on the Primitives that would be executed, in such a way that the *total cost* incurred is minimized; this is a form of the classic [assignment problem](https://en.wikipedia.org/wiki/Assignment_problem) which we solve using the [Hungarian algorithm](https://en.wikipedia.org/wiki/Hungarian_algorithm). + +In order to produce the map returned by `Tactic::get`, every Tactic must have one Tactic FSM for every robot. Each FSM tracks the state of the Tactic for one robot and produces the Primitive that the robot would execute. + +#### Hierarchical Tactic FSMs + +Tactics FSMs may make use of other simpler Tactic FSMs that implement common behaviours. These simpler Tactics can be thought of as [Skills](#skills) since they do not represent any strategic role, but instead execute a well-defined action. For example: + +- `MoveTactic` simply returns a `MovePrimitive` constructed using the `MoveTactic`'s [control parameters](#control-parameters) (which match `MovePrimitive`'s constructor parameters). +- `PivotKickTactic` approaches the ball, pivots around the ball to face towards a specified target, and then kicks the ball in that direction. + +The FSMs of these simpler Tactics are used as "sub-FSMs" and are treated as a state in more complex Tactic FSMs (read about [hierarchical FSMs in the FSM section](#what-are-finite-state-machines)). When a Tactic FSM is in one of its sub-FSM states, it should update the sub-FSM with the [World](#world) and return the Primitive returned by the sub-FSM. This means that the Tactic FSM will effectively "run" the sub-FSM and do its bidding while in the sub-FSM state, and it will leave the state once the sub-FSM terminates (e.g. `MoveTacticFSM` terminates once the robot has successfully reached the target destination). + +This pattern promotes code reuse, allowing us to compose complex Tactic FSMs from smaller, skill-like Tactic FSMs that can be reused in different contexts. + +#### Control Parameters + +Every Tactic defines its own custom `ControlParams` struct representing the specific settings and parameters we can set to influence the Tactic's behavior. At every timestep, alongside the current [World](#world), we update the Tactic's FSM with `ControlParams` carrying information about what want we want the Tactic to do (e.g. defend against a certain enemy robot). Tactics making use of other Tactics ([as sub-FSMs](#hierarchical-tactic-fsms)) can use control parameters to control the behaviour of the sub-FSM, and [Plays](#plays) can set each of its Tactics' control params to coordinate responsibilities (e.g. tell each defensive Tactic to defend against a different enemy robot) + +### Plays + +The P in STP stands for **Plays**. A Play represents a "team-wide goal" for the robots. They can be thought of much like plays in real-life soccer. Examples include: + +- A play for taking friendly free kicks +- A play for defending enemy kickoffs +- A general defense play +- A passing-based offense play +- A dribbling-based offense play + +Plays are made up of [Tactics](#tactics). A Play chooses which Tactics it wants to assign based on the state of the game. This logic is implemented in an associated Play FSM (see [Finite State Machines](#finite-state-machines)) that gets updated with the current [World](#world) and returns a list of Tactics to assign. The Play then attempts to assign robots to these desired Tactics in an optimal manner (see [Tactic Assignment](#tactic-assignment)). + +Plays return `PrimitiveSet`s that map each robot to the [Primitive](#primitives) they should execute. These Primitives are produced by the [Tactics](#tactics) assigned by the Play to each robot. + +The AI chooses which Play to run using `PlaySelectionFSM`. `PlaySelectionFSM` is updated with the current World and returns the Play to run based on the current game state. + +## Finite State Machines + +### What Are Finite State Machines? +A finite state machine (FSM) is a system with a finite number of states with defined transitions and outputs based on the inputs to the system. A FSM is in exactly one of its states at any time, and it can change (transition) from its current state to another in response to some input. In particular, we are interested in defining _when_ a FSM should transition to another state (_guards_) and _what_ should happen when transitions occur (_actions_), given a specific input (_event_). + +Hierarchical state machines are state machines that are composed of one or more FSMs, which we call sub-FSMs. The parent FSM can treat a sub-FSM as a state with _guards_ and _actions_ when transitioning to and from the sub-FSM. When the sub-FSM enters a terminal state, the parent FSM is able to automatically transition to another state. + +![Finite State Machine Diagram](images/finite_state_machine_diagram.png) +[source](https://www.block-net.de/Programmierung/cpp/fsm/fsm.html) + +### Boost-ext SML Library +We use [Boost-ext SML](https://github.com/boost-ext/sml), short for State Machine Library, to create our finite state machines. This library defines state machines through a transition table, where a row indicates the transition from one state to another state using _guards_, _actions_ and _events_. The syntax of a row of the transition table looks like this: +``` +src_state + event [guard] / action = dest_state +``` +where the `src_state` transitions to the `dest_state`, while performing the `action`, only if the `event` is received and the `guard` is true. _Events_ are structs of new information that FSMs receive, and _guards_ and _actions_ are functions that take events as arguments. _Guards_ must return a boolean and _actions_ must return void. An asterix (\*) at the start of a row indicates that the state is an initial state. The rows of the transition table are processed in order from top to bottom and the first row to match is executed. + +The library also supports hierarchical FSMs. Sub-FSMs are treated as states where an unconditional transition occurs when the sub-FSM is in the terminal state, `X`. +```c++ +const auto SubFSM_S = boost::sml::state; + +// ...in the transition table... +SubFSM_S = next_state // Transitions to next_state only when the SubFSM is in the terminal state, X +``` +In order to update a sub-FSM with an event, we need to do the following: +```c++ +const auto update_sub_fsm_action = + [](auto event, boost::sml::back::process processEvent) { + TypeOfSubFSMEvent sub_fsm_event = // initialize the sub-FSM event + processEvent(sub_fsm_event); + }; + +// ...in the transition table... +SubFSM_S + event / update_sub_fsm_action +// When the parent FSM is updated with an event and it is +// in the SubFSM_S state, the update_sub_fsm_action will be +// invoked in the parent FSM. In turn, the update_sub_fsm_action +// will update the SubFSM with an event by calling processEvent +``` +The convenience of this syntax comes at the cost of hard to read error messages due to the functor and templating system. + +### How Do We Use SML? +We use SML to implement our [Plays](#plays) and [Tactics](#tactics). Each state represents a stage in the play/tactic where the team/robot should be doing a particular thing or looking for certain conditions to be true. A simple example of this is the `MoveFSM`. While the robot is not at the destination and oriented correctly, the FSM is in the move state. Once the robot reaches its destination, it enters the terminal state, `X`, to indicate that it's done. SML also allows us to easily reuse FSMs in other tactics. For example, if a shadowing tactic needs to move to a particular destination with a certain orientation, then it can use the MoveFSM as a sub-FSM state (see [Hierarchical Tactic FSMs](#hierarchical-tactic-fsms)). + +If you're having trouble understanding the SML syntax, take a look at [`docs/fsm-diagrams.md`](./fsm-diagrams.md). It contains automatically generated diagrams of the FSMs in our codebase, which can help with visualizing the FSM control flow. + +### SML Best Practices +Boost-ext SML is a library that supports complex functionality with similarly complex syntax and semantics. If complex syntax is misused, the complicated error messages can make development difficult. Thus, we have carefully chosen a standardized subset of the library's syntax to implement our functionality while maintaining high readability. + +* Define _guards_ and _actions_ outside of the transition table: The names of _guards_ and _actions_ should be succinct so that transition tables rows fit on one line and readers can easily understand the FSM from the transition table. In other words, do not insert lambdas/anonymous functions directly in transition tables. + + To aid with this, we have **macros** in [`software/util/sml_fsm/sml_fsm.h`](../src/software/util/sml_fsm/sml_fsm.h) that generate named lambda wrappers around guard and action methods you define in your FSM struct. There are also macros for generating state and events that will be compatible with the SML library. + + ```c++ + struct KickFSM + { + // Define states inside the FSM struct + class KickState; + + // Define an event for this FSM + class Update; + + // Define your guards and actions as member functions + bool isBallInDribbler(Update event); + void kickBall(Update event); + + auto operator()() + { + using namespace boost::sml; + + DEFINE_SML_STATE(KickState) + // Equivalent to: + // const auto KickState_S = boost::sml::state; + + DEFINE_SML_EVENT(Update) + // Equivalent to: + // const auto Update_E = boost::sml::event; + + DEFINE_SML_GUARD(isBallInDribbler) + // Equivalent to: + // const auto isBallInDribbler_G = [this](auto event) { return isBallInDribbler(event); }; + + DEFINE_SML_ACTION(kickBall) + // Equivalent to: + // const auto kickBall_A = [this](auto event) { kickBall(event); }; + + // You can use the macro-generated states, events, guards, and actions + // in the transition table + return make_transition_table( + *KickState_S + Update_E[isBallInDribbler_G] / kickBall_A, + ); + } + }; + + // We can instantiate this FSM and update it with an event + FSM fsm(KickFSM()); + fsm.process_event(KickFSM::Update()); + ``` + We also have macros for working with sub-FSMs: + ```c++ + struct KickFSM + { + class Update; + + // Define an action that will update the sub-FSM + void moveToKickOrigin(Update event, + boost::sml::back::process processEvent) + { + // Make sure to call processEvent to update the sub-FSM! + processEvent(MoveFSM::Update()); + } + + auto operator()() + { + using namespace boost::sml; + + // Declare the sub-FSM as a state + DEFINE_SML_STATE(MoveFSM) + + DEFINE_SML_EVENT(Update) + + // Similar to DEFINE_SML_ACTION -- generates moveToKickOrigin_A + DEFINE_SML_SUB_FSM_UPDATE_ACTION(moveToKickOrigin) + + return make_transition_table( + *MoveFSM_S + Update_E / moveToKickOrigin_A + ); + } + }; + + // When instantiating an FSM with sub-FSMs, you need to + // pass in instances of all the sub-FSMs structs along with the + // parent FSM struct into the FSM constructor + FSM fsm(KickFSM(), MoveFSM()); + ``` +* Avoid entry and exit actions: these are actions that are always executed when entering or exiting a state. Everything that can be implemented with entry and exit actions can easily be implemented as actions, so this rule reduces source of confusion for the reader. +* Avoid self transitions, i.e. `src_state + event [guard] / action = src_state`: self transitions call entry and exit actions, which complicates the FSM. If we want a state to stay in the same state while performing an action, then we should use an internal transition, i.e. `src_state + event [guard] / action`. +* Avoid orthogonal regions: Multiple FSMs running in parallel is hard to reason about and isn't necessary for implementing single robot behaviour. Thus, only prefix one state with an asterix (\*) so that there is only one initial state. +* Use callbacks in _events_ to return information from the FSM: Since the SML library cannot directly return information, we need to return information through callbacks. For example, if we want to return a double from an FSM, we can pass in `std::function callback` as part of the event and then make the _action_ call that function with the value we want returned. +* When a variable needs to be shared between multiple states or can be initialized upon construction of the FSM, then define a private member and constructor in the FSM struct, and pass that in when constructing the FSM. Here's a code snippet: + ```c++ + struct DriveForwardFSM + { + public: + DriveForwardFSM(double max_speed): max_speed(max_speed){} + // ... + private: + double max_speed; + } + + FSM fsm(DriveForwardFSM(10.0)); + ``` + +## Coroutines + +> [!IMPORTANT] +> We are currently in the process of moving away from using coroutines and transitioning to using [finite-state machines](#finite-state-machines) for all our STP logic. + +### What Are Coroutines? Coroutines are a general control structure where the flow control is cooperatively passed between two different routines without returning, by allowing execution to be suspended and resumed. This is very similar to the `yield` statement and generators in `Python`. Rather than using the `return` keyword to return data, coroutines use the `yield` keyword. The main difference is that when `return` is encountered, the data is returned and the function terminates. If the function is called again, it starts back from the beginning. On the other hand, when `yield` is encountered some data is returned, but the state of the function / coroutine is saved and the function does not terminate. This means that when the function is called again, execution resumes immediately after the `yield` statement that previously returned the data, with all the previous context (variables, etc) as if the function never stopped running. This is the "suspend and resume" functionality of coroutines. See the following C++ pseudocode for an example. This coroutine function computes and returns the fibonacci sequence. -``` -int fib(Coroutine::push_type& yield) { +```c++ +int fib(Coroutine::push_type& yield) +{ int f1 = 1; int f2 = 0; - while(true) { + while (true) + { int fn = f1 + f2; // Compute the next value in the sequence f2 = f1; // Save the previous 2 values f1 = fn; @@ -235,7 +456,8 @@ int fib(Coroutine::push_type& yield) { } } -int main() { +int main() +{ // Coroutine setup stuff // Lets pretend that we have created the Coroutine and called it `yield` std::cout << fib(yield) << std::endl; // Prints 1 @@ -249,39 +471,43 @@ int main() { Lets walk through what's happening here: 1. The first time the `fib` function is called, the variables `f1` and `f2` are initialized, and we go through the first iteration of the loop until `yield` is encountered 2. The `yield` statement is going to return the currently computed value of the fibonacci sequence (the variable `fn`) and save the state of the `fib` function - * "yielding" the data here is effectively returning it so that the code in the `main` function can print the result +* "yielding" the data here is effectively returning it so that the code in the `main` function can print the result 3. The second time `main()` calls the `fib()` function, the function will resume immediately after the `yield()` statement. This means that execution will go back to the top of the loop, *and still remember the values of `f1` and `f2` from the last time the function was called*. Since the coroutine saved the function state, it still has the previous values of `f1` and `f2` which it uses to compute the next value in the sequence. 4. Once again when the `yield()` statement is reached, the newly computed value is returned and the function state is saved. You can think of this as "pausing" the function. 5. As `main()` keeps calling the `fib()` function, it is computing and returning the values of the fibonacci sequence, and this only works because the coroutine "remembers" the values from each previous fibonacci computation which it uses to compute the next value the next time the function is called. - * If the `yield` was replaced with a regular `return` statement, the function would only ever return the value `1`. This is because using `return` would not save the function state, so the next time it's called the function would start at the beginning again, and only ever compute the first value of the sequence. +* If the `yield` was replaced with a regular `return` statement, the function would only ever return the value `1`. This is because using `return` would not save the function state, so the next time it's called the function would start at the beginning again, and only ever compute the first value of the sequence. This example / pseudocode does hide away some details about how coroutines are set up and how we extract values from them, but it's most important to understand how coroutines change the flow of control in the program. -## What Coroutines Do We Use? +### What Coroutines Do We Use? We use the [boost Coroutine2 library](https://www.boost.org/doc/libs/1_71_0/libs/coroutine2/doc/html/index.html). Specifically, we use Asymmetric Coroutines. -[This stackoverfow answer](https://stackoverflow.com/a/42042904) gives a decent explanation of the difference between Symmetric and Asymmetric Coroutines, but understanding the difference is not critical for our purposes. We use Asymmetric Coroutines because boost does not provide Symmetric Coroutines, and the hierarchical structure of Asymmetric Coroutines is more useful to us. +[This Stack Overflow answer](https://stackoverflow.com/a/42042904) gives a decent explanation of the difference between Symmetric and Asymmetric Coroutines, but understanding the difference is not critical for our purposes. We use Asymmetric Coroutines because boost does not provide Symmetric Coroutines, and the hierarchical structure of Asymmetric Coroutines is more useful to us. -## How Do We Use Coroutines? -We use Coroutines to write our [strategy logic](#strategy). The "pause and resume" functionality of Coroutines makes it much easier to write [Plays](#plays). +### How Do We Use Coroutines? + +We use Coroutines to write some of our [strategy logic](#strategy). The "pause and resume" functionality of Coroutines makes it much easier to write [Plays](#plays). Specifically, we use Coroutines as a way to break down our strategy into "stages". Once a "stage" completes we generally don't want to re-evaluate it, and would rather commit to a decision and move on. Coroutines makes it much easier to write "stages" of strategy without requiring complex state machine logic to check what stage we are in, and it's easier for developers to see what the intended order of operations is (eg. "Line up to take the shot" -> "shoot"). In the past, we had issues with our gameplay logic "committing" to decisions if we were near certain edge cases. This caused robots to behave oddly, and sometimes get significantly slowed down in "analysis paralysis". Coroutines solve this problem by allowing us to write "stages" that execute top-to-bottom in a function, and once we make a decision we commit to it and move on to the next stage. Here's a more specific example. In this example we are going to pretend to write a [Tactic](#tactic) that will pass the ball. -``` -def executeStrategy(IntentCoroutine::push_type& yield, Pass pass) { - do { +```c++ +def executeStrategy(IntentCoroutine::push_type& yield, Pass pass) +{ + do + { yield(/* align the robot to make the pass */) - }while(current_time < pass.start_time); + } while (current_time < pass.start_time); - do { + do + { yield(/* kick the ball at the pass location */) - }while(/* robot has not kicked the ball */) + } while (/* robot has not kicked the ball */) } ``` We will pretend that this function is getting called 30 times per second to get the most up-to-date gameplay decision. @@ -292,7 +518,7 @@ Once it is time to start the pass, the condition for the loop will become false Once we have entered the second stage, we know we don't have to look at the first stage again. Because the coroutine "remembers" where the execution is each time the function is called, we will resume inside the second stage and therefore never execute the first stage again! This makes it much easier to write and read this strategy code, because we can clearly see the 2 stages of the strategy, and we know they will be executed in order. -## Coroutine Best Practices +### Coroutine Best Practices Coroutines are a complex feature, and the boost coroutines we use don't always behave in was we expect. We have done extensive testing on how coroutines are safe (or not safe) to us, and derived some best practices from these examples. See [coroutine_test_exmaples.cpp](coroutine_test_examples.cpp) for the full code and more detailed explanantions. To summarize, the best practices are as follows: @@ -300,217 +526,39 @@ To summarize, the best practices are as follows: 2. Avoid using coroutines with resizable containers. If they must be used, make sure that the coroutines are allocated on the heap. 3. Pass data to the coroutine on creation as much as possible, avoid using member variables. -# Finite State Machines -## What Are Finite State Machines? -A finite state machine (FSM) is a system with a finite number of states with defined transitions and outputs based on the inputs to the system. In particular, we are interested in hierarchical state machines where we can transition between states in terms of _when_ states should transition (_guards_) and _what_ should happen when transitions occur (_actions_), given a specific input (_event_). Hierarchical state machines are state machines that are composed of one or more FSMs, which we call sub-FSMs. The parent FSM can treat a sub-FSM as a state with _guards_ and _actions_ when transitioning to and from the sub-FSM. When the sub-FSM enters a terminal state, the parent FSM is able to automatically transition to another state. - -![Finite State Machine Diagram](images/finite_state_machine_diagram.png) -[source](https://www.block-net.de/Programmierung/cpp/fsm/fsm.html) - -## Boost-ext SML Library -We use the [Boost-Ext SML](https://github.com/boost-ext/sml), short for State Machine Library, to manage our finite state machines. This library defines state machines through a transition table, where a row indicates the transition from one state to another subject to _guards_, _actions_ and _events_. The syntax of a row of the transition table looks like this: -``` -src_state + event [guard] / action = dest_state -``` -where the src\_state transitions to the dest\_state, while performing the _action_, only if the _event_ is processed and the _guard_ is true. Events are structs of new information that FSMs receive, so _guards_ and _actions_ take events as arguments. _Guards_ must return a boolean and _actions_ must return void. An asterix (\*) at the start of a row indicates that the state is an initial state. The rows of the transition table are processed in order and the first row to match is executed. - -The library also supports hierarchical FSMs. Sub-FSMs are treated as states where an unconditional transition occurs when the sub-FSM is in the terminal state, X. -``` -/* omitted rows of transition table */ -SubFSM = next_state, // Transitions to next_state only when the SubFSM is in the terminal state, X -/* omitted rows of transition table */ -``` -In order to update a subFSM with an event, we need to do the following: -``` -const auto update_sub_fsm_action = - [](auto event, back::process processEvent) { - TypeOfSubFSMEvent sub_fsm_event = // initialize the subFSM event - processEvent(sub_fsm_event); - }; -``` -The convenience of this syntax comes at the cost of hard to read error messages due to the functor and templating system. - -## How Do We Use SML? -We use SML to manage our [Tactics](#tactic). Each state represents a stage in the tactic where the robot should be doing a particular action or looking for certain conditions to be true. An example of this is the MoveFSM. While the robot is not at the destination and oriented correctly, the FSM is in the move state. Once the robot reaches its destination, it enters the terminal state, _X_, to indicate that it's done. SML also allows us to easily reuse FSMs in other tactics. For example, if a shadowing tactic needs to move to a particular destination with a certain orientation, then it can use the MoveFSM as a sub-FSM state. - -## SML Best Practices -Boost-ext SML is a library that supports complex functionality with similarly complex syntax and semantics. If complex syntax is misused, the complicated error messages can make development difficult. Thus, we need to carefully choose a standardized subset of the library's syntax to implement our functionality while maintaining high readability. -* Only use one _event_ per FSM: In gameplay, we react to changes in the [World](#world), so since there's only one source of new information, we should only need one _event_ -* Only one _guard_ or _action_ per transition: For readability of the transition table, we should only have one _guard_ or _action_ per transition. This can always be achieved by defining a _guard_ or _action_ outside of the transition table that checks multiple conditions or performs multiple actions if that's required. -* Define _guards_ and _actions_ outside of the transition table: The names of _guards_ and _actions_ should be succinct so that transition tables rows fit on one line and readers can easily understand the FSM from the transition table. In other words, no lambdas/anonymous functions in transition tables. -* States should be defined as classes in the FSM struct so that users of the FSM can check what state the FSM is in: -``` - // inside the struct - class KickState; - // inside the operator()() - const auto kick_s = state; - // allows for this syntax - fsm.is(boost::sml::state) -``` -* Avoid entry and exit conditions: Everything that can be implemented with entry and exit conditions can easily be implemented as actions, so this rule reduces source of confusion for the reader -* Avoid self transitions, i.e. `src_state + event [guard] / action = src_state`: self transitions call entry and exit conditions, which complicates the FSM. If we want a state to stay in the same state while performing an action, then we should use an internal transition, i.e. `src_state + event [guard] / action`. -* Avoid orthogonal regions: Multiple FSMs running in parallel is hard to reason about and isn't necessary for implementing single robot behaviour. Thus, only prefix one state with an asterix (\*) -* Use callbacks in _events_ to return information from the FSM: Since the SML library cannot directly return information, we need to return information through callbacks. For example, if we want to return a double from an FSM, we can pass in `std::function callback` as part of the event and then make the _action_ call that function with the value we want returned. -* When a variable needs to be shared between multiple states or can be initialized upon construction of the FSM, then define a private member and constructor in the FSM struct, and pass that in when constructing the FSM. Here's a code snippet: -``` -(drive_forward_fsm.h) -DriveForwardFSM -{ - public: - DriveForwardFSM(double max_speed): max_speed(max_speed){} - private: - double max_speed; -} -(drive_forward_tactic.h) - FSM fsm; -(drive_forward_tactic.cpp: constructor) - fsm(DriveForwardFSM(10.0)) -``` - -# Conventions -Various conventions we use and follow that you need to know. - -## Coordinates -We use a slightly custom coordinate convention to make it easier to write our code in a consistent and understandable way. This is particularly important for any code handling gameplay logic and positions on the field. - -The coordinate system is a simple 2D x-y plane. The x-dimension runs between the friendly and enemy goals, along the longer dimension of the field. The y-dimension runs perpendicular to the x-dimension, along the short dimension of the field. - -Because we have to be able to play on either side of a field during a game, this means the "friendly half of the field" will not always be in the positive or negative x part of the coordinate plane. This inconsistency is a problem when we want to specify points like "the friendly net", or "the enemy corner". We can't simple say the friendly net is `(-4.5, 0)` all the time, because this would not be the case if we were defending the other side of the field where the friendly net would be `(4.5, 0)`. - -In order to overcome this, our convention is that: -* The **friendly half** of the field is **always negative x**, and the **enemy half** of the field is **always positive x** -* `y` is positive to the "left" of someone looking at the enemy goal from the friendly goal -* The center of the field (inside the center-circle) is the origin / `(0, 0)` +## Motion Planning -This is easiest to understand in the [diagram](#convention-diagram) below. +Our **motion planning** (navigation) system is responsible for trajectory planning and obstacle avoidance. -Based on what side we are defending, [Sensor Fusion](#sensor-fusion) will transform all the coordinates of incoming data so that it will match our convention. This means that from the perspective of the rest of the system, the friendly half of the field is always negative x and the enemy half is always positive x. Now when we want to tell a robot to move to the friendly goal, we can simply tell it so move to `(-4.5, 0)` and we know this will _always_ be the friendly side. All of our code is written with the assumption in mind. +A **trajectory** (or **motion profile**) describes the ideal motion that the robot should follow to get from one point to another point. It is a function of time that returns the target (a.k.a. reference) position, velocity, and acceleration for the given timestep. For every `MovePrimitive` (see [Primitives](#primitives)) indicating a target destination, our trajectory planner generates and stores with the `MovePrimitive` a corresponding trajectory that the robot can take to reach that destination. -## Angles -Going along with our coordinate convention, we have a convention for angles as well. An Angle of `0` is along the positive x-axis (facing the enemy goal), and positive rotation is counter-clockwise (from a perspective above the field, looking at it like a regular x-y plane where +y is "up"). See the [diagram](#convention-diagram) below. +### Trajectory Planner -Because of our [Coordinate Conventions](#coordinates), this means that an angle of `0` will always face the enemy net regardless of which side of the field we are actually defending. +Our trajectory planner is heavily based off of TIGERs Mannheim's trajectory planner (read their [2019 TDP](https://ssl.robocup.org/wp-content/uploads/2019/03/2019_ETDP_TIGERs_Mannheim.pdf)). At the most basic level, our trajector planner generates trapezoidal "bang-bang" trajectories. The name comes from the shape of the profile's speed vs. time graph (which is trapezoidal), and "bang-bang" refers to how the acceleration switches abruptly between zero and a fixed limit. -## Convention Diagram -![Coordinate Convention Diagram](images/coordinate_and_angle_convention_diagram.svg) +We create 2D trajectories by generating two 1D trajectories to describe the motion along each Cartesian coordinate axis over time. The two 1D trajectories are synchronized such that they complete in approximately the same time. We also generate a separate trajectory for the angular motion of the robot, describing how the robot's angular position, velocity, and acceleration should change over time. -# Architecture Overview - -At a high-level, our system is split into several independent processes that [communicate with each other](#inter-process-communication). Our architecture is designed in this manner to promote decoupling of different features, making our system easier to expand, maintain, and test. - -- [**Thunderscope**](#thunderscope) is main entry point of our system and provides the GUI for our software. - -- [**Fullsystem**](#fullsystem) is the "backend" that processes data and makes decisions for a [team](#team) of [robots](#robot). It manages [Sensor Fusion](#sensor-fusion), which is responsible for processing and filtering raw data, and the [**AI**](#ai) that makes gameplay decisions. - -- The [**Simulator**](#simulator) provides a physics simulation of the [World](#world), enabling testing of our gameplay when we don't have access to a real field. This process is optional and used only for development and testing purposes; in a real match, our system will receive data from [SSL-Vision](#ssl-vision). - -- [**Thunderloop**](/docs/robot-software-architecture.md#thunderloop) is responsible for coordinating communication between our [AI](#ai) computer and the motor and power boards in our robots. It is part our robot software architecture, which is documented [here](/docs/robot-software-architecture.md). - -# Fullsystem - -Fullsystem processes data and makes decisions for a [team](#team) of [robots](#robot). It manages [Sensor Fusion](#sensor-fusion), which is responsible for processing and filtering raw data, and the [AI](#ai) that makes gameplay decisions. - -Data within Fullsystem is shared between components using the [observer pattern](#observer-pattern); for instance, [Sensor Fusion](#sensor-fusion) and the [Backend](#backend) are `Subject`s that the [AI](#ai) observes. - -## Backend -Fullsystem contains a `Backend` responsible for all communication with the "outside world". The responsibilities of the `Backend` can be broken down into communication using `SensorProto` and [Primitives](#primitives) messages: - -* Upon receiving the following messages from the network, the `Backend` will store it in a `SensorProto` message and send it to [Sensor Fusion](sensor-fusion): - * Robot status messages - * Vision data about where the robots and ball are (typically from [SSL-Vision](#ssl-vision)) - * Referee commands (typically from the [SSL-Gamecontroller](#ssl-gamecontroller) - -* Upon receiving [Primitives](#primitives) from the [AI](#ai), `Backend` will send the primitives to the robots or the [Simulator](#simulator). - -The `Backend` was designed to be a simple interface that handles all communication with the "outside world", allowing for different implementations that can be swapped out in order to communicate with different hardware/ protocols/programs. - -#### Backend Diagram -![Backend Diagram](images/backend_diagram.svg) - -## Sensor Fusion -`Sensor Fusion` is responsible for processing the raw data contained in SensorProto into a coherent snapshot of the [World](#world) that the [AI](#ai) can use. It invokes filters to update components of [World](#world), and then combines the components to send out the most up-to-date version. - -### Filters -Filters take the raw data from SensorProto and returns an updated version of a component of the [World](#world). For example, the `BallFilter` takes `BallDetection`s and returns an updated `Ball`. - -> **Why we need to do this:** Programs that provide data like [SSL-Vision](#ssl-vision) only provide raw data. This means that if there are several orange blobs on the field, [SSL-Vision](#ssl-vision) will tell us the ball is in several different locations. It is up to us to filter this data to determine the "correct" position of the ball. The same idea applies to robot positions and other data we receive. - -Filters provide a flexible way to modularize the processing of raw data, making it easy to update filters and add new ones. Filters are sometimes stateful. For example, the `BallFilter` "remembers" previous locations of the ball in order to estimate the ball's current velocity. - - -# AI -The `AI` is the part of the [Fullsystem](#fullsystem) where all of our gameplay logic takes place, and it is the main "brain" of our system. It uses the information received from [Sensor Fusion](#sensor-fusion) to make decisions, and then sends [Primitives](#primitives) to the [Backend](#backend) for the robots to execute. Altogether, this feedback loop is what allows us to react to what's happening on the field and play soccer in real-time. - -The two main components of the `AI` are strategy and navigation. - -## Strategy -We use a framework called `STP (Skills, Tactics, Plays)` to implement our stratgy. The `STP` framework was originally proposed by Carnegie Mellon University back in 2004. The original paper can be found [here](https://kilthub.cmu.edu/articles/STP_Skills_Tactics_and_Plays_for_Multi-Robot_Control_in_Adversarial_Environments/6561002/1). - -`STP` is a way of breaking down roles and responsibilities into a simple hierarchy, making it easier to build up more complex strategies from simpler pieces. This is the core of where our strategy is implemented. - -When the [AI](#ai) is given new information and asked to make a decision, our `STP` strategy is what is executed first. It takes in a [World](#world) and returns [Intents](#intents). - -### STP Diagram -The STP diagram shows how this works. Functions to assign tactics to robots and build motion constraints are passed into a `Play`'s `get` function, which the `Play` uses to generate tactics with assigned robots and with updated motion constraints. - -![STP Diagram](images/STP.svg) - -### Tactics -The `T` in `STP` stands for `Tactics`. A `Tactic` represents a "single-robots' role" on a team. Examples include: -1. Being a goalie -2. Being a passer or pass receiver -3. Being a defender that shadows enemy robots -4. Being a defender that tries to steal the ball from enemies - -They can also represent lower level behaviours, such as -1. Moving to a position (without colliding with anything) -2. Shooting the ball at a target -3. Intercepting a moving ball - -The high level behaviours can use the lower level behaviours in a hierarchical way. - -Tactics use [Intents](#intents) to implement their behaviour, so that it can decouple strategy from the [Navigator](#navigation). - -### Plays -The `P` in `STP` stands for `Plays`. A `Play` represents a "team-wide goal" for the robots. They can be thought of much like Plays in real-life soccer. Examples include: -1. A Play for taking friendly free kicks -2. A Play for defending enemy kickoffs -3. A general defense play -4. A passing-based offense play -5. A dribbling-based offense play +The purpose of using trapezoidal trajectories is to decrease the amount of error that the [PID motor controller](https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller) has to correct for by making the target velocity closer to the motor's current velocity. If we instead had a constant velocity trajectory, the distance at the start between the motor's current velocity (at standstill) and the target velocity would be large, so the PID controller would issue a large motor input in response to the large error. Large motor inputs cause *slip*, leading to loss in traction and unpredictable movement. -Plays are made up of `Tactics`. Plays can have "stages" and change what `Tactics` are being used as the state of the game changes, which allows us to implement more complex behaviour. Read the section on [Coroutines](#coroutines) to learn more about how we write strategy with "stages". +### Trajectory Generation and Obstacle Avoidance -Furthermore, every play specifies an `Applicable` and `Invariant` condition. These are used to determine what plays should be run at what time, and when a Play should terminate. +Given a destination, the trajectory planner will generate a number of potential trajectories that will reach the destination. Each trajectory is scored based on its duration and whether it collides with any *obstacles*. The trajectory planner will return the trajectory with the best score. -`Applicable` indicates when a `Play` can be started. For example, we would not want to start a `Defense Play` if our team is in possession of the ball. The `Invariant` condition is a condition that must always be met for the `Play` to continue running. If this condition ever becomes false, the current `Play` will stop running and a new one will be chosen. For example, once we start running a friendly `Corner Kick` play, we want the `Play` to continue running as long as the enemy team does not have possession of the ball. +We check whether a trajectory collides with any obstacles on the field by stepping over the trajectory in fixed steps (e.g. 100 ms) and checking whether the trajectory's position at any step lies inside an obstacle. We penalize a trajectory's score based on the amount of time until the first collision. This is because a collision that occurs earlier in a trajectory is more likely to happen (for real) compared to a collision occurring later in a trajectory, since obstacles may move and the state of the World may change by the time that later collision would have happened. That is, we cannot reliably predict where obstacles will be in the faraway future, so we are less harsh in penalizing trajectories with late collisions. +Trajectories are generated by sampling intermediate *sub-destinations* around the current robot position. One trajectory is generated from the current robot position and velocity to the sub-destination, and then additional trajectories to the destination are generated starting from sampled locations along the trajectory to the sub-destination. We can then form a *trajectory path* by connecting a portion of the trajectory to the sub-destination end-to-end with the trajectory to the destination. This approach enables generation of many possible trajectory paths, with the hope being that some will avoid obstacles that would otherwise be encountered in a direct trajectory to the destination. -## Navigation -The `Navigator` is responsible for path planning and navigation. Once our strategy has decided what it wants to do, it passes the resulting [Intents](#intents) to the `Navigator`. The `Navigator` is then responsible for breaking down the [Intents](#intents) and turning them into [Primitives](#primitives). - -[DirectPrimitiveIntents](#intents) are easy to break down into [Primitives](#primitives), and can be converted directly without having to do any extra work. - -However, [NavigatingIntents](#intents) like the `MoveIntent` rely on the navigator to implement more complex behaviour like obstacle avoidance. In order for a robot to move to the desired destination of a [NavigatingIntents](#intents), the `Navigator` will use various path-planning algorithms to find a path across the field that does not collide with any robots or violate any restrictions set on the [NavigatingIntents](#intents). The `NavigatingPrimitiveCreator` then translates this path into a series of [Primitives](#primitives), which are sent to the robot sequentially so that it follows the planned path across the field. - -### Path Manager -The `Path Manager` is responsible for generating a set of paths that don't collide. It is given a set of [Path Objective](#path-objective)s and [Path Planner](#path-planner), and it will generate paths using the given path planner and arbitrate between paths to prevent collisions. - -### Path Objective -A path objective is a simple datastructure used to communicate between the navigator and the path manager. It conveys information for generating one path, such as start, destination, and obstacles. Path Objectives use very simple datastructures so that Path Planners do not need to know about any world-specific datastructures, such as Robots or the Field. - -### Path Planner -The `Path Planner` is an interface for the responsibility of path planning a single robot around a single set of obstacles from a given start to a given destination. The interface allows us to easily swap out path planners. +# Thunderscope -## [AI](#ai) Diagram -![AI Diagram](images/ai_diagram.svg) +**Thunderscope** chiefly refers to the [GUI application](#thunderscope-gui) we use to visualize and interact with our robots and software. Some non-UI related functionality (namely, Robot Communication) is implemented as part of Thunderscope since it acts as a central point in our architecture, making it convenient for coordinating activities between different modules. -# Thunderscope +[`thunderscope_main.py`](/src/software/thunderscope/thunderscope_main.py) serves as the main entry point ("launcher") for our entire system. You can run the script to start up the [Thunderscope GUI](#thunderscope-gui) and a number of other optional processes, such as a [Fullsystem](#fullsystem) for each [AI](#ai) team, the [Simulator](#simulator), and [SSL Gamecontroller](#ssl-gamecontroller). -[`Thunderscope Main`](/src/software/thunderscope/thunderscope_main.py) serves as the main entry point for our entire system. It starts up the [Thunderscope GUI](#thunderscope-gui) and other processes, such as a [Fullsystem](#fullsystem) for each [AI](#ai) team. ## Thunderscope GUI +![Thunderscope GUI](images/thunderscope.png) + [Thunderscope](#thunderscope) is our main visualizer of our [AI](#ai). It provides a GUI that shows us the state of the [World](#world), and it is also able to display extra information that the [AI](#ai) would like to show. For example, it can show the planned paths of each friendly robot on the field, or highlight which enemy robots it thinks are a threat. Furthermore, it displays any warnings or status messages from the robots, such as if a robot is low on battery. Thunderscope also lets us control the [AI](#ai) by setting [Dynamic Parameters](#dynamic-parameters). The GUI lets us choose what strategy the [AI](#ai) should use, what colour we are playing as (yellow or blue), and tune more granular behaviour such as how close an enemy must be to the ball before we consider them a threat. @@ -521,7 +569,7 @@ Thunderscope is implemented using [PyQtGraph](https://www.pyqtgraph.org/), a Pyt * [Layouts](https://doc.qt.io/qt-6/layout.html) * [Signals and Slots](https://doc.qt.io/qt-5/signalsandslots.html) -## 3D Visualizer +### 3D Visualizer Thunderscope has a field visualizer that uses [PyQtGraph's 3D graphics system](https://pyqtgraph.readthedocs.io/en/latest/api_reference/3dgraphics/index.html) to render 3D graphics with OpenGL. PyQtGraph handles all the necessary calls to OpenGL for us, and as an abstraction, provides a [scenegraph](https://en.wikipedia.org/wiki/Scene_graph) to organize and manipulate entities/objects within the 3D environment (the scene). @@ -531,24 +579,49 @@ Thunderscope has a field visualizer that uses [PyQtGraph's 3D graphics system](h - `/graphics` contains custom "graphics items". Graphics items (or just "graphics" for short) are objects that can be added to the [3D scenegraph](https://en.wikipedia.org/wiki/Scene_graph). Graphics should inherit from [`GLGraphicsItem`](https://pyqtgraph.readthedocs.io/en/latest/api_reference/3dgraphics/glgraphicsitem.html) and represent 3D objects that can be visualized in the scene (e.g. a robot, a sphere, a circle, etc.). - `/layers` contains all the [layers](#layers) we use to organize and group together graphics. -### Layers +#### Layers We organize our graphics into "layers" so that we can toggle the visibility of different parts of our visualization. Each layer is responsible for visualizing a specific portion of our AI (e.g. vision data, path planning, passing, etc.). A layer can also handle layer-specific functionality; for instance, `GLWorldLayer` lets the user place or kick the ball using the mouse. The base class for a layer is [`GLLayer`](../src/software/thunderscope/gl/layers/gl_layer.py). A `GLLayer` is in fact a `GLGraphicsItem` that is added to the scenegraph. When we add or remove `GLGraphicsItem`s to a `GLLayer`, we're actually setting the `GLLayer` as the parent of the `GLGraphicsItem`; this is because the scenegraph has a hierarchical tree-like structure. `GLLayer`s can also be nested within one another, i.e. a `GLLayer` can be added as a child of another `GLLayer`. +## Inter-Process Communication +Since Thunderscope runs in a separate process from [Fullsystem](#fullsystem), we use [Unix domain sockets](https://en.wikipedia.org/wiki/Unix_domain_socket) to facilitate communication between Fullsystem and Thunderscope. Unix sockets [have high throughput and are very performant](https://stackoverflow.com/a/29436429/20199855); we simply bind the unix socket to a file path and pass data between processes, instead of having to deal with TCP/IP overhead just to send and receive data on the same computer. + +The data sent between Fullsystem and Thunderscope is serialized using [protobufs](#protobuf). Some data, such as data that goes through our [Backend](#backend) (vision data, game controller commands, [Worlds](#world) from [Sensor Fusion](#sensor-fusion), etc.), is sent using unix senders owned by those parts of the Fullsystem directly. In other higher level components of the Fullsystem (such as FSMs, pass generator, navigator, etc.), we want to delegate away the responsibility of managing unix senders directly and have a lightweight way of sending protobufs to Thunderscope. To avoid needing to dependency inject a "communication" object in places we have visualizable data to send to Thunderscope, we take advantage of the [`g3log`](https://kjellkod.github.io/g3log/) logger already used throughout the codebase to log and send visualizable data. + +`g3log` is a fast and thread-safe way to log data with custom handlers called “sinks". Importantly, it gives us a static [singleton](#singleton-pattern) that can be called anywhere. Logging a protobuf will send it to our custom protobuf `g3log` sink, which lazily initializes unix senders based on the type of protobuf that is logged. The sink then sends the protobuf over the socket to listeners. + +
+Aside: calling g3log to log protobuf data +Logging protobufs is done at the VISUALIZE level (e.g. LOG(VISUALIZE) << some_random_proto;). Protobufs need to be converted to strings in order to log them with g3log. We've overloaded the stream (<<) operator to automatically pack protobufs into a google::protobuf::Any and serialize them to a string, so you don't need to do the conversion yourself. +

+ +In Thunderscope, the [`ProtoUnixIO`](../src/software/thunderscope/proto_unix_io.py) is responsible for communicating protobufs over unix sockets. `ProtoUnixIO` utilizes a variation of the [publisher-subscriber ("pub-sub")](#publisher-subscriber-pattern) messaging pattern. Through `ProtoUnixIO`, clients can register as a subscriber by providing a type of protobuf to receive and a [`ThreadSafeBuffer`](../src/software/thunderscope/thread_safe_buffer.py) to place incoming protobuf messages. The `ProtoUnixIO` can then be configured with a unix receiver to receive protobufs over a unix socket and place those messages onto the `ThreadSafeBuffer`s of that proto's subscribers. Classes can also publish protobufs (for other classes to receive or to send messages back to Fullsystem) via `ProtoUnixIO` by configuring it with a unix sender. + +## Proto Log Replay + +[Fullsystem](#fullsystem) has a `ProtoLogger` that serializes and writes all incoming and outgoing protobufs to a folder. Thunderscope can be launched in a "replay mode" that will load and play back a proto log folder, allowing us to replay old matches and [simulated tests](#simulated-tests). + +## Dynamic Parameters + +**Dynamic Parameters** are the system we use to change values in our code at runtime through Thunderscope. The reason we want to change values at runtime is primarily because we may want to tweak our strategy or aspects of our gameplay very quickly. During games we are only allowed to touch our computers and make changes during halftime or a timeout, so every second counts! Using Dynamic Parameters saves us from having to stop the [AI](#ai), change a constant, recompile the code, and restart the [AI](#ai). + +Dynamic Parameters are stored in one large [protobuf message](#protobuf) that is communicated between Thunderscope and [Fullsystem](#fullsystem). Thunderscope has a widget that displays all our Dynamic Parameters and lets the user change their values. Whenever a Dynamic Parameter value is changed in the widget, [Fullsystem](#fullsystem) and [AI](#ai) will be updated with the new value. For example, we can define a Dynamic Parameter called `run_ai` that is a boolean value. The `run_ai` param will show up in the Parameters widget in [Thunderscope](#thunderscope) with a checkbox that sets the value of `run_ai`. In the "main loop" for the [AI](#ai), it will check if the value of `run_ai` is true before running its logic. + +Here's a slightly more relevant example of how we used Dynamic Parameters during a game in RoboCup 2019. We had a parameter called `enemy_team_can_pass`, which indicates whether or not we think the enemy team can pass. This parameter was used in several places in our defensive logic, and specifically affected how we would shadow enemy robots when we were defending them. If we assumed the enemy team could pass, we would shadow between the robots and the ball to block any passes, otherwise we would shadow between the enemy robot and our net to block shots. During the start of a game, we had `enemy_team_can_pass` set to `false` but the enemy did start to attempt some passes during the game. However, we didn't want to use one of our timeouts to change the value. Luckily later during the half, the enemy team took a time out. Because Dynamic Parameters can be changed quick without stopping [AI](#ai), we were quickly able to change `enemy_team_can_pass` to `true` while the enemy team took their timeout. This made our defence much better against that team and didn't take so much time that we had to burn our own timeout. Altogether this is an example of how we use Dynamic Parameters to control our [AI](#ai) and other parts of the code. + +It is worth noting that constants are still useful, and should still be used whenever possible. If a value realistically doesn't need to be changed, it should be a constant (with a nice descriptive name) rather than a Dynamic Parameter. Having too many Dynamic Parameters is overwhelming because there are too many values to understand and change, and this can make it hard to tune values to get the desired behaviour while under pressure during a game. + # Simulator -The `Simulator` is what we use for physics simulation to do testing when we don't have access to real field. In terms of the architecture, the `Simulator` "simulates" the following components' functionalities: +Our simulator is what we use for physics simulation to do testing when we don't have access to real field. The simulator is a standalone application that simulates the following components' functionalities: * [SSL-Vision](#ssl-vision) by publishing new vision data * the robots by accepting new [Primitives](#primitives) -Using the current state of the simulated world, the `Simulator` simulates the new [Primitives](#primitives) over some time step and publishes new ssl vision data based on the updated simulated world. The `Simulator` is designed to be "perfect", which means that -* the vision data it publishes exactly reflects the state of the simulated world -* the simulation perfectly reflects our best understanding of the physics (e.g. friction) with no randomness. +Using the current state of the simulated world, the simulator simulates the new [Primitives](#primitives) over some time step and publishes new SSL vision data based on the updated simulated world. Since the simulator interfaces with the our software over the network, it is essentially indistinguishible from robots receiving [Primitives](#primitives) and an [SSL-Vision](#ssl-vision) client publishing data over the network. -The `Simulator` uses `Box2D`, which provides 2D physics simulation for free. While this simplifies the simulator greatly, it means that we manually implement the physics for "3D effects", such as dribbling and chipping. +The simulation can also be configured with "realism" parameters that control the amount of noise added to vision detections, simulate missed vision detections and packet loss, and add artificial vision/processing delay, all with some degree of randomness. -## Standalone Simulator -The `Standalone Simulator` is a wrapper around the `Simulator` so that we can run it as a standlone application that publishes and receives data over the network. The `Standalone Simulator` is designed to interface with the [WifiBackend](#backend) over the network, and so it is essentially indistinguishible from robots receiving [Primitives](#primitives) and an [SSL-Vision](#ssl-vision) client publishing data over the network. The `Standalone Simulator` also has a [GUI](#gui) that provides user-friendly features, such as moving the ball around. +The code for our simulator is adapted from the [ER Force Simulator](https://github.com/robotics-erlangen/framework?tab=readme-ov-file#simulator-cli) built by another team in our league. The simulator uses the real-time [Bullet physics engine](https://github.com/bulletphysics/bullet3), which simulates collision detection and soft/rigid-body dynamics in 3D. ## Simulated Tests @@ -597,21 +670,69 @@ Notice this is very similar to the [Architecture Overview Diagram](#architecture ![Simulated Testing High-level Architecture Diagram](images/simulated_test_high_level_architecture.svg) -# Inter-process Communication -Since [Thunderscope](#thunderscope) runs in a separate process from [Fullsystem](#fullsystem), we use [Unix domain sockets](https://en.wikipedia.org/wiki/Unix_domain_socket) to facilitate communication between Fullsystem and Thunderscope. Unix sockets [have high throughput and are very performant](https://stackoverflow.com/a/29436429/20199855); we simply bind the unix socket to a file path and pass data between processes, instead of having to deal with TCP/IP overhead just to send and receive data on the same computer. +# E-Stop +The **E-Stop** allows us to quickly and manually command physical robots to stop what they are doing. It is a physical push button that is connected to the computer via a USB cable. We also have a `--keyboard_estop` flag you can use when running Thunderscope that lets you use the spacebar as the E-Stop. When Thunderscope is launched, a `ThreadedEstopReader` is initialized (within `RobotCommunication`) that is responsible for communicating and reading values from the E-Stop via UART. While running, it will poll the status of the E-Stop to determine whether it is in the `STOP` or `PLAY` state: +- If the E-Stop is in the `STOP` state, it overrides the [Primitives](#primitives) sent to the robots with `Stop` primitives. On the robot, Thunderloop is responsible for handling the primitive message and ensuring that the power & motor boards receive the correct inputs for the robot to stop. +- If the E-Stop is in the `PLAY` state, primitives are communicated as normal. -The data sent between Fullsystem and Thunderscope is serialized using [protobufs](#protobuf). Some data, such as data that goes through our [Backend](#backend) (vision data, game controller commands, [Worlds](#world) from [Sensor Fusion](#sensor-fusion), etc.), is sent using unix senders owned by those parts of the Fullsystem directly. In other higher level components of the Fullsystem (such as FSMs, pass generator, navigator, etc.), we want to delegate away the responsibility of managing unix senders directly and have a lightweight way of sending protobufs to Thunderscope. To avoid needing to dependency inject a "communication" object in places we have visualizable data to send to Thunderscope, we take advantage of the [`g3log`](https://kjellkod.github.io/g3log/) logger already used throughout the codebase to log and send visualizable data. +# Design Patterns -`g3log` is a fast and thread-safe way to log data with custom handlers called “sinks". Importantly, it gives us a static [singleton](#singleton-pattern) that can be called anywhere. Logging a protobuf will send it to our custom protobuf `g3log` sink, which lazily initializes unix senders based on the type of protobuf that is logged. The sink then sends the protobuf over the socket to listeners. +Below are the main design patterns we use in our code, and what they are used for. -
-Aside: calling g3log to log protobuf data -Logging protobufs is done at the VISUALIZE level (e.g. LOG(VISUALIZE) << some_random_proto;). Protobufs need to be converted to strings in order to log them with g3log. We've overloaded the stream (<<) operator to automatically pack protobufs into a google::protobuf::Any and serialize them to a string, so you don't need to do the conversion yourself. -

+## Abstract Classes and Inheritance +While not a "design pattern" per se, inheritance and related OOP paradigms are prevalent throughout of our code base. Abstract classes let us define interfaces for various components of our code. Then we can implement different objects that obey the interface, and use them interchangeably, with the guarantee that as long as they follow the same interface we can use them in the same way. + +To learn more about inheritance in C++, read the following `learncpp.com` articles: +- [Introduction to inheritance](https://www.learncpp.com/cpp-tutorial/introduction-to-inheritance/) +- [Virtual functions](https://www.learncpp.com/cpp-tutorial/pointers-and-references-to-the-base-class-of-derived-objects/) + +Examples of this can be found in many places, including: +* [Plays](#plays) +* [Tactics](#tactics) + +## Singleton Pattern +The Singleton pattern is useful for having a single, global instance of an object that can be accessed from anywhere. Though it's generally considered an anti-pattern (aka _bad_), it is useful in specific scenarios. Read the Refactoring Guru article on the [Singleton pattern](https://refactoring.guru/design-patterns/singleton) for more information. + +We use the Singleton pattern for our logger. This allows us to create a single logger for the entire system, and code can make calls to the logger from anywhere, rather than us having to pass a `logger` object literally everywhere. + + +## Factory Pattern +The Factory pattern is useful for hiding or abstracting how certain objects are created. Read the Refactoring Guru articles on the [Factory Method pattern](https://refactoring.guru/design-patterns/factory-method) and the [Abstract Factory pattern](https://refactoring.guru/design-patterns/abstract-factory) for more information. + +Because the Factory needs to know about what objects are available to be created, it can be taken one step further to auto-register these object types. Rather than a developer having to remember to add code to the Factory every time they create a new class, this can be done "automatically" with some clever C++ code. This helps reduce mistakes and saves developers work. Read this [post about auto-registering factories](http://derydoca.com/2019/03/c-tutorial-auto-registering-factory/) for more information. + +The auto-registering factory is particularly useful for our `PlayFactory`, which is responsible for creating [Plays](#plays). Every time we run our [AI](#ai) we want to know what [Plays](#plays) are available to choose from. The Factory pattern makes this really easy, and saves us having to remember to update some list of "available Plays" each time we add or remove one. + + +## Visitor Pattern +The Visitor pattern is useful when we need to perform different operations on a group of "similar" objects, like objects that inherit from the same parent class (e.g. [Tactic](#tactics)). We might only know all these objects are a [Tactic](#tactic), but we don't know specifically which type each one is (eg. `AttackerTactic` vs `ReceiverTactic`). The Visitor Pattern helps us "recover" that type information so we can perform different operations on the different types of objects. It is generally preferred to a big `if-block` with a case for each type, because the compiler can help warn you when you've forgotten to handle a certain type, and therefore helps prevent mistakes. + +Read the Refactoring Guru article on the [Visitor pattern](https://refactoring.guru/design-patterns/visitor) for more information. + +An example of where we use the Visitor pattern is in our `MotionConstraintVisitor`. This visitor allows us to update the current set of motion constraints based on the types of tactics that are currently assigned. + +## Observer Pattern +The Observer pattern is useful for letting components of a system "notify" each other when something happens. Read the Refactoring Guru article on the [Observer pattern](https://refactoring.guru/design-patterns/observer) for a general introduction to the pattern. + +Our implementation of this pattern consists of two classes, `Observer` and `Subject`. `Observer`s can be registered with a `Subject`, after which new values will be sent from each `Subject` to all of it's registered `Observer`s. Please see the headers of both classes for details. Note that a class can extend both `Observer` and `Subject`, thus receiving and sending out data. In this way we can "chain" multiple classes. -In Thunderscope, the [`ProtoUnixIO`](../src/software/thunderscope/proto_unix_io.py) is responsible for communicating protobufs over unix sockets. `ProtoUnixIO` utilizes a variation of the [publisher-subscriber ("pub-sub")](#publisher-subscriber-pattern) messaging pattern. Through `ProtoUnixIO`, clients can register as a subscriber by providing a type of protobuf to receive and a [`ThreadSafeBuffer`](../src/software/thunderscope/thread_safe_buffer.py) to place incoming those protobuf messages. The `ProtoUnixIO` can then be configured with a unix receiver to receive protobufs over a unix socket and place those messages onto the `ThreadSafeBuffer`s of that proto's subscribers. Classes can also publish protobufs via `ProtoUnixIO` by configuring it with a unix sender. +### Threaded Observer +In our system, we need to be able to do multiple things (receive camera data, run the [AI](#ai), send commands to the robots) at the same time. In order to facilitate this, we extend the `Observer` to the `ThreadedObserver` class. The `ThreadedObserver` starts a thread with an infinite loop that waits for new data from `Subject` and performs some operation with it. + +> [!WARNING] +> If a class extends multiple `ThreadedObserver`s (for example, [AI](#ai) could extend `ThreadedObserver` and `ThreadedObserver`), then there will be two threads running, one for each observer. We **do not check** for data race conditions between observers, so it's entirely possible that one `ThreadedObserver` thread could read/write from data at the same time as the other `ThreadedObserver` is reading/writing the same data. Please make sure any data read/written to/from multiple `ThreadedObserver`s is thread-safe. + +One example of this is [SensorFusion](#sensor-fusion), which extends `Subject` and the [AI](#ai), which extends `ThreadedObserver`. [SensorFusion](#sensor-fusion) runs in one thread and sends data to the [AI](#ai), which receives and processes it another thread. -# Estop -The `Estop` allows us to quickly and manually command physical robots to stop what they are doing. It is a physical push button that is connected to the computer via a USB cable. When Thunderscope is launched, a `ThreadedEstopReader` is initialized (within `RobotCommunication`) that is responsible for communicating and reading values from the `Estop` via UART. While running, it will poll the status of the `Estop` to determine whether it is in the `STOP` or `PLAY` state: -- If the `Estop` is in the `STOP` state, it overrides the [Primitives](#primitives) sent to the robots with `Stop` primitives. On the robot, `Thunderloop` is responsible for handling the primitive message and ensuring that the power & motor boards receive the correct inputs for the robot to stop. -- If the `Estop` is in the `PLAY` state, primitives are communicated as normal. +## Publisher-Subscriber Pattern + +The publisher-subscriber pattern ("pub-sub") is a messaging pattern for facilitating communication between different components. It is closely related to the [message queue](https://en.wikipedia.org/wiki/Message_queue) design pattern. + +In this pattern, `Publisher`s send messages without knowing who the recipients (`Subscriber`s) are. `Subscriber`s express interest in specific types of messages by subscribing to relevant topics; when a `Publisher` sends a message of a topic, the messaging system ensures that all interested subscribers receive the message. + +Read the [Wikipedia article on pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) for an introduction to the pub-sub pattern. + +We use the pub-sub pattern to facilitate [inter-process communication](#inter-process-communication) in our system. Through a class called [`ProtoUnixIO`](../src/software/thunderscope/proto_unix_io.py), components can subscribe to receive certain [Protobuf](#protobuf) message types sent out by other processes or system components. + +## C++ Templating +While debatably not a design pattern depending on who you ask, templating in C++ is a powerful tool that is very useful to understand. We use templating throughout around our codebase. `learncpp.com` has a good explanation on [function templates](https://www.learncpp.com/cpp-tutorial/function-templates/) and [class templates](https://www.learncpp.com/cpp-tutorial/class-templates/). diff --git a/docs/useful-robot-commands.md b/docs/useful-robot-commands.md index 431f7c3c45..297153cc19 100644 --- a/docs/useful-robot-commands.md +++ b/docs/useful-robot-commands.md @@ -1,27 +1,29 @@ -Table of Contents -================= - -* [Table of Contents](#table-of-contents) -* [Common Debugging Steps](#common-debugging-steps) -* [Off Robot Commands](#off-robot-commands) - * [Wifi Disclaimer](#wifi-disclaimer) - * [Miscellaneous Ansible Tasks & Options](#miscellaneous-ansible-tasks--options) - * [Flashing the robot's compute module](#flashing-the-robots-compute-module) - * [Flashing the powerboard](#flashing-the-powerboard) - * [Setting up the embedded host](#setting-up-the-embedded-host) - * [Jetson Nano](#jetson-nano) - * [Raspberry Pi](#raspberry-pi) - * [Robot Diagnostics](#robot-diagnostics) - * [For Just Diagnostics](#for-just-diagnostics) - * [For AI + Diagnostics](#for-ai--diagnostics) - * [Robot Auto Test](#robot-auto-test) -* [On Robot Commands](#on-robot-commands) - * [Systemd Services](#systemd-services) - * [Debugging Uart](#debugging-uart) - * [Redis](#redis) - - - +# Useful Robot Commands + +# Table of Contents + + + +- [Table of Contents](#table-of-contents) +- [Common Debugging Steps](#common-debugging-steps) +- [Off Robot Commands](#off-robot-commands) + - [Wifi Disclaimer](#wifi-disclaimer) + - [Miscellaneous Ansible Tasks & Options](#miscellaneous-ansible-tasks--options) + - [Flashing the robot's compute module](#flashing-the-robots-compute-module) + - [Flashing the powerboard](#flashing-the-powerboard) + - [Setting up the embedded host](#setting-up-the-embedded-host) + - [Jetson Nano](#jetson-nano) + - [Raspberry Pi](#raspberry-pi) + - [Robot Diagnostics](#robot-diagnostics) + - [For Just Diagnostics](#for-just-diagnostics) + - [For AI + Diagnostics](#for-ai--diagnostics) + - [Robot Auto Test](#robot-auto-test) +- [On Robot Commands](#on-robot-commands) + - [Systemd Services](#systemd-services) + - [Debugging Uart](#debugging-uart) + - [Redis](#redis) + + # Common Debugging Steps ```mermaid diff --git a/environment_setup/install_clion.sh b/environment_setup/install_clion.sh deleted file mode 100755 index 2191094039..0000000000 --- a/environment_setup/install_clion.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -echo "================================================================" -echo "Installing CLion" -echo "================================================================" - -clion_version_year="2021" -clion_version_major="2" -clion_version_minor="4" -clion_version="${clion_version_year}.${clion_version_major}.${clion_version_minor}" -clion_executable_path="/usr/local/bin/clion" - - -# Check the correct clion version is installed -if [ -e "/opt/clion-${clion_version}/bin/clion.sh" ] && [ -e ${clion_executable_path}-${clion_version} ] -then - echo "================================================================" - echo "CLion is already installed" - echo "================================================================" -else - # Download clion - wget -O /tmp/CLion-${clion_version}.tar.gz "https://download.jetbrains.com/cpp/CLion-${clion_version}.tar.gz" - - # Unzip and symlink to usr location - sudo tar xfz /tmp/CLion-${clion_version}.tar.gz -C /opt - - # Install clion desktop entry - wget -O ~/.local/share/applications/jetbrains-clion.desktop "https://raw.githubusercontent.com/pld-linux/clion/master/clion.desktop" - echo "Icon=/opt/clion-${clion_version}/bin/clion.png" >> ~/.local/share/applications/jetbrains-clion.desktop -fi - -echo "================================================================" -echo "Symlinking CLion" -echo "================================================================" -# Symlink clion binary to a location in the PATH -# We do this outside the above `if` because this gives us the ability to -# change clion versions without re-downloading -sudo ln -s -f /opt/clion-${clion_version}/bin/clion.sh ${clion_executable_path}-${clion_version} -sudo ln -s -f ${clion_executable_path}-${clion_version} $clion_executable_path - -echo "================================================================" -echo "Installing Bazel Plugin" -echo "================================================================" - -clion_plugin_dir="${HOME}/.CLion${clion_version_year}.${clion_version_major}/config/plugins" -bazel_plugin_version_year="2022" -bazel_plugin_version_major="03" -bazel_plugin_version_minor="22" -bazel_plugin_version="v${bazel_plugin_version_year}.${bazel_plugin_version_major}.${bazel_plugin_version_minor}" - -if [ -d "${clion_plugin_dir}/clwb" ] -then - echo "================================================================" - echo "Bazel Plugin Already Installed" - echo "To force reinstallation please remove ${clion_plugin_dir}/clwb". - echo "================================================================" -else - # Download bazel plugin - wget -O /tmp/bazelbuild-${bazel_plugin_version}.tar.gz "https://github.com/bazelbuild/intellij/archive/${bazel_plugin_version}.tar.gz" - - # Unpack and build the plugin from the source - mkdir -p /tmp/bazelbuild-${bazel_plugin_version} - tar xfz /tmp/bazelbuild-${bazel_plugin_version}.tar.gz -C /tmp/bazelbuild-${bazel_plugin_version} - cd /tmp/bazelbuild-${bazel_plugin_version}/intellij* || exit - bazel build //clwb:clwb_bazel_zip --define=ij_product=clion-${clion_version_year}.${clion_version_major} - - # Copy the compiled plugin to the CLion directory - mkdir -p "$clion_plugin_dir" - unzip bazel-bin/clwb/clwb_bazel.zip -d "$clion_plugin_dir" - - # Cleanup - rm -rf /tmp/bazelbuild -fi - - -echo "================================================================" -echo "Done" -echo "================================================================" diff --git a/environment_setup/install_vscode.sh b/environment_setup/install_vscode.sh deleted file mode 100755 index 436d4cd6a7..0000000000 --- a/environment_setup/install_vscode.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -echo "================================================================" -echo "Installing VSCode" -echo "================================================================" - -if [ "$EUID" -ne 0 ] - then echo "Please run as root" - exit -fi - -if [ -d "/opt/VSCode-linux-x64" ] -then - echo "Old VSCode installation detected, please delete /opt/VSCode-linux-x64 and re-run" - exit 1 -fi - -# permalink obtained from here: https://github.com/microsoft/vscode/issues/1084 -download_permalink_linux64=http://go.microsoft.com/fwlink/?LinkID=620884 -vscode_executable_path="/usr/local/bin/vscode" - -echo "Downloading Stable VSCode" -wget -O /tmp/vscode-stable.tar.gz $download_permalink_linux64 - -echo "Unzipping to /opt folder" -tar -xvf /tmp/vscode-stable.tar.gz -C /opt - -echo "Creating Desktop Entry" - -VSCODE_DESKTOP_ENTRY=''' -[Desktop Entry] -Name=Visual Studio Code -Comment=Programming Text Editor -Exec=/opt/VSCode-linux-x64/code -Icon=/opt/VSCode-linux-x64/resources/app/resources/linux/code.png -Terminal=false -Type=Application -Categories=Programming; -''' -echo "$VSCODE_DESKTOP_ENTRY" > ~/.local/share/applications/vscode.desktop - -echo "Symlink VSCode" -sudo ln -s -f /opt/VSCode-linux-x64/code ${vscode_executable_path} - -echo "================================================================" -echo "Done" -echo "================================================================" diff --git a/environment_setup/ubuntu20_requirements.txt b/environment_setup/ubuntu20_requirements.txt index 2b74622d02..a9fd1b040a 100644 --- a/environment_setup/ubuntu20_requirements.txt +++ b/environment_setup/ubuntu20_requirements.txt @@ -7,3 +7,4 @@ python-Levenshtein==0.25.1 psutil==5.9.0 PyOpenGL==3.1.6 ruff==0.5.5 +md-toc==9.0.0 diff --git a/environment_setup/ubuntu22_requirements.txt b/environment_setup/ubuntu22_requirements.txt index e2eb4e2135..d4b7c89c28 100644 --- a/environment_setup/ubuntu22_requirements.txt +++ b/environment_setup/ubuntu22_requirements.txt @@ -8,3 +8,4 @@ psutil==5.9.0 PyOpenGL==3.1.6 numpy==1.26.4 ruff==0.5.5 +md-toc==9.0.0 diff --git a/environment_setup/ubuntu24_requirements.txt b/environment_setup/ubuntu24_requirements.txt index f6e9adcc1d..0d60d2eba0 100644 --- a/environment_setup/ubuntu24_requirements.txt +++ b/environment_setup/ubuntu24_requirements.txt @@ -5,3 +5,4 @@ python-Levenshtein==0.25.1 psutil==5.9.0 PyOpenGL==3.1.6 ruff==0.5.5 +md-toc==9.0.0 diff --git a/scripts/lint_and_format.sh b/scripts/lint_and_format.sh index 0f6fc83187..6d262e15e3 100755 --- a/scripts/lint_and_format.sh +++ b/scripts/lint_and_format.sh @@ -99,6 +99,14 @@ function run_code_spell(){ fi } +function run_md_toc() { + printf "Adding table of contents to Markdown files...\n\n" + for file in $CURR_DIR/../docs/*.md + do + /opt/tbotspython/bin/python3 -m md_toc --in-place --no-list-coherence --skip-lines 1 github $file + done +} + function run_git_diff_check(){ printf "Checking for merge conflict markers...\n\n" cd $CURR_DIR && git -c "core.whitespace=-trailing-space" --no-pager diff --check @@ -124,6 +132,7 @@ run_code_spell run_clang_format run_bazel_formatting run_ruff +run_md_toc run_eof_new_line run_git_diff_check diff --git a/src/software/embedded/BUILD b/src/software/embedded/BUILD index d43be47e00..9da22336b0 100644 --- a/src/software/embedded/BUILD +++ b/src/software/embedded/BUILD @@ -42,7 +42,9 @@ cc_library( "//software/ai/navigator/trajectory:trajectory_path", "//software/math:math_functions", "//software/physics:velocity_conversion_util", - "//software/world", + "//software/time:duration", + "//software/world:robot_state", + "//software/world:team_colour", ], ) @@ -60,7 +62,6 @@ cc_library( "//software/logger:network_logger", "//software/tracy:tracy_constants", "//software/util/scoped_timespec_timer", - "//software/world:team", "@tracy", ], ) diff --git a/src/software/embedded/primitive_executor.h b/src/software/embedded/primitive_executor.h index c95b4638f2..891ff31bd0 100644 --- a/src/software/embedded/primitive_executor.h +++ b/src/software/embedded/primitive_executor.h @@ -5,7 +5,9 @@ #include "software/ai/navigator/trajectory/bang_bang_trajectory_1d_angular.h" #include "software/ai/navigator/trajectory/trajectory_path.h" #include "software/geom/vector.h" -#include "software/world/world.h" +#include "software/time/duration.h" +#include "software/world/robot_state.h" +#include "software/world/team_types.h" class PrimitiveExecutor { diff --git a/src/software/embedded/thunderloop.cpp b/src/software/embedded/thunderloop.cpp index dd4d344b34..5715bf4e78 100644 --- a/src/software/embedded/thunderloop.cpp +++ b/src/software/embedded/thunderloop.cpp @@ -15,8 +15,6 @@ #include "software/logger/network_logger.h" #include "software/tracy/tracy_constants.h" #include "software/util/scoped_timespec_timer/scoped_timespec_timer.h" -#include "software/world/robot_state.h" -#include "software/world/team.h" /** * https://web.archive.org/web/20210308013218/https://rt.wiki.kernel.org/index.php/Squarewave-example @@ -143,7 +141,6 @@ void Thunderloop::runLoop() struct timespec poll_time; struct timespec iteration_time; struct timespec last_primitive_received_time; - struct timespec last_world_received_time; struct timespec current_time; struct timespec last_chipper_fired; struct timespec last_kicker_fired; @@ -151,7 +148,6 @@ void Thunderloop::runLoop() // Input buffer TbotsProto::PrimitiveSet new_primitive_set; - TbotsProto::World new_world; const TbotsProto::PrimitiveSet empty_primitive_set; // Loop interval @@ -163,7 +159,6 @@ void Thunderloop::runLoop() // CLOCK_REALTIME can jump backwards clock_gettime(CLOCK_MONOTONIC, &next_shot); clock_gettime(CLOCK_MONOTONIC, &last_primitive_received_time); - clock_gettime(CLOCK_MONOTONIC, &last_world_received_time); clock_gettime(CLOCK_MONOTONIC, &last_chipper_fired); clock_gettime(CLOCK_MONOTONIC, &last_kicker_fired); clock_gettime(CLOCK_MONOTONIC, &prev_iter_start_time); @@ -189,7 +184,7 @@ void Thunderloop::runLoop() // Collect jetson status jetson_status_.set_cpu_temperature(getCpuTemperature()); - // Network Service: receive newest world, primitives and set out the last + // Network Service: receive newest primitives and send out the last // robot status { ScopedTimespecTimer timer(&poll_time); @@ -204,8 +199,8 @@ void Thunderloop::runLoop() uint64_t last_handled_primitive_set = primitive_set_.sequence_number(); - // Updating primitives and world with newly received data - // and setting the correct time elasped since last primitive / world + // Updating primitives with newly received data + // and setting the correct time elasped since last primitive struct timespec time_since_last_primitive_received; clock_gettime(CLOCK_MONOTONIC, ¤t_time); diff --git a/src/software/embedded/thunderloop.h b/src/software/embedded/thunderloop.h index 33fadb2577..2804155b70 100644 --- a/src/software/embedded/thunderloop.h +++ b/src/software/embedded/thunderloop.h @@ -15,26 +15,25 @@ #include "software/embedded/services/network/network.h" #include "software/embedded/services/power.h" #include "software/logger/logger.h" -#include "software/world/robot_state.h" class Thunderloop { public: /** * Thunderloop is a giant loop that runs at THUNDERLOOP_HZ. - * It receives Primitives and World from AI, executes the primitives with - * the most recent vison data, and polls the services to interact with the hardware - * peripherals. + * It receives Primitives from AI, executes the Primitives with + * the most recent vison data, and polls the services to interact + * with the hardware peripherals. * * High Level Diagram: Service order in loop not shown * * ┌─────────────────┐ * │ │ - * │ ThunderLoop │ + * │ Thunderloop │ * │ │ * Primitives───────► │ Target Vel ┌────────────┐ * │ ├────────────► │ - * World ───────────► │ │ MotorBoard │ + * | │ │ MotorBoard │ * │ Services ◄────────────┤ │ * │ │ Actual Vel └────────────┘ * │ Primitive Exec │ @@ -107,7 +106,6 @@ class Thunderloop // Input Msg Buffers TbotsProto::PrimitiveSet primitive_set_; - TbotsProto::World world_; TbotsProto::Primitive primitive_; TbotsProto::DirectControlPrimitive direct_control_; diff --git a/src/software/field_tests/field_test_fixture.py b/src/software/field_tests/field_test_fixture.py index 82445f8938..2dc8de39f8 100644 --- a/src/software/field_tests/field_test_fixture.py +++ b/src/software/field_tests/field_test_fixture.py @@ -199,7 +199,7 @@ def excepthook(args): def load_command_line_arguments(): - """Load from command line arguments using argpase + """Load in command-line arguments using argparse NOTE: Pytest has its own built in argument parser (conftest.py, pytest_addoption) but it doesn't seem to play nicely with bazel. We just use argparse instead. diff --git a/src/software/logger/logger.h b/src/software/logger/logger.h index 461ff8f732..ea6f5a4b6f 100644 --- a/src/software/logger/logger.h +++ b/src/software/logger/logger.h @@ -85,7 +85,7 @@ class LoggerSingleton // tests:bazel-out/k8-fastbuild/bin/software/simulated_tests/TEST_NAME.runfiles/__main__/ // where TEST_NAME is the name of the simulated test - // Log locations can also be defined by setting the --logging_dir command line + // Log locations can also be defined by setting the --logging_dir command-line // arg. Note: log locations are defaulted to the bazel-out folder due to Bazel's // hermetic build principles diff --git a/src/software/network_log_listener_main.cpp b/src/software/network_log_listener_main.cpp index b762e63ef1..62ddfef28c 100644 --- a/src/software/network_log_listener_main.cpp +++ b/src/software/network_log_listener_main.cpp @@ -38,7 +38,6 @@ void logFromNetworking(TbotsProto::RobotLog log) int main(int argc, char **argv) { - // load command line arguments struct CommandLineArgs { bool help = false; diff --git a/src/software/simulated_tests/simulated_test_fixture.py b/src/software/simulated_tests/simulated_test_fixture.py index 2ff467bc13..1f0014f62a 100644 --- a/src/software/simulated_tests/simulated_test_fixture.py +++ b/src/software/simulated_tests/simulated_test_fixture.py @@ -385,7 +385,7 @@ def run_test( def load_command_line_arguments(): - """Load from command line arguments using argpase + """Load in command-line arguments using argparse NOTE: Pytest has its own built in argument parser (conftest.py, pytest_addoption) but it doesn't seem to play nicely with bazel. We just use argparse instead.