Software, sensors, network, and GUI
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.
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).
The default card does not support roaming (needed for mesh). Use an Intel AC9260 on the bottom of the Jetson (screwdriver).
Power on with a monitor, connect to Wi‑Fi so you can SSH in.
sudo apt update sudo apt install nano
cd /opt/nvidia/jetson-io sudo python3 jetson-io.py
Manually configure the header: activate SPI1 and I2S. Reboot when done.
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*
If /dev/ttyUSB0 has issues: sudo apt remove brltty
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_SCLK | 23 | SCK |
| SPI1_CS0 | 24 | SS |
| SPI1_din | 21 | SDO |
| SPI1_dout | 19 | SDI |
| GND | 25 | GND |
Full main-board wiring (motors, power, buttons) is in the Electrical README.
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 # |
|---|---|---|
| CS | I2S2_FS | 35 |
| SDI | I2S2_DOUT | 40 |
| SDO | I2S2_DIN | 38 |
| SCK | I2S2_SCLK | 12 |
| GND | GND | 14 |
| 3V3 | 3.3V | 17 |
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).
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.
ml_ros imageOn 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
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:
ROS_IP in robot_env.sh).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.
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.
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'
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.
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).
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 filesAfter setup here, use the Operation Guide → Starting User GUI / DDS for development vs. production workflows.
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
<NetworkInterface name="…"/> to the interface that reaches the robot fleet (run ifconfig or ip link; common names include wlan0, wlp2s0, eth0).<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.
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 Stack → Docker → Start (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
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 DDS → Start (DDS stays disabled until Docker is running).
When finished, stop DDS scripts:
docker exec dds ./stop_scripts.sh
Or DDS → Stop in the GUI Local Stack panel.
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).
gui-installer-windows-latest — unzip, run DDS Robot GUI Setup … .exe, launch from Start menu. Unsigned builds may show SmartScreen — More info → Run anyway if you trust the source.gui-installer-macos-latest — open .dmg, drag DDS Robot GUI to Applications. First launch may need right-click → Open.gui-installer-ubuntu-latest — unzip .AppImage, chmod +x, run. Install FUSE / libfuse2 if the AppImage will not start.http://localhost:8000/graphql unless changed at build time via REACT_APP_GRAPHQL_HTTP_URL in gui.
node -v, npm -vcd 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
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.
Verify teleoperation before moving to full operation.
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
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.
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