Skip to content

Building Robots in Teams

In this tutorial you will work with your team to learn the basics of ROS2 and create a shared GitHub project. You'll make your first commits allowing you to share code via git. You will set up your editor, learn to use AI Coding tools responsibly, as well as best practices effective collaboration. With the ability to edit and share code, your team will implement several ROS2 nodes to solidify the core concepts of the Robot Operating System.

Learning Objectives

  • Understand the basics of ROS2 architecture
  • Understand basic Git commands and workflows
  • Collaborate effectively using GitHub repositories
  • Practice making commits, pushing, and pulling changes
  • Set up your coding environment with your teammate
  • Use an AI coding agent to assist with coding tasks
  • Implement simple ROS2 nodes collaboratively
  • Create, build, and run a package with airfield, the tool the car uses to build and launch its software

Deliverables

  • A shared GitHub repository with contributions from both students
  • Several simple ROS2 nodes implemented collaboratively

Prerequisites

  • Have Git installed (git --version should work in the terminal).
  • Both have GitHub accounts and are logged in.
  • Have the VSCode editor installed.
  • Have airfield available on the machine you build on (airfield doctor reports anything it is missing). You used airfield to build and launch the car in the previous tutorial; here you use it to create a package of your own.

git

git is a command-line tool for version control that allows multiple people to work on the same codebase simultaneously. It is closely related to github.com, which is one of the most popular platforms for hosting Git repositories. While git can be self-hosted or hosted on other platforms, such as gitlab.com, we will use github.com in this tutorial and for the class. In this portion of the tutorial, you will create a shared repository on GitHub and practice basic git commands such as clone, add, commit, push, and pull.

This portion of the tutorial should be done on your laptop.

1. Student A creates the repository

  • Go to https://github.com and click New repository.
  • Check Add a README file.
  • Click Create repository.

Now Student A has a GitHub repo!

2. Student A clones the repo to their computer

In a terminal:

git clone https://github.com/<studentA-username>/git-practice.git
cd git-practice

3. Student A makes their first change and pushes it

  1. Create a new file, for example:

    echo "Hello from Student A" > hello.txt
    
  2. Check what’s new:

    git status
    
  3. Stage and commit the file:

    git add hello.txt
    git commit -m "Add hello.txt from Student A"
    
  4. Push to GitHub:

    git push
    

    Now, ensure the file is visible on the GitHub website: https://github.com//git-practice

4. Student B joins the project

  1. Student A should go to the repo on GitHub, open: Settings, Collaborators, Add collaborator, enter Student B’s GitHub username and invite them.

  2. Student B accepts the invitation (check email or notifications).

5. Student B clones the repo and pulls the latest changes

git clone https://github.com/<studentA-username>/git-practice.git
cd git-practice

Now Student B has the same files locally.

6. Student B makes a new change and pushes it

  1. Add another file:

    echo "Hello from Student B" > hello_b.txt
    
  2. Stage and commit:

    git add hello_b.txt
    git commit -m "Add hello_b.txt from Student B"
    
  3. Push the change:

    git push
    

7. Student A pulls the new changes

Back on Student A’s computer:

git pull

Student A will now see both hello.txt and hello_b.txt locally.

Lab Notebook

Include two screenshots of your terminal. One screenshot should show the contents of the project (run the ls command). One screenshot should show the output of the git log command displaying the commit history with both students' commits.

git Summary

You just:

  • Created a GitHub repo
  • Made commits
  • Pushed and pulled between collaborators

Version control systems are a powerful collaboration tool that allow multiple people to work on the same codebase without conflicts. They track changes, allow you to revert to previous versions, and provide a history of who made what changes.

For reference, recall the git workflow is:

make changes -> git add -> git commit -> git push -> git pull -> make changes -> ...

git workflow

Updating the Code in the Course Repositories

It's good practice to keep your local codebase up to date with the latest changes from the course repositories. This ensures you have access to the most recent tutorials, bug fixes, and features. Follow these steps regularly to sync your local repository with the remote course repository.

The workspace is managed with airfield, which keeps each ROS2 package in its own git repository and records where each one comes from in ~/roboracer_ws/airfield.yaml. Updating is therefore two steps: pull the workspace itself, then pull the packages inside it.

Airfield requires a workspace that has been migrated to it. Check for an airfield.yaml file and a packages/ folder in ~/roboracer_ws. If your machine has a src/ folder and a container script instead, it is still on the older layout and you should follow the fallback instructions below. The full airfield reference for this workspace is at ~/roboracer_ws/docs/AIRFIELD.md.

  1. Navigate to your local repository:
cd ~/roboracer_ws/
  1. Fetch the latest changes for the workspace itself:
git pull
  1. Clone any packages that are listed in airfield.yaml but are missing from your packages/ folder:
airfield subpackages checkout
  1. Pull the latest changes in every package:
airfield subpackages pull

airfield subpackages status prints the git status of every package at once. Run it before you start working to see which packages have uncommitted changes, and which are behind their remote.

If airfield fails: update with the checkout script

The workspace previously kept its packages in ~/roboracer_ws/src and updated them with a shell script. This flow still works if airfield is unavailable, and it is the only flow available on a machine that has not been migrated.

  1. Navigate to your local repository:
cd ~/roboracer_ws/
  1. Fetch the latest changes from the remote repository:
git pull
  1. Update the repositories in the workspace:
./scripts/checkout.sh

The Robot Operating System (ROS) 2

The robot operating system (ROS) is a flexible framework for writing robot software. It is a collection of tools, libraries, and conventions that aim to simplify the task of creating complex and robust robot behavior across a wide variety of robotic platforms. ROS2 is the second generation of ROS, which includes improvements in performance, security, and real-time capabilities.

ROS2

If you're not familiar with ROS2, read the ROS2 overview.

Creating A New ROS 2 Package

Building systems in teams is a critical skill for robotics researchers. A key challenge in this process is organizing code so that multiple people can work on different parts of the system without conflicts. ROS 2 packages provide a modular way to encapsulate functionality, making it easier to manage and share code among team members.

In this section, you will work in a team to create a new ROS2 package (or two, your choice) in your shared repository. Your implementation should contain two nodes: node A and node B. Node A will publish a simple message, and Node B will subscribe to that message and print it to the console.

Read through the instructions and then decide with your teammate how the work will be fairly divided.

Lab Notebook

Note how you plan to divide the work between team members.

1. ssh into your lab machine ssh <csid>@<lab machine name>.cs.utexas.edu

If you haven't checked out the roboracer_ws repository yet, you can do so by following the instructions on the ROS Workspace documentation page.

2. Create your airfield package

In airfield, a package is the unit everything else is built around: it is one buildable ROS2 package, it becomes one container image, and it is its own git repository. Creating one is the first thing you do, and it happens on the host — there is no container to enter first.

Airfield requires a workspace that has been migrated to it. Check for an airfield.yaml file and a packages/ folder in ~/roboracer_ws. If your lab machine has a src/ folder and a container script instead, follow the container shell instructions at the end of this step. The full airfield reference for this workspace is at ~/roboracer_ws/docs/AIRFIELD.md.

Note: only one team member should create the new package.

cd ~/roboracer_ws
airfield package init team_tutorial

The output in your terminal should look like this:

jet$ airfield package init team_tutorial
Initialized Airfield package team_tutorial at /u/tsoi/roboracer_ws/packages/team_tutorial

Airfield created the package under packages/:

packages/team_tutorial/
  airfield.yaml     what to build, what to install, and how to run it
  src/              your ROS2 code goes here (empty for now)
  README.md

The airfield.yaml file is short, and it is the file that makes this folder a package:

kind: package
name: team_tutorial
dependencies: []
source_path: src
ros_distro: jazzy

dependencies are the libraries airfield installs into your package's container image. rclpy and std_msgs come with the ROS2 base image, so you can leave this list empty. You only add names here when you need something the base image does not ship, and each name has to have a dependency manifest describing how to install it (see dependencies/ in the workspace and docs/AIRFIELD.md).

On a lab machine, override the base image

The workspace airfield.yaml pins a container base image built for the car's Jetson (roboracer/l4t-jazzy:r39.2), and every package inherits it. That image only exists on the cars, so on a lab machine add a base image line to packages/team_tutorial/airfield.yaml:

base_image: osrf/ros:jazzy-desktop

Leave it out when you build the same package on the car, so it inherits the car's image.

If airfield fails: create the package in the container shell

The workspace previously kept its packages in ~/roboracer_ws/src and entered a single container for the whole workspace. This flow still works if airfield is unavailable, and it is the only flow available on a machine that has not been migrated.

If you haven't checked out the roboracer_ws repository or built your container yet, be sure to follow the ROS Workspace documentation page.

Be sure to build your container once, before calling ./container shell

Run ./container build once, before calling ./container shell.

cd ~/roboracer_ws
./container shell

Note: read more about what a container is and why we're using it on the containers documentation page.

Then create the ROS2 package in the workspace src directory and exit the container:

cd ~/roboracer_ws/src
ros2 pkg create team_tutorial --build-type ament_python --license MIT --dependencies rclpy std_msgs
exit

Your package is then at ~/roboracer_ws/src/team_tutorial rather than ~/roboracer_ws/packages/team_tutorial/src/team_tutorial. Use that path everywhere the rest of this tutorial refers to your package, and build with colcon build --packages-select team_tutorial from inside the container shell instead of the airfield commands below.

3. Where your source code lives

source_path: src in airfield.yaml tells airfield which folder holds your code. Airfield mounts that folder into the package's container at ~/workspace/src/team_tutorial and starts every command there, so a file you create in the container appears in packages/team_tutorial/src on the host, and a file you edit on the host is immediately visible in the container. There is nothing to copy, and no image to rebuild after editing code.

~/roboracer_ws/packages/team_tutorial/src   (host)
                    ↕  same files
~/workspace/src/team_tutorial               (inside the container)

Note: the shared build output (~/workspace/build, install, and log in the container) is mounted the same way, from ~/roboracer_ws/.airfield/workspace on the host. That is why you build a package once and every container that runs it sees the result.

4. Create a new ROS2 package named team_tutorial with dependencies on rclpy and std_msgs using the ros2 pkg create command

ros2 is installed in the package's container, not on the host, so run it through airfield. airfield package cmd <package> -- <command> builds the package image if it does not exist yet, then runs one command inside it:

airfield package cmd team_tutorial -- ros2 pkg create team_tutorial --build-type ament_python --license MIT --dependencies rclpy std_msgs

The first run builds your container image and takes a few minutes. Later runs reuse it and start in about a second.

The output in your terminal should look like this:

jet$ airfield package cmd team_tutorial -- ros2 pkg create team_tutorial --build-type ament_python --license MIT --dependencies rclpy std_msgs
Loading package team_tutorial...
Build successful. Running command in airfield-pkg-team_tutorial:latest: ros2 pkg create team_tutorial --build-type ament_python --license MIT --dependencies rclpy std_msgs
going to create a new package
package name: team_tutorial
destination directory: /home/tsoi/workspace/src/team_tutorial
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['tsoi <nathan.tsoi@utexas.edu>']
licenses: ['MIT']
build type: ament_python
dependencies: ['rclpy', 'std_msgs']
creating folder ./team_tutorial
creating ./team_tutorial/package.xml
creating source folder
creating folder ./team_tutorial/team_tutorial
creating ./team_tutorial/setup.py
creating ./team_tutorial/setup.cfg
creating folder ./team_tutorial/resource
creating ./team_tutorial/resource/team_tutorial
creating ./team_tutorial/team_tutorial/__init__.py
creating folder ./team_tutorial/test
creating ./team_tutorial/test/test_copyright.py
creating ./team_tutorial/test/test_flake8.py
creating ./team_tutorial/test/test_pep257.py

The command ran in the container, but the files it wrote are on the host. Check with the pwd and ls commands:

jet$ cd ~/roboracer_ws/packages/team_tutorial
jet$ pwd
/u/tsoi/roboracer_ws/packages/team_tutorial
jet$ ls src/team_tutorial
LICENSE  package.xml  resource  setup.cfg  setup.py  team_tutorial  test

If you want an interactive shell in the container instead of a single command, use airfield package shell team_tutorial. It is the airfield equivalent of the old ./container shell, except that it is scoped to one package and it starts you in that package's source folder. Type exit to leave it.

5. Initialize git in your new package

Note: only one team member should initialize the git repository.

In order to create a new git repository and share the code with your partner for your new ros package, you will run the git init command from the root directory of the project.

Initialize git at the root of the airfield package, packages/team_tutorial, not at the ROS2 package inside it. That way the repository holds both your code and the airfield.yaml that says how to build and run it, and your teammate gets a package that works as soon as they clone it.

Use the pwd command to check your current directory. You should be in your new team_tutorial package.

jet$ pwd
/u/tsoi/roboracer_ws/packages/team_tutorial

Now run the git init command to initialize the git repository, you'll see this output:

jet$ git init .
hint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint:  git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:  git branch -m <name>
hint:
hint: Disable this message with "git config set advice.defaultBranchName false"
Initialized empty Git repository in /u/tsoi/roboracer_ws/packages/team_tutorial/.git/

Check the status of your git repository by running git status.

Note: that git status is a great command to run anytime you're wondering what state your git repository is in.

You should see output in your terminal like this:

jet$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
 .dockerignore
 .gitignore
 README.md
 airfield.yaml
 src/

nothing added to commit but untracked files present (use "git add" to track)

All of the files are currently "untracked" meaning that git does not know about them.

The .gitignore that airfield package init wrote already excludes the build output (build/, install/, log/), airfield's scratch folder (.airfield/), and the per-machine .air config. Those are generated files that differ on every machine, so they should never be committed.

Add all the files in the current directory (represented by the .) to git with the git add . command:

git add .

Check the git status again and you'll see that all the files are now ready to be committed (aka "staged"):

jet$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
 new file:   .dockerignore
 new file:   .gitignore
 new file:   README.md
 new file:   airfield.yaml
 new file:   src/team_tutorial/LICENSE
 new file:   src/team_tutorial/package.xml
 new file:   src/team_tutorial/resource/team_tutorial
 new file:   src/team_tutorial/setup.cfg
 new file:   src/team_tutorial/setup.py
 new file:   src/team_tutorial/team_tutorial/__init__.py
 new file:   src/team_tutorial/test/test_copyright.py
 new file:   src/team_tutorial/test/test_flake8.py
 new file:   src/team_tutorial/test/test_pep257.py

So, we can commit the staged files with the git commit command:

Note: every git commit needs a descriptive message. The -m flag allows you to easily add this commit message. Run:

git commit -m "Initial commit"

Note: If you haven't configured your git username and email, or they were configured automatically, for example, on the lab machines, you'll be prompted to do this before pushing. Follow the instructions returned by the git commit command as necessary

The output will look something like this:

[master (root-commit) d2e1091] Initial Commit
 13 files changed, 228 insertions(+)
 create mode 100644 .dockerignore
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 airfield.yaml
 create mode 100644 src/team_tutorial/LICENSE
 create mode 100644 src/team_tutorial/package.xml
 create mode 100644 src/team_tutorial/resource/team_tutorial
 create mode 100644 src/team_tutorial/setup.cfg
 create mode 100644 src/team_tutorial/setup.py
 create mode 100644 src/team_tutorial/team_tutorial/__init__.py
 create mode 100644 src/team_tutorial/test/test_copyright.py
 create mode 100644 src/team_tutorial/test/test_flake8.py
 create mode 100644 src/team_tutorial/test/test_pep257.py

You can run git status again to see that your files are now committed and you'll see:

jet$ git status
On branch master
nothing to commit, working tree clean

But it looks like your files are gone? No, they're just tracked by git now. You can see the commit you just made by running git log:

jet$ git log
commit d2e10917d932dcd7071d5d043c8f532cdcec56d2 (HEAD -> master)
Author: Nathan Tsoi <tsoi@cs.utexas.edu>
Date:   Wed Jan 14 10:40:46 2026 -0600

    Initial Commit

Now that your local git repository is up to date, we'll share our code by pushing it to github.

Try to push the files to the remote repository using the git push command, it will fail with a useful error message:

jet$ git push
fatal: No configured push destination.
Either specify the URL from the command-line or configure a remote repository using

    git remote add <name> <url>

and then push using the remote name

    git push <name>

This means that we need to configure our new project with a remote repository, telling git where it should push the code.

Go to github.com and create a new repository. Name it team_tutorial, leaving all the other configurations as default. Importantly, don't initialize the repository with a README.

team_tutorial

Note: be sure to go to "Settings" in your new repository and share it with your team members.

Upon creation, you'll be presented with a series of commands to run to push to your new repository, we've already completed most of the steps. The only command you need to run is the git remote add command. Be sure to use the correct URL for your repository:

git remote add origin git@github.com:nathantsoi/team_tutorial.git

Once you've configured the remote repository, you can push your code to it using the git push command, this command will fail again, but when it does, re-run the command with the suggested flag. This is what it will look like the first time:

jet$ git push
fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin master

To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.

Then re-run it with the --set-upstream flag:

git push --set-upstream origin master

Now, check github to make sure your new project has been pushed. You should see the code on github:

team_tutorial_code

Have your partner checkout the new repository.

Your package now has a home on GitHub, but the workspace does not know about it yet. Record it in the workspace so anyone can restore it later. Run this from the workspace root:

cd ~/roboracer_ws
airfield subpackages track

Airfield reads the remote URL and branch from your package's git repository and adds them to the subprojects list in ~/roboracer_ws/airfield.yaml:

subprojects:
  team_tutorial:
    url: git@github.com:nathantsoi/team_tutorial.git
    version: master

Your partner can then clone every package the workspace knows about, including yours, with one command:

cd ~/roboracer_ws
airfield subpackages checkout

This is the same mechanism that brings in the course packages (ut_automata, av_navigation, and the rest). Each one is an independent repository with its own history and its own collaborators, and the project's airfield.yaml is the list that ties them together.

If airfield fails: share the package by hand

Without airfield, there is no list of packages to track. Your partner clones your repository directly into the workspace source folder:

cd ~/roboracer_ws/src
git clone git@github.com:nathantsoi/team_tutorial.git

6. Implement the two nodes in separate Python files within the team_tutorial package

Remember to fairly divide the work between team members. Also, you can git add, git commit, and git push each step, practicing making small commits with logical changes.

The contents of your package will go inside of a folder called team_tutorial inside of the ROS2 package that ros2 pkg create made for you. Create the files node_a.py and node_b.py inside of that folder:

cd ~/roboracer_ws/packages/team_tutorial/src/team_tutorial/team_tutorial
touch node_a.py node_b.py

You can check the folder contents to see if the files were created:

cd ~/roboracer_ws/packages/team_tutorial/src/team_tutorial/team_tutorial
ls
__init__.py  node_a.py node_b.py

You are editing these files on the host, with your normal editor. They are the same files the container sees at ~/workspace/src/team_tutorial/team_tutorial, so there is nothing to sync.

7. Edit node_a.py to publish a simple message

Note: now is a great time to start up your editor, like VSCode and open your new project directory (~/roboracer_ws/packages/team_tutorial) to edit the project files.

#!/usr/bin/env python3

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class NodeA(Node):
    def __init__(self):
        super().__init__('node_a')
        self.publisher_ = self.create_publisher(String, 'hello', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello from Node A'
        self.publisher_.publish(msg)
        self.get_logger().info(f'Publishing: {msg.data}')

def main(args=None):
    rclpy.init(args=args)
    node_a = NodeA()
    rclpy.spin(node_a)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

8. Register the node in setup.py

To run the node using ros2 run, you need to register the entry point in setup.py. Open setup.py and add the following line to the entry_points console_scripts list:

The setup.py file

    entry_points={
        'console_scripts': [
            'node_a = team_tutorial.node_a:main',
        ],
    },

9. Build the workspace to install the new node

colcon is the ROS2 build tool, and like ros2 it lives in the container. Run it through airfield, from the shared workspace root (~/workspace) inside the container, so the result lands in the shared install/ folder that every container reads:

cd ~/roboracer_ws
airfield package cmd team_tutorial -- bash -lc "cd ~/workspace && colcon build --packages-select team_tutorial"

You'll get output like this:

jet$ airfield package cmd team_tutorial -- bash -lc "cd ~/workspace && colcon build --packages-select team_tutorial"
Loading package team_tutorial...
Build successful. Running command in airfield-pkg-team_tutorial:latest: bash -lc 'cd ~/workspace && colcon build --packages-select team_tutorial'
Starting >>> team_tutorial
Finished <<< team_tutorial [1.04s]

Summary: 1 package finished [1.48s]

"Build successful" in the second line refers to the container image, not to your ROS2 code. Airfield makes sure the image is up to date before it runs anything in it. The colcon lines below it are your package being compiled.

Unlike the old workflow, you do not need to source install/setup.bash afterwards. Every airfield command starts a fresh login shell in the container, and that shell sources ~/workspace/install/setup.bash for you, so the next command already knows about the node you just built.

Airfield will also build a package for you the first time you run something from it, so if you forget this step, the ros2 run command in the next section builds team_tutorial before starting the node. It only does that when the package has never been built; after you change your code, rebuild it yourself with the command above.

If airfield fails: build in the container shell

The workspace previously built every package at once from a single container shell. This flow still works if airfield is unavailable.

cd ~/roboracer_ws
./container shell
colcon build --packages-select team_tutorial

You'll get output like this:

[docker]:tsoi@jet:~/roboracer_ws$ colcon build --packages-select team_tutorial
[0.394s] colcon.colcon_ros.prefix_path.ament WARNING The path '/home/tsoi/roboracer_ws/src/amrl_maps/install' in the environment variable AMENT_PREFIX_PATH doesn't exist
[0.394s] colcon.colcon_ros.prefix_path.ament WARNING The path '/home/tsoi/roboracer_ws/src/amrl_msgs/install' in the environment variable AMENT_PREFIX_PATH doesn't exist
Starting >>> team_tutorial
Finished <<< team_tutorial [1.04s]

Summary: 1 package finished [1.48s]

In that flow you do have to re-source the environment variables in your shell after creating a package, by running source install/setup.bash. There is no output from this command, so the expected output is an empty line:

[docker]:tsoi@jet:~/roboracer_ws$ source install/setup.bash
[docker]:tsoi@jet:~/roboracer_ws$

10. Edit node_b.py to subscribe to the message and print it

#!/usr/bin/env python3

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class NodeB(Node):
    def __init__(self):
        super().__init__('node_b')
        self.subscription = self.create_subscription(
            String,
            'hello',
            self.listener_callback,
            10)
        self.subscription  # prevent unused variable warning

    def listener_callback(self, msg):
        self.get_logger().info(f'Received: {msg.data}')

def main(args=None):
    rclpy.init(args=args)
    node_b = NodeB()
    rclpy.spin(node_b)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

11. Register the node in setup.py

Add the entry point for node_b to setup.py:

entry_points={
    'console_scripts': [
        'node_a = team_tutorial.node_a:main',
        'node_b = team_tutorial.node_b:main',
    ],
},

12. Rebuild the workspace

Make the Python files executable:

cd ~/roboracer_ws/packages/team_tutorial/src/team_tutorial/team_tutorial
chmod +x node_a.py node_b.py

Then rebuild, the same way you built the first time:

cd ~/roboracer_ws
airfield package cmd team_tutorial -- bash -lc "cd ~/workspace && colcon build --packages-select team_tutorial"

Rebuild whenever you change your code or add an entry point. colcon build copies your Python files into install/, so the running nodes use the built copy rather than the file you just edited.

If airfield fails: rebuild in the container shell
cd ~/roboracer_ws
./container shell
colcon build --packages-select team_tutorial
source install/setup.bash

Running the Nodes

Open 2 shell windows by using tmux panes.

In the first pane, run Node A:

airfield package cmd team_tutorial -- ros2 run team_tutorial node_a

In the second pane, run Node B:

airfield package cmd team_tutorial -- ros2 run team_tutorial node_b

You should see Node A publishing messages and Node B receiving and printing them.

Each command starts its own container from your package's image. The containers share the host's network and inter-process communication, so the two nodes discover each other exactly as they would if you had run them directly on the machine. Stop either node with Ctrl+C; airfield removes its container when it exits.

Lab Notebook

Include screenshots of both terminal panes showing Node A publishing messages and Node B receiving them.

Naming the commands in airfield.yaml

Typing the full command each time gets old, and a teammate who clones your package has no way of knowing how it is meant to be run. Record the commands in your package's airfield.yaml under a run: map:

kind: package
name: team_tutorial
dependencies: []
source_path: src
ros_distro: jazzy
run:
  default: ros2 run team_tutorial node_a
  node_a: ros2 run team_tutorial node_a
  node_b: ros2 run team_tutorial node_b

Then run them by name:

airfield package run team_tutorial node_a
airfield package run team_tutorial node_b

Commit the change so your partner gets the commands along with the code. This is how every package in the workspace documents what it can do; airfield package run team_tutorial with no name lists the available commands.

Launching both nodes with a plan

In the previous tutorial you launched the whole car with airfield project up teleop. A plan is just a file listing the panes to open and the command each one runs, so you can write one for your own two nodes. Create ~/roboracer_ws/plans/team_tutorial.yaml:

name: team_tutorial
windows:
  - name: nodes
    layout: even-horizontal
    panes:
      - package: team_tutorial
        cmd: ros2 run team_tutorial node_a
      - package: team_tutorial
        cmd: ros2 run team_tutorial node_b

Launch it, and tear it down when you are done:

airfield project up team_tutorial
airfield project down

This opens one tmux window with both nodes side by side, which is the same thing the car does with a dozen panes when it brings up teleoperation.

As in the previous tutorial, always shut down with airfield project down rather than killing the tmux session, so the containers stop with it.

If airfield fails: run the nodes in the container shell

Open 2 shell windows by using tmux panes and enter the container shell in each one:

cd ~/roboracer_ws
./container shell

In the first pane, run Node A:

ros2 run team_tutorial node_a

In the second pane, run Node B:

ros2 run team_tutorial node_b

Commit any remaining changes

Make sure your code is up to date on github by committing your changes and pushing to github.

When you're done, your git status should look like this:

jet$ git status
On branch master
Your branch is up to date with 'origin/master'.

Because the workspace is a collection of separate repositories, it is easy to leave changes behind in one of them. From the workspace root, check all of them at once:

cd ~/roboracer_ws
airfield subpackages status

airfield subpackages commit -m "your message" and airfield subpackages push work across every package that has changes, which is useful once you are editing more than one. Both ask you to confirm each package before they touch it.

Lab Notebook

Include a screenshot of your shared repositories commits.

For example:

team_tutorial_commits