Build pathAdvancedA few weekends

Build a ROS 2 Robot That Sees and Drives to a Marker

A robot that spots a marker across a room, works out where it is relative to itself, and drives up to it head-on — structured as a real ROS 2 node graph.

Build a ROS 2 Robot That Sees and Drives to a Marker technical schematicPOSE → STANDOFF → DOCK

What you are building

A robot that is put down somewhere in a room, turns until it finds a printed marker on a wall, and drives up to it — stopping half a metre away, square to the marker’s face rather than merely close to it.

Every other build on this site runs one program on a microcontroller. This one is a computer with wheels, and that changes what the project is about. The hard parts are no longer timing and wiring; they are knowing where things are relative to each other, and keeping several pieces of software honest with one another while they all run at once.

Why this robot needs a computer

An Arduino cannot do this, and the reason is worth being precise about. It is not raw speed — it is that a camera frame is a quarter of a megabyte, ArUco detection wants floating-point linear algebra, and the whole thing has to happen while the wheels are still being commanded at 50 Hz. That is a job for an operating system with real memory and real scheduling.

What you get in exchange for that Linux box is the ability to look inside a running robot. Every message between every part is a topic you can subscribe to from a laptop. When the robot drives past the marker instead of to it, you do not add print statements and reflash — you ros2 topic echo the pose and watch it be wrong in real time.

The shape of the node graph

Five responsibilities, one node each:

Node Publishes Job
camera /image_raw, /camera_info Pull frames off the CSI bus
aruco_detector TF camera_link → marker_7 Find the tag, solve its pose
docking_controller /cmd_vel Decide where to drive
diff_drive serial to the Arduino Twist to wheel speeds
odometry TF odom → base_link Integrate encoder ticks

The interfaces are what make this worth structuring. The controller never touches a camera or a motor — it consumes a transform and produces a Twist. That means you can drive the same robot with keyboard teleop, test the controller against a fake transform with no hardware at all, and later drop in a navigation stack that publishes the identical topic.

Read nodes, topics, services, and actions before drawing your own version of that table. The most common mistake at this stage is making the docking controller a service — it is a long-running goal you must be able to cancel, which is exactly what an action is for.

Build order

1 — Get ROS 2 running and prove the network. Follow installing ROS 2 on a Raspberry Pi. Do not move on until ros2 topic echo on your laptop shows a topic published on the robot. Discovery over Wi-Fi is the single most likely thing to eat an evening, and you want it eaten now rather than while debugging vision.

2 — Make the wheels obey a Twist. Wire the Arduino, encoders and TB6612FNG, and build the cmd_vel bridge. Milestone: keyboard teleop from the laptop drives the real robot. Calibrate wheel separation here — everything downstream inherits it.

3 — Calibrate the camera. Properly, with a real target, following camera calibration and ArUco pose. An uncalibrated camera produces poses that look plausible and are wrong by tens of percent at the edges of the frame, which is the worst failure mode available to you.

4 — Publish the marker as a transform. Not as a custom message with an x and a y in it. Publishing camera_link → marker_7 into TF2 means the controller can ask “where is the marker, in base_link?” and the answer accounts for exactly how your camera is mounted — including the fact that it is tilted, which it is.

5 — Drive at the marker. A proportional controller on bearing: turn toward it, drive forward, stop at the standoff distance. This works, and it arrives at an angle, which is the point of the next step.

6 — Approach along the normal. Instead of steering at the marker, compute a waypoint standing off from the marker’s face along its normal, drive to that, and only then drive straight in. This is the difference between reaching the dock and docking. It is also where the marker’s orientation — the part of the pose you did not need until now — starts to matter, and where you will meet pose ambiguity.

7 — Wrap it in a state machine. Search, approach, align, final, done, plus the transitions out of each when the marker is lost. Finite state machines for robot behaviour covers the structure; the specific thing this robot needs is a lost-marker timeout that returns to search rather than continuing on the last known pose.

Hardware notes that matter

Mount the camera rigidly, and measure how. Every pose the robot computes is relative to the camera, and TF2 turns that into a robot-relative pose using the transform you declare from base_link to camera_link. A camera on a flexible bracket makes that declared transform a lie that varies with acceleration.

Tilt the camera down, and say so. A camera pointed at the horizon loses the marker exactly when the robot gets close. Ten to fifteen degrees down keeps it in frame through the final approach — and that tilt goes into the static transform, not into a fudge factor in the controller.

Measure the marker with calipers. Pose scale is directly proportional to the marker size you tell the solver. Printing at 96% because the printer scaled to fit makes every distance 4% wrong, silently and consistently.

Feed the Pi separately from the motors. A stalling motor drops the rail, and where an Arduino would reset harmlessly, a Pi corrupts its filesystem. Either a separate regulator or a separate pack — this is not the place to save a component.

Keep the camera ribbon away from motor leads. It is an unshielded high-speed link. Motor noise coupled into it appears as corrupted frames, which appear as detections that flicker for no visible reason.

When it goes wrong

Symptom Usually
Laptop sees no topics Multicast blocked by the access point, or mismatched ROS_DOMAIN_ID
Marker detected, robot does not move Twist published against a TwistStamped subscriber
Distance consistently off by a few percent Marker size wrong, or the printer scaled the page
Pose jitters wildly at range Marker too small in frame — get closer or print bigger
Marker normal flips back and forth Pose ambiguity — approach off-axis, or use a ChArUco board
Robot arrives near the marker but crooked Steering at the marker instead of along its normal
Overshoots on the final approach Steering on a pose that is 50–100 ms old — slow down near the end
Drives on after the marker leaves frame No lost-marker timeout in the state machine

The distribution in that table is worth noticing before you start: almost none of it is computer vision. It is networking, frames, units and control — which is what building a ROS 2 robot actually teaches, and why the ROS 2 foundations roadmap spends its time on communication patterns and coordinate frames rather than on OpenCV.

If this is a step too far, the micromouse teaches mapping and planning on a microcontroller with no Linux involved, and the phone-controlled robot covers the “commands arrive over a network” half of this problem for a fraction of the parts.

Project roadmap

The build path

Follow the tech tree from parts to a robot that follows a taped line. Each node unlocks when its prerequisites are done, and your progress saves on this device.

0 / 19 done

100%
Learning path

ROS 2 foundations

Learning path

Open path
Build

Drive it from a laptop

Build

Build

Approach along the normal

Build

Goal

Dock on a marker, repeatably

Goal

Components

Tutorials in this path

Frequently asked questions

Why use ROS 2 for this instead of a single Python script?

For this robot alone, a single script would work, and it would be shorter. What it would not survive is the second feature. The moment the camera loop, the drive loop and the decision logic have to run at different rates and keep running when one of them stalls, a single script becomes a scheduling problem you have to solve yourself. ROS 2 gives you process separation, a wire format between the parts, and — the part you feel immediately — the ability to inspect any topic from a laptop while the robot is running. The project is worth doing because it teaches that structure on a problem small enough to still understand.

Do I need an Arduino as well as the Raspberry Pi?

You do not strictly need one, but the robot is much better with it. A Pi runs Linux, which means your motor loop is scheduled alongside everything else and will occasionally be late by milliseconds. That is invisible for vision and fatal for a wheel PID. Splitting the robot — Pi for perception and decisions, microcontroller for the loops that must not jitter — is the standard architecture for a reason, and it is exactly the split ROS 2 encourages anyway.

Why an ArUco marker rather than detecting a real object?

Because the marker gives you a full 6-DOF pose from one frame with no training data and no ambiguity about which object you found. Detecting a chair tells you roughly where a chair is; detecting an ArUco tag tells you its position and orientation in metres, which is what a controller actually needs. It is also the honest starting point — real docking stations, warehouse robots, and drone landing pads use fiducial markers for the same reason.

Why does the marker's orientation flip back and forth?

This is pose ambiguity, and it is a property of the geometry rather than a bug in your code. A flat square viewed nearly head-on has two 3D orientations that project onto almost the same pixels, so tiny amounts of noise flip the solver between them. The distance stays stable while the estimated normal jumps. Use a physically larger marker, approach from slightly off-axis where the two solutions separate, or move to a ChArUco board or several markers on the dock — all of which give the solver more geometry to disagree with itself about.

How accurate is the docking?

With a calibrated camera and a marker measured to the millimetre, a centimetre or two at a half-metre standoff is realistic. The errors that dominate are not the ones people expect: marker size entered wrong scales every range estimate proportionally, and camera pipeline latency means the pose you are steering on describes where the marker was 50 to 100 milliseconds ago. Slowing down over the last 30 cm removes most of the second problem for free.

Can I do this on a Raspberry Pi Zero?

For this project, no — not comfortably. A Zero 2 W can run ROS 2 and can decode a camera stream, but ArUco detection at a useful frame rate on top of that leaves nothing behind, and building a workspace on it is painful. Use a Pi 4 with 4 GB or a Pi 5. The Zero is a good board for a robot whose Pi only relays commands.