Mustafa Sepen Robotics Software Engineer

Project 01

Pathfinder

Multi-platform autonomous ground vehicle development

Role
Autonomy, localization, embedded integration
Year
2025 — present
Platform
Tracked UGV · 4×4 UGV · Ackermann RC crawler
Vehicles
Three, one shared stack
Testing
Physical hardware, outdoors
The tracked skid-steer UGV outdoors on wet ground, with its roof-mounted dual GNSS antenna mast, during an autonomous waypoint run.
Tracked UGV — the largest of the three platforms, running an outdoor waypoint mission.

The problem

One stack, three vehicles

Outdoor autonomy stacks tend to be written for one robot. The kinematics, the sensor set and the tuning end up braided through the navigation code, and moving to a second platform means rewriting rather than reconfiguring.

Pathfinder is the opposite exercise. One ROS 2 autonomy architecture — dual-EKF RTK localization, Nav2 waypoint navigation and a micro-ROS motor interface — is carried across three physically different ground vehicles: a tracked skid-steer UGV, a 4×4 differential rover, and an Ackermann RC crawler with no wheel encoders at all.

Each vehicle is maintained on its own branch with its own kinematics, sensor set and field-validated tuning. What has to stay identical is everything above the motor interface. What is allowed to change is configuration.

The system

Sensing → localization → navigation → motors

Every vehicle runs a Raspberry Pi 5 with ROS 2 Humble in Docker, an RTK GNSS receiver providing position and dual-antenna heading, a BNO055 IMU over UART, and an ESP32-S3 running micro-ROS behind the motor interface.

Localization is a dual EKF from robot_localization. A local filter fuses wheel odometry and gyro to publish a smooth, continuous odom → base_link. A global filter fuses RTK position and absolute GNSS heading to publish map → odom, anchoring everything to real-world coordinates. navsat_transform converts latitude and longitude into the map frame between them.

Nav2 plans with NavFn and tracks with a Regulated Pure Pursuit controller, retuned per vehicle. twist_mux arbitrates between navigation, the operator joystick and the emergency stop, and publishes the result on /cmd_vel_out.

Architecture

The abstraction boundary is two topics

The boundary between shared autonomy and vehicle-specific hardware is exactly two topics: /cmd_vel_out going down, and /odom_esp coming back.

Everything above that line — the dual EKF, navsat_transform, Nav2, the mission console — is identical across the fleet. Everything below it is the vehicle: tracks, four wheels, or a steering servo. Porting to a new platform means writing the firmware behind the boundary and retuning the layer above it, not rewriting the autonomy.

Detailed Pathfinder autonomy architecture. RTK GNSS, a BNO055 IMU and wheel odometry feed a local EKF; the local EKF and GNSS feed navsat_transform and a global EKF; the global EKF feeds Nav2, which feeds twist_mux and the /cmd_vel_out topic, consumed by an ESP32-S3 running micro-ROS and then the vehicle-specific motor interface. A web mission console attaches to Nav2.
Localization, navigation and the embedded boundary. Diagram from the project repository.

Engineering work

What I implemented

The autonomy stack, the localization configuration and the operator tooling, carried across all three platforms:

  • A custom ROS 2 driver for the dual-antenna RTK GNSS receiver — binary protocol parsing, position and absolute heading, and an integrated NTRIP client injecting RTK corrections in real time.
  • Dual-EKF localization in robot_localization: separating relative gyro yaw from absolute GNSS heading across the local and global filters, and configuring navsat_transform so the map frame is anchored correctly.
  • A datum node that waits for several consistent RTK-fixed positions and a valid heading before fixing the map origin, so one bad fix during acquisition cannot offset the entire map.
  • Nav2 bring-up and per-vehicle tuning — planner and controller configuration, costmap footprints, kinematic limits, and behaviour-tree selection.
  • The /cmd_vel_out and /odom_esp boundary to the ESP32-S3 micro-ROS motor interface, plus a yaw-priority velocity budget enforced in firmware.
  • A browser-based mission console: a FastAPI and WebSocket backend bridging ROS 2 to a Leaflet map front end for waypoint missions, live telemetry and emergency stop.
  • Field tooling — wheel-odometry scale calibrated against pure RTK displacement, and JSONL mission recording so tuning changes are argued from logs rather than impressions.

The vehicles

Three platforms, three drive geometries, three different reasons the localization stack had to be argued from first principles.

Tracked UGV

Tracked skid-steer · 1.0 × 0.74 m · 0.625 m between track centrelines

Autonomous waypoint mission — tracked skid-steer platform.
Drive Tracked, differential / skid-steer
Compute Raspberry Pi 5 · ROS 2 Humble in Docker
Motor control ESP32-S3 micro-ROS bridge to the drive controllers
Positioning RTK GNSS at 20 Hz with dual-antenna heading
IMU BNO055 over UART
Perception Hesai XT16 3D LiDAR — visualisation only
Localization Dual EKF — wheel odometry and gyro locally, RTK and GNSS heading globally
Navigation Nav2, Regulated Pure Pursuit, ~0.95 m/s target, plus a planner-free straight-line mode

Challenge —
velocity budgeting

On a skid-steer platform each track's speed is the sum of the body's linear and angular commands:

vtrack = v ± ω · (track separation / 2)

Ask for speed and yaw at the same time and the outer track saturates, the turn degrades, and the vehicle drifts off the path. The fix is a yaw-priority budget enforced in firmware: the commanded turn rate is always delivered in full, and forward speed is clipped to whatever headroom remains. Nav2's regulated speed scaling handles it feed-forward; the firmware budget guarantees it.

Streaming the LiDAR's 2.4 MB point cloud at 10 Hz over the vehicle's own WiFi needed CycloneDDS tuning — unicast data with multicast limited to discovery, and a 10 MB socket receive buffer — to stop fragment loss.

The LiDAR feeds the mission console's 2D obstacle view. It is not wired into the Nav2 costmaps, and this vehicle does not perform autonomous obstacle avoidance.

4×4 UGV

4-wheel differential · 0.5 × 0.4 m · where the localization stack was brought to a working state

Autonomous waypoint mission — 4×4 differential rover.
Drive 4-wheel differential (skid) drive
Compute Raspberry Pi 5 · ROS 2 Humble in Docker
Motor control ESP32-S3 micro-ROS → 2 × RoboClaw, closed-loop velocity on the controllers
Positioning RTK GNSS with dual-antenna heading, binary protocol parsed in a custom driver
IMU BNO055 over UART
Localization Dual EKF — wheel odometry and gyro locally, RTK and GNSS heading globally
Navigation Nav2, Regulated Pure Pursuit, 0.8 m/s target, rotate-to-heading enabled

Challenge —
heading and datum correctness

Outdoor GPS navigation fails in a specific and confusing way when heading is wrong: the vehicle drives confidently in the wrong direction. This platform is where that got solved, and the fix is a separation of two different yaw signals.

Relative gyro yaw feeds the local filter. It starts at zero wherever the vehicle happens to be pointing, and it only has to be smooth — it makes short-term motion continuous and jitter-free.

Absolute GNSS heading, from the dual-antenna receiver, feeds the global filter. It is slower, but it is true north, and it is what anchors the map frame.

Mixing them — or letting relative yaw into the global filter — produces a vehicle that navigates smoothly along the wrong bearing. Keeping them in separate filters is what makes waypoint missions land where they should.

The map origin is set the same way. A dedicated node waits for several consistent RTK-fixed positions and a valid heading before it fixes the datum, so a single bad fix during acquisition cannot offset the entire map.

RC Crawler

Ackermann steering · 0.313 m wheelbase · up to 46° · no wheel encoders anywhere on the vehicle

Autonomous waypoint mission — Ackermann RC crawler.
Drive Ackermann steering, 0.313 m wheelbase, up to 46°
Compute Raspberry Pi 5 · ROS 2 Humble in Docker
Motor control ESP32-S3 micro-ROS bridge
Positioning RTK GNSS at 20 Hz with dual-antenna heading
IMU BNO055 over UART
Wheel odometry None
Navigation Nav2 tuned for non-holonomic motion — up to 1.6 m/s, reversing enabled, rotate-to-heading disabled, recovery-free behaviour trees

Challenge —
localization with no odometry

With no encoders there is no dead-reckoning source. The accelerometer is not a substitute: integrating it twice with no absolute reference to correct against guarantees the position runs away. So the local filter is deliberately reduced to a heading estimator — gyro only, accelerometer explicitly disabled — and all position information comes from RTK GNSS through the global filter.

That topology has a trap. If the GPS-derived odometry is published in the odom frame and then fed back into the filter that produces map → odom, the estimate reinforces its own error and the vehicle's position drifts away without bound. Resolving it meant referencing the GPS conversion to the global estimate so its output lands in the map frame, breaking the loop.

The correction is documented at the point where it matters, and a diagnostic script asserts on a live system that the fix is actually in effect — so the bug cannot quietly return after a config change.

Nav2 needed matching treatment. A car cannot spin in place or shuffle backwards out of trouble, so the default spin and back-up recoveries are removed entirely and replaced with recovery-free behaviour trees, with reversing allowed as a deliberate manoeuvre instead.

Mission console

FastAPI · WebSocket · Leaflet

A FastAPI service bridges ROS 2 to a browser over WebSocket, with a Leaflet map front end. Verified functionality:

  • Click the map to append waypoints, drag to reorder, delete individually, then send the mission to Nav2.
  • Position, speed, heading and GNSS fix quality — including RTK FIX state — streamed continuously.
  • The driven path drawn on the map alongside the planned route.
  • Send, cancel, and an emergency stop that publishes a zero velocity command at the highest mux priority.
  • A manual on-screen joystick that drives the vehicle through the same arbitration chain.

Result and scope

What it does, and what it does not

Three physical vehicles, three drive geometries, one autonomy architecture. All three run outdoors on real hardware, navigating GPS waypoint missions with RTK-corrected GNSS and dual-antenna heading.

The evidence is the footage above: every clip on this page is a physical vehicle outdoors, not a simulator. Wheel-odometry scale was calibrated against pure RTK displacement rather than estimated, and missions are recorded to JSONL — waypoints, plan, pose, commands and RTK status — so tuning is argued from logs.

Scope limits

  • Navigation is outdoor GPS waypoint following. There is no SLAM and no map building.
  • There is no simulation — all development and tuning was done on physical hardware outdoors.
  • The costmaps carry no obstacle sensor, so there is no autonomous obstacle avoidance. The tracked vehicle's LiDAR is a visualisation feed only.
  • Vehicles are maintained on separate branches, not selected at runtime from a single configurable package.

Tech stack

Autonomy

  • ROS 2 Humble
  • Nav2
  • NavFn planner
  • Regulated Pure Pursuit
  • twist_mux

Localization

  • robot_localization
  • Dual EKF
  • navsat_transform
  • RTK GNSS
  • Dual-antenna heading
  • NTRIP client
  • BNO055 IMU

Embedded

  • ESP32-S3
  • micro-ROS
  • RoboClaw controllers
  • Wheel encoders

Platform

  • Raspberry Pi 5
  • Docker
  • C++
  • Python
  • FastAPI
  • Leaflet
  • CycloneDDS