Building a $300 Research Robot — Part 0: Surviving the Setup

28 May 2026

Intro

Robotics people use Linux. Not because they’re trying to be difficult. Just because that’s how it works — ROS2 (the standard robotics middleware everyone uses) runs natively on Ubuntu, drivers for robotics hardware are written for Linux first, and if you show up to a robotics lab running Windows you’ll get a very polite but firm look.

So here I am. Ubuntu 24.04. Staring at a terminal. Building a robot.

This post is for anyone in the same situation — technically capable, completely new to Linux, and learning by breaking things.


What I’m building

I’m building a 2 DOF pan-tilt robot — a small assembly of two smart servo motors with a depth camera mounted on top. It can rotate left/right and tilt up/down. The camera watches the world, detects objects in 3D space, and the robot points toward them automatically — not because I hand-coded “if object is left, rotate left” but because a small AI policy learned how to do it through thousands of simulated practice runs.

Total hardware cost: about $239. The whole thing sits on a wooden block on my desk.

The software stack is:

  • ROS2 Jazzy — the robotics middleware that lets different parts of the system talk to each other
  • EMOS — a higher-level framework built on top of ROS2 that handles perception, planning, and control
  • Ubuntu 24.04 — the Linux distribution everything runs on

This post is just Part 0 — getting the environment working before touching any robot code.


Linux terms I had to learn before I could even read the error messages

Before the story, a quick glossary. Skip this if you already know Linux. If you don’t — read it, because none of what follows will make sense without it.

The kernel is the core of the operating system. It is the first program that is uploaded to your device and the first program that activates. It acts as a bridge between the hardware and software input outputs. It controls which program gets access to how much RAM.

A driver is software that lets your OS talk to a specific piece of hardware. It allows the kernel to communicate with the hardware components.

DKMS (Dynamic Kernel Module Support) is a system that automatically recompiles drivers whenever the kernel updates. When a new kernel version installs, DKMS tries to rebuild all hardware drivers for it. If it fails — which it did in my case — you end up with a broken state where the new kernel exists but your GPU driver doesn’t work on it.

apt is Ubuntu’s package manager — the tool you use to install, update, and remove software. Most commands start with sudo apt .... The sudo part means “run this as administrator” — like right-clicking and choosing “Run as Administrator” on Windows.

dpkg is the lower-level tool that apt uses under the hood. You touch dpkg when apt can’t fix itself and you have to go one level deeper.

When I say “broken packages,” I mean packages that are partially installed or stuck in a state where the system doesn’t know what to do with them.

HWE (Hardware Enablement Stack) is Ubuntu’s system for keeping your kernel and drivers up to date on LTS (Long Term Support) releases. HWE kernels are newer than the default and pulled in automatically during upgrades. This is what pulled in kernel 6.17 without me asking for it.


My machine

  • Laptop: Dell G3 3500
  • GPU: NVIDIA GeForce GTX 1650 (4GB VRAM)
  • OS: Ubuntu 24.04.3 LTS
  • ROS2: Jazzy (pre-installed)
  • NVIDIA Driver: 580.65.06 (when I started)
  • CUDA: 13.0

The plan

Simple: update the system, then install EMOS.

sudo apt update && sudo apt upgrade -y

What this does: sudo apt update tells the system to check for newer versions of installed software. sudo apt upgrade -y actually downloads and installs those updates. The -y means “say yes to everything automatically.”

This should have taken 5 minutes. Instead it took an hour.


What broke: kernel 6.17 vs NVIDIA 580

So the upgrade finished and I had a wall of red text staring back at me. Great start.

What happened, as far as I can tell: the upgrade quietly pulled in a new kernel — version 6.17 — as part of the routine update. I didn’t ask for it, it just came along for the ride. The kernel is essentially the core of the operating system, the thing that talks to all your hardware. When it updates, all your hardware drivers need to be rebuilt for it — including the NVIDIA GPU driver. That rebuilding process is handled automatically by something called DKMS.

Now here’s something that confused me at first — if 6.17 was broken, why was my laptop still running fine? The answer is that Linux doesn’t replace the old kernel when a new one installs. It sits alongside it. Both live on your disk at the same time. When you boot up, a menu called GRUB appears (usually just for a second before it auto-selects) and picks which kernel to load. By default it picks the newest one.

Think of it like downloading an app update on your phone that gets stuck halfway. The old version still works. But now your storage has both the old and the broken new version sitting on it, and the app store is confused about what state everything is in.

So in my case:

  • Kernel 6.14 was already installed and working
  • The upgrade downloaded and partially installed kernel 6.17 alongside it
  • DKMS failed during that installation — 6.17 never fully completed
  • On reboot, GRUB tried to load 6.17 since it was newest — but fell back to 6.14 because the installation was broken
  • I ended up running 6.14 while 6.17 sat half-installed on the disk doing nothing except breaking apt DKMS failed with this:
Error! Bad return status for module build on kernel: 6.17.0-29-generic (x86_64)
dkms autoinstall on 6.17.0-29-generic/x86_64 failed for nvidia(10)

Honestly I’m still not 100% sure why. My best guess is that the kernel headers — the instruction manual DKMS needs to rebuild the driver — arrived in the wrong order during the upgrade and the rebuild started before everything was ready. It probably wasn’t a genuine incompatibility, because later when I cleaned everything up the exact same driver compiled fine on kernel 6.17 without any changes. So something in the installation process just went sideways.

The result was four packages stuck in limbo — half installed, half not, blocking each other:

Errors were encountered while processing:
 linux-headers-6.17.0-29-generic
 linux-headers-generic-hwe-24.04
 linux-generic-hwe-24.04
 linux-image-6.17.0-29-generic
E: Sub-process /usr/bin/dpkg returned an error code (1)

The one good thing: my laptop was still running on the old kernel (6.14) so nothing was actually broken yet. The GPU still worked. I just had this mess sitting in the background that needed sorting out before I could do anything else.


Attempt 1: the wrong fix

My first instinct was sudo apt --fix-broken install followed by sudo apt autoremove.

apt --fix-broken install tries to resolve broken dependencies automatically. Reasonable first step.

apt autoremove removes packages that are “no longer needed.” Sounds safe.

It wasn’t.

autoremove looked at Ubuntu’s dependency graph — which was unreliable because packages were in a broken state — and decided to remove:

  • The working NVIDIA 575 drivers
  • The older kernel 6.14.0-29 Quick detour: a dependency graph is just Ubuntu’s way of tracking which software needs which other software to work. A video player might depend on an audio library, which depends on a codec package, and so on. Ubuntu keeps a map of all these relationships so when you install or remove something it knows what else to bring along or clean up. When packages are broken or half-installed, that map becomes unreliable — like Google Maps that doesn’t know a road is closed. autoremove trusted the map, the map was wrong, and it removed things it shouldn’t have.

Things I didn’t ask it to remove. Things that were working fine.

The 6.17 kernel stayed because it had a dependency holding it in place:

linux-image-generic-hwe-24.04 depends on linux-image-6.17.0-29-generic

So now I had: broken 6.17 kernel, partially removed NVIDIA drivers, and a bigger mess than before.

Lesson: never run apt autoremove when packages are in a broken state.

What that means in plain terms: autoremove is a cleanup tool, not a repair tool. It’s meant to be run when everything is healthy and you just want to tidy up unused packages. Running it when things are already broken is like asking someone to clean your house while it’s on fire — they’ll start throwing things out without knowing what’s important and what isn’t.

What I should have done instead:

Step 1 — Don’t touch autoremove at all. When you see broken packages, fix them first.

Step 2 — Run these repair commands first:

sudo dpkg --configure -a
sudo apt --fix-broken install -y

dpkg --configure -a tells dpkg to finish anything it left half-done. apt --fix-broken install tells apt to look at what’s broken and try to resolve it cleanly. These are the “try to repair yourself” commands.

Step 3 — Only run autoremove after those succeed. Once the system is healthy, autoremove can safely figure out what’s genuinely unused.

The short version: see broken packages → run dpkg --configure -a and apt --fix-broken install → only then consider cleanup.


Attempt 2: trying to force-remove the 6.17 kernel

Since normal removal was blocked by dependencies, I tried force-removal:

sudo dpkg --remove --force-remove-reinstreq linux-image-6.17.0-29-generic linux-headers-6.17.0-29-generic linux-headers-generic-hwe-24.04 linux-generic-hwe-24.04

--force-remove-reinstreq removes packages stuck in a “needs reinstall” broken state, ignoring dependency checks. This got most of them — but not the image:

dpkg: dependency problems prevent removal of linux-image-6.17.0-29-generic:
 linux-image-generic-hwe-24.04 depends on linux-image-6.17.0-29-generic.

The meta-package linux-image-generic-hwe-24.04 was still holding the kernel image hostage. I had to remove that first.


The actual fix — in order

Step 1 — Remove the meta-package holding the kernel in place:

sudo dpkg --remove --force-remove-reinstreq linux-image-generic-hwe-24.04

A meta-package is a package that doesn’t contain any software itself — it just declares dependencies on other packages. linux-image-generic-hwe-24.04 existed only to pull in linux-image-6.17.0-29-generic. Removing it broke the dependency chain.

Step 2 — Now remove the kernel image:

sudo dpkg --remove --force-remove-reinstreq linux-image-6.17.0-29-generic

Step 3 — Clean up dpkg’s internal state:

sudo dpkg --configure -a
sudo apt --fix-broken install -y

dpkg --configure -a finishes configuring any packages that got interrupted mid-install. apt --fix-broken install resolves remaining dependency problems. Run both every time after force-removing packages.

Step 4 — Reinstall NVIDIA drivers:

sudo apt install nvidia-driver-580 -y

This time, apt pulled in version 580.159.03 — a newer release that actually supports kernel 6.17. The original upgrade had been trying to install this all along. The broken dpkg state prevented it from completing. Clearing the state let it finish.

Step 5 — Reboot:

sudo reboot

After reboot — everything working:

uname -r
# 6.17.0-29-generic ✓

nvidia-smi
# NVIDIA-SMI 580.159.03   Driver Version: 580.159.03   CUDA Version: 13.0 ✓

The irony: Think about what the upgrade was actually trying to do — bring in kernel 6.17 and a newer NVIDIA driver that supports it. That’s the correct thing to do. The problem wasn’t the plan, it was the execution. Packages installed in the wrong order, DKMS ran before the kernel headers were ready, and dpkg froze halfway through. When I force-removed the broken packages and reinstalled cleanly, all I did was give that original upgrade a clean slate to finish what it started. Same kernel, same driver, same end result — just without the half-done mess in the way. Mildly annoying in hindsight. Two hours of debugging to end up exactly where the upgrade was trying to take me anyway.. Once I cleared the mess, it completed exactly as intended — new kernel, new driver, everything working.


Installing EMOS

With the system clean, EMOS installed without drama.

Install the EMOS CLI:

curl -sSL https://raw.githubusercontent.com/automatika-robotics/emos/main/stack/emos-cli/scripts/install.sh | sudo bash

curl downloads a file from a URL. Piping it to sudo bash runs it immediately with admin privileges. This is standard practice for installing developer tools from trusted sources.

Install EMOS natively:

emos install --mode native

Native mode builds EMOS directly into your existing ROS2 installation. The other modes are Container (runs inside Docker, no ROS2 needed) and Pixi (isolated environment, no root required). Since I already have ROS2 Jazzy, native is cleanest.

Gotcha — ROS2 not sourced:

First attempt failed with:

E: Unable to locate package python3-ament-package

This happened because the terminal didn’t know where ROS2 was installed. In Linux, software installed in non-standard locations needs to be “sourced” — you run a script that tells the current terminal session where everything lives.

Fix:

source /opt/ros/jazzy/setup.bash
emos install --mode native

source loads a configuration file into the current terminal session. You need to run this in every new terminal before using ROS2 commands. To make it automatic so you never have to think about it again:

echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc

This adds the source command to ~/.bashrc — a file that runs automatically every time you open a new terminal. After running this once, every new terminal will have ROS2 available automatically.

After sourcing, EMOS installed cleanly.


TL;DR — What to do when a kernel upgrade breaks your NVIDIA driver

If you ran sudo apt upgrade, got a wall of red text, and nvidia-smi stopped working, this is probably what happened and how to fix it.

Step 1 — Try the gentle fix first:

sudo apt --fix-broken install -y
sudo dpkg --configure -a

Run these first. Sometimes this alone fixes it. If it doesn’t, continue.

Step 2 — Find the broken kernel packages:

dpkg -l | grep linux-image
dpkg -l | grep linux-headers

Look for packages marked with iF or iU at the start of the line — those are broken. Note their names.

Step 3 — Force-remove them, meta-packages first:

sudo dpkg --remove --force-remove-reinstreq <package-name>

Meta-packages are the ones without a version number in the name — like linux-image-generic-hwe-24.04. Remove those before the versioned ones like linux-image-6.17.0-29-generic.

Step 4 — Clean up and reinstall your NVIDIA driver:

sudo dpkg --configure -a
sudo apt --fix-broken install -y
sudo apt install nvidia-driver-$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | cut -d. -f1) -y

Step 5 — Reboot and verify:

sudo reboot
# After reboot:
uname -r
nvidia-smi

If nvidia-smi shows your GPU, you’re done.