{"id":49617410,"url":"https://github.com/xinlei-robotics/mininav","last_synced_at":"2026-05-16T01:01:22.160Z","repository":{"id":355474758,"uuid":"1197633661","full_name":"xinlei-robotics/MiniNav","owner":"xinlei-robotics","description":"Indoor mobile robot localization and navigation system. C++23, Eigen, ROS 2, Raspberry Pi.","archived":false,"fork":false,"pushed_at":"2026-05-12T21:45:13.000Z","size":879,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-12T23:30:45.523Z","etag":null,"topics":["cmake","cpp","cpp23","eigen","ekf","localization","mobile-robot","navigation","raspberry-pi","robotics","ros2","slam"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xinlei-robotics.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-31T18:40:14.000Z","updated_at":"2026-05-11T17:27:36.000Z","dependencies_parsed_at":"2026-05-16T01:01:19.034Z","dependency_job_id":null,"html_url":"https://github.com/xinlei-robotics/MiniNav","commit_stats":null,"previous_names":["xinlei-robotics/mininav"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/xinlei-robotics/MiniNav","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xinlei-robotics%2FMiniNav","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xinlei-robotics%2FMiniNav/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xinlei-robotics%2FMiniNav/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xinlei-robotics%2FMiniNav/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xinlei-robotics","download_url":"https://codeload.github.com/xinlei-robotics/MiniNav/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xinlei-robotics%2FMiniNav/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33086736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-15T20:25:35.270Z","status":"ssl_error","status_checked_at":"2026-05-15T20:25:34.732Z","response_time":103,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cmake","cpp","cpp23","eigen","ekf","localization","mobile-robot","navigation","raspberry-pi","robotics","ros2","slam"],"created_at":"2026-05-04T23:02:54.771Z","updated_at":"2026-05-16T01:01:22.148Z","avatar_url":"https://github.com/xinlei-robotics.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# MiniNav\n\n[![CI](https://github.com/xinlei-robotics/MiniNav/actions/workflows/ci.yml/badge.svg)](https://github.com/xinlei-robotics/MiniNav/actions/workflows/ci.yml)\n[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue.svg)](https://en.cppreference.com/w/cpp/23)\n[![CMake](https://img.shields.io/badge/CMake-3.28-064F8C.svg)](https://cmake.org/)\n[![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](LICENSE)\n\n**Indoor mobile robot localization \u0026 navigation system in modern C++.**\n\nFrom kinematic simulation to a Raspberry Pi 5 + 4WD car indoor navigation\ndemo — built incrementally, version by version.\n\n\u003cimg src=\"results/v1_three_trajectories.gif\" alt=\"MiniNav V1 — three-trajectory 3D replay in Rerun\" width=\"720\"/\u003e\n\n*V1, replayed in Rerun's 3D view: command path (green), ground truth\n(blue), and wheel-odometry estimate (orange) starting together and\ndrifting apart over 20 s. The orange–blue gap is the problem V2's EKF\nis built to fix.*\n\n\u003c/div\u003e\n\n---\n\n## What is MiniNav?\n\nMiniNav is a personal robotics project that builds a complete — though\ndeliberately simplified — indoor navigation stack for a mobile robot.\nEvery layer of the classic mobile-robotics pipeline is implemented from\nscratch in modern C++, validated in simulation first, and progressively\nbrought onto real hardware.\n\nIt answers the three core questions of mobile robot navigation:\n\n| Question                | Topic        | Core technique                         |\n|-------------------------|--------------|----------------------------------------|\n| **Where am I?**         | Localization | Wheel odometry, IMU, EKF sensor fusion |\n| **Where am I going?**   | Planning     | Occupancy grid map, A\\* global planner |\n| **How do I get there?** | Control      | Pure Pursuit path tracking             |\n\nThe project is organized as a multi-stage roadmap (V0 → V6), each\nversion solving one well-scoped problem and building on the previous one.\n\n\u003e **Current status: V1 complete.** V2 (EKF sensor fusion) is the next\n\u003e milestone. See [`docs/v1_summary.md`](docs/v1_summary.md) for a deep\n\u003e dive into V1's design decisions, the noise models, and the odometry\n\u003e drift problem.\n\n---\n\n## Latest milestone — V1: Sensors, noise \u0026 odometry drift\n\nV1 is the first version of MiniNav that *has something to fix*. V0\nestablished a clean codebase running an ideal differential-drive robot\nthat perfectly executes every command. V1 introduces **two independent\nimperfect channels** on top of that codebase — one at the actuator,\none at the sensor — and quantifies the resulting drift.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"results/v1_trajectory.png\" alt=\"V1 — truth vs odom trajectory\" width=\"80%\"/\u003e\n  \u003cimg src=\"results/v1_drift_over_time.png\" alt=\"V1 — drift error over time\" width=\"80%\"/\u003e\n\u003c/p\u003e\n\n### Two-channel architecture\n\n```mermaid\ngraph LR\n    cmd[StagedCommandSource] --\u003e|cmd| actuator[ActuatorModel]\n    actuator --\u003e|true_velocity| truth[\"differential_drive_step\u003cbr/\u003etruth channel\"]\n    actuator --\u003e|true_velocity| encoder[WheelEncoderModel]\n    truth --\u003e|truth_pose| state[SimStateV1]\n    encoder --\u003e|EncoderTicks| odometry[WheelOdometry]\n    odometry --\u003e|odom_pose| state\n    state --\u003e|append| traj[\"Trajectory\u0026lt;SimStateV1\u0026gt;\"]\n    state -.-\u003e|log_to_rerun| rerun[Rerun]\n    traj --\u003e|write_csv_with_metadata| csv[traj_v1.csv]\n\n    style state fill:#0a4f3f,color:#fff\n    style truth fill:#1f4f1f,color:#fff\n    style odometry fill:#1f3f5c,color:#fff\n```\n\nThe truth channel and the estimation channel use **the same**\n`differential_drive_step` integrator. Any divergence between\n`truth_pose` and `odom_pose` therefore comes entirely from differences\nbetween their inputs — not from numerical asymmetry between two\nparallel pipelines. This means the drift is *causally attributable* to\nsensor and actuator imperfections, exactly the property a probabilistic\nfilter needs.\n\n### What V1 actually contains\n\n- **Velocity Motion Model** (Thrun, *Probabilistic Robotics* §5.3) for\n  actuator noise: variance is `α₁v² + α₂ω²`, so a stationary command\n  has zero variance — no static drift, unlike naive additive Gaussian.\n- **Physically causal encoder model**: inverse kinematics → multiplicative\n  slip noise → accumulated arc length → quantize to integer ticks →\n  differential output. The accumulate-then-diff pattern correctly handles\n  low-speed undersampling (where one tick can take many simulation steps\n  to register).\n- **`WheelOdometry` as a dependency-inverted estimator**. Its\n  `update(EncoderTicks, dt)` signature has no knowledge of where the\n  ticks came from. On the V6 real car, a Pi 5 GPIO interrupt counter\n  will feed the *same* `EncoderTicks` struct — the estimator code does\n  not change.\n- **`RngFactory` with per-tag seed derivation** (FNV-1a hash). Each\n  noise source owns an independent RNG, so adding a new noise source\n  in a future PR does not shift the random sequence of existing ones.\n  `--seed` controls every stochastic decision in the simulation.\n- **Three preset noise levels** (`low-noise` / `default` / `high-noise`)\n  swappable via `--preset`. The `default` preset is tuned so drift is\n  clearly visible in 20 s without being unrealistic.\n- **Self-documenting CSV output**. Every `traj_v1.csv` carries header\n  comments embedding `seed`, `preset`, `dt`, `duration`, and\n  `generated_at` — any saved trajectory can be exactly re-run.\n\nV1 is where MiniNav stops being a textbook toy and starts looking like\na real estimation problem.\n\n---\n\n## Roadmap\n\n| Version | Theme                    | Key deliverables                                                                                   | Status |\n|---------|--------------------------|----------------------------------------------------------------------------------------------------|--------|\n| **V0**  | Simulation scaffolding   | Differential-drive kinematics, `Trajectory\u003cT\u003e`, CSV/Rerun dual output, GoogleTest, strict warnings | ✅      |\n| **V1**  | Sensors, noise, odometry | Velocity Motion Model, encoder slip + quantization, `WheelOdometry`, drift experiments             | ✅      |\n| **V2**  | EKF state estimation     | IMU model, EKF predict + update, RMSE quantification vs odom baseline                              | → next |\n| **V3**  | Path planning            | Occupancy grid map, A\\* global planner                                                             |        |\n| **V4**  | Control + ROS 2          | Pure Pursuit tracker, packaged as ROS 2 nodes                                                      |        |\n| **V5**  | Full simulation loop     | Goal-pose → plan → track → arrive demo in ROS 2                                                    |        |\n| **V6**  | Real-world deploy        | Sim-to-real on Pi 5 + 4WD car, indoor navigation video                                             |        |\n\n---\n\n## Engineering foundations\n\nCapabilities established in V0 and reused by every subsequent version.\n\n- **C++23 modules** via CMake 3.28 `FILE_SET CXX_MODULES`. Module\n  interface files (`.ixx`) export only the API surface; heavy headers\n  like Eigen stay in implementation files or global module fragments,\n  keeping the module scan cost manageable.\n- **ADL-based extension points**. `csv_row(T)` and `log_to_rerun(T, ...)`\n  are free functions resolved by Argument-Dependent Lookup. Adding a\n  new `SimStateV2` only requires writing new overloads in new files —\n  V0 and V1 code is never touched. This is the Open/Closed Principle\n  expressed in C++ at its most natural.\n- **Versioned state structs**. `SimStateV0`, `SimStateV1`, … evolve by\n  addition rather than inheritance. State is plain data; the `Trajectory`\n  container is a class template so each version reuses the same\n  container code.\n- **PIMPL-isolated visualization**. The `viz` static library hides the\n  Rerun SDK behind `unique_ptr\u003cImpl\u003e`. Downstream targets do not\n  transitively `#include \u003crerun.hpp\u003e` — neither symbols nor compile time\n  leak through.\n- **Dual-track output**. CSV is deterministic and diff-able (regression\n  baseline + Python post-processing); Rerun is interactive (live\n  development); static PNGs are the publication artifact. Each format\n  has a different reader and a different job.\n- **Strict warning policy**. `-Wall -Wextra -Wconversion -Werror` on\n  Debug, with `SYSTEM` exemption for third-party headers — *strict on\n  our code, permissive on theirs*.\n- **Modern toolchain**. Clang 18, CMake 3.28 + Ninja, CLI11 / GoogleTest /\n  Rerun SDK fetched via `FetchContent`, `gtest_discover_tests` for\n  per-test CTest registration, `compile_commands.json` for clangd\n  integration.\n\n---\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────┐\n│ Layer 5: Real Robot Deployment              │  Raspberry Pi 5 + 4WD car  (V6)\n├─────────────────────────────────────────────┤\n│ Layer 4: Motion Control                     │  Pure Pursuit / PID        (V4)\n├─────────────────────────────────────────────┤\n│ Layer 3: Global Planning                    │  Occupancy grid + A*       (V3)\n├─────────────────────────────────────────────┤\n│ Layer 2: Localization \u0026 State Estimation    │  Odom + IMU + EKF          (V1 ✅, V2)\n├─────────────────────────────────────────────┤\n│ Layer 1: Kinematic Simulation               │  Differential-drive model  (V0 ✅)\n└─────────────────────────────────────────────┘\n```\n\n### Module dependencies (after V1)\n\n```mermaid\ngraph TD\n    sim_v1[sim_v1] --\u003e core[core]\n    sim_v1 --\u003e sensors[sensors]\n    sim_v1 --\u003e localization[localization]\n    sim_v1 --\u003e viz[viz]\n    sim_v1 --\u003e cli11[CLI11]\n\n    sim_v0[sim_v0] --\u003e core\n    sim_v0 --\u003e viz\n\n    sensors --\u003e core\n    localization --\u003e core\n    viz --\u003e core\n    viz --\u003e rerun[Rerun SDK]\n    core --\u003e eigen[Eigen3]\n\n    style core fill:#0a4f3f,color:#fff\n    style sensors fill:#1f4f1f,color:#fff\n    style localization fill:#1f3f5c,color:#fff\n    style viz fill:#3a1f5c,color:#fff\n    style sim_v1 fill:#5c4a1f,color:#fff\n    style sim_v0 fill:#3a2f1a,color:#fff\n```\n\n`sensors` and `localization` are **independent of each other** — they\nonly communicate through the `EncoderTicks` plain struct, passed through\nthe `sim_v1` main loop. This dependency inversion is what makes V6 work\nwithout modifying the estimator: real GPIO ticks plug into the same\nstruct the simulated encoder produces.\n\n`sim_v0` is preserved as a regression baseline; V1 does not replace it.\n\n---\n\n## Build \u0026 run\n\n### Prerequisites\n\n- Linux (or WSL 2) — tested on Ubuntu 24.04\n- Clang 18+ with C++23 modules support\n- CMake 3.28+, Ninja\n- Eigen3 ≥ 3.4 (`sudo apt install libeigen3-dev`)\n- Python venv with `rerun-sdk==0.31.4`:\n\n  ```bash\n  python3 -m venv .venv\n  .venv/bin/pip install -r requirements.txt\n  ```\n\nCLI11, GoogleTest, and the Rerun SDK are fetched automatically via\n`FetchContent`.\n\n### Build\n\n```bash\n# First-time configure (downloads Rerun SDK on first run)\ncmake --preset clang18-debug\n\n# Incremental builds\ncmake --build --preset build-debug -j\n\n# Run all tests (core / sensors / localization)\nctest --preset test-debug --output-on-failure\n```\n\n### Run the V1 simulation\n\n```bash\n# Default: random seed, default preset, spawns Rerun Viewer via gRPC\n./build/clang18-debug/sim_v1\n\n# Fully reproducible run (the seed prints to stdout when omitted)\n./build/clang18-debug/sim_v1 --seed 42 --preset default\n\n# Cycle through the three noise levels\n./build/clang18-debug/sim_v1 --preset low-noise\n./build/clang18-debug/sim_v1 --preset default\n./build/clang18-debug/sim_v1 --preset high-noise\n\n# Save a .rrd recording, replay later with `rerun results/v1.rrd`\n./build/clang18-debug/sim_v1 --rrd results/v1.rrd\n\n# Headless / CI mode — only writes data/traj_v1.csv\n./build/clang18-debug/sim_v1 --no-viz\n```\n\n### Generate drift figures\n\n```bash\nsource .venv/bin/activate\npython scripts/analyze_v1_drift.py\n```\n\nProduces `results/v1_trajectory.png` and `results/v1_drift_over_time.png`,\nand prints final / peak position error and final yaw error to stdout.\n\n### V0 simulation (preserved as regression baseline)\n\n```bash\n./build/clang18-debug/sim_v0 --no-viz   # writes data/traj.csv\n```\n\n---\n\n## Visualization\n\nThe Rerun Viewer is the primary live-development surface. The header\nanimation at the top of this README is a screen capture of the 3D view\nduring a `default`-preset run; locally, you can scrub, pause, change\nthe camera, toggle individual entities on/off, and inspect the\nunderlying time series.\n\nEntity paths logged by `sim_v1`:\n\n| Entity path                   | Meaning                                               |\n|-------------------------------|-------------------------------------------------------|\n| `/world/robot/cmd_traj/trail` | Where a perfect executor would go (≡ V0 trajectory)   |\n| `/world/robot/truth/trail`    | Actual ground truth (after actuator noise)            |\n| `/world/robot/odom/trail`     | Wheel-odometry estimate (after the full sensor chain) |\n\nAdditional scalar time series are logged for diagnostics:\n`cmd_v` / `cmd_w`, `true_velocity_v` / `true_velocity_w`, encoder\n`dticks_l` / `dticks_r`, and direct `error/position` + `error/yaw`\nchannels.\n\n---\n\n## Documentation\n\nPer-version retrospectives and design notes live under `docs/`:\n\n- [`docs/project-overview.md`](docs/project-overview.md) — full vision, V0 → V6 roadmap, technology choices\n- [`docs/v0_summary.md`](docs/v0_summary.md) — V0 retrospective: scaffolding design, alternatives, lessons learned\n- [`docs/v1_summary.md`](docs/v1_summary.md) — V1 retrospective: noise modelling, encoder physics, RNG design, drift analysis\n\n---\n\n## License\n\nMIT — see [`LICENSE`](LICENSE).","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxinlei-robotics%2Fmininav","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxinlei-robotics%2Fmininav","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxinlei-robotics%2Fmininav/lists"}