OOMWOO Install
September 26, 2026 · View on GitHub
OOMWOO Install
Open-source robot vacuum you build yourself.
Raspberry Pi · ROS 2 · Docker · Ubuntu · Dev environment
Raspberry Pi software + development software for OOMWOO open-source robot vacuum.

Deploy on Raspberry Pi
On your Raspberry Pi 4/5 2GB+ run
git clone https://github.com/makerspet/oomwoo-install
source oomwoo-install/ubuntu/install_oomwoo_runtime_jazzy.sh
Measure the onboard runtime before changing the minimum Pi/CM memory profile:
oomwoo-runtime-benchmark record --help
oomwoo-runtime-benchmark compare --help
The Pi runtime benchmark guide covers repeatable idle, 5 Hz SLAM, and Nav2 runs, ROS 2 composition comparisons, and the measured decision gate for a lower-cost 2 GB target.
Develop on Linux/Windows WSL2 - no hardware needed
The quickest way to try OOMWOO is the ROS 2 development environment in Docker — no robot, GPU, or display required:
docker pull makerspet/oomwoo:jazzy-dev
docker run -d --name oomwoo makerspet/oomwoo:jazzy-dev sleep infinity
docker exec -it oomwoo bash
Develop on Mac
Tutorials
- Simulate vacuum in Gazebo: Simulate OOMWOO-One in Gazebo with ROS 2 (no hardware needed)
- Write a Hello-World software package
- Install, operate a real (temp placeholder) vacuum cleaner Part 1 and Part 2.
- Simulate temp vacuum cleaner
- Run coverage cleaning (agent quickstart): Headless sim & coverage cleaning for LLM agents
- All tutorials: makerspet.com/learn
- Questions & help: Discord
Commands reference
Run these inside the dev container (docker exec -it oomwoo bash). The default robot
model is oomwoo_one; switch it with kaia config robot.model <package> or a
robot_model:=<package> launch argument.
Simulate in Gazebo
ros2 launch oomwoo_gazebo world.launch.py # with the Gazebo GUI (needs a display)
ros2 launch oomwoo_gazebo world.launch.py headless:=true # headless (Docker / CI, no display)
Drive the robot
ros2 run kaiaai_teleop teleop_keyboard # keyboard teleop
ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.2}}' # or publish velocity
Map & navigate (SLAM) — with a world running, in another terminal
ros2 launch oomwoo_bringup navigation.launch.py use_sim_time:=true slam:=True # build a map
ros2 run nav2_map_server map_saver_cli -f ~/maps/map # save the map
ros2 launch oomwoo_bringup navigation.launch.py use_sim_time:=true map:=~/maps/map.yaml # navigate a saved map
ros2 launch oomwoo_bringup monitor_robot.launch.py use_sim_time:=true # RViz view
Coverage cleaning (headless)
ros2 launch oomwoo_sim_support coverage_regression.launch.py # sim + Nav2 + coverage planner + meter
ros2 topic echo /coverage_meter/ratio # coverage fraction, 0.0 -> 1.0
See the headless sim & coverage cleaning quickstart for the agent/CI workflow.
Inspect sensors
ros2 topic echo /scan # 2D LiDAR
ros2 topic echo /bumper_left/contact ros_gz_interfaces/msg/Contacts # front bumpers
Physical robot (placeholder Proscenic M6 Pro) — see the connect and drive, map & navigate tutorials
kaia config robot.model proscenic_m6pro
ros2 launch proscenic_m6pro bringup.launch.py robot_ip:=<robot-ip>
ros2 launch oomwoo_bringup navigation.launch.py slam:=True
Release history
9/26/2026
halt_on_bump:=falsewas being silently ignored - fixed - the contour follower's launch file only passed three settings through to the node, andros2 launchaccepts any othername:=valuewithout complaint and quietly drops it. So every run meant to have halting off had it on, and the latest torture-course run stopped at its first bump.halt_on_bump,use_body_bearinganduse_curvature_ffare now passed through, checked on a running node. Worth knowing in general: a misspelled or undeclared launch argument produces no error;ros2 param get /contour_follower <name>shows what the node actually got
ros2 launch oomwoo_clean contour_follow.launch.py use_sim_time:=true halt_on_bump:=false
- The LiDAR noise estimate is now averaged over about 2 seconds - a single scan's estimate scattered from 6 to 19 mm around the true 10 mm (the halting bump logged
noise 17.2 mm), while a sensor's noise doesn't change from one scan to the next. The average keeps the corner guard steady and makes the logged figure meaningful
9/25/2026
- The corner guard now sizes its margin from the LiDAR's own noise, not a fixed 1 cm - yesterday's fix lifted the guard by a fixed 1 cm, which is really "1 cm, given this LiDAR's noise": another number to retune for a different sensor, a dark or shiny surface, or bright light. The follower now estimates the range noise from every scan - along a surface, the second difference of neighbouring ranges is pure noise, and taking its median ignores the few points at a corner - and lifts the guard by that. Wall offset the guard adds at 0.5 / 1 / 2 cm of range noise: fixed 1 cm margin 0.0 / 1.4 / 13.5 mm; median-5 filter 1.5 / 3.9 / 7.6 mm; median-9 0.8 / 1.9 / 6.3 mm; estimated 0.3 / 1.0 / 4.7 mm. A median filter was the natural first idea and does help, but only removes part of the bias, and its window is counted in beams, so it covers a different arc on a LiDAR with a different resolution - a sensor-dependent setting of its own
- The log line now reports the measured LiDAR noise, e.g.
[fit 64 pts, straight, noise 9.8 mm]- a free check of the real LiDAR's range noise on real surfaces, when it arrives
9/24/2026
-
Found the other half of the curve error: the corner guard was costing 1 cm on every wall - raising the follower's gain barely moved the curve error, so part of it wasn't the control law at all. It was the guard that stops the circle fit rounding off sharp corners ("never report further than the 3rd-nearest scan point"): the 3rd-nearest of ~60 noisy points sits about 1 cm closer than the surface really is, so on a smooth surface the guard won every frame and held the robot 9.9 mm out along every straight wall. It now carries a 1 cm allowance for its own noise, so it only takes over where the fit really is wrong - at sharp corners, where it's 23-54 mm off. Offset from the standoff, before -> after: straight wall +9.9 mm -> +1.1 mm; stool seat +1.7 -> +1.0 cm; bay R 0.50 +1.2 -> +0.4 cm; tight bay R 0.35 +2.3 -> +1.7 cm; 2 cm leg unchanged at +0.6 cm. Closest approaches everywhere stay at least 31 mm clear of the bumper
-
Why the follower settles off the standoff on curves, and why that turned out fine - on a curve a proportional controller has to hold an error to keep turning. On top of that, the bearing was measured from the LiDAR, 7.45 cm ahead of the body centre, and from there a curve's nearest point is off abeam even when the robot is perfectly tangent: by atan(offset / path radius). The logged bearings match that geometry: 16.6 deg predicted vs 14-20 logged around a leg, 31.8 vs 25-32 in the R 0.35 bay
-
The textbook fix was built, measured, and left off - measuring the bearing at the body centre (
use_body_bearing) plus a feed-forward of the turn the fitted curve needs (use_curvature_ff). True clearance on the curve, target 0.23 m, today vs both on: 2 cm leg 0.236 vs 0.248; 5 cm leg 0.238 vs 0.242; 15 cm seat 0.247 vs 0.243; bays R 1.00/0.75/0.50 0.233/0.239/0.242 vs 0.236/0.234/0.230; but the tight R 0.35 bay 0.253 vs 0.211 - and it touches the wall. The two effects in today's law nearly cancel, so it already holds curves within about 2 cm and always errs outward, the safe side. Both switches stay off, documented with the numbers, for A/B runs in Gazebo -
The test harness now drives the node's own control law instead of a copy of it, so a steering change can no longer slip past it. New
concave_bayscenario and a test pinning legs and bays within -1..+3 cm of the standoff with no contact
9/23/2026
-
Every bump is logged now, halting or not - with
halt_on_bump:=falsethe follower used to say nothing when it hit something. Each bump now gets one line saying which half, the state, and what the follower was tracking at that moment, e.g.BUMP (right bumper) during FOLLOW. Follower was tracking: surface at 0.21 m, -86 deg [fit 38 pts, R=0.08 convex]. Carrying on (halt_on_bump is off).Gazebo publishes a contact message on every physics step the bumper is pressed, so only the start of a bump is logged: a side counts as bumped again once it has been clear for half a second -
Fixed the bumper's top flickering (oomwoo-one) - making the bumper flush put its plates inside the body's drawn outline, with their tops exactly level with the body's top, and two faces in the same plane flicker (Z-fighting). The body is now drawn at its collision radius, so the dark bumper band shows across the front again, and the bumper plates are drawn 1 mm short at top and bottom. Visual only: the collision shapes are identical, so the bumper sensors are unaffected
-
bumper_probe: a live check that each bumper half fires (oomwoo_sim_support) - the sim bumpers have failed without any error before: Gazebo renames every collision when it merges the robot's links, so the bumper sensors must list the renamed names.test_bumper_wiring.pychecks the names; this checks the geometry, by driving the robot into walls at three angles and confirming which half reports each hit - head-on, 30 deg on the right, 30 deg on the left. Turns and back-offs are measured on/odom, so a slow headless sim does not skew them, and it exits 0 or 1, so it can gate any change to the robot's collision shapes. Verified both ways: it passes on the new flush bumper, and it fails on a deliberately broken model with the body 5 mm outside the bumper - not by going silent, but by the wrong half firing, because Gazebo lets surfaces sink up to 1 cm into each other and the robot twists on the body contact. Checking which side fires is what catches it
ros2 launch oomwoo_gazebo world.launch.py world:=contour_torture.world x_pose:=2.3 y_pose:=-2.6 headless:=true
ros2 run oomwoo_sim_support bumper_probe
-
The robot is round now: the bumper sits flush with the body (oomwoo-one) - the bumper facets used to be centred on the body radius, so they stood 5 mm proud and their corners reached 0.1814 m against a 0.1745 m body. Consumer robot vacuums are plain circles, and for a reason: a round outline can always turn in place, which is how a robot gets back out of the tightest dead end. The facets now sit just inside the body radius with their outer corners landing exactly on it, and there are twice as many (24), which keeps the faceting error to 0.5 mm. Measured from Gazebo's own conversion of the model: outermost point 0.1745 m, the body radius exactly
-
...with the one trap flush bumpers have, handled - if the body's collision cylinder had the same radius as the bumper, the two would tie, and wherever the body touched first the bumper sensor would never fire. So the body's collision cylinder is 2 mm inside the bumper's faces, the way a real robot's chassis sits behind its bumper shell, and the bumper leads it by at least 2.0 mm at every angle across the front. The body's visual stays full size, nothing with mass moved, and the bumper-wiring test passes against the new model
-
The contour follower's test harness scores contact against the new 0.1745 m, so every scenario now clears by about 7 mm more than before; the 0.23 m standoff is unchanged for now
-
Video: automatic docking tests - OOMWOO automatic docking tests. Docking in the simulated kitchen from a series of starting spots: the robot turns in place until it picks up the dock's IR beacon, finds and confirms the dock with the LiDAR, goes to a staging point in front of it, turns its back to the dock and reverses straight in. Recorded just before the forward-driving change below, so here it still reverses to the staging point in arcs
-
The blog post has a docking section now - How OOMWOO cleaning algorithms work gained Docking: a beacon to point, the LiDAR to aim: the video, the five steps, why it takes both a beacon and a LiDAR, what the recessed beacon can and cannot see, and the move to driving forwards
-
Docking ends by itself (oomwoo-ros2-tools
oomwoo_dock) -dock_driveexits once the attempt is over, with code 0 when docked and 1 when it gave up, and the docking launch shuts down with it. So there's no Ctrl-C, and nothing is left sending zero velocity over teleop or whatever drives the robot next. Passexit_when_done:=falsefor the old behaviour -
The simulated beacon was blind until now -
ir_beacon_simgot the robot's position only from/odom_truth, which carries nothing under the defaultodom_source:=ground_truth. The launch also placed the beacon 0.24 m from the dock's actual beacon, shining south instead of out of the bay. So docking in the sim, including the video above, ran on the LiDAR alone, which says something good about the LiDAR. Both are fixed. In headless runs from three starting spots, the receivers see the beacon, the robot docks within 4 mm of centre, and the launch exits by itself -
The blog post now shows how to reproduce docking - Try docking: pull the image, start the kitchen, dock, watch the states go by, park the robot elsewhere with teleop, or restart at one of three checked starting spots. One spot to avoid: at
x_pose:=-1.5 y_pose:=-0.15the robot starts with its bumper pressed against the dining table and can't move -
Docking, first version: the robot finds the dock and backs into it (oomwoo-ros2-tools
oomwoo_dock, new package) - park the robot roughly in front of the dock and it does the rest. It turns in place until its rear IR receivers see the dock's beacon, stops, and the LiDAR fits the dock's shape at the spot the beacon points to. It then drives to a staging point centred on the dock, 0.35 m out, which puts its tail at the edge of the tray (assuming the tray sticks out by the robot's radius). From there it turns its tail to the dock and reverses in. It enters only when lined up within 25 mm and 5 deg; otherwise it pulls back out and tries again, up to 3 times. It stops 15 mm short of the back so it never shoves the dock across the floor. It moves only once two scans agree on where the dock is: a robot that drove on a guess once went straight into the dock -
Why both the beacon and the LiDAR: the LiDAR gives the dock's position to about a centimetre, but on its own it can't always tell which thing is the dock. A dock standing against a wall produces a convincing false match 0.2 m to one side: the wall plus one of the dock's side panels. The beacon settles which one is real. Viewed from well off to one side, the dock's far side panel is hidden, so on the LiDAR alone the robot rejected the real dock and never moved. Now a fit is also believed if it puts the beacon where the receivers see it, which the false match misses by about 12 deg. In the 2D test rig, from 12 parked positions in a furnished room (walls, a dining table, two chairs): 11 docked with the beacon, 10 with the LiDAR alone, none touched the dock, worst sideways error 10.9 mm against 25.5 mm of room in the bay
-
The beacon is in the sim too (oomwoo-one, oomwoo_gazebo) - the robot has two receivers at the back, angled 25 deg either side of straight back, with a small divider (baffle) between them. The dock has the beacon at the back of its bay, at the receivers' height. Gazebo has no IR sensor, so
ir_beacon_simcomputes what each receiver would read and turns the difference between them into a direction, just as the hardware will. The model also records two limits worth designing around. The beacon sits at the back of the bay, so it only shines out within about 40 deg of straight in front of the dock: a robot parked beside the dock sees nothing (that is the one position that fails above). And both receivers see it only when the robot's back points within about 25 deg of it, which is why the robot turns first. If the robot should find the dock from further round the side, a second, wide-angle emitter on the dock's front would cover that -
The dock's bay looked wider than it was (oomwoo_gazebo
vacuum_dock) - the sim LiDAR sees the visual model, and the visible side panels were thinner and further out than the solid ones. So the bay measured 0.43 m wide to the LiDAR but 0.40 m to physics, and a docking robot would have aimed for a gap 15 mm wider on each side than the real one. The visual panels now match the solid ones exactly, and the two posts at the back of the bay, which now sat inside the panels, are gone -
Forwards to the staging point, backwards only into the bay - the robot used to reverse to a staging point behind it in arcs, so anything it backed into went unnoticed. Now it turns in place to face each point and drives there forwards, so the front bumper leads. Only the last leg, into the bay, is driven in reverse. It looks more deliberate too, and it lines up better: in the test rig's 64 parked positions, 60 docked on the LiDAR alone (58 before) and 63 with the beacon, none touching the dock, and the worst sideways error at the dock fell from 14 mm to 4.5 mm
-
Not yet:
oomwoo_dockhas no obstacle sensing of its own. On the way to the staging point it goes by the dock's position only, so anything in between is found by the bumper
ros2 launch oomwoo_gazebo world.launch.py world:=kitchen_dining.world
ros2 launch oomwoo_sim_support dock_sim.launch.py use_sim_time:=true
9/18/2026
-
Video: a full lap of the torture course - OOMWOO wall following torture course. LiDAR only, no map and no SLAM, in the folded 6 x 6 m room: into the wide gap between the first two teeth and straight past the narrow ones, a 180 deg wrap around the peninsula's tip, round the arcs and blocks, with the odd clip along the way. A lap is about four minutes
-
The blog post now explains the whole loop in one place - How OOMWOO cleaning algorithms work gained A lap of the torture course (the video) and What the follower does with each scan: the nine steps from scan to wheel commands, and an honest list of what it still gets wrong - skipped narrow gaps, the occasional clipped leg, no loop closure, no front guard
-
What the two halting runs say: both ended the same way,
[fit 15 pts, R=0.02 convex]and[fit 13 pts, R=0.01 convex]at 0.11-0.16 m. That is a table leg, not a wall: 13-15 points is about what a 4 cm cylinder subtends at close range, and the fitted radius matches the leg rather than a corner. Legs are now the tightest case left, since the follower rounds one at a radius of a few centimetres while the bumper reaches 0.1814 m from the robot's centre -
The fit was rounding off sharp corners, and the log said so. A Gazebo run halted with
surface at 0.21 m, -107 deg [fit 38 pts, R=0.08 convex], and every dip in that run carries the sameR=0.08 convexsignature. A fitted circle rounds a corner, so while wrapping one the arc passes inside the real corner and the reported distance is too large: traced against truth in the harness, +23 mm around a box corner and +54 mm in a room's inside corners - most of what the standoff buys, which is why the robot kept bumping corners while its own log showed a healthy 0.21 m. The scan points do not round anything off, since the corner tip is one of them, so the follower now reports the smaller of the fitted distance and the distance to the 3rd-nearest point in the fit window. Rank 3 rather than 1 because the single nearest beam carries the full noise: ~7 mm of average extra clearance instead of ~14 mm. Worst margin to the bumper across seven scenarios improves from +28.9 mm to +35.5 mm, and nothing touches even at the old 0.20 m standoff -
The torture course is folded and compact now - a 6 x 6 m room (half the floor area) with a peninsula jutting in from one wall. The boundary stays one continuous wall, so the follower traces the perimeter and both faces of the peninsula without having to choose: 33 m of wall inside 36 m², a lap of about 220 s, and it all fits on screen. Running anticlockwise from the start: four square teeth with gaps of 2.0, 1.1 and 0.9 robot diameters; a small then a wide block; a leg, two legs in a row and two thick legs along the peninsula; a 180° wrap around its tip; blocks standing 0.25 m and 0.50 m off it; a convex half-cylinder; a wedge, a concave bay of R 0.35, a convex R 0.15; a concave bay of R 0.17 that is too tight to enter, and the V trap; then plain wall home
ros2 launch oomwoo_gazebo world.launch.py world:=contour_torture.world x_pose:=-2.6 y_pose:=-2.77 odom_source:=robot_wheels
ros2 launch oomwoo_clean contour_follow.launch.py use_sim_time:=true
- The preview caught two layout bugs before they cost a run - the V trap's ledge had swallowed the corner including the robot's start pose, and left a slot against the neighbouring bay.
script/make_contour_torture.pyholds every coordinate once, writes the SDF, audits every gap against the robot's size and drawsworlds/contour_torture.png; the 2D rehearsal then drives the whole lap and completes one plus most of a second before grazing - Tip: a new world does not need an image rebuild. The dev container builds with
colcon --symlink-install, sodocker cpinto/ros_ws/src/...is enough for worlds, launch files and Python nodes - the build and install trees are symlinks back to the source. Only C++ or new entry points needcolcon build
9/17/2026
contour_followerstandoff 0.20 -> 0.23 m: the bumper, not the body, touches first. A Gazebo run halted on a right-bumper contact just after an inside corner, with the body centre 0.18 m from the wall. That is outside the 0.1745 m body radius, but the bumper is a ring of 10 mm facets centred on the body radius, so its face stands 5 mm proud and each facet's corners reach 0.1814 m. The test harness now scores contact against that radius, and it overturned an earlier result: the body-clearance fix had raised the wall-end and box-corner clearances to 0.181 and 0.180 m, which were counted as passes but were contacts. Worst margin to the bumper across seven scenarios:
| standoff | worst margin |
|---|---|
| 0.20 m | -1.1 mm (two contacts) |
| 0.21 m | +6.8 mm |
| 0.22 m | +19.9 mm |
| 0.23 m | +28.9 mm |
| 0.24 m | +37.2 mm |
Gazebo dipped about 13 mm deeper than the harness at the corner that halted, so 0.23 m is the smallest value with real headroom. The cost is edge cleaning: a 5.5 cm gap between body and wall instead of 2.5 cm, for the side brush to cover. Override with standoff_m:=0.20 to compare
- A new torture course: three combs (oomwoo_gazebo
worlds/contour_torture.world, preview inworlds/contour_torture.png) - a 12 x 6 m room with one comb per wall, run anticlockwise with the wall on the robot's right. South wall: four 0.6 m square teeth with gaps of 2.0, 1.1 and 0.9 robot diameters (roomy, tight, impossible). East wall: a small block, a wide block, a single table leg, two legs in a row, two thick legs in a row. North wall: a wedge, blocks standing 0.25 m and 0.50 m off the wall, convex and concave arcs of R 0.35 and a tighter R 0.15 / R 0.17, and a V-shaped pocket. West wall: plain, back to the start. The course is generated byscript/make_contour_torture.py, which holds every coordinate once, checks every gap against the robot's size, and lists the deliberately impassable ones in the world file's header
ros2 launch oomwoo_gazebo world.launch.py world:=contour_torture.world x_pose:=-5.4 y_pose:=-2.77 odom_source:=robot_wheels
ros2 launch oomwoo_clean contour_follow.launch.py use_sim_time:=true
- What the 2D rehearsal predicts for it, driving the real follower around the same geometry, which is worth comparing against Gazebo: it goes into the 2.0-diameter gap but bridges straight across the 1.1 and 0.9 ones; it rounds every block, leg and convex arc without contact, and passes behind the block that stands 0.50 m off the wall; it skims the mouth of the R 0.35 concave bay rather than going in, so a bay narrower than about twice the standoff goes uncleaned; and the V pocket depends on the approach, bridged on one run and entered and bumped on another. The rehearsal leaves out the ALIGN and ARC states and real dynamics, so treat these as expectations, not results
9/16/2026
contour_followerhalts on any bump - a debug stand-in until a real front guard exists. Any contact on either bumper while the follower is active zeroes the command, sends it at once rather than on the next timer tick, and parks the robot in a newHALTEDstate. A warning records which bumper it was and what the follower was tracking at that moment, so each collision is left exactly where it happened, ready to be looked at.cleaning_activedrops while halted. Resume with the command below;halt_on_bump:=falserestores the old behaviour for A/B runs
ros2 topic pub --once /contour_follower/enable std_msgs/msg/Bool '{data: true}'
-
Blog - the algorithms post gained Seen, but never picked: the course's slot, the front-guard gap it exposed, and the bump-halt stand-in. It also retracts the earlier line that the follower "clears every shape it is given", which the Gazebo run disproved
-
Torture course fixed: it had two slots the robot could never fit through - the first Gazebo run followed the wall straight into a panel I had placed 0.275 m off the south wall, against a 0.349 m robot: a slot, not a corridor. An audit of every obstacle footprint against every other found a second impassable slot (0.302 m) and three squeezes under 0.45 m. The corridor is now a single fin 0.5 m off the wall, the diagonal is shorter, and the audit finds no gap under 0.45 m, so a crash on the course now means the follower failed rather than the course being unwinnable (the 6 cm lip stays the one deliberate exception)
-
...but the crash itself was a real finding: the follower has no front guard. The panel's end was in view for the whole approach, at +5 deg to +16 deg, but the wall beside the robot was always nearer, and the follower only ever steers on the single nearest surface. By the time the panel end was as close as the wall, it had swung past the search sector's +20 deg edge and dropped out of view. It was never picked. The earlier collision with a second table leg was the same thing. Reproduced in the rehearsal as
post_in_path(a post 10 cm left of the path: the body centre gets to 0.079 m from it) and recorded as a strictxfail, so it flips loudly when a front guard lands -
RViz: the orange lines are gone - they were the search sector's two edges (-170 deg and +20 deg), not leftover text. The markers now show only what the controller steers on
-
FastDDS shared memory opt-out fixed - shared memory stays on by default.
docker run -e MICROROS_DISABLE_SHM=1turns it off again: the UDP-only profiles are back in the image (setting it used to log anXMLPARSER Errorand leave shared memory on), and it now applies indocker execshells too. Measured at 640x480, 30 Hz: 24 UDP datagrams per 10 s with shared memory (discovery only) vs ~7,700 without. If another ROS2 container or the host shares this container's network (--net=host), also share/dev/shmwith--ipc=host, or topics show up but no messages arrive -
RViz de-cluttered -
contour_followerno longer floats a text label over the robot; the markers are geometry only (fitted curve, window ends, picked point, standoff target, search sector). The numbers live in the once-a-second log line, which now also ends with the shape the estimate came off, e.g.[fit 73 pts, straight]or[fit 15 pts, R=0.02 convex] -
Blog - How OOMWOO cleaning algorithms work now covers the circle fit, the torture course and 2D rehearsal, and why the follower measures clearance at the body rather than the LiDAR
-
Shipped: the follower now holds the BODY's clearance, not the LiDAR's range, and both collisions are gone. The follower servos the range the LiDAR reports, but the LiDAR sits 0.0745 m ahead of the wheel axle, so on a tight turn the shell swings wide of wherever the LiDAR is pointing. Measuring the same 0.20 m to the body centre instead - which needs no new sensing, because the fitted circle can simply be evaluated at the body centre - turns both failures into passes, at no cost in distance covered:
| scenario | LiDAR range (before) | body clearance (shipped) |
|---|---|---|
| wall tip (180 deg wrap) | 0.169 m HIT | 0.181 m ok |
| box corner | 0.174 m HIT | 0.180 m ok |
| table leg, 2 cm | 0.182 m | 0.196 m |
| table leg, 5 cm | 0.188 m | 0.200 m |
| room walls | 0.186 m | 0.193 m |
| leg beside a wall | 0.175 m | 0.185 m |
The fitted conic is just a curve in the scan frame, so the body centre is another point to evaluate it at - no new sensing and no second estimator. Running parallel to a wall the two measures are identical (the body centre sits directly behind the LiDAR), so wall following is unchanged by construction, and a test pins that invariant; they separate only in turns, which is where the grazing was. The two harness xfails are now ordinary passing gates, the diagnostic log prints both numbers (d=0.20m (lidar 0.23)), and use_body_clearance:=false restores the old behaviour for comparison
-
Slowing down in sharp turns was tried as the backup plan, and it makes things worse - the intuition is sound and real vacuums do it (a Dreame L60 audibly slows for corners), but in this control law speed and turn radius are coupled: the turn rate comes from the bearing error, so cutting v while omega holds shrinks the path radius
r = v / omegaand the robot spirals inward. Measured around a 2 cm leg: clearance falls from 0.182 m to 0.083 m as the speed cap tightens, with the bearing droop growing from 18 to 27 deg. It would work only alongside a matching turn-rate limit, or after the body-clearance fix above removes the reason for it. Recording the negative result because it is a genuinely reasonable idea that the rig disproved in a minute -
CI: retry
rosdep updateinstead of reddening on a dropped connection - a build failed withurlopen error [Errno 104] Connection reset by peerfetching rosdistro YAML from raw.githubusercontent.com. The step runs undersh -e, so one reset failed the whole build with nothing wrong in the tree; the same commit passed on the next push. Now retried 3 times, 15 s apart -
A torture course for contour following (oomwoo_gazebo
worlds/contour_torture.world) - a 5 x 5 m room ringed with the shapes that break a reactive follower, each isolated so a failure names itself instead of being "it got stuck somewhere": a square pillar (an unrounded convex corner), a 3 cm fin whose free end is a 180 deg tip to wrap, a round pillar, a coffee table's four 4 cm legs, a 45 deg diagonal, a 0.5 m corridor, a concave alcove, and a 6 cm lip. The lip is deliberately unwinnable: the scan plane sits at 8.8 cm, so the LiDAR passes clean over a 6 cm obstacle and only the bumper can catch it - it is in the course to keep the missing bumper handoff visible rather than quietly absent
ros2 launch oomwoo_gazebo world.launch.py world:=contour_torture.world odom_source:=robot_wheels
ros2 launch oomwoo_clean contour_follow.launch.py use_sim_time:=true
- ...and a 2 second version of it that needs no Gazebo -
oomwoo_clean/contour_harness.pydrives the real boundary estimator and the real control law around a ray-traced 2D world with a kinematic robot carrying oomwoo-one's geometry, LiDAR offset included. A 40 s scenario runs in about two seconds, so it gates CI and answers "does the control law survive this shape" before Gazebo is even started. Gazebo stays the authority on what 2D leaves out - the lip below the scan plane, carpet, wheel slip, real timing
python3 -m oomwoo_clean.contour_harness # the scenario table
- The harness immediately found two things, both matching the live runs. First, the ~18 deg bearing droop while circling a table leg is proportional control working exactly as specified, not an estimator bug: the turn command has to come from somewhere and the only source is the bearing error itself, so at equilibrium
e_b = omega / k_heading = 0.45 / 1.5 = 17.2 deg. Harness: -18.4 deg. Live log: -17 to -21 deg. Second, and more serious: a sharp convex corner has no clearance margin at all. Wrapping a wall's tip puts the body centre 0.169 m from it, a box corner 0.174 m, against a 0.1745 m body radius - the robot grazes what the LiDAR clears, because the follower servos the LiDAR's range while the body swings wide of where the LiDAR points. Recorded as anxfailwith the arithmetic rather than a loosened threshold; the fix is to hold the body's clearance, which the fitted conic can be evaluated for directly, with no extra sensing - A fix that looked obviously right, measured wrong - feeding the fitted curvature forward should cancel that droop. The harness measured the clearance around a table leg falling from 0.178 m to 0.048 m as the robot spiralled inward. Dropped, in about a minute, for the price of writing the scenario. That is the case for the harness in one line
contour_followerrejects blown-up fits - on a short arc (a 2 cm leg is ~15 beams) the algebraic circle fit occasionally converges to a tiny circle sitting nowhere near the surface: 0.2-1% of frames, worst case 99 deg of bearing error, and a live log caught one commanding a full-rate turn off a single garbage frame (d=0.06m toward=+260.8). The fit is now cross-checked against the beam that seeded it (fit_max_dev_m0.05,fit_max_dev_deg35) and falls back to that beam on disagreement: fires on 0% of wall, corner and large-curve frames, and cuts the worst case to 6.7 deg. The fitted radius is also signed now, so the marker text readsR=0.02 (convex)for a leg versus(concave)inside a corner
9/15/2026
contour_followernow fits a circle to a short window of the boundary, so it follows any shape — the 9/2 line fit cured the weave but broke corners: an inside corner has no gap in the scan, so growing the surface ran straight around it onto the front wall, and one line across both walls reads ~10° tilted and 2–5 cm close. The robot turned away about 0.8 m early and arced gracefully from wall to wall instead of cleaning into the corner. A line is only ever right for walls, so the fix was to stop assuming one: fit a circle to a 0.15 m window around the nearest point and report the distance to the fitted curve plus the bearing of its nearest point. The curvature term goes to zero on a flat wall, so this reproduces the line fit there, while the short window keeps the corner out of the estimate until the robot is actually at the standoff. Measured on synthetic scans at the sim LiDAR's specs (360 beams, 1 cm noise), bearing error mean/sd in degrees:
| scene | line fit (whole surface) | circle fit, 0.15 m window | nearest beam + median-5 |
|---|---|---|---|
| wall | +0.0 / 0.1 | −0.0 / 0.7 | +0.4 / 7.4 |
| inside corner, 0.8 m ahead | +9.8 / 0.1 | +0.0 / 1.0 | −0.5 / 7.8 |
| stool seat, R 0.15 | −0.0 / 1.2 | −0.0 / 0.4 | −0.1 / 4.5 |
| stool leg, R 0.03 | +2.3 / 14.7 | −0.0 / 0.4 | −0.1 / 1.9 |
| leg beside a wall | −0.0 / 0.1 | +0.2 / 1.3 | +0.3 / 3.4 |
- Median-filtering the scan was tried first and does not work — it is the obvious cheap fix, so it is worth recording that it fails: median-3, median-5, median-9 and repeated passes all leave 6–8° of bearing noise, against 7–8° unfiltered. The problem is not outliers, it is that the range minimum along a wall is flat — at a 0.20 m standoff a ±20° swing moves the range by 1.3 cm against ~2 cm of beam scatter — so smoothing the ranges never sharpens the minimum and "which beam is nearest" stays close to random. An ill-conditioned arg-min has to be replaced, not denoised
- Convex (outside) corners now get rounded by the normal follow law — sweeping the estimator past a box corner, the fit tracks the corner vertex to within 1–2 cm and a few degrees rather than extrapolating a wall that has ended, and the reported distance grows smoothly (largest frame-to-frame step 0.026 m against the 0.30 m
convex_jump_mthreshold). So the follower stays in FOLLOW and arcs around the corner on its own; the explicit ARC recovery is now a backstop for when the boundary genuinely disappears, not the normal path around an obstacle. One transient remains: a single frame at the corner itself reads ~20° off while the window straddles both faces - Debug markers were never reaching RViz —
wall_follow.rvizasks for TRANSIENT_LOCAL durability on~/debug_markerswhile the publisher was VOLATILE. That pair is silently incompatible: RViz connects, logs one QoS warning at startup, then draws nothing. The publisher is now TRANSIENT_LOCAL, which satisfies transient-local and volatile subscribers alike, so RViz and Foxglove both work with no config change - ...and now they show what the fit is thinking — the fitted curve draws as a polyline projected onto the fitted circle (visibly bending: 3 mm of sag across the window on a wall, 64 mm on a 15 cm stool seat), raised clear of the scan; its endpoints draw as spheres so the extent of the window being used is obvious; and the state text gained a line reading
fit 73 pts, straightorfit 50 pts, R=0.14. Above a 2 m fitted radius it prints "straight" rather than a number, because scan noise alone bends a wall fit to R ≈ 4 m - New knob
fit_window_m(0.15) — shorten it to track small objects more tightly, lengthen it for smoother walls.oomwoo_cleannow depends onpython3-numpy
ros2 launch oomwoo_clean contour_follow.launch.py use_sim_time:=true
ros2 launch oomwoo_sim_support spawn_obstacle.launch.py x:=0.0 y:=-0.5 length:=0.5 # box: state should stay FOLLOW around all four corners
9/7/2026
- Two builds - OOMWOO will come in two configurations: a basic version (vacuum only, simple to assemble, charging-only dock) and a full-featured one (mop, auto-empty, extendable side brush). Which build an RFC serves now matters, so it is called out on the RFC board
- An old smartphone can be the compute - the I/O board now has a designed path to run the robot off a phone instead of a Raspberry Pi, which drops the single most expensive part for anyone who has a drawer phone. Thanks to krazca for the idea
- Electronics (oomwoo-pcb) - smartphone-as-compute support designed; schematic about 50% hand-checked (first review round); carpet sensor driver added
- 3D design (oomwoo-one-cad) - vacuum assembly, extendable side brush and air vent are available, none optimized for 3D printing yet; dock design started (simple for now)
- Cleaning - wall following and cleaning with the LiDAR works end to end (v1, needs iteration); see the 9/2 notes below for the line-fit and approach-cascade fixes that got it there
9/2/2026
-
contour_followernow fits a line to the wall instead of picking the nearest beam — a live capture showed the distance loop holding ±0.03 m while the bearing error thrashed ±20° frame to frame, with the heading error mirroring it exactly: the controller was steering on noise. Cause: near the perpendicular the range is almost flat — at a 0.20 m standoff, swinging 20° changes it by 1.3 cm, while the LiDAR's beam-to-beam scatter is about 2 cm — so the arg-min (which beam is nearest) is essentially random across a wide arc, andmin()over noisy beams is a biased distance, part of why it hugged closer than the target._boundary()now seeds on the nearest beam, grows the contiguous surface around it, and total-least-squares fits a line, reporting the fitted perpendicular distance and the bearing to it. Every point contributes, so the noise averages down and the wall angle falls out directly. Checked against synthetic data: exact on clean input, and 0.5° bearing error with 2 cm noise where arg-min gave ±20°. Falls back to the nearest beam belowmin_fit_points; new knobsfit_gap_m(0.10) andmin_fit_points(6); the fitted segment is drawn in~/debug_markersso you can see which surface it locked onto -
ALIGN tightened, and the follower now narrates itself —
align_tol_degwas 10°, so ALIGN handed over to FOLLOW up to 10° off parallel (exactly the residual seen in testing); now 3°. FOLLOW also logs one throttled line per second — picked distance vs target, how far the robot is currently angled toward the wall, how far the capped approach angle wants it angled, and the resulting command — which separates "approach still in progress" from "locked onto the wrong boundary" without needing a plot -
Debugging note:
ros2 topic listcan be unreliable in this image — live, actively-published topics have gone missing from the listing. The cause is not known yet; it is not FastDDS shared memory, which only carries message data (discovery always runs over UDP) and has been on since 8/17. Useros2 node info /<node>, which asks the node directly. This briefly looked like the follower's error topics had never been created; they had -
Chased down a phantom "wall" in the LiDAR scan — a wall-like return a couple of metres ahead that followed the robot everywhere, through spins and drives. It was not a leftover scan injector (that had been removed cleanly), not the front-ToF display, and not a stray spawned model — it was the floor. In a captured scan
r*cos(bearing)is constant at 1.07 m across −19°..−63°, the signature of a plane perpendicular to the heading, and it occluded all the real geometry beyond it.gzreported the robot pitched +3.97° nose-down, andasin(floor_clearance / (base_diameter/2)) = 4.11°is exactly the angle at which the body's front edge reaches the floor. Root cause: mounting the LiDAR forward pushed the centre of mass 1.6 mm past the wheel axle, and the only support behind the axle was the single rear caster — so the robot tipped onto its nose and aimed the scan plane at the floor about 1.1 m out, quietly corrupting both wall-following and scan matching -
Caster moved to the front, forward LiDAR restored — these turn out to be one coupled decision rather than two. With the caster at the rear the centre of mass had to stay behind the axle (margin: 1.6 mm); with it at the front the requirement inverts and the CoM must stay ahead — which is exactly what the forward turret provides. Measured from the generated URDF: CoM +7.2 mm ahead of the axle (+2.2 mm from the LiDAR, +4.9 mm from the caster's own 50 g moving forward), wheel and caster contacts both at z = −0.0125 so the robot sits level, the caster carries 5% of the weight, and it takes 4.31 m/s² of forward acceleration to unload it against the 1 m/s² limit — a 4.3× margin in place of a knife edge. Both parameters now carry the arithmetic in their comments, because changing either one alone re-tips the robot
-
wall_follow.rviz(inoomwoo_one) — a decluttered view for the follower runs. Keeps the grid, the robot, the live/scanandcontour_follower's debug markers; TF, the side range sensors and odometry are present but switched off; drops the front-ToF cloud, the cameras and the bump map; only the Displays panel is shown. RViz is deliberately not started bycontour_follow.launch.py, so the UI can come up first — arrange the windows, set the camera, start the screen capture — and the follower can then be restarted without RViz cycling underneath
ros2 launch oomwoo_bringup monitor_robot.launch.py use_sim_time:=true rviz_config:=wall_follow.rviz
- created vacuum dock, added to
kitchen_world
8/30/2026
- New
contour_follower— reactive LiDAR obstacle-following (Phase 1) — the proactive, any-shape generalization of the bumper-basedwall_clean. Off the LiDAR it finds the nearest boundary point in a forward-biased follow-side sector and servos two errors (standoff distance + bearing-to-abeam), which traces straight walls and concave corners; convex outside corners trigger an explicit "lose the wall, arc toward it" recovery until the boundary is re-acquired. Rotate-in-place ALIGN entry, left/right follow, live params,cleaning_activefor the coverage meter,~/state(ALIGN/FOLLOW/ARC/LOST) for observability. This is the core primitive for cleaning around obstacles of any continuous shape. Phase 1 = the follow loop + convex arc; no loop-closure yet (runs until stopped) and no bump-safety yet (steers off the LiDAR only). Design inoomwoo_clean/docs/contour_follower_spec.md
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels
ros2 run kaiaai_teleop teleop_keyboard # park within ~1 m of a wall (on the right), then quit
ros2 launch oomwoo_clean contour_follow.launch.py use_sim_time:=true
ros2 topic echo /contour_follower/state # ALIGN -> FOLLOW -> ARC (on a convex corner)
# convex test: drop a box on open floor and follow it
ros2 launch oomwoo_sim_support spawn_obstacle.launch.py x:=0.0 y:=-0.5 length:=0.5
-
contour_followerfix: approaching a wall from a distance — the first sim run showed the robot turning parallel at ~1 m and then crawling in at the speed floor instead of closing. The speed ease-off keyed off the raw bearing error, but a large bearing error is exactly what a legitimate far approach looks like, so the follower throttled itself the moment it angled in. Recast as a cascade: the outer loop turns standoff error into a desired approach angle, capped atalpha_max_deg(40°), and the inner loop steers onto it — with the ease-off now keyed to the inner (heading) error, which sits at ~0 during a steady approach. It closes at full speed and straightens on arrival. Gains are nowk_approach/k_heading(replacingk_dist/k_bearing) -
contour_followerdebug output —~/debug_markersshows what the controller is actually thinking: the boundary point it picked (and the ray to it), the standoff target it is steering toward, the search sector, and the current state as text. Plus~/standoff_err_m,~/bearing_err_degand~/heading_err_degto plot. View them in RViz withwall_follow.rviz(see the 9/2 notes) -
Known gaps from that run, not yet fixed: a slight left/right weave on straights (the nearest-point bearing is ill-conditioned — range varies as
d/cos θ, so the minimum is very flat and the pick hops between neighbouring beams; a least-squares line fit over the sector is the candidate fix), and the robot wedging on a bookshelf whose front edge sits outside the LiDAR's single scan plane — the LiDAR-invisible case that the Phase-4 bumper handoff exists for -
Moved the 2D LiDAR forward on the robot model now feeds this: the ~0.0745 m forward mount brings a convex corner into view a little sooner, and
contour_follower's acceptance test can A/Blidar_center_offset0.0 vs 0.0745 to measure what that buys -
minimized Gazebo simulation assets
- replaced racoon with a primitives vase
8/28/2026
-
Moved the 2D LiDAR forward on the robot model (
oomwoo-one) — from the centred turret tobase_diameter/2 − lidar_min_range(~0.0745 m) forward of centre, so its minimum range now lands right at the front body edge. That keeps the sensing floor at the bumper and brings a convex corner into view a little sooner — earlier look-ahead for the upcoming clean-around-any-obstacle behavior (following an obstacle's contour, concave or convex, off the LiDAR). It stays the singlelidar_center_offsetknob (now formula-driven; set0.0for the centred A/B baseline), andlidar_min_rangeis a shared property wired into the sensor<min>so the two can't drift. Everything downstream reads thebase_scanTF, so it's a transparent move — no re-mapping, and the saved maps stay valid (sanity-check localization after pulling the rebuilt image) -
Fixed the Gazebo GUI dying on startup under Docker on Windows — the GUI failed intermittently with an OGRE exception, while the server loaded the world fine.
ogre2needs OpenGL 3.3+; running Docker against an external X server (DISPLAY=host.docker.internal:0.0) with no GPU passed through, GLX cannot supply it, so gz-rendering falls back to its EGL PBuffer path — which has noFull Screenconfig option — and the render engine never initializes. It is intermittent because it depends on what the X server negotiates on that run
[GUI] [Err] [Ogre2RenderEngine.cc] OGRE EXCEPTION(2:InvalidParametersException):
Option named Full Screen does not exist. in EglPBufferSupport::setConfigOption
[GUI] [Err] [BaseRenderEngine.cc] Render-engine must be loaded first
[GUI] [Err] [BaseRenderEngine.cc] Render-engine has not been initialized
- Two ways out, both landed.
LIBGL_ALWAYS_SOFTWARE=1+GALLIUM_DRIVER=llvmpipemake Mesa's software rasteriser supply OpenGL 4.5 inside the container, soogre2keeps its normal windowed path and stops depending on the X server's GL at all:docker/utils/start_jazzy_dev_libgl_always_software.cmd— the plain launcher plus those two variables. Container-wide, so it covers every GL client including RViz2. Use this one if the GUI keeps dyingworld.launch.py software_gl:=true(oomwoo_gazebo) — sets the same two variables for Gazebo only, for when you would rather not slow RViz2 down. Implied byheadless:=true, which already forced software GL
- Hardware GL on Windows, via
docker/utils/gpu_start_jazzy_dev.cmd— software rasterising is the fallback, not the ceiling. On Windows 11 + WSL2 the container can reach the real GPU, but not the way a Linux host does: there is no/dev/driin WSL, and--gpus allcovers CUDA compute rather than OpenGL. The route is Mesa's d3d12 Gallium driver, which maps OpenGL onto DirectX 12 through WSL's/dev/dxg. Three pieces must line up — the/dev/dxgdevice, WSL'slibd3d12.so/libdxcore.sobind-mounted from/usr/lib/wsl, and Mesa'sd3d12_dri.so, already present in thejazzy-devimage. Measured on an RTX 5070 Laptop GPU, over the very same X server that fails without it:
| setting | renderer | GL |
|---|---|---|
| default (what fails today) | llvmpipe | 4.5 |
LIBGL_ALWAYS_SOFTWARE=1 | llvmpipe | 4.5 |
GALLIUM_DRIVER=d3d12 | D3D12 (NVIDIA GeForce RTX 5070 Laptop GPU) | 4.6 |
GALLIUM_DRIVER=d3d12is required rather than optional: with no/dev/drito probe, Mesa's auto-detection falls back to llvmpipe.MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIApins the discrete GPU — without it a hybrid-graphics laptop can come up on the Intel iGPU. Do not combine this withLIBGL_ALWAYS_SOFTWARE=1; that flag wins and silently puts you back on llvmpipe
docker\utils\gpu_start_jazzy_dev.cmd :: hardware GL (fastest)
docker\utils\start_jazzy_dev_libgl_always_software.cmd :: software GL (fallback)
docker\utils\start_jazzy_dev_libgl_always_software.cmd
ros2 launch oomwoo_gazebo world.launch.py world:=kitchen_dining.world
- Gazebo
kitchen_dining.world— yesterday's work-in-progress world is now a complete kitchen and dining room: 4.90 x 3.80 m (18.6 m²) under a 2.5 m ceiling, composed from individually vendored models rather than one monolithic mesh, so every piece can be reused by the bedroom / office / bathroom worlds later. Nothing is fetched at run time. An L-shaped counter run wraps the north and east walls at true residential dimensions — 0.900 m counter height, 0.635 m depth, and a 0.089 m high x 0.076 m deep toe kick (3.5" x 3", US standard). That kick is deliberately lower than the robot's 0.097 m, so it cannot drive in: this is the case the extendable side brush and mop exist for - Fittings: range centred on the north wall (Fuel
Oven, uniformly scaled 0.8452 so its top lands level with the counter and its depth within 3 mm of it), brushed-stainless range hood, double sink under the east window, integrated under-counter dishwasher, French-door fridge, tall pantry, wall cabinets on both walls, coffee maker, and a dining table the robot drives under — 0.622 m between legs with the lowest stretcher at 0.187 m. Wall cabinetry, fridge, pantry, dishwasher, hood and the 2.5 m walls are authored inoomwoo_gazebo/models/(Fuel has no residential-height counter, no toe kick anywhere, and no range hood at all); the table, chairs, range and coffee maker are vendored Fuel assets (CC-BY 4.0) - Default spawn (
x_pose -2.0,y_pose -0.5) lands on open floor, so no override is needed. There is no saved map for this world yet — run it with SLAM
ros2 launch oomwoo_gazebo world.launch.py world:=kitchen_dining.world
- SLAM shrugs off a big obstacle — ran the large-obstacle stress test end to end (drive a real wall up to the vacuum, score
loc_err_slamvs ground truth, raw scan vs/scan_filtered). With the wall throwing ~⅓ of the beams as off-map outliers, slam_toolbox held ~1 cm / ~0.2° either way: the correlative matcher locks onto the two-thirds of beams that still fit the map and ignores the coherent off-map chunk. So the scan filter is a clean feed for perception and a bit of map hygiene, not a localization crutch at this scale — good to know which job it does. Write-up: How OOMWOO cleaning algorithms work - Stress test now scores against a fixed map — slam_toolbox in localization mode keeps a rolling scan buffer and briefly draws a lingering obstacle into the
/mapit publishes (it heals, and never touches the saved map on disk). That made slam's own/mapa poor off-map reference, solocalization_stress.launch.pynow brings up a nav2map_serverwith the fixed saved map forlocalization_healthto score against, and remaps slam's own/map→/map_slam - Fixed a filter-arm startup deadlock — with
filter:=true, slam matches/scan_filteredwhilelocalization_healthneeds slam'smap→odomTF to produce it, so neither started and themapframe never appeared.localization_healthnow fails open — republishing the raw scan on/scan_filteredwhenever it cannot filter yet (no map, no pose TF, too few beams) — so a consumer like slam is never starved
8/27/2026
- Large-obstacle localization stress test —
spawn_obstacle.launch.py(oomwoo_sim_support) drops a real thin wall into the running sim: a 3D box the map does not know about, so it occludes the LiDAR physically (just shows up in/scan), is anchored in the room (drive up to it and around it), and is visible in Gazebo and RViz.localization_stress.launch.pylocalizes slam_toolbox against it and scoresloc_err_slamvs ground truth;filter:=trueroutes slam throughlocalization_health's/scan_filtered, to A/B whether stripping the obstacle recovers the pose. slam_toolbox publishes/maphere, so no nav2/AMCL/map_server is needed
# terminal 1 — sim (robot_wheels = wheel odom, /odom_truth is ground truth)
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels
# terminal 2 — BASELINE: slam matches the raw /scan (obstacle present)
ros2 launch oomwoo_sim_support localization_stress.launch.py use_sim_time:=true map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml
# terminal 3 — drop a wall on open floor ahead, then drive at it
ros2 launch oomwoo_sim_support spawn_obstacle.launch.py x:=0.0 y:=-0.5
ros2 run kaiaai_teleop teleop_keyboard
# then re-run terminal 2 with filter:=true and compare loc_err_slam:
ros2 run foxglove_bridge foxglove_bridge # plot /loc_err_slam/pos_err_m and /loc_err_slam/yaw_err_deg
- Gazebo
kitchen-dining.world, work in progress
8/26/2026
- Sharper scan tracking through fast turns — slam_toolbox was re-matching at only ~2 Hz (
minimum_time_interval: 0.5), so a fast-spinning robot dead-reckoned ~0.5 s of odometry between matches and the scan cloud visibly lagged then snapped in RViz. Tightened to0.1(under the 5 Hz scan period) and zeroedminimum_travel_distance/minimum_travel_heading, so it re-matches on essentially every scan and stays registered through aggressive spins - Tracked the residual "stop flick" to wheel slip — the one-frame angular jump left after the tuning is wheel slip, not a localizer bug: on a hard stop from a fast spin the velocity-controlled wheels halt while the body coasts on inertia, so wheel odometry undershoots the true rotation for a frame (and symmetrically over-reads on spin-up). Each aggressive spin-stop leaves a few degrees of net over-read, which is what accumulates into raw wheel-odom heading drift — the drift slam quietly corrects against the map every scan. It's real physics (a real vacuum slips too), only exercised far harder than a vacuum ever would
- new
odom_slipdiagnostic (oomwoo_sim_support) — pairs/odom(wheel) and/odom_truthby header stamp and publishes the slip for Foxglove:~/slip_rate_dps(≈ ω_wheel − ω_truth, ~0 while rolling true, spikes + on wheelspin and − on the inertial coast) and de-trended~/slip_deg. The raw yaw difference is dominated by a constant frame offset, so the rate/accumulation is the real signal - hushed the Ceres
num_threads 50 > 24glog spam from slam_toolbox (a harmless thread-count cap) viaGLOG_minloglevel
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels
ros2 run oomwoo_sim_support odom_slip --ros-args -p use_sim_time:=true # slip on ~/slip_rate_dps, ~/slip_deg
ros2 run foxglove_bridge foxglove_bridge # plot the slip live
ros2 run kaiaai_teleop teleop_keyboard # spin up, then stop abruptly, and watch ~/slip_rate_dps spike
- moved
localization_lostfrom localization_health to localization_manager- localization_manager handles robot-lost policy (when to flag robot as lost - and what to do when that happens)
- localization_health handles scan quality compute
- moved
dynamic_obstaclesfrom localization_health to the perception node- the perception node is better suited to decide which obstacles are dynamic
8/24/2026
- manually testing
/dynamic_obstaclesdetection
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels
ros2 launch oomwoo_sim_support localization_relocalize.launch.py use_sim_time:=true auto_recovery:=false map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml
ros2 run oomwoo_localization localization_health --ros-args -p use_sim_time:=true
ros2 run kaiaai_teleop teleop_keyboard
ros2 topic echo /scan_filtered
ros2 topic echo /localization_health/dynamic_obstacles
ros2 topic echo /localization_health/scan_scored # walls bright = static, ball dark = dynamic
ros2 topic echo /localization_health/quality
8/24/2026
- stress mode — the relocalizer's confidence gate is now proven to refuse when it genuinely can't tell. An adversarial regression feeds the real branch-and-bound matcher scans corrupted on purpose (a dynamic obstacle, a removed wall, a symmetric room) and asserts the product-grade invariant: it accepts a fix only when that fix is correct, and on a symmetric room (match score near 1.0, confidence 0.0) it refuses rather than commit to a coin-flip. Runs in ~1 s as a CI test
- scan filtering (
localization_health) — dynamic obstacles (a stray box, a rolling ball) are stripped from a republished/scan_filteredso they stop dragging the running scan match down; the segmented clusters go out on~/dynamic_obstacles. It filters only while the pose is trusted, and never blanks more than a set fraction of the scan ~/scan_scored— the full scan (nothing dropped) republished with each ray's static-ness in its intensity:exp(-d²/2σ²), 1.0 on a mapped wall and → 0 for a dynamic return. A perception/ML node can threshold and cluster it however it likes (experimental API)- new
oomwoo_perceptionpackage with a placeholderdynamic_object_detector— a starting point for contributors: it reads~/scan_scored, groups the dynamic rays into blobs, and publishes their centroids on~/objects(MarkerArray) for RViz. Classification, tracking, and gestures (e.g. tap-a-foot-to-spot-clean) are what you add on top
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels
ros2 launch oomwoo_sim_support localization_relocalize.launch.py use_sim_time:=true auto_recovery:=false map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml
ros2 launch oomwoo_localization localization_recovery.launch.py use_sim_time:=true # localization_health -> scan_scored + /scan_filtered
ros2 launch oomwoo_perception dynamic_object_detector.launch.py use_sim_time:=true # dynamic blobs on ~/objects
# push the living-room toy ball in front of the vacuum and watch it flagged in RViz:
# MarkerArray on /dynamic_object_detector/objects
# LaserScan on /localization_health/scan_scored (Color Transformer = Intensity)
8/22/2026
- Cartographer-style Hess, et al 2016 global relocalizer works,
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels
ros2 launch oomwoo_sim_support localization_relocalize.launch.py use_sim_time:=true auto_recovery:=false map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml
ros2 launch oomwoo_localization localization_recovery.launch.py use_sim_time:=true
ros2 topic echo /localization_manager/recovery_action
ros2 run foxglove_bridge foxglove_bridge # Optional, plot localization error
ros2 run kaiaai_teleop teleop_keyboard # Optional, drive around
ros2 service call /kidnap_injector/kidnap std_srvs/srv/Trigger {} # Kidnap to random location
ros2 topic pub --once /kidnap_injector/kidnap_to geometry_msgs/msg/PoseStamped "{header: {frame_id: map}, pose: {position: {x: 0.03, y: 1.69}, orientation: {z: -0.939, w: 0.344}}}" # TV stand
- batch/regression test works
- added hold_s (wait after each relocalization) for visual demo
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels
ros2 launch oomwoo_sim_support localization_relocalize.launch.py use_sim_time:=true auto_recovery:=false map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml
ros2 launch oomwoo_localization reloc_eval.launch.py use_sim_time:=true publish_initialpose:=true csv_path:=/root/maps/reloc_eval.csv hold_s:=3.0
8/20/2026
- branch-and-bound global relocalizer (
oomwoo_localization): a principled, guaranteed answer to "where am I?" instead of AMCL's stochastic global filter. It correlates the current/scanagainst the whole map over all headings (Olson-style correlative matching accelerated with a Cartographer-style max-pool pyramid + branch-and-bound), returning the exact global optimum of the search grid — no local-minimum lottery. In sim testing AMCL failed a kidnap ~20% of the time, non-repeatably, confusing one corner for another; the BnB search removes that. It also reports an explicit confidence margin (how much the best pose beats the next distinct cluster), so ambiguity (e.g. a symmetric room) is known and flagged, not silently guessed — something a particle filter can't do - new node
global_relocalizer: call the~/relocalizeservice (oomwoo_localization_msgs/Relocalize) and get back a pose, score, confidence, and runtime. It is pure mechanism — no motion, no reinit, no "should I trust this" policy (that belongs to an application node); it only reports where the scan says the robot is - new
reloc_evalbatch harness + launch: kidnaps the robot across a systematic pose grid, scores the relocalizer against ground truth, and prints success rate, position/heading error, runtime, and whether the confidence flag actually predicts correctness. Exits non-zero below a threshold, so the same run doubles as a CI regression gate
# Evaluate the global relocalizer across a systematic kidnap grid
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels
ros2 launch oomwoo_sim_support localization_relocalize.launch.py use_sim_time:=true \
map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml
ros2 launch oomwoo_localization reloc_eval.launch.py use_sim_time:=true csv_path:=/root/reloc_eval.csv
# or one-shot relocalize by hand:
ros2 service call /global_relocalizer/relocalize oomwoo_localization_msgs/srv/Relocalize {}
8/19/2026
- new package
oomwoo_localizationwith alocalization_healthmonitor: it scores every/scanagainst the static map at the primarymap→basepose read from TF (continuous — unlike slam_toolbox's sparse/pose, whose covariance stays confidently small even on a kidnap). Quality = the fraction of beams whose endpoint lands withinmatch_dist_mof a mapped wall; a kidnap collapses it (nothing matches) and fires/localization_lost, while an unmapped shoe/box only dents it. Debug/visibility outputs —~/quality, a~/dist_histogram, an intensity-labelled~/scan_annotatedcloud (inlier / outlier / clustered), and a throttled console histogram — let you eyeball which rays matched and the outlier clusters in RViz. Detection only for now; scan filtering and dynamic-obstacle rejection come later - automatic relocalization now converges reliably: AMCL's
recovery_alpha(Augmented-MCL random-particle injection) is off by default (recovery:=false) inlocalization_relocalize.launch.py. Left on, the continuous injection fought the global/reinitialize_global_localizationand could settle AMCL on the wrong cluster (the far side of the room); passrecovery:=trueonly to exercise passive covariance-based lost-detection - dev image adds
python3-scipy(thelocalization_healthmap distance transform)
# Localization health: scan-vs-map match quality + robot-lost detection
ros2 launch oomwoo_localization localization_health.launch.py use_sim_time:=true
# RViz: add a PointCloud2 on /localization_health/scan_annotated, Color Transformer = Intensity
# watch /localization_health/quality collapse on a kidnap; box in front only dents it
8/18/2026
navigation.launch.pygains alocalizationargument (defaultslam_toolbox): navigate a saved map with slam_toolbox scan-matching localization — it loads the map's serialized pose-graph and runs the full Nav2 stack composed in one container, with slam_toolbox owningmap→odom— orlocalization:=amclfor the particle filter. Falls back to AMCL automatically if the map has no<map>_serial.posegraph- relocalization + automatic recovery:
localization_relocalize.launch.pyruns the AMCL-vs-slam_toolbox compare stack +kidnap_injector+relocalize_on_lost. Teleport ("kidnap") the robot — random via/kidnap_injector/kidnap, or to a specified pose via~/kidnap_to— and it recovers automatically:relocalize_on_lostdetects the robot is lost (AMCL covariance), calls/reinitialize_global_localizationand spins in place to re-localize AMCL globally, then re-seeds slam_toolbox (/initialpose) at the recovered pose so accurate scan-matching resumes. AMCL does the global "find myself" that scan matching can't, then hands the pose back odom_sourcevalues renamed for clarity:truth→ground_truth,wheel→robot_wheels- AMCL tuning is now reproducible:
oomwoo_one/config/etc/navigation_tight.yaml(tuned) andnavigation_loose.yaml(stock-ish) differ only in the amcl block — pass either asnav_params:=…tolocalization_compare - swapped PlotJuggler for Foxglove in the dev image (−~200 MB):
ros2 run foxglove_bridge foxglove_bridge, then open Foxglove Studio (browser) →ws://localhost:8765 navigation.launch.pyno longer depends on the sim packageoomwoo_gazebo: themapargument has no default — passmap:=…to localize, orslam:=Trueto map- fixed a sim-clock crash in the
localization_errormeter andbump_map: the sliding-window prune subtracted below zero while sim time was still under the window
# Navigate a saved map (slam_toolbox localization by default)
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels # robot_wheels (NOT ground_truth): else the sim odom teleports with the robot
ros2 launch oomwoo_bringup navigation.launch.py use_sim_time:=true \
map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml rviz_config:=bump_map.rviz # localization:=amcl switch to AMCL localization
ros2 run foxglove_bridge foxglove_bridge # Monitor /loc_err_amcl|slam/pos|yaw_err_m/data localization errors
ros2 run kaiaai_teleop teleop_keyboard # Drive robot a little before kidnap to let AMCL point cloud converge
# Relocalization: kidnap the robot, watch it auto-recover
ros2 launch oomwoo_sim_support localization_relocalize.launch.py use_sim_time:=true \
map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml # recovery:=true auto-detects robot-lost, messes up localization on kidnap
ros2 topic pub --once /kidnap_injector/kidnap_to geometry_msgs/msg/PoseStamped \ # kidnap to a given position -> robot auto-recovers
"{header: {frame_id: map}, pose: {position: {x: 0.03, y: 1.69}, orientation: {z: -0.939, w: 0.344}}}"
# ros2 service call /kidnap_injector/kidnap std_srvs/srv/Trigger {} # kidnap to a random position -> robot auto-recovers
8/16/2026
- Gazebo
living_roomworld map includesslam_toolboxpose graph - Fixed
localization_compare.launch.pylaunching second copy of Nav2 - Fixed
localization_compare.launch.pytracking the wrong odom ground_truth - Enabled FastDDS shared memory
- Compared
slam_toolboxscan matching with AMCL (linear, angular errors); tightened AMCL inoomwoo_one/config/navigation.yaml
8/15/2026
- localization A/B tooling for the "LiDAR scan vs map walls" misregistration: a new
localization_errormeter (inoomwoo_sim_support) scores a localizer's estimate against the sim's ground truth and logsLOC_ERR pos/yawplus a windowed RMS, publishing~/pos_err_m/~/yaw_err_degfor plotting. It is localizer-agnostic — reads themap→baseTF, or a pose topic like/amcl_posewithestimate_topic:=…— so the same meter scores AMCL and slam_toolbox alike. Run it withodom_source:=ground_truth localization_compare.launch.pyruns AMCL and slam_toolbox localization side by side: slam_toolbox owns themap→odomTF while AMCL runs withtf_broadcast:false(only/amcl_pose), so there is no TF conflict, and two meters plot both error curves live. AMCL uses your unmodifiednavigation.yaml; slam_toolbox loads a serialized pose-graph (mapper_params_localization.yaml, localization mode)- to make the graph: map with
slam:=True, thenros2 service call /slam_toolbox/serialize_map slam_toolbox/srv/SerializePoseGraph "{filename: '…/living_room_serial'}"andmap_saver_clifrom the same session so the pgm and the graph share an origin
- to make the graph: map with
ros2 launch oomwoo_gazebo world.launch.py odom_source:=ground_truth # :=robot_wheels
ros2 launch oomwoo_sim_support localization_compare.launch.py \
use_sim_time:=true map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml
ros2 run kaiaai_teleop teleop_keyboard
ros2 run rqt_plot rqt_plot # add /loc_err_amcl/pos_err_m/data /loc_err_slam/pos_err_m/data
# ros2 run plotjuggler plotjuggler --layout /path/to/layout.xml
ros2 launch oomwoo_clean wall_clean_bump_out.launch.py use_sim_time:=true
8/11/2026
- one RViz window for wall-segment estimation:
navigation.launch.pynow takes anrviz_configargument (just likemonitor_robot.launch.py), andbump_map.rvizfolds in the Nav2 displays (global/local costmaps, plans, AMCL particle swarm, Nav2 goal tool) on top of the bump-map layers- launch navigation straight into the bump map — no second RViz window from
monitor_robot.launch.py
- launch navigation straight into the bump map — no second RViz window from
wall_clean_bump_out.launch.pynow startsbump_map.launch.pyfor you (passbump_map:=falseto skip) — one fewer terminal to build the tactile keep-out map while cleaningbump_map.rvizdecluttered for wall-segment estimation: the semi-transparent/bump_mapkeep-out overlay and the Global Planner / Controller costmap groups are now off by default (the red bump-wall segments stay on), and the top-down view is rotated to match the Gazebo default orientation; the Selection / Tool Properties / Views panes are hidden and the/odomheading arrow is shown so you can see which way the vacuum faces- wall cleaning cruise arc is now tuned by radius instead of angular rate: the
arc_omegaparameter is replaced byarc_radius(metres, default 1.5), and the turn rate is derived asv_cruise / arc_radius— so the arc shape stays the same at any cruise speed. Retune withkaia set clean.arc_radius 1.0(smaller = tighter into the wall). If you had setclean.arc_omega, switch it toclean.arc_radius bump_mapnow builds its tactile map in themapframe: it starts inodom, then promotes tomapthe instant localization comes up and rebases the contacts captured so far — so the wall segments stay nailed to the map instead of drifting with the LiDAR scan when AMCL wobbles (the pure-bumper, no-localization mode stays inodom)
ros2 launch oomwoo_gazebo world.launch.py
ros2 launch oomwoo_bringup navigation.launch.py use_sim_time:=true \
map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml rviz_config:=bump_map.rviz
ros2 launch oomwoo_clean wall_clean_bump_out.launch.py use_sim_time:=true # also starts bump_map
8/11/2026
- the Gazebo sim now starts with the stereo cameras off by default (heaviest sensor, unused for now) — faster out of the box; turn them on with
enable_cameras:=true navigation.launch.pycan auto-localize: it seeds AMCL at the known start pose so themapframe is available without the manual RViz "2D Pose Estimate" (sim only by default). This unblocks bump-map wall-segment estimation, which needs the map frame- auto-localize runs in simulations, when
use_sim_time:=true - force disable
auto_localize:=false
- auto-localize runs in simulations, when
- added
bump_map.rvizto visualize the bump map over the SLAM map
ros2 launch oomwoo_gazebo world.launch.py
ros2 launch oomwoo_bringup navigation.launch.py use_sim_time:=true \
map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml # auto-localizes now
ros2 launch oomwoo_clean bump_map.launch.py use_sim_time:=true
ros2 launch oomwoo_bringup monitor_robot.launch.py use_sim_time:=true rviz_config:=bump_map.rviz
8/10/2026
- added per-sensor on/off switches to speed up the Gazebo simulation
- the rendering sensors (cameras and the front ToF most of all, then the side ranges and LiDAR) slow the sim down; turn the ones you don't need off at launch
- the sensor frames stay in the model; only the gz sensor (the render cost) is dropped
ros2 launch oomwoo_gazebo world.launch.py enable_cameras:=false enable_tof:=false # faster
ros2 launch oomwoo_gazebo world.launch.py \
enable_ranges:=false enable_tof:=false enable_cameras:=false enable_imu:=false # nav only (LiDAR)
8/10/2026
- added an RViz config to eyeball all the sim sensors at once (LiDAR, side ranges, front ToF cloud, both cameras, bump map)
- except IMU
monitor_robot.launch.pynow takes anrviz_configargument to pick any.rvizfile from the robot packagerviz/folder
ros2 launch oomwoo_gazebo world.launch.py
ros2 launch oomwoo_bringup monitor_robot.launch.py use_sim_time:=true rviz_config:=sensors.rviz
8/10/2026
- added a tactile "bump map" — the truly-solid keep-out layer, built from the bumpers alone
- LiDAR/cameras see couch skirts, bed valances and curtains as solid, but a vacuum should clean under/through them; only a physical bump proves something is truly solid, so a coverage planner can still clean the rest
- the
bump_mapnode turns bumper contacts into/bump_map(OccupancyGrid keep-out layer) +/bump_map/walls(RViz wall segments) +/bump_event(newoomwoo_msgs/BumpEvent: contact point, approach, which bumper side) - contacts are placed along the robot's approach heading and accumulated; the map is in
mapwhen localized, elseodom
ros2 launch oomwoo_gazebo world.launch.py
# localize (map->odom)
ros2 launch oomwoo_bringup navigation.launch.py use_sim_time:=true map:=/ros_ws/src/oomwoo_gazebo/maps/living_room.yaml
ros2 launch oomwoo_clean bump_map.launch.py use_sim_time:=true
ros2 run kaiaai_teleop teleop_keyboard
# point the vacuum at a wall, then bump-out clean to build the map
ros2 launch oomwoo_clean wall_clean_bump_out.launch.py use_sim_time:=true
# RViz: add a Map on /bump_map and a MarkerArray on /bump_map/walls
8/10/2026
- added more sensors to oomwoo-one URDF (Gazebo simulation)
- front multizone ToF depth sensor (16x8 zones, 120° FoV, models two VL53L7CX) →
/tof_front/points - front stereo cameras (VGA, 120° FoV, OV5647-equivalent) →
/camera_left/image,/camera_right/image - IMU: gyro + accelerometer + orientation →
/imu
- front multizone ToF depth sensor (16x8 zones, 120° FoV, models two VL53L7CX) →
- added a simulation odometry source switch: ground-truth model pose (default) or wheel-encoder odometry
- the selected source drives
/odom+/tf; the other is always published on/odom_truth//odom_wheelso wheel slip can be measured later
- the selected source drives
- documented oomwoo-one simulation sensors, topics, URDF parameters and world launch arguments in its README
- gave the reactive bump-out cleaner its own
wall_clean_bump_out.launch.py, freeingwall_clean.launch.pyfor the upcoming full wall following
ros2 launch oomwoo_gazebo world.launch.py # ground-truth odom (default)
ros2 launch oomwoo_gazebo world.launch.py odom_source:=robot_wheels # wheel-encoder odom, slip drifts
ros2 topic echo /imu
ros2 topic hz /tof_front/points
ros2 run rqt_image_view rqt_image_view # view /camera_left/image
8/9/2026
- wall-follow-bump-out now backs vacuum "out" the way the vacuum drove "in" - as opposed to backing "up" straight
- back-out retracing its path makes vacuum less likely to wedge somewhere new
- documentation https://github.com/makerspet/oomwoo-ros2-tools/blob/jazzy/docs/wall-follow-bump-out.md
8/9/2026
- fixed non-interactive bash to have same context as interactive
- that caused OpenGL go missing, broke LiDAR scans in headless Gazebo simulations
- added side distance sensors to oomwoo-one URDF
- kaia CLI sets ROS2 parameters, documentation
ros2 topic echo /range_right
ros2 topic echo /range_left
8/8/2026
- added rudimentary reactive cleaning along the wall by "bumping out" the wall
- works, LiDAR is not used
- "bumping out" is needed when furniture has covers that appear solid in LiDAR scans, but the vacuum can still get under the furniture cover (e.g. to clean under the sofa)
- fixed oomwoo-one URDF bumper height to match the vacuum cylinder body height
- upgraded kaia CLI to manage configuration variables
ros2 launch oomwoo_gazebo world.launch.py
ros2 launch oomwoo_bringup monitor_robot.launch.py
ros2 run kaiaai_teleop teleop_keyboard
# Point the vacuum at the wall to be cleaned
ros2 launch oomwoo_clean wall_clean.launch.py use_sim_time:=true
# Optional - wall clean bump-out settings
# kaia set clean.arc_omega 0.1
# kaia set clean.turn_right_deg 10
# turn_right_deg / turn_left_deg / turn_both_deg 20/90/60
7/24/2026
- Rviz shows cleaning plan
ros2 launch oomwoo_bringup monitor_robot.launch.py- add
/coverage_planner/plan, Fixed Frame = map
- reactive navigation for cleaning
- experimental, replaces Nav2 for cleaning tasks
ros2 launch oomwoo_sim_support coverage_regression.launch.py gui:=true \
world:=$(ros2 pkg prefix oomwoo_gazebo)/share/oomwoo_gazebo/worlds/living_room.world \
map:=$(ros2 pkg prefix oomwoo_sim_support)/share/oomwoo_sim_support/maps/living_room.yaml \
x_pose:=0.32 y_pose:=1.59 executor:=reactive
7/21/2026
- clean using an existing map; Boustrophedon, clunky, slow, fails often
- packages in makerspet/oomwoo-ros2-tools
- added bumpers, verified working
- added localization (kidnapped robot); not tested
- forked kaiaai_gazebo, kaiaai_bringup to oomwoo_gazebo, oomwoo_bringup
7/8/2026
- added a first Raspberry Pi 4/5 4GB runtime install plan
- added simulated CPU-MCU serial I/O placeholder
7/1/2026
- added oomwoo-one ROS2 robot description package (simulation only)
6/26/2026
- added https://github.com/remakeai/vacuum_ros2_bridge
- LiDAR compute moved to vacuum_ros2_bridge
6/18/2026
- added bumper sensors for proscenic-m6pro
- fixed Gazebo living world marble table collision mesh
6/16/2026
- added Proscenic M6 Pro robot description
kaia config robot.model proscenic_m6pro