Robot-side interface for running a visual navigation model on the
Kachaka mobile robot: four ROS 2 Humble nodes
(image relay, model, command sender, robot executor), a clamped cmd_vel bridge
with a dead-man timeout, and a typed wrapper over kachaka-api.
What this repository is not: a working navigation system. On main, the
NoMaD adapter does not run inference, waypoint commands have no executor, and no
closed-loop navigation run on the robot is claimed. The status table
lists what works and how each item was checked; it describes main only (see
the nomad-integration branch).
The executor's velocity gate replayed with a simulated clock, without ROS or a
robot. Dots are the commands a model sends, the line is what reaches cmd_vel.
Data: docs/assets/velocity_gate_timeline.csv,
regenerated by python -m kachaka_navigation.scripts.simulate_velocity_gate --plot.
The original French README is kept as README.fr.md.
This code was written by Guilhem Carmouze, robotics engineering student at UPSSITECH (University of Toulouse), during a research internship at the Computer Vision Research Team, Artificial Intelligence Research Center, AIST, Tsukuba, Japan (April to August 2026). The internship subject was the creation of a 360-degree navigation dataset using 3D Gaussian Splatting.
During the internship, a ROS 2 deployment interface for a visual navigation model (NoMaD) on the Kachaka robot was prepared. This repository is that interface:
camera -> image transfer -> navigation model -> command conversion -> Kachaka API
On main, the image transfer and the basic safety layer were prepared; the
trained model and the conversion from predicted waypoints to robot velocities
were not connected. The dataset side of the internship lives in two other
repositories:
nav_3dgs_pano (navigation and
panoramic rendering in a simulated 3DGS scene) and
artifixer-360-pipeline
(360-degree video from a plain video). Nothing here uses 3D Gaussian Splatting.
| Component | State | How it was checked |
|---|---|---|
| Message contracts, JSON serialization, model registry | Works | Unit tests |
| Stale-frame rejection (0.5 s) and stale-command rejection | Works | Unit tests and fake-clock node tests; stale frames also in the ROS 2 loopback tests |
| ROS 2 chain: image relay, trajectory generator, command sender, executor in dry-run | Works in a ROS 2 Humble container with synthetic images | tests/ros2; the four entry points were also started by hand in the container |
cmd_vel bridge: clamp, 20 Hz re-publication, dead-man timeout |
Works up to the Twist topic |
Fake-clock tests, ROS 2 loopback test, replay above. No robot was listening |
kachaka-api wrapper, command executor and the 15 direct command-line scripts |
Call mapping works against a stand-in client | Unit tests. A real run needs the robot; none is recorded in this repository |
| TCP JSON-lines transport | Works on localhost | Unit tests. The robot-side client script reads a ROS 1 topic and is untested |
| ROS 2 Docker image | Builds, runs the tests and the nodes | Built and run on 5 October 2026 |
| ROS 1 legacy path | Untested | ROS Noetic ships Python 3.8, this package needs 3.10 |
NoMaD inference (nomad_original) |
Not wired on main |
The adapter fills a 5-frame queue, then raises; every output is a stop command (tested) |
| Waypoint commands | Not executable on main |
Defined in the contract only; the executors reject them |
| Closed-loop navigation on the robot | Not claimed | No run is recorded on main |
This table describes main. The same repository has a second branch,
nomad-integration:
13 commits from 9 to 12 June 2026, branched from the original main before any
of the October 2026 work. According to its commit messages and files it is an
attempt to drive the robot from NoMaD directly through kachaka-api, without
ROS (a NoMaD wrapper of about 800 lines, a controller, command-line scripts, two
test files). That branch was not merged, reviewed, tested or run for this
README, so nothing it may have achieved on the robot is claimed here, and the
tests and numbers on this page do not cover it.
flowchart TB
camera(["Camera topic<br/>Image or CompressedImage"])
subgraph camera_host["Camera or robot host"]
relay["image_sender<br/>relays any image topic"]
end
subgraph model_host["Model host"]
generator["trajectory_generator<br/>frame older than 0.5 s: stop<br/>model error: stop"]
model[["NavigationModel<br/>noop, constant_velocity,<br/>nomad_original (not wired)"]]
sender["command_sender<br/>command older than 0.5 s: stop"]
end
subgraph robot_host["Robot host"]
executor["kachaka_command_executor<br/>dry-run mode logs only"]
gate["VelocityCommandGate<br/>off unless enabled<br/>clamp 0.2 m/s, 0.5 rad/s<br/>zero Twist after timeout"]
end
api(["Kachaka API<br/>gRPC, port 26400"])
cmdvel(["cmd_vel topic<br/>Twist at 20 Hz"])
camera --> relay
relay -- "/kachaka_navigation/image" --> generator
generator <--> model
generator -- "/kachaka_navigation/trajectory" --> sender
sender -- "/kachaka_navigation/robot_command" --> executor
executor -- "high-level commands" --> api
executor -- "velocity commands" --> gate
gate --> cmdvel
The three hosts can be one machine. Commands travel as JSON in
std_msgs/String; image topics use the sensor-data QoS and command topics a
queue depth of 1, so the newest message always wins.
A model only sees an ImageFrame and returns a NavigationCommand
(stop, noop, velocity, waypoint, move_to_location, return_home,
dock_shelf, undock_shelf, cancel, speak). It never talks to the robot,
which is what makes models interchangeable. The same contract also runs over a
JSON-lines TCP link (run_navigation_server) so that inference can live on
another machine without ROS.
| Model | Purpose | Output |
|---|---|---|
noop (default) |
Validate the wiring | Always stop |
constant_velocity |
Exercise the cmd_vel path |
A fixed small velocity |
nomad_original |
Integration point for NoMaD | stop only: inference is not implemented |
| Check | Default | Where |
|---|---|---|
Image older than the limit, or stamped in the future, becomes stop |
0.5 s | core/realtime.py, trajectory generator |
Any model exception becomes stop |
server/navigation_service.py |
|
Command older than the limit becomes stop |
0.5 s | command sender |
Velocity control is off unless --enable-velocity-control is passed |
off | executor |
| Linear and angular speed are clamped | 0.2 m/s, 0.5 rad/s | core/velocity_safety.py |
| Command duration is capped, with a default when none is given | 1.0 s, 0.25 s | core/velocity_safety.py |
Active velocity is re-published, then replaced by one zero Twist when it expires |
20 Hz | core/velocity_gate.py |
stop, cancel and undecodable commands zero the velocity at once |
executor | |
| NaN or infinite speeds, durations, limits and timestamps are refused | core/messages.py, core/velocity_safety.py |
|
--dry-run logs commands instead of acting |
executor |
In the scripted replay shown above (26 commands over 5.6 s), 8 commands
exceeded the linear limit and 5 the angular limit; no published Twist
exceeded 0.2 m/s or 0.5 rad/s. The zero Twist came 0.23 s after the last
command when the stream stopped (0.2 s command duration plus the wait for the
next 20 Hz tick), at once on a stop command, and 1.03 s after a command that
asked for 5 s. These numbers are in
docs/assets/velocity_gate_summary.json,
and a test fails if the committed CSV and JSON differ from what the script produces.
git clone https://github.com/guilhem0908/KachakaNavigation.git
cd KachakaNavigation
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install -e ".[dev,ros2]"
python -m pytest
python -m kachaka_navigation.scripts.simulate_velocity_gateThe last command rewrites the CSV and the JSON summary in docs/assets; add
--plot (and the figures extra) to redraw the figure. Everything runs on a
CPU. Without ROS the node classes are tested on a small fake of the rclpy
node API, with a clock the tests advance by hand.
python -m pytest on 5 and 7 October 2026, on a laptop CPU:
| Environment | Result | Duration |
|---|---|---|
Windows 11, Python 3.14 (no kachaka-api, no ROS) |
165 passed, 2 skipped | 3 to 5 s |
Linux container, Python 3.12 (with kachaka-api, no ROS) |
180 passed, 1 skipped | 2 to 9 s |
| ROS 2 Humble container, Python 3.10 (see below) | 148 passed, 38 skipped | 10 to 36 s |
Skipped tests are the ones that need ROS 2 or kachaka-api where these are
missing. Inside the ROS 2 container the 38 fake-clock node tests are skipped
instead, because the real rclpy replaces the fake one and tests/ros2 takes
over. The container was run through Docker Desktop on Windows with the sources
bind-mounted, and its duration varied between runs; the six loopback tests
alone took 8 to 16 s because they wait on real timers.
kachaka-api pins grpcio==1.66.1, which has wheels for CPython 3.8 to 3.12
only. On Python 3.13 and newer the two packages are skipped by an environment
marker: the tests above still run, the robot client is unavailable.
docker compose build ros2
docker compose run --rm ros2 python -m pytestThe second command includes tests/ros2, which runs the four nodes on a real
ROS 2 graph with a probe node playing the camera. To start the nodes yourself,
open four shells with docker compose run --rm ros2 (or docker exec into one
container) and run:
python -m kachaka_navigation.scripts.run_ros2_trajectory_generator --model noop
python -m kachaka_navigation.scripts.run_ros2_command_sender
python -m kachaka_navigation.scripts.run_ros2_image_sender \
--input-image-topic /camera/image_raw \
--output-image-topic /kachaka_navigation/image
python -m kachaka_navigation.scripts.run_ros2_kachaka_command_executor --dry-runWithout a camera, publish a synthetic image and watch the commands:
ros2 topic pub -r 10 /camera/image_raw sensor_msgs/msg/Image \
"{height: 2, width: 2, encoding: 'rgb8', step: 6, data: [255,0,0, 0,255,0, 0,0,255, 9,9,9]}"
ros2 topic echo /kachaka_navigation/robot_commandWith the noop model every command is a stop whose reason is
No navigation model is active. More in docs/ros2_docker.md
and docs/low_level_velocity.md.
cp .env.example .env # then set KACHAKA_HOST to the robot address
python -m kachaka_navigation.scripts.check_api_connection
python -m kachaka_navigation.scripts.smoke_test_connection
python -m kachaka_navigation.scripts.list_locationsThese need Python 3.10 to 3.12, a robot with the Kachaka API enabled, and
network access to its port 26400. The other direct commands are listed in
docs/kachaka_api_commands.md. Velocity commands
additionally need a ROS 2 bridge on the robot side that subscribes to the
cmd_vel topic; this repository only publishes the Twist.
src/kachaka_navigation/
├── core/ contracts and safety logic, no ROS dependency
├── models/ model adapters and their registry
├── server/ NavigationService: one model behind a fail-safe API
├── ros2/ the four ROS 2 nodes, message conversion, lazy rclpy import
├── robot/ command executors and the TCP robot-side loop
├── clients/ typed wrapper over kachaka-api
├── transport/ JSON-lines TCP server and client
├── ros/ ROS 1 legacy image source and command publisher
└── scripts/ entry points, direct Kachaka commands, gate replay
models/ expected location of model assets (none are distributed)
docs/ architecture, model integration, cmd_vel, Docker, API commands
tests/ unit tests, fake-clock node tests, tests/ros2 loopback tests
The original code came with ten unit tests. Extending them and building the Docker image exposed the problems below, all fixed in the history of this repository:
- NaN defeated the velocity limits. A NaN speed was clamped to the negative limit (-0.2 m/s) and a NaN duration never expired, so the dead-man timer never fired. NaN survives the JSON transport. Non-finite values are now refused at every level.
- The default raw-image path could not work in the Docker image.
kachaka-apirequires NumPy 2.2 or newer, while the ROS 2 Humblecv_bridgeand Ubuntu's OpenCV are built against NumPy 1.x and failed to import. Raw images are now decoded with NumPy alone. - The image had no
pythoncommand, although every documented command starts withpython -m, and pytest could not start in it. - Ctrl+C ended every node with a traceback and exit code 1, because the ROS context was shut down twice.
- No learned model runs on
main. Connecting NoMaD to it still requires loading the checkpoint, the original image preprocessing, and a conversion from predicted waypoints to velocities. Thenomad-integrationbranch is described above. - The freshness checks compare timestamps taken on different machines and assume synchronised clocks.
- The dead-man timer compares deadlines with the executor node's ROS clock,
which is wall-clock time by default. It assumes a clock that does not step
backwards: after a backward step, the active
Twistis re-published until the clock reaches the old deadline, which can exceed the 1 s cap. - Raw images are limited to the 8-bit encodings
bgr8,rgb8,bgra8,rgba8,mono8and8UC1; other cameras must publish a compressed topic. - The safety layer bounds speed and time only. There is no obstacle detection and no emergency-stop input.
- The
cmd_veltopic name is a default that has to be checked against the bridge running on the robot. docker-compose.ymluses host networking, which behaves as intended on Linux only.- No experimental result (success rate, latency, distance travelled) exists for this code.
- Guilhem Carmouze is the author of the repository. The two original commits
on
maindate from 26 May 2026. The October 2026 commits (tests, fixes, continuous integration, English documentation) were written with an AI coding assistant, as their commit trailers state. The internship report likewise declares that AI tools were used for research and for writing code. - Host laboratory: Computer Vision Research Team, Artificial Intelligence Research Center, AIST, Tsukuba, Japan.
- kachaka-api by Preferred Robotics (Apache-2.0) is the gRPC client this package wraps.
- NoMaD: A. Sridhar, D. Shah, C. Glossop and S. Levine, "NoMaD: Goal Masked
Diffusion Policies for Navigation and Exploration", ICRA 2024; code in
robodhruv/visualnav-transformer.
On
mainit is referenced as the intended model and is neither included nor loaded. - ROS 2 Humble, NumPy, OpenCV, pytest and ruff.
No licence file has been added yet.
