Setup Guide

Software, sensors, network, and GUI

Prerequisites

  • Fully assembled robot with all components connected
  • Computer with SD card reader for Jetson imaging
  • Stable internet for downloads
  • SSH client and basic command-line familiarity

NVIDIA Jetson Setup

The robot uses an 8 GB NVIDIA Jetson Orin Nano Developer Kit for high-level control. Full details are in the Jetson README on the Robot repository.

1

Flash Jetpack to microSD

I recommend Jetpack 5.x (e.g. 5.1.5 as of May 2025) — Jetpack 6.x has caused WiFi card issues. Image: Jetpack SDK 5.1.5.

Format the SD card with the SD Association formatter, then flash with Balena Etcher. Alternatively use NVIDIA SDK Manager (native Ubuntu or VMware Workstation VM with enough disk and USB passthrough to the Jetson).

2

Replace WiFi card

The default card does not support roaming (needed for mesh). Use an Intel AC9260 on the bottom of the Jetson (screwdriver).

Important: Reconnect the antennas to the new WiFi card.
3

First boot

Power on with a monitor, connect to Wi‑Fi so you can SSH in.

4

Basic packages

sudo apt update
sudo apt install nano
5

SPI on 40-pin header (MCU)

cd /opt/nvidia/jetson-io
sudo python3 jetson-io.py

Manually configure the header: activate SPI1 and I2S. Reboot when done.

6

Load spidev at boot

The Jetson must load the spidev kernel module at boot for MCU communication over SPI. Use spidev_setup.sh from the Jetson directory in the Robot repo (run on the Jetson host, outside Docker):

cd ~
wget "https://raw.githubusercontent.com/satomm1/Robot/main/Jetson/spidev_setup.sh"
chmod +x spidev_setup.sh
sudo ./spidev_setup.sh

The script writes spidev to /etc/modules-load.d/spidev.conf, loads the module immediately if possible, and backs up any existing config. Reboot when prompted, then verify:

sudo reboot
# after reboot:
ls /dev/spidev*
7

Serial / USB (optional)

If /dev/ttyUSB0 has issues: sudo apt remove brltty

8–9

Wiring

Jetson ↔ main board (SPI1)

Connect the main board’s 6-position Molex (first five positions) to the Jetson 40-pin header with dupont wires. Match SPI1 on the Jetson to the PCB labels:

Jetson pin name Jetson GPIO PCB pin name
SPI1_SCLK23SCK
SPI1_CS024SS
SPI1_din21SDO
SPI1_dout19SDI
GND25GND
Note: If using twisted-pair wire, do not twist the SDO and SDI wires together — that can cause crosstalk and degrade SPI communication.

Full main-board wiring (motors, power, buttons) is in the Electrical README.

Jetson ↔ microphone board (I2S2)

Use the PIC32 board’s 6-pin connector (not the 5-pin header). Set the switch to the upward position. Connect to Jetson I2S2 (pin numbers may vary with your header config):

PIC32 pin Jetson I2S pin Jetson pin #
CSI2S2_FS35
SDII2S2_DOUT40
SDOI2S2_DIN38
SCKI2S2_SCLK12
GNDGND14
3V33.3V17

Microphone assembly, PIC32 firmware, and Jetson audio software: Microphone README and mattbot_record (noetic).

Roaming lets the Jetson switch access points when signal is weak. Use wifi_roaming_setup.sh from the Jetson directory in the Robot repo.

Prerequisites: Intel AC9260 installed, bring-up through step 4, sudo apt install wpasupplicant, and a wlan* interface (ip link show | grep wlan).

Copy script to Jetson (wget):

cd ~
wget "https://raw.githubusercontent.com/satomm1/Robot/main/Jetson/wifi_roaming_setup.sh"
chmod +x wifi_roaming_setup.sh

Run:

sudo ~/wifi_roaming_setup.sh

Follow prompts (SSID, password, US/KR regulatory domain), then sudo reboot. Verify: sudo systemctl status wpa_supplicant@wlan0 custom_wifi.service (replace wlan0 if needed).

Assign a fixed DHCP reservation for the Jetson’s WiFi MAC on your router for a stable IP.

Undo/revert steps are in the Jetson README.

Use the pre-built ml_ros image from GitHub Container Registry (ghcr.io/satomm1/ml_ros:latest). Building from scratch on the Jetson is only needed if you are customizing the container — see the Jetson README.

First configure Docker on the Jetson per jetson-containers setup (through at least Relocating Docker Data Root). If using NVMe, set data-root in /etc/docker/daemon.json (e.g. "/mnt/data"), then sudo systemctl restart docker.

1

Pull pre-built ml_ros image

On the Jetson, after Docker is installed:

docker pull ghcr.io/satomm1/ml_ros:latest

Verify:

docker images ghcr.io/satomm1/ml_ros:latest

No docker login is required — the package is public on GHCR. To refresh after an image update, run docker pull ghcr.io/satomm1/ml_ros:latest again.

Offline / no registry access: on a connected machine, run docker pull ghcr.io/satomm1/ml_ros:latest, then docker save ghcr.io/satomm1/ml_ros:latest | gzip > ml_ros.tar.gz, copy to the Jetson, and sudo docker load < ml_ros.tar.gz.

Start the container (example):

sudo docker run --runtime nvidia --network=host -v ~/workspaces/catkin_ws:/workspace/catkin_ws -v ~/gemini_api:/gemini_code -v /dev/bus/usb:/dev/bus/usb -v /dev/video0:/dev/video0 -v /dev/video1:/dev/video1 -it --device=/dev/ttyUSB0 --device=/dev/spidev0.0 --rm --privileged --name ros_noetic ghcr.io/satomm1/ml_ros:latest

Inside the container: source /opt/ros/noetic/setup.bash

2

ROS workspace

cd /workspace/catkin_ws
mkdir -p devel src
catkin_make
source devel/setup.bash
cd src

Inside the container, download and run clone_repos.sh from the /workspace/catkin_ws/src directory. It clones the mattbot packages and third-party ROS repos (all on the noetic branch) and downloads robot_env.sh, startup_script.py, and cyclonedds.xml:

cd /workspace/catkin_ws/src
wget "https://raw.githubusercontent.com/satomm1/Robot/main/Jetson/clone_repos.sh"
chmod +x clone_repos.sh
./clone_repos.sh

Repositories cloned: mattbot_bringup, mattbot_dds, mattbot_record, mattbot_image_detection, mattbot_mcl, mattbot_navigation, mattbot_teleop, mattbot_database, ros_astra_camera, rplidar_ros, twist_mux, slam_gmapping.

cd /workspace/catkin_ws
catkin_make
source devel/setup.bash

Configure Cyclone DDS peer discovery in cyclonedds.xml (downloaded by clone_repos.sh above). This file disables multicast and uses an explicit peer list for WiFi mesh discovery:

nano /workspace/catkin_ws/src/cyclonedds.xml

You must update the <Peer Address="…"/> entries so they list the actual IP addresses of every robot and device that should participate in DDS discovery on your network:

  • Include this Jetson’s IP address (the same value you set for ROS_IP in robot_env.sh).
  • Add the IP addresses of any other robots or machines running ROS on the mesh.
  • Remove any placeholder entries that do not correspond to a real device.

The default file assumes the wlan0 interface; change <NetworkInterface name="…"/> only if your WiFi interface has a different name (run ip link or ifconfig to check).

Edit robot_env.sh with this Jetson’s IP address, robot ID, MCU SPI number, camera type, and robot height. CYCLONEDDS_URI is already set to point at cyclonedds.xml — leave that line unchanged unless you move the file.

nano /workspace/catkin_ws/src/robot_env.sh

Replace the placeholder values on these lines (leave export and the variable names unchanged):

export ROS_IP=              # this Jetson's WiFi IP address
export ROBOT_ID=              # unique integer for this robot (match MCU robot ID)
export MCU_SPI=               # 1 or 3 — SPI used for MCU comms (SPI1 on header → usually 1)
export CAMERA_TYPE=           # astra_pro_plus, astra, or astra_pro
export ROBOT_HEIGHT=          # short or tall
export ROBOT_CAR=             # true for car configuration, false otherwise

Use the arrow keys to move the cursor. Type each value after the =. Save and exit nano: Ctrl+O, Enter, then Ctrl+X.

Add these lines to ~/.bashrc so ROS and robot settings load in every shell:

nano ~/.bashrc

Append at the bottom of the file:

source /workspace/catkin_ws/devel/setup.bash
source /workspace/catkin_ws/src/robot_env.sh

Save and exit nano (Ctrl+O, Enter, Ctrl+X), then reload:

source ~/.bashrc

Test with roscore. For the Astra camera, outside the container:

cd ~/workspaces/catkin_ws/src/ros_astra_camera
./scripts/create_udev_rules
sudo udevadm control --reload && sudo udevadm trigger

Save changes (with container running):

sudo docker commit ros_noetic ghcr.io/satomm1/ml_ros:latest

This tag matches the Jetson host service and GHCR image name.

3

Gemini container (optional)

Pull the pre-built image from GitHub Container Registry (ghcr.io/satomm1/gemini:latest). No docker login is required — the package is public on GHCR.

docker pull ghcr.io/satomm1/gemini:latest

Verify:

docker images ghcr.io/satomm1/gemini:latest

To refresh after an image update, run docker pull ghcr.io/satomm1/gemini:latest again.

Offline / no registry access: on a connected machine, run docker pull ghcr.io/satomm1/gemini:latest, then docker save ghcr.io/satomm1/gemini:latest | gzip > gemini.tar.gz, copy to the Jetson, and sudo docker load < gemini.tar.gz.

Clone the API repo and start the container:

cd ~ && git clone https://github.com/satomm1/gemini_api.git
sudo docker run -v ~/gemini_api:/gemini_code -v ~/Desktop/audio:/audio -w /gemini_code -it --rm --privileged -p 5000:5000 --name gemini ghcr.io/satomm1/gemini:latest

Inside: . start_scripts.sh

After Docker and the ROS workspace are set up, install the Jetson host service on the base machine (not inside Docker). The operator GUI Robot Startup panel uses HTTP on port 8081 to start/stop the ros_noetic container and power off the Jetson. ROS launch and software updates remain on port 8080 inside the container (startup_script.py).

You do not need the GUI repo on the Jetson — only the install script from the Robot repo. Full reference: JETSON_HOST_SERVICE.md.

1

Get the install script

On the Jetson (recommended):

cd ~
wget "https://raw.githubusercontent.com/satomm1/Robot/main/Jetson/jetson-host-install.sh"
chmod +x jetson-host-install.sh

From your development machine (if you have the Robot repo cloned):

scp Jetson/jetson-host-install.sh YOUR_USER@JETSON_IP:~/
ssh YOUR_USER@JETSON_IP 'chmod +x ~/jetson-host-install.sh'
2

Run the installer

On the Jetson:

sudo ./jetson-host-install.sh

The installer writes /opt/robot/host_service.py and enables the robot-host-service systemd unit so the service starts on boot. Re-running the installer replaces the service file and restarts it.

If you see /usr/bin/env: 'bash\r': No such file or directory, the script has Windows (CRLF) line endings. On the Jetson run sed -i 's/\r$//' ~/jetson-host-install.sh and try again, or re-copy with scp from a checkout that uses LF for *.sh.

3

Verify

curl -s http://127.0.0.1:8081/status

Expected response (example): {"host_service": true, "docker_running": false, "container": "ros_noetic"}

Open port 8081 on the Jetson firewall if your operator PC cannot reach the Robot Startup panel. To change Docker paths or the docker run command after install, edit /opt/robot/host_service.py and run sudo systemctl restart robot-host-service.

Run all of the following on the Jetson host (outside the Docker container), not inside ros_noetic or other containers.

git clone https://github.com/satomm1/mattbot_display.git
cd mattbot_display
pip3 install pygame
pip3 install -U pyinstaller
mkdir -p ~/Desktop/audio
cp default.mp3 ~/Desktop/audio
python3 display_app.py

Build desktop executable: pyinstaller display_app.spec, then cp dist/display_app ~/Desktop. After copying to the Desktop, you can launch the display app by double-tapping its icon on the touchscreen. Adjust AUDIODEV in the script if needed (see Jetson README).

User GUI / communication

The dds_robot_platform repository provides software for a human observer to connect to the mobile robot fleet: DDS agent communication, a web-based GUI, GraphQL API, and Ignite logging.

Repository: github.com/satomm1/dds_robot_platform · Full README on GitHub

Repo layout:

  • ./dds — connect to other agents (mobile robots) via DDS
  • ./gui — web-based GUI for human interaction
  • ./graphql — GraphQL API
  • ./ignite — Ignite database log files

After setup here, use the Operation Guide → Starting User GUI / DDS for development vs. production workflows.

Firewall: Allow inbound and outbound traffic to your robot IP addresses. Blocked firewall rules are a common cause of connection failures.

Clone the repository before installing dependencies or running DDS/GUI:

git clone https://github.com/satomm1/dds_robot_platform.git
cd dds_robot_platform

Windows: Clone into your WSL filesystem (e.g. under ~/ in Ubuntu) so docker compose runs where the README expects. For GUI development from source, you may also clone a second copy on a Windows path and run npm start from Windows command line.

Install Docker Desktop and ensure the Docker daemon is running.

Copy dds/dds_env.sh.example to dds/dds_env.sh and set AGENT_ID, INFLUXDB_TOKEN, and any other operator variables. The compose stack, DDS container, and GUI read this file. The example file also sets CYCLONEDDS_URI to dds/cyclonedds.xml, which CycloneDDS uses for discovery.

cp dds/dds_env.sh.example dds/dds_env.sh
nano dds/dds_env.sh

Before connecting to mobile robots, edit dds/cyclonedds.xml for your network:

nano dds/cyclonedds.xml
  • Network interface — set <NetworkInterface name="…"/> to the interface that reaches the robot fleet (run ifconfig or ip link; common names include wlan0, wlp2s0, eth0).
  • Peer addresses — replace the placeholder <Peer Address="…"/> entries with the IP address of each robot (or other DDS participant) on that network.

Windows workflow: Run docker compose and DDS commands via WSL. Run the GUI from the Windows command line (desktop app) or from source. See the Operation Guide for day-to-day production steps.

The local stack runs entirely in Docker. DDS and GraphQL use the ghcr.io/satomm1/matt_python image from GitHub Container Registry (no manual download from Google Drive). The ./dds directory is mounted into the container; DDS scripts are started and stopped on demand inside the dds container.

1

Pull images and start the stack

From the dds_robot_platform repo root:

docker compose pull
docker compose up -d

docker compose up -d alone also pulls missing images. This starts InfluxDB, Ignite, GraphQL, and the idle dds container.

Alternatively: GUI Local StackDockerStart (runs docker compose up -d via WSL on Windows). Requires compose.yaml and dds/dds_env.sh.

To tear down the stack:

docker compose down
2

Start and stop DDS scripts (in container)

After Docker is up, start DDS inside the dds container:

docker exec -d dds ./start_scripts.sh

Alternatively: GUI Local Stack — verify repo path, start Docker, then DDSStart (DDS stays disabled until Docker is running).

When finished, stop DDS scripts:

docker exec dds ./stop_scripts.sh

Or DDSStop in the GUI Local Stack panel.

3

Verify DDS scripts (optional)

docker exec dds bash -lc "pgrep -af python"

You should see the publisher/subscriber scripts (entry_exit.py, heartbeat_publisher.py, goal_publisher.py, etc.).

If you hit a DDS error during setup, contact Matthew Sato at satomm@stanford.edu.

Pre-built installers are produced by GitHub Actions (recommended for end users; no Node.js required).

  1. Go to dds_robot_platform Actions
  2. Open the most recent successful workflow run
  3. Under Artifacts, download one ZIP for your system (you must be logged in to GitHub):
    • Windows: gui-installer-windows-latest — unzip, run DDS Robot GUI Setup … .exe, launch from Start menu. Unsigned builds may show SmartScreen — More infoRun anyway if you trust the source.
    • macOS: gui-installer-macos-latest — open .dmg, drag DDS Robot GUI to Applications. First launch may need right-click → Open.
    • Linux: gui-installer-ubuntu-latest — unzip .AppImage, chmod +x, run. Install FUSE / libfuse2 if the AppImage will not start.
Backend: The desktop app is only the UI. Start the Docker stack and DDS scripts so the GraphQL API is available (see DDS (Docker) above). The app expects http://localhost:8000/graphql unless changed at build time via REACT_APP_GRAPHQL_HTTP_URL in gui.
  1. Install Node.js and verify: node -v, npm -v
  2. Install dependencies:
cd gui
npm install

Start the dev server:

npm start

Production build (alternative):

npm run build
npm install -g serve
serve -s build

Production build for Electron app:

npm run build
npm run electron

(Optional) RVIZ visualization

RVIZ helps debug maps, LiDAR, and goals. On Windows, use a VirtualBox VM with Ubuntu 20.04 and ROS Noetic on the same network as the Jetson (try Bridged Adapter in VM network settings).

Install ROS Noetic per the official guide (Desktop Install; steps 1.1–1.5 only).

Add to ~/.bashrc on the VM:

export ROS_MASTER_URI=http://[IP_ADDRESS_OF_JETSON]:11311
export ROS_HOSTNAME=[IP_ADDRESS_OF_VM]

Save, then run source ~/.bashrc. With the Jetson ROS master running:

rosrun rviz rviz

In Displays → Add → By Topics, add LaserScan and other relevant topics. Use 2D Goal Pose on the toolbar to send navigation goals.

Initial system test

Verify teleoperation before moving to full operation.

1

Start Docker container

jetson-containers run -v ~/workspaces/catkin_ws:/workspace/catkin_ws -v ~/gemini_api:/gemini_code -v /dev/bus/usb:/dev/bus/usb -v /dev/video0:/dev/video0 -v /dev/video1:/dev/video1 -i --device=/dev/ttyUSB0 --device=/dev/spidev0.0 --rm --privileged --name ros_noetic $(autotag ml_ros:latest)

In a second terminal, enter the container:

docker exec -it ros_noetic bash
2

Run bringup and teleop

Terminal 1 — minimal bringup:

roslaunch mattbot_bringup minimal.launch

Terminal 2 — keyboard teleop:

roslaunch mattbot_teleop keyboard.launch

Use the keypad (u i o j k l m , .) to drive. If the robot moves, setup is complete.

3

Shut down

Press Ctrl+C in each ROS terminal. Exit the container with exit, or run docker stop ros_noetic from another terminal.

sudo shutdown now -h
Setup complete! Continue to the Operation Guide for GUI control, mapping, and autonomous navigation.