commit 43552b54237a791988b0ce01f56ac7ffdf8cedef Author: wehub-resource-sync Date: Mon Jul 13 12:31:04 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f40e495 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.private/ +.claude/ +.vscode/ \ No newline at end of file diff --git a/BOM.md b/BOM.md new file mode 100644 index 0000000..b9597c5 --- /dev/null +++ b/BOM.md @@ -0,0 +1,119 @@ +# Bill of Materials (work in progress) + +First working BoM is targeted for *mid-July '26* + +A final, fully-costed BoM is targeted for *end of August '26* + +Blog post - how I'm making this BOM [How-to: Source BOM for OOMWOO Open-Source Vacuum Robot](https://makerspet.com/blog/how-to-source-bom-for-oomwoo-open-source-vacuum-robot/) + +Rationale in [docs/design-document.md](docs/design-document.md). + +--- + +## Budget target + +~$200 in sourced parts + a Raspberry Pi 5 (4 GB), aiming at the capability of a +mid-range ($500–600) commercial vacuum with mopping, possibly premium obstacle detection (ToF + +color camera + an NPU accelerator). + +## Robot BoM (work in progress) + +Retail / low-qty prices, INCLUDES shipping, excludes tax. Read [how I calculate BoM costs](#how-i-calculate-costs). + +| Item | Qty | ~USD | Notes | Source | +|---|---|---|---|---| +| Drive wheel assembly pair | 1 | $24-$33 | Motor + encoder + suspension + tire + cables + wheel-drop sensors | Roborock S4 Max, S45 Max, S5 Max, S50 Max, S55 Max, S6 MaxV, S6 Pure, S65 Pure, S65 MaxV, S7, S7 Pro, S7 MaxV, S7 Max Ultra, S70, S75, E4, E45, E5, E50, E55, G10, T7, T7S, Q5, Q7, Q7 Max, and Q Revo [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-s5-maxv-wheels.html) / [Amazon](https://www.amazon.com/s?k=roborock+s5+maxv+wheels) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+s5+maxv+wheels) | +| Drive wheel assembly pair | 1 | $24-$33 | | Roborock S5, S50, S51, S52, S55, S502, and the budget C10, E20, E25 and E35 [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-s5-wheels.html) / [Amazon](https://www.amazon.com/s?k=roborock+s5+wheels) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+s5+wheels) | +| Caster wheel | 1 | $2.50-$5 | Push-in | iRobot Roomba I3/4/6/8, J7/Plus J7 E5/6 500 600 700 800 900 [AliExpress](https://www.aliexpress.us/w/wholesale-roomba-caster.html) / [Amazon](https://www.amazon.com/s?k=roomba+caster) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roomba+caster) | +| Suction fan | 1 | $10–23 | 6 kPa option | Dreame MSD-C-3, Nidec 20N709U020; fits Dreame L10s Prime/Pro, *L10s Ultra Gen 1* (5.3 kPa — Gen 2 is 10 kPa!), D10s Plus, X10+ (6 kPa); X20+ verify (~7 kPa?) [AliExpress](https://www.aliexpress.us/w/wholesale-Dreame-L10s-fan.html) / [Amazon](https://www.amazon.com/s?k=Dreame+L10s+fan) / [eBay](https://www.ebay.com/sch/i.html?_nkw=Dreame+L10s+fan) | +| | | $17–28 | 5.1-6 kPa option | Nidec 22N704W150, 20N704S980, 20N704R980L; fits Roborock S8 Pro Ultra, S7 MaxV (5.1 kPa), S8/S8+/S8 Plus (6 kPa); G20 (Q7 Max/Max+ excluded — they're 4.2 kPa, not 5-6) [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-s8-pro-fan.html) / [Amazon](https://www.amazon.com/s?k=roborock+s8+pro+fan) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+s8+pro+fan) | +| | | $12–24 | 10 kPa option | Roborock BL24131616; Nidec 22N704V160; fits Roborock S8 MaxV Ultra (10 kPa); G20S [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-g20s-fan.html) / [Amazon](https://www.amazon.com/s?k=roborock+g20s+fan) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+g20s+fan) | +| | | $34–45 | 36 kPa option | Roborock Saros 20 [AliExpress](https://www.aliexpress.us/w/wholesale-Roborock-Saros-20-fan.html) / [Amazon](https://www.amazon.com/s?k=Roborock+Saros+20+fan) / [eBay](https://www.ebay.com/sch/i.html?_nkw=Roborock+Saros+20+fan) | +| | | $12–25 | 2-2.5 kPa option | Nidec 20N704P200, 20N704R500, 20N704R310, 20N704P160; Xiaomi Roborock S50 S51 S55 S60 S61 S65 S5 MAX S6 E25 E35, S5, S6 Pure [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-s5-fan.html) / [Amazon](https://www.amazon.com/s?k=roborock+s5+fan) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+s5+fan) | +| Main brush | 1 | $5-$8 | Single roller, rubber and bristles | Fits Roborock S4, S4 Max, S5, S5 Max, S50/S55, S6, S6 Pure/MaxV, S60/S65, E2,E3,E4,E5, E20,E25,E35, C10, Xiaomi Mijia [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-s5-brush.html) / [Amazon](https://www.amazon.com/s?k=roborock+s5+brush) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+s5+brush) | +| | | $8-$12 | Anti-tangle dual roller, rubber only | Fits Roborock S8 MaxV Ultra, G20S V20, P10S Pro/Pro Plus [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-G20S-v20-brush.html) / [Amazon](https://www.amazon.com/s?k=roborock+G20S+v20+brush) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+G20S+v20+brush) | +| | | $3-$5 | Single roller, rubber only | Fits Roborock S7, S70, S7+/MaxV/MaxV Plus/MaxV Ultra/Pro, Q Revo/Pro/Plus, Q5, Q5+, Q7, Q7 Max, QV 35S, T7S, T7S Plus [AliExpress](https://www.aliexpress.us/w/wholesale-Roborock-S7-main-brush.html) / [Amazon](https://www.amazon.com/s?k=Roborock+S7+main+brush) / [eBay](https://www.ebay.com/sch/i.html?_nkw=Roborock+S7+main+brush) | +| | | $5-$10 | Anti-tangle split single roller, rubber and bristles | Fits Roborock Saros 10/10R, Saros 20, Saros 20 Sonic, S10 MaxV Ultra, S9 MaxV/MaxV Ultra, QRevo 5AE/Edge/Edge C/Edge S5A/5V1/X/Edge T/Curv/S5V/Curv S5X/P20 Pro [AliExpress](https://www.aliexpress.us/w/wholesale-Roborock-saros-10-main-brush.html) / [Amazon](https://www.amazon.com/s?k=Roborock+saros+10+main+brush) / [eBay](https://www.ebay.com/sch/i.html?_nkw=Roborock+saros+10+main+brush) | +| | | $20-$30 | Anti-tangle hair-cutting dual roller, rubber and bristles | Fits Dreame L40s Ultra, L40 Ultra, X40, X40 Ultra, X30 Ultra, L30 Ultra, L20 Ultra, L10s Ultra/Ultra Gen 2/Pro Ultra, S10 Pro, S20, S20 Pro/Pro Plus, X10, X10+, X20 Pro, D9 Max Gen2; Mova E30 Ultra, S10 Plus, P10 Pro Ultra; Xiaomi S10+, X20 Pro, X10+, Mijia M30S/M40 [AliExpress](https://www.aliexpress.us/w/wholesale-dreame-tricut-brush.html) / [Amazon](https://www.amazon.com/s?k=dreame+tricut+brush) / [eBay](https://www.ebay.com/sch/i.html?_nkw=dreame+tricut+brush) | +| | | $7-$10 | Anti-tangle tapered dual roller, rubber and bristles | Fits Dreame X60 Max, X50 Ultra/Master/Pro Ultra, L40S Pro Ultra/Ultra; MOVA V50 Ultra [AliExpress](https://www.aliexpress.us/w/wholesale-dreame-x50-ultra-main-brush.html) / [Amazon](https://www.amazon.com/s?k=dreame+x50+ultra+main+brush) / [eBay](https://www.ebay.com/sch/i.html?_nkw=dreame+x50+ultra+main+brush) | +| | | $7-$10 | Anti-tangle dual roller, rubber only + rubber and bristles options| Fits Roborock Roborock S8, S8+/Pro Ultra, Q5 Pro/Pro+, Q8 Max/Max+, G20 [AliExpress](https://www.aliexpress.us/w/wholesale-Roborock-Q8-Max-main-brush.html) / [Amazon](https://www.amazon.com/s?k=Roborock+Q8+Max+main+brush) / [eBay](https://www.ebay.com/sch/i.html?_nkw=Roborock+Q8+Max+main+brush) | +| Battery pack + BMS | 1 | $16–30 | OEM BRR-2P4S-5200, 4S2P-MMBK P2150-4S2P-XWDLS; 14.4V Li-ion, ~5200 mAh / 75 Wh in-pack BMS. | Fits Xiaomi Mijia 1/1S/1C/1T/G1, Roborock S4/S5/S5 Max/S6/S6 Pure/MaxV/S7/S7 MaxV, Q5/Q7 Max, T4/T6/T7/T60/T65, Xiaowa E20/E25/E35, C10 [AliExpress](https://www.aliexpress.us/w/wholesale-BRR-2P4S-5200-battery.html) / [Amazon](https://www.amazon.com/s?k=BRR+2P4S+5200+battery) / [eBay](https://www.ebay.com/sch/i.html?_nkw=BRR+2P4S+5200+battery) 4-pin connector B+, B−, NTC, sense (TODO verify). Safety review narrows to: 16.8 V CC/CV charge + NTC temp sense. | +| 2D LiDAR | 1 | $16-26 | PCB mark X-WPFTB-V2.6.2, possibly Camsense | Fits Dreame L10s/Pro/Ultra/Prime, L10 Ultra, L20 Pro/Ultra, L30 Ultra, W10s/Pro, W20, Xiaomi X10+, X30s Pro, X30 Plus/Pro/Ultra, X20 Pro, X40/Pro/Pro Plus, X10/Plus, X20/Plus, S10/Pro/Plus/Ultra, S20+ | +| | | $16-27 | PCB mark X-Wireless board-V1.13.3, possibly Camsense | Fits Dreame W10, F9, D9, D9 Pro/Plus/max, L10 Pro, Z10 Pro [AliExpress](https://www.aliexpress.us/w/wholesale-Dreame-L10s-lds.html) / [Amazon](https://www.amazon.com/s?k=Dreame+L10s+lds) / [eBay](https://www.ebay.com/sch/i.html?_nkw=Dreame+L10s+lds) | +| | | $14-32 | Xiaomi LDS02RR, LDS01RR | Fits Roborock S5/S50/51/55/S6/S7, S502-00/01/02/03, S550-00, S5 Max, S6 MaxV/Pure, S45 Max, S7 Max, Xiaomi Mi 1s [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-s5-lds.html) / [Amazon](https://www.amazon.com/s?k=roborock+s5+lds) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+s5+lds) | +| | | $13-25 | 3irobotix Delta-2A/B/G | Fits Xiaomi Mijia Mop P Pro, Mop 2S, 3C S10, S12 T12 [AliExpress](https://www.aliexpress.us/w/wholesale-xiaomi-mop-lds.html) / [Amazon](https://www.amazon.com/s?k=xiaomi+mop+lds) / [eBay](https://www.ebay.com/sch/i.html?_nkw=xiaomi+mop+lds) | +| | | $13-23 | Possibly LDROBOT LD14P | Fits Dreame X30 X40 Series, Xiaomi Mijia X20 Plus, Mop2 [AliExpress](https://www.aliexpress.us/w/wholesale-dreame-x30-lds.html) / [Amazon](https://www.amazon.com/s?k=dreame+x30+lds) / [eBay](https://www.ebay.com/sch/i.html?_nkw=dreame+x30+lds) | +| Compute Module | 1 | $95 | Raspberry Pi CM4 ≥4 GB | CM4104000 [PiShop](https://www.pishop.us/product/raspberry-pi-compute-module-4-wireless-4gb-lite-cm4104000/) [Newark](https://www.newark.com/raspberry-pi/cm4104000/rpi-compute-module-4-lite-4gb/dp/86AH2101) [DigiKey](https://www.digikey.com/en/products/detail/raspberry-pi/SC0671/13530944) CM4104008 [Newark](https://www.newark.com/raspberry-pi/cm4104008/rpi-module-4-4gb-ram-8gb-emmc/dp/86AH2103) CM4104016 CM4104032 | +| | | $72.50 | Raspberry Pi CM5 ≥4 GB | CM5102000 [PiShop](https://www.pishop.us/product/raspberry-pi-compute-module-5-wireless-4gb-ram-lite-cm5104000/) [Newark](https://www.newark.com/raspberry-pi/cm5104000/som-rpi-compute-mod-5-lite-2gb/dp/20AM3783) [DigiKey](https://www.digikey.com/en/products/detail/raspberry-pi/SC1592/25805568) CM5102016 CM5102032 CM5104064 | +| | | $62.50* | Raspberry Pi CM4 ≥2 GB | *if software fits CM4102000 [PiShop](https://www.pishop.us/product/raspberry-pi-compute-module-4-wireless-2gb-lite-cm4102000/) [Newark](https://www.digikey.com/en/products/detail/raspberry-pi/SC0667/13530921) [DigiKey](https://www.newark.com/raspberry-pi/cm4102000/rpi-compute-module-4-lite-2gb/dp/86AH2093) CM4102008 CM4102016 CM4102032 | +| | | $72.50* | Raspberry Pi CM5 ≥2 GB | *if software fits CM5102000 [PiShop](https://www.pishop.us/product/raspberry-pi-compute-module-5-wireless-2gb-ram-lite-cm5102000/) [Newark](https://www.newark.com/raspberry-pi/cm5102000/som-rpi-compute-mod-5-lite-2gb/dp/20AM3775) [DigiKey](https://www.digikey.com/en/products/detail/raspberry-pi/SC1586/25805584) CM5102016 CM5102032 CM5104064 | +| Cliff sensors | 4 | $1.50-2.50 ea | 4x cliff + 2x bumper w/cables bundle | iRobot Roomba 500 600 700 800 528 552 564 595 560 570 610 615 620 625 630 650 [AliExpress](https://www.aliexpress.us/w/wholesale-irobot-roomba-500-cliff.html) / [Amazon](https://www.amazon.com/s?k=irobot+roomba+500+cliff) / [eBay](https://www.ebay.com/sch/i.html?_nkw=irobot+roomba+500+cliff) | +| Bumper switches | 2 | $0 | Included in cliff sensors bundle | | +| Main brush motor, gearbox | 1 | $7-11 | Requires brush socket adapter | Fits Roborock S5, S50, S51, S52, 55 502-00/01/02/03**S552-00, S6; Xiaowa C10, E20, E25, E35 [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-s5-main-brush-motor.html) / [Amazon](https://www.amazon.com/s?k=roborock+s5+main+brush+motor) / [eBay](https://www.ebay.com/sch/i.html?_nkw=roborock+s5+main+brush+motor) | +| Side brush + motor | 1–2 | 3–8 | fixed (extendable is later) | | +| Mop spin motor(s) + pads | 1–2 | 6–15 | mopping models only | | +| Water pump + valve + tubing | 1 | 4–10 | mopping models only | | +| Mop lift servo | 1 | 2–6 | mopping models only | | +| VL53L7CX multizone ToF | 1 | 8–15 | obstacle detection (90° FoV) | | +| Color camera | 1 | 5–15 | connects to the SBC | | +| Side proximity sensors | 3–4 | 3–8 | | | +| Ultrasonic carpet sensor | 1 | 2–5 | | | +| Speaker + amp, mic, LEDs, buttons | — | 3–8 | | | +| Custom I/O PCB | 1 | 20–40 | STM32 + motor drivers + sensor front-ends | | +| Wiring, connectors, fasteners, magnets, gaskets, filter | — | 12–25 | | | +| Printed parts (filament) | — | 5–15 | you print these yourself | | +| *Robot subtotal (sourced parts)* | | *~$130–270* | excludes SBC | | + +> *Fan sourcing caveat:* the *kPa is the fan's own rating* — verify it against the fan's +> model number / datasheet. The vacuum models are a *sourcing search aid only*: a fan listed +> as "fits vacuum X" is *not* necessarily X's original fan (lower-power replacements are sold as +> compatible for higher-suction models). Omit any model whose known suction contradicts the row. + +## Dock (by tier) + +Three dock tiers share one robot base, released in order: + +| Tier | Adds | Rough extra parts | +|---|---|---| +| Basic charge (first release) | charging only | printed housing + contacts/magnets + wall adapter + IR beacon | +| Auto-empty | dust auto-emptying | dock fan + bin/bag + sealed port | +| Auto-empty + wash + dry | mop wash + hot-air dry | clean + dirty tanks, 2 pumps, heater + fan, own ESP32 + WiFi controller | + +## Sourcing strategy + +- Print geometry, source mechanisms and wear items. See the print-vs-source table in + [docs/design-document.md](docs/design-document.md#2-print-vs-source-strategy). +- Spec wear parts (brushes, filters, wheel modules) in *common, abundant sizes* so + builders can buy cheap universal replacements anywhere. +- Per-module sourcing details will land in the relevant + [contributions/](contributions) RFCs as they mature. + +## How I calculate BoM costs + +Prices for vacuum cleaner aftermarket parts have a wide spread (3x max/min as a ballpark). +You can buy same part for, say, $15 or $30 or even $45. + +Therefore, if you search [AliExpress](https://www.aliexpress.us/w/wholesale-roborock-s5-lidar.html)/eBay/Amazon +for, say, "Roborock S5 LiDAR", you will likely see offers around $30. + +There will probably several pages worth of those offers. And if you dig through those pages, you will probably start +finding parts for $25. + +If you are shopping on AliExpress, open a part listing and check for free coupon offers like "$2.00 off on $18.00". +That brings your $25 price down to $23. Also, check the fine print in red font saying something like "$21.43 each, ≥ 3 pieces". + +That's not the end of it. The next AliExpress trick is to wait for seasonal sales and promotions. Those happen relatively often. +Sales and promotions can bring prices down even futher. + +How can you get to the rock-bottom $15 price? Aftermarket parts prices depend on your search keywords. +Try searching for multiple keyword variations: "Roborock S5 LDS", "Roborock S5 LiDAR", "original laser distance lds", +"LDS02RR", "LDS02RR LiDAR", "Roborock LDS02RR", "LDS laser sensor for xiaomi Roborock" and so on. Be creative and persistent. +Try searching Google "site:aliexpress.com lds02rr" and other variations. Be creative and persistent. + +Combining all these methods is how you can get the rock bottom $15 price. + +I record two prices for each part in the BoM table, for example "$15-$25". +The first ($15) price is the rock bottom one that often requires a minimum purchase of 3 pieces - what I'd (or anyone) be paying putting together an OOMWOO all-parts-included convenience kit. +The second ($25) price is what you can realistically get when purchasing 1 piece, *applying coupons* and doing *a few minutes of search*. + +Why record the first price if requires a 3 pcs minimum? This is because I intend to assemble a convenient parts kit - and make it available to everyone. diff --git a/BUILD_INSTRUCTIONS.md b/BUILD_INSTRUCTIONS.md new file mode 100644 index 0000000..23c97be --- /dev/null +++ b/BUILD_INSTRUCTIONS.md @@ -0,0 +1,31 @@ +# Build Instructions + +> *Not available yet — this is a placeholder.* +> +> OOMWOO is currently at the *design / RFC stage*, not the build-it-yourself stage. +> Step-by-step build instructions will arrive once the first +> [Bill of Materials](BOM.md) and parts are validated (*first BoM targeted ~mid-July*) +> and the modules are proven on real hardware. + +## Where the project is right now + +- *Design + interfaces:* [ARCHITECTURE.md](docs/ARCHITECTURE.md) +- *Parts list (draft):* [BoM.md](BOM.md) +- *Modules being built (RFCs):* [README → Requests for Contributions](README.md#requests-for-contributions) +- *Design decisions + research:* [docs/design-document.md](docs/design-document.md) + +## What you can do now + +- *Star / watch* the repo to follow along — that's the best way to know when build + instructions land. +- *Contribute:* pick a module from the [RFC list](README.md#requests-for-contributions) + and dive in — every skill level welcome. +- *Join the community:* [Discord](https://discord.gg/3y2JKz5T25) and + [GitHub Discussions](https://github.com/makerspet/oomwoo/discussions). +- *Follow the build in public:* [YouTube](https://www.youtube.com/@makerspet). + +## What this page will become + +A complete, zero-to-hero guide: sourcing the parts, printing the chassis, assembling the +robot, flashing the firmware, bringing up ROS2, and first clean — with photos and videos. +We'll make it a clear, standalone guide the moment there's something to build. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..207ea0a --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +
+ +# OOMWOO + +*Open-source robot vacuum you build yourself.* + +Clean well · Hackable · Raspberry Pi · ROS2 · Home Assistant · 2D LiDAR · 3D printed · ESP32 · Arduino + +![License](https://img.shields.io/badge/license-Apache--2.0-blue) +![Status](https://img.shields.io/badge/status-early%20development-orange) + +
+ +## What is this? + +OOMWOO is an *open-source home robot vacuum* you can build yourself, made for the +Raspberry Pi, ROS2, Home Assistant, and 3D-printing communities. It uses an +affordable 2D LiDAR to map your home and navigate on its own. Local, no +cloud required for regular functionality, no vendor lock-in. Follow us building in public +[Discord](https://discord.gg/3y2JKz5T25) | [X](https://x.com/@0OMWO0) | [Instagram](https://www.instagram.com/oomw0o/) | [Facebook](https://www.facebook.com/profile.php?id=61591466775035) | [Reddit](https://www.reddit.com/r/oomwoo/) | [newsletter](https://stats.sender.net/forms/bo2rAK/view) | [YouTube](https://www.youtube.com/@makerspet) | [oomwoo.com](oomwoo.com) | [Tutorials](https://makerspet.com/learn/) + +Reference design images - this is approximately how the finished design will look: + +![Reference robot vacuum cleaner top](./assets/vacuum_model_top.webp) +![Reference robot vacuum cleaner bottom](./assets/vacuum_model_bottom.webp) +![Reference robot vacuum cleaner - top cover removed](https://github.com/makerspet/oomwoo/blob/main/assets/vacuum-no-top-back.webp) + +## Goals + +- Affordable, fully open hardware, software and firmware +- Home appliance product quality - not a throwaway build +- Easy to build, with step-by-step zero-to-hero instructions +- 2D LiDAR mapping and autonomous navigation (ROS2 / Nav2) +- Native Home Assistant integration for local control +- 3D-printable, documented, and hackable chassis +- Buildable from parts you source yourself +- Local, no cloud required for regular functionality +- Optional extra functionality when connected cloud +- Apps on top of ROS2 to customize vacuum operation +- Stretch goal: App store +- Stretch goal: LeRobot integration, OpenClaw + +*v0 target: bare-bones build:* + +- 3D-printed chassis +- ROS2 Gazebo sim +- LiDAR with manual SLAM +- ROS2 on Raspberry Pi 5 AND/OR ESP32 running micro-ROS with ROS2 on local PC - decision TBD + +Open Source Deliverables: + +- [x] [Software development environment](https://github.com/makerspet/oomwoo-install), robot [description package](https://github.com/makerspet/oomwoo-one/) and [tutorials](https://makerspet.com/blog/simulate-oomwoo-one-robot-vacuum-in-gazebo-with-ros-2/) (ROS2) +- [x] Placeholder real [vacuum cleaner](https://github.com/makerspet/proscenic-m6pro) and [tutorials](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/) (temporary while OOMWOO is being designed) +- [ ] [Bill of materials (BoM)](BOM.md) (in progress) +- [ ] 3D-printable files +- [ ] Firmware +- [ ] Motor drivers and sensors [I/O PCB](https://github.com/makerspet/oomwoo-io-board) +- [ ] Build, setup, bringup and troubleshooting [instructions](BUILD_INSTRUCTIONS.md) +- [ ] Demo video(s) + +## Contributing + +Would you like to contribute? See [CONTRIBUTING](docs/CONTRIBUTING.md) for the full guide. + +OOMWOO is organized to built by the community, massively *in parallel*. +The vacuum and its software are subdivided into [modules](#requests-for-contributions), see list below. + +A volunteer picks whatever module she wants and works on it whenever she wants. +For *code and simulation* modules she builds her package in her *own repo* and sends +a short PR *linking* it from the module; for *docs and specs* she contributes files +in-tree under `contributions/module-name/`. See +[CONTRIBUTING](docs/CONTRIBUTING.md) for how this works. + +Multiple developers are welcome to work on the same module. +The best solution for each module surfaces over time, with the project master having the last call. + +1. Pick a contribution from the [list below](#requests-for-contributions). +2. [Let us know](https://github.com/makerspet/oomwoo/discussions) you're working on it and your progress. +3. Check [ARCHITECTURE.md](docs/ARCHITECTURE.md) and + [SOFTWARE_INTERFACES.md](docs/SOFTWARE_INTERFACES.md) for the system design + and ROS2 interfaces. + +## Requests for Contributions + +Every module below is *actionable now* — build it against the Gazebo simulation +([oomwoo-one](https://github.com/makerspet/oomwoo-one)) or a real *placeholder robot* +(a [Proscenic M6 Pro connected to ROS2](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/)), +until OOMWOO hardware is ready. Pick one, tell us in +[Discussions](https://github.com/makerspet/oomwoo/discussions), build it in your own +repo (docs and specs go in-tree), and send a short PR linking it from the module. + +| Module | ID | Status | Notes | +|---|---|---|---| +| ROS2 URDF + Gazebo sim | [urdf-gazebo-sim](./contributions/urdf-gazebo-sim) | In progress | Placeholder URDF + Gazebo sim (reference: [oomwoo-one](https://github.com/makerspet/oomwoo-one); [@alvarosamudio](https://github.com/alvarosamudio/oomwoo_gazebo) featured), refined when hardware lands | +| First clean: coverage + mapping + exploration | [clean-and-map](./contributions/clean-and-map) | Ready to start work | Coverage cleaning while SLAM-mapping and exploring | +| Auto cleaning | | In progress | Clean the entire room using an existing map (using coverage path planning) | +| Regression tests | | In progress | Set up simulatior regression test framework (auto cleaning in Gazebo) | +| Localization & navigation on a known map | [nav-localize](./contributions/nav-localize) | In progress | Nav2 nav, AMCL localization, relocalize when lost, resume map | +| Dock cycle: undock, dock, recharge | [dock-cycle](./contributions/dock-cycle) | Ready to start work | Undock, return-to-dock, precise docking, station services, find dock when lost | +| Recovery behaviors & safety | [recovery-safety](./contributions/recovery-safety) | Ready to start work | Recovery ladder, escalation, pause-and-alert, safety sensors, status reporting | +| Compute benchmark & memory reduction | [compute-benchmark](./contributions/compute-benchmark) | In progress | Measure ROS2/Nav2/SLAM memory, compare composable nodes, and track the 4 GB -> 2 GB target | +| Floor-surface handling & edge cleaning | [floor-care](./contributions/floor-care) | Ready to start work | Wall/edge following, carpet vs hardwood, mop lift/lower | +| Cleaning modes, zones & job orchestration | [cleaning-jobs](./contributions/cleaning-jobs) | Ready to start work | Modes (regular/spot), virtual walls, room segmentation, job splitting + resume | +| Live robot bring-up & validation | [live-robot-bringup](./contributions/live-robot-bringup) | Ready to start work | Connect the placeholder Proscenic M6 Pro to ROS2, re-run sim tests on hardware | +| Source 3D models (STEP) for BOM parts | [source-3d-models](./contributions/source-3d-models) | In progress | Obtain / measure / model STEP files of off-the-shelf parts (wheels, fans, caster…) so mounts fit | +| Procure part specs & datasheets | [part-specs](./contributions/part-specs) | In progress | Find/measure/reverse-engineer specs (pinouts, encoder PPR, torque, how to drive fans…) for sourced parts | +| I/O + motor-driver PCB | [io-pcb](./contributions/io-pcb) | In progress | I/O board with CM4/CM5 socket, STM32G070 MCU - motors, sensors, 4S2P charging, safety, FreeRTOS, custom serial to CM4/CM5, 2D LiDAR header, IMU, audio serial/amp/speaker, MIPI camera(s) i/f; KiCad, JLCPCB | +| Fit software into 2GB RAM | [compute-benchmark](./contributions/compute-benchmark) | In progress | ROS2 node composition, Rust; remove Gazebo, desktop UI | + +> Planned and on-hold modules (mechanical design, later-phase software) live in the +> [RFC backlog](docs/RFC_BACKLOG.md). + +## Source code reference + +- [OOMWOO ROS2 and Ubuntu installation](https://github.com/makerspet/oomwoo-install/) source code +- [OOMWOO ROS2 URDF package and config](https://github.com/makerspet/oomwoo_urdf/) source code +- [remakeai reference vacuum teardown](https://github.com/remakeai/vacuum-cleaner-teardown) — a consumer LiDAR vacuum with a basic dock and stationary mop. + +## Related prior art + +- [AlieksieievYurii/vacuum-cleaner](https://github.com/AlieksieievYurii/vacuum-cleaner) — a DIY 3D-printed robot vacuum (Raspberry + Pi Zero W, gyroscope-based, Fusion 360, Android control app, no dock) +- [kaiaai/LDS](https://github.com/kaiaai/LDS), [kaiaai/lds2d](https://github.com/kaiaai/lds2d) — open-source 2D LiDAR libraries (C++, Python) supporting 23+ LiDAR models +- [remakeai/vacuum_ros2_bridge](https://github.com/remakeai/vacuum_ros2_bridge) — ROS2 bridge for a 3irobotix CRL-200-based vacuum (Proscenic), full ROS2 control +- [Valetudo](https://github.com/Hypfer/Valetudo) — cloud-free firmware replacement for commercial vacuums (local app-level control, not ROS2) +- [Dennis Giese / robotinfo.dev](https://robotinfo.dev) — teardowns and rootability of commercial robot vacuums. +- [codetiger/VacuumTiger](https://github.com/codetiger/VacuumTiger) - 3irobotix CRL-200-based vacuum low-level control reverse engineered +- [Build a ROS2/LiDAR robot crash course](https://makerspet.com/blog/build-arduino-self-driving-robot-video-instructions/) - watch this if you have no robotics experience +- [Open Mower](openmower.de) - open-source outdoor lawn mower + +## Design research + +We reviewed the 2025–2026 consumer robot vacuum landscape (global + China-sourceable +brands, all price tiers) to decide which solutions to copy and which to skip. Key +takeaways for the build: + +- *Suction is a sourcing problem, not an engineering one.* Real-world cleaning does + *not* track advertised suction (Pa); ~$500 mid-tier models beat flagships. A + moderate *sealed* sourced motor + a good brush + tight airflow sealing matches + flagships — *no custom impeller needed.* +- *"Never gets stuck" needs camera + AI sensor fusion*, not LiDAR alone — LiDAR is + blind below its ~10 cm turret (cables, socks). v1 leans on the *bumper* for low + obstacles; vision-based avoidance is a later / experimental goal, not an MVP promise. +- *Anti-tangle brush:* a *tapered rubber roller* resists hair-wrap best (a top user + complaint) and is easy to 3D-print. +- *Mop:* a 3D-printed *dual-spinning* mop is competitive; the self-washing roller + mop's edge is overstated and hard to replicate — skip it for now. + +*Well-loved models worth studying:* Eufy Omni S2 (obstacle avoidance), Narwal Flow +(roller mop), Ecovacs Deebot T90 Pro Omni (~$499 all-rounder), Dreame X40 Ultra +(dual-spinning mop). *Dreame* is also the most [Valetudo](https://github.com/Hypfer/Valetudo)-rootable +brand — the safest donor to study. *(Per-model rankings are directional, from +single-run reviewer tests.)* + +## About + +The project name "OOMWOO" is a rotational ambigram - it reads the same flipped 180°, like the robot itself, roaming your floor in every direction. + +The project is sponsored by makerspet.com and remake.ai. We are reusing their open-source solutions. +- If you'd rather skip the parts hunt, a kit (motors, PCB, brushes, gaskets, LiDAR) will be available at [makerspet.com](https://makerspet.com), from the same maker behind this project. The kit is a convenience, never a requirement. *Everything here stays open.* +- When we get to apps, [remake.ai](https://remake.ai) will be providing its robot apps platform and app store. Using the app store will be entirely optional. The vacuum will *always support cloud-free, local operation for regular functionality out-of-the-box*. + +## License + +Code is released under the [Apache License 2.0](LICENSE). + +Hardware design files, once added, to be released under an open hardware +license (TBD). + + + + + + Star History Chart + + diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..c16c736 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`makerspet/oomwoo` +- 原始仓库:https://github.com/makerspet/oomwoo +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/assets/dust-bin-back.webp b/assets/dust-bin-back.webp new file mode 100644 index 0000000..3ac188d Binary files /dev/null and b/assets/dust-bin-back.webp differ diff --git a/assets/dust-bin-front.webp b/assets/dust-bin-front.webp new file mode 100644 index 0000000..e0a6d63 Binary files /dev/null and b/assets/dust-bin-front.webp differ diff --git a/assets/vacuum-no-dock-front.webp b/assets/vacuum-no-dock-front.webp new file mode 100644 index 0000000..5325c1e Binary files /dev/null and b/assets/vacuum-no-dock-front.webp differ diff --git a/assets/vacuum-no-top-back.webp b/assets/vacuum-no-top-back.webp new file mode 100644 index 0000000..5f6dabd Binary files /dev/null and b/assets/vacuum-no-top-back.webp differ diff --git a/assets/vacuum-no-top-vacuum-fan.webp b/assets/vacuum-no-top-vacuum-fan.webp new file mode 100644 index 0000000..4f523d6 Binary files /dev/null and b/assets/vacuum-no-top-vacuum-fan.webp differ diff --git a/assets/vacuum_model_bottom.webp b/assets/vacuum_model_bottom.webp new file mode 100644 index 0000000..567b9db Binary files /dev/null and b/assets/vacuum_model_bottom.webp differ diff --git a/assets/vacuum_model_top.webp b/assets/vacuum_model_top.webp new file mode 100644 index 0000000..3e6739a Binary files /dev/null and b/assets/vacuum_model_top.webp differ diff --git a/contributions/clean-and-map/README.md b/contributions/clean-and-map/README.md new file mode 100644 index 0000000..17038af --- /dev/null +++ b/contributions/clean-and-map/README.md @@ -0,0 +1,87 @@ +# First Clean: coverage cleaning with mapping and exploration (ROS2 package) + +A ROS2 package for the robot's *first clean*. The vacuum starts with *no map*, +cleans the whole reachable floor using coverage path planning, builds a map with +SLAM *while* it cleans, and keeps exploring until the map is complete. Because the +physical robot isn't built yet, this is a *Gazebo simulation*. + +> *Status — ready to start work.* No need to wait for OOMWOO hardware — develop it in the +> Gazebo sim ([urdf-gazebo-sim](../urdf-gazebo-sim)) or on the real +> [placeholder Proscenic M6 Pro](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/). +> Say so in the [discussions](https://github.com/makerspet/oomwoo/discussions) so we can coordinate. + +> *Scope.* This RFC is only the *first clean from scratch*. Operating on a *saved* map, +> docking, recovery, floor-surface handling, and cleaning modes are deliberately *out of +> scope* and live in their own RFCs: +> [nav-localize](../nav-localize) (navigate / localize / resume a saved map), +> [dock-cycle](../dock-cycle) (undock / dock / recharge), +> [recovery-safety](../recovery-safety) (recovery & safety), +> [floor-care](../floor-care) (wall/edge following, carpet vs hardwood, mop), and +> [cleaning-jobs](../cleaning-jobs) (modes, zones, job orchestration). +> Keep this package focused on producing a *complete map* and *full first-pass coverage*; +> the others build on top of it. + +# Important References +- [urdf-gazebo-sim RFC](../urdf-gazebo-sim) — provides the robot URDF, the Gazebo world(s), and the *bumper* this package depends on. +- [ROS2 software interfaces](../../docs/SOFTWARE_INTERFACES.md) — shared topic/action/service contract for simulation-first modules. +- [m-explore-ros2 (kaiaai fork)](https://github.com/kaiaai/m-explore-ros2) — frontier exploration, tested and working. It maps and explores but does *not* clean — a good starting point to build on. +- [m-explore-ros2 demo + step-by-step instructions (video)](https://www.youtube.com/watch?v=81-9q7QfkHs&list=PLOSXKDW70aR8uA1IFahSKVuk5ODDfjTZV) — shows m-explore-ros2 in action and how to run it. +- [Gazebo simulation setup instructions](https://makerspet.com/blog/tutorial-map-navigate-ros2-robot-in-simulation/) — a simple differential-drive robot with a LiDAR in a Gazebo living room world; another possible starting point. +- [OOMWOO ROS2 development](https://github.com/makerspet/oomwoo-install) — build OOMWOO ROS2 Docker image(s) with your packages. +- [Project discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- [Discord server](https://discord.gg/3y2JKz5T25) + +# Request for Contribution - Instructions + +- reproduce the baseline simulation first + - follow the [Gazebo simulation setup instructions](https://makerspet.com/blog/tutorial-map-navigate-ros2-robot-in-simulation/) to run the diff-drive + LiDAR robot in the Living Room world + - get [m-explore-ros2](https://github.com/kaiaai/m-explore-ros2) running for frontier exploration (see the [demo video](https://www.youtube.com/watch?v=81-9q7QfkHs&list=PLOSXKDW70aR8uA1IFahSKVuk5ODDfjTZV)) — this gives you mapping + exploration *without* cleaning + - build on the [urdf-gazebo-sim RFC](../urdf-gazebo-sim) robot model, world(s) and bumper + - post in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) to let everyone know you're working on it, and post your progress +- add coverage cleaning + - the robot starts with *no prior map* + - plan and execute a *coverage path* that cleans the whole reachable floor (e.g. boustrophedon / back-and-forth, plus corner and wall-edge handling) + - build the map with *SLAM while cleaning* (e.g. slam_toolbox or cartographer) — mapping and cleaning happen together, not in separate passes + - keep *exploring frontiers* until the map is complete, so no reachable area is missed + - define and document a clear *done* condition (full coverage *and* complete map), then save the map +- make it robust + - *dynamic obstacles:* a person, pet, or object moving into the robot's path must not break coverage or mapping — the robot should replan and continue + - *LiDAR-invisible static obstacles:* the LiDAR may report open floor the robot cannot actually reach (glass, objects below the LiDAR plane, thresholds, ledges). Detect these via *bumper* contact, mark them as obstacles, recover, and replan around them + - react to *bumper events* (left switch, right switch, front bumper) published by the urdf-gazebo-sim bumper + - never get permanently stuck — recover from collisions and wedged situations (a basic local recovery here; the full recovery ladder lives in [recovery-safety](../recovery-safety)) +- test it well + - start the robot from *various initial locations* and verify it still achieves full coverage and a complete map + - add *regression tests* (headless, CI-friendly) that verify both: + - *map completeness* — the built map covers the whole reachable area + - *coverage completeness* — the cleaning path covers the whole reachable floor + - test recovery from dynamic obstacles and from bumper hits on LiDAR-invisible obstacles +- additional Gazebo worlds (highly valued) + - create and test extra worlds: *multiple rooms, different floorplans*, narrow passages, furniture, and LiDAR-invisible obstacles + - contribute these worlds back — they help everyone test +- submit a PR (pull request) to `contributions/clean-and-map//` + - link to ROS2 package(s) + - instructions, documentation - how to install, run, configure, troubleshoot, test results + - the Gazebo worlds you added + - videos of full clean-and-map runs from several start poses + - announce your submission in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- iterate with review +- TBD, expect the RFC to evolve + +## Acceptance criteria + +Objective, measurable. Examples: +- Starting with no map in the Living Room world, from *multiple initial poses*, the robot: + - achieves *full coverage* of the reachable floor + - builds a *complete map* of the reachable area + - detects a clear done condition and saves the map +- Robust to *dynamic obstacles* — a moving obstacle in the path does not break coverage or mapping +- Robust to *LiDAR-invisible static obstacles* — bumper contacts are detected, the obstacle is marked and avoided, the robot replans and does not get stuck +- Reacts correctly to *left / right / front bumper* events +- *Regression tests* pass and verify both map completeness and coverage completeness, runnable headless in CI +- Works in at least one *additional multi-room / different floorplan* world +- Documented and reliably reproducible by someone else +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple +attempts are welcome and useful even if not selected — modules are swappable, and +a non-selected design is still a valid learning exercise and a fallback. diff --git a/contributions/cleaning-jobs/README.md b/contributions/cleaning-jobs/README.md new file mode 100644 index 0000000..c406cdd --- /dev/null +++ b/contributions/cleaning-jobs/README.md @@ -0,0 +1,71 @@ +# Cleaning Modes, Zones & Job Orchestration (ROS2 package) + +The user-facing *"what and where to clean,"* plus managing *long jobs*. This +package adds cleaning *modes* (regular whole-floor, spot), *virtual walls / +no-go zones*, *segmenting the map into rooms*, and a *job orchestrator* that +splits a clean into manageable pieces — pausing to *recharge, auto-empty, or wash +the mop*, then *resuming where it left off*. Because the physical robot isn't +built yet, this is a *Gazebo simulation*; it is later re-validated on hardware in +the [live-robot-bringup RFC](../live-robot-bringup). + +> *Status — ready to start work.* No need to wait for OOMWOO hardware — develop it in the +> Gazebo sim ([urdf-gazebo-sim](../urdf-gazebo-sim)) or on the real +> [placeholder Proscenic M6 Pro](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/). +> Say so in the [discussions](https://github.com/makerspet/oomwoo/discussions) so we can coordinate. + +# Important References +- [clean-and-map RFC](../clean-and-map) — coverage cleaning and its done condition; this orchestrates and segments it. +- [nav-localize RFC](../nav-localize) — the saved map this segments and the resume-after-interruption support. +- [dock-cycle RFC](../dock-cycle) — the recharge / auto-empty / mop-wash station services a job pauses for. +- [ROS2 software interfaces](../../docs/SOFTWARE_INTERFACES.md) — shared topic/action/service contract for simulation-first modules. +- [OOMWOO ROS2 development](https://github.com/makerspet/oomwoo-install) — build OOMWOO ROS2 Docker image(s) with your packages. +- Nav2 keepout/zone costmap filters are a good starting point for no-go zones. +- [Project discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- [Discord server](https://discord.gg/3y2JKz5T25) + +# Request for Contribution - Instructions + +- *map segmentation into rooms / zones* + - split a saved map into rooms/zones (automatic segmentation plus manual labeling) and persist it + - post in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) to let everyone know you're working on it, and post your progress +- *cleaning modes* + - *regular* (whole map, or selected rooms), *spot* (clean a small local area), configurable + - defer wall/edge mode to [floor-care](../floor-care); call it where useful +- *virtual walls / no-go zones* + - define keep-out regions the planner respects (e.g. Nav2 keepout filter); editable and persisted +- *job orchestration (split a clean into manageable pieces)* + - monitor battery / dust bin / mop state during a clean + - *suspend* the job and dock for *recharge / auto-empty / mop-wash* (via [dock-cycle](../dock-cycle)), then *resume coverage exactly where it stopped* (coverage memory; ties to [clean-and-map](../clean-and-map) + [nav-localize](../nav-localize) resume) + - guarantee the full job still reaches full coverage across one or more interruptions +- *job interface* + - an action/service API to *start / pause / resume / cancel* a job and report *status*, suitable for Home Assistant +- test it well + - multi-room worlds; verify whole-map, per-room, and spot jobs; verify no-go zones are respected + - force a recharge / auto-empty / mop-wash mid-job and verify the job resumes to *full coverage* +- regression tests (headless, CI-friendly) + - per-room / spot / whole-map jobs clean exactly the intended area + - no-go zones are never entered + - *coverage completeness is preserved across forced interruptions* +- submit a PR (pull request) to `contributions/cleaning-jobs//` + - link to ROS2 package(s) + - instructions, documentation - how to install, run, configure, troubleshoot, test results + - videos of segmented/spot cleaning and a job interrupted by a dock service then resumed + - announce your submission in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- iterate with review +- TBD, expect the RFC to evolve + +## Acceptance criteria + +Objective, measurable. Examples: +- *Modes* work: whole-map, per-room, and spot cleaning each clean exactly the intended area +- *Virtual walls / no-go zones* are defined, persisted, and never entered +- The map is *segmented into rooms* that can be targeted individually +- A long job *splits into pieces* — suspends for recharge / auto-empty / mop-wash and *resumes to full coverage* +- A *start / pause / resume / cancel / status* interface exists and is Home-Assistant-friendly +- *Regression tests* pass, including coverage-preserved-across-interruptions, runnable headless in CI +- Documented and reliably reproducible by someone else +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple +attempts are welcome and useful even if not selected — modules are swappable, and +a non-selected design is still a valid learning exercise and a fallback. diff --git a/contributions/compute-benchmark/README.md b/contributions/compute-benchmark/README.md new file mode 100644 index 0000000..444641c --- /dev/null +++ b/contributions/compute-benchmark/README.md @@ -0,0 +1,86 @@ +# Compute Benchmark & Memory Reduction + +Measure whether OOMWOO can keep ROS2/Nav2/SLAM onboard while reducing the +minimum compute target from a comfortable 4 GB class system toward a practical +2 GB class system. + +This module is for repeatable measurements, not guesses. It should help compare +Python/C++ ROS2 nodes, ROS2 composable-node layouts where supported, process +layout changes, and later optional Rust experiments under the same workload. + +> Status: in progress. Use the ROS2 Jazzy development container and the current +> simulation stack where possible. Hardware runs on Pi 4, CM4, CM5, or compatible +> modules are especially useful when available. + +## Current Working Assumptions + +- The consumer vacuum profile keeps SLAM and navigation onboard. +- The practical near-term target is Pi 4 / CM4-class compute with 4 GB RAM. +- The stretch target is to reduce the minimum memory requirement toward 2 GB + without giving up ROS2. +- The compute-module direction is CM4/CM5 or compatible modules on a carrier + board; ignore the older RK3562 reference path. +- The MCU owns motors, sensors, safety, battery/charging supervision, watchdogs, + and the custom serial protocol. +- STM32G070RBT6 is a strong MCU candidate because of its GPIO/ADC count, low + cost, and LQFP manufacturability, while the MCU choice remains reviewable. +- The MVP 2D LiDAR target is 5 Hz with no scan dropping. +- Rust/rclrs is an optional late-summer integration candidate, not a baseline + dependency today. + +Context: this module follows the compute discussion in +[issue #18](https://github.com/makerspet/oomwoo/issues/18). + +## Request For Contribution + +Submit a benchmark contribution under: + +```text +contributions/compute-benchmark// +``` + +Include: + +- a reproducible benchmark plan +- scripts that capture RSS/PSS/CPU/startup timing where possible +- a hardware/software run matrix +- a compute BOM table with regional price and stock fields +- clear notes on ROS distro, RMW, hardware, RAM size, LiDAR rate, and scenario +- a short decision record after measurements + +## Metrics + +Minimum useful metrics: + +| Metric | Why it matters | +|---|---| +| RSS/PSS per process | Shows where memory is actually going. | +| CPU idle / mapping / navigation | Separates always-on cost from workload cost. | +| Startup time | Exposes heavy node initialization and launch overhead. | +| LiDAR update rate | Confirms the benchmark is not cheating by dropping scans. | +| Nav2/SLAM headroom | Determines whether 2 GB is plausible with ROS2 retained. | +| Recovery-event latency | Keeps safety-adjacent nodes honest under optimization. | + +## Strategies To Compare + +1. Current Python/C++ baseline. +2. ROS2 composable nodes where supported, plus launch/process layout changes. +3. C++/rclcpp for selected hot or always-on custom nodes. +4. Optional Rust/rclrs spike for selected custom nodes after dev-image setup is + reproducible. + +Rust should only move from optional to recommended if it demonstrates a useful +RSS/PSS or latency improvement and does not make the default Jazzy developer +experience fragile. + +## Acceptance Criteria + +- Measurements are reproducible by another contributor. +- The benchmark records hardware, RAM, ROS distro, RMW, scenario, and git SHA. +- Results distinguish process RSS from PSS when PSS is available. +- Benchmarks do not reduce LiDAR update rate below the target unless explicitly + marked as an experiment. +- The contribution explains whether the result supports 4 GB only, 2 GB as a + stretch target, or a different hardware class. +- BOM notes include module/board, carrier, power, cooling, storage, MCU add-ons, + regional price, and stock status. diff --git a/contributions/compute-benchmark/xbattlax/README.md b/contributions/compute-benchmark/xbattlax/README.md new file mode 100644 index 0000000..e8eea2f --- /dev/null +++ b/contributions/compute-benchmark/xbattlax/README.md @@ -0,0 +1,100 @@ +# Compute Benchmark Plan by xbattlax + +This contribution turns the compute discussion in issue #18 into a concrete +benchmark scaffold for OOMWOO. + +The immediate goal is to measure whether OOMWOO can keep onboard ROS2, Nav2, and +SLAM while reducing the practical memory target from 4 GB toward 2 GB. The first +optimization path should be ROS2 composable nodes where supported, plus +launch/process layout. Rust with rclrs is kept as a serious optional spike for +late August / September 2026, gated by measurements and dev-image +reproducibility. + +## Scope + +Included: + +- a Linux `/proc`-based process sampler for RSS/PSS/CPU +- a run matrix template for repeatable benchmark scenarios +- a compute BOM template for board/module, carrier, power, cooling, storage, + MCU add-ons, regional pricing, and stock status +- a short architecture decision record for the current memory-reduction plan + +Not included yet: + +- a Rust/rclrs node implementation +- a C++/rclcpp equivalent node +- physical Pi 4 / CM4 / CM5 results +- automated launch files for full SLAM/Nav2 runs + +## Hardware Profiles To Record + +| Profile | Purpose | Notes | +|---|---|---| +| Dev machine | Reference only | Useful for repeatability, not a robot target. | +| Pi 4 4 GB | Minimum prior-art target | Validate onboard SLAM/Nav2 feasibility. | +| CM4 4 GB | Consumer carrier-board target | Low profile and product-friendly. | +| CM5 4 GB+ | Higher headroom target | Useful if camera/NPU alternatives are evaluated. | +| 2 GB class SBC/module | Stretch target | Requires real measured headroom, not assumptions. | +| ESP32 educational profile | Offboard ROS2/SLAM learning setup | Not a consumer autonomous vacuum profile. | + +## Measurement Scenarios + +Start with these scenarios: + +1. ROS2 graph idle after launch. +2. SLAM running with 5 Hz LiDAR input and no scan dropping. +3. Nav2 navigating on a known map. +4. Recovery/safety node idle. +5. Recovery/safety event burst. +6. Same workload after composable-node or process-layout changes. +7. Later: same selected custom node in Python, C++/rclcpp, and Rust/rclrs. + +## Using The Sampler + +Run this inside the Linux ROS2 environment while the target workload is already +running: + +```bash +bash contributions/compute-benchmark/xbattlax/scripts/measure_ros_processes.sh \ + --pattern 'ros2|component_container|python3|slam_toolbox|nav2' \ + --duration 60 \ + --interval 2 \ + --label slam_5hz_baseline \ + --output /tmp/oomwoo-slam-5hz-baseline.csv +``` + +The script writes CSV with: + +- timestamp +- sample index +- label +- PID +- process name +- CPU percent +- RSS KiB +- PSS KiB when `/proc//smaps_rollup` is available +- command line + +PSS is usually more useful than RSS for ROS2 systems because shared libraries and +middleware pages can make RSS look worse than the actual proportional memory +pressure. + +## Templates + +- `templates/run_matrix.csv`: planned benchmark runs and environment details. +- `templates/compute_bom.csv`: compute BOM and regional stock/price snapshot. + +Copy the templates into a results folder for real benchmark submissions. Do not +edit old result rows after a run; add a new row for a new measurement. + +## Expected Decision Output + +Each benchmark batch should end with a short note: + +- current measured minimum RAM class +- biggest memory users +- whether composable nodes helped +- whether any Python node is worth porting to C++ or Rust +- whether Rust/rclrs setup is reproducible enough to keep testing +- what hardware profile the result supports diff --git a/contributions/compute-benchmark/xbattlax/docs/adr-0001-memory-reduction-strategy.md b/contributions/compute-benchmark/xbattlax/docs/adr-0001-memory-reduction-strategy.md new file mode 100644 index 0000000..ea75944 --- /dev/null +++ b/contributions/compute-benchmark/xbattlax/docs/adr-0001-memory-reduction-strategy.md @@ -0,0 +1,58 @@ +# ADR 0001: Memory-Reduction Strategy + +## Status + +Proposed. + +## Context + +OOMWOO should remain approachable for builders, so the first consumer vacuum +profile should run SLAM and navigation onboard. Requiring a separate workstation +for mapping would make the regular product harder to build and use. + +The maintainer has clarified these current assumptions: + +- Pi 4 / CM4-class compute with 4 GB RAM is a realistic near-term baseline. +- Reducing toward 2 GB while keeping ROS2 would be valuable. +- CM4/CM5 or compatible modules are the compute-module direction. +- The older RK3562 reference schematic should be ignored for new compute-module + planning. +- The MCU role is fixed around motors, sensors, safety, battery/charging + control, watchdogs, and custom serial protocol. +- ESP32/micro-ROS is better suited to an educational/offboard profile than to + the consumer safety/controller role. + +## Decision + +Use this priority order for memory-reduction work: + +1. Measure the current Python/C++ ROS2 baseline. +2. Try ROS2 composable nodes where supported, plus launch/process layout + improvements first. +3. Consider C++/rclcpp for selected memory-heavy or latency-sensitive custom + nodes when composable layout is insufficient. +4. Keep Rust/rclrs as an optional late-summer 2026 spike for selected custom + nodes, not as a baseline dependency today. + +Rust/rclrs should be evaluated seriously, but only promoted if: + +- RSS/PSS or latency wins are meaningful against Python and C++ baselines +- the Jazzy build/setup is reproducible in the OOMWOO dev image +- contributor onboarding remains reasonable +- the node selected for porting is small enough to keep the experiment bounded + +## Consequences + +- The first benchmark contribution can avoid adding Rust dependencies. +- The benchmark still leaves a clear path for a later rclrs experiment. +- The project can pursue the 4 GB -> 2 GB target with lower dependency risk. +- Results should identify whether memory pressure comes from custom Python + nodes, ROS2 process layout, Nav2/SLAM, or other runtime overhead. + +## Open Questions + +- Which launch graph should be the canonical benchmark workload? +- Which ROS2 RMW should be the baseline for memory measurements? +- Which node is the best first Rust/rclrs candidate if measurements justify it? +- How should PSS be collected on target hardware when permissions differ? +- What minimum headroom should qualify a 2 GB target as realistic? diff --git a/contributions/compute-benchmark/xbattlax/scripts/measure_ros_processes.sh b/contributions/compute-benchmark/xbattlax/scripts/measure_ros_processes.sh new file mode 100755 index 0000000..9488732 --- /dev/null +++ b/contributions/compute-benchmark/xbattlax/scripts/measure_ros_processes.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + measure_ros_processes.sh --pattern REGEX [options] + +Options: + --pattern REGEX Process command-line regex to sample. Required. + --duration SECONDS Total sample duration. Default: 30. + --interval SECONDS Seconds between samples. Default: 1. + --label LABEL Free-form label written to each row. Default: run. + --output FILE CSV output path. Default: stdout. + --help Show this help. + +Examples: + bash measure_ros_processes.sh \ + --pattern 'ros2|component_container|python3|slam_toolbox|nav2' \ + --duration 60 \ + --interval 2 \ + --label slam_5hz_baseline \ + --output /tmp/oomwoo-slam-5hz-baseline.csv + +Notes: + This script is intended for Linux ROS2 targets. It reads /proc for RSS and + PSS. PSS is blank when /proc//smaps_rollup is not readable. +EOF +} + +pattern="" +duration="30" +interval="1" +label="run" +output="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --pattern) + pattern="${2:-}" + shift 2 + ;; + --duration) + duration="${2:-}" + shift 2 + ;; + --interval) + interval="${2:-}" + shift 2 + ;; + --label) + label="${2:-}" + shift 2 + ;; + --output) + output="${2:-}" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "$pattern" ]]; then + echo "--pattern is required" >&2 + usage >&2 + exit 2 +fi + +if [[ ! -d /proc ]]; then + echo "This sampler requires Linux /proc." >&2 + exit 1 +fi + +if ! [[ "$duration" =~ ^[0-9]+$ ]] || [[ "$duration" -lt 1 ]]; then + echo "--duration must be a positive integer" >&2 + exit 2 +fi + +if ! [[ "$interval" =~ ^[0-9]+$ ]] || [[ "$interval" -lt 1 ]]; then + echo "--interval must be a positive integer" >&2 + exit 2 +fi + +csv_escape() { + local value="${1:-}" + value="${value//$'\n'/ }" + value="${value//$'\r'/ }" + value="${value//\"/\"\"}" + printf '"%s"' "$value" +} + +read_status_kib() { + local pid="$1" + local key="$2" + awk -v key="$key" '$1 == key ":" { print $2; found=1; exit } END { if (!found) print "" }' "/proc/$pid/status" 2>/dev/null || true +} + +read_pss_kib() { + local pid="$1" + if [[ -r "/proc/$pid/smaps_rollup" ]]; then + awk '$1 == "Pss:" { print $2; found=1; exit } END { if (!found) print "" }' "/proc/$pid/smaps_rollup" 2>/dev/null || true + fi +} + +read_cmdline() { + local pid="$1" + if [[ -r "/proc/$pid/cmdline" ]]; then + tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | sed 's/[[:space:]]*$//' + fi +} + +read_comm() { + local pid="$1" + if [[ -r "/proc/$pid/comm" ]]; then + tr -d '\n' < "/proc/$pid/comm" 2>/dev/null + fi +} + +read_cpu_percent() { + local pid="$1" + ps -p "$pid" -o %cpu= 2>/dev/null | awk '{ print $1 }' +} + +emit_header() { + printf 'timestamp_utc,sample_index,label,pid,comm,cpu_percent,rss_kib,pss_kib,cmdline\n' +} + +emit_row() { + local timestamp="$1" + local sample_index="$2" + local pid="$3" + local comm="$4" + local cpu_percent="$5" + local rss_kib="$6" + local pss_kib="$7" + local cmdline="$8" + + printf '%s,%s,' "$timestamp" "$sample_index" + csv_escape "$label" + printf ',%s,' "$pid" + csv_escape "$comm" + printf ',%s,%s,%s,' "$cpu_percent" "$rss_kib" "$pss_kib" + csv_escape "$cmdline" + printf '\n' +} + +run_sampler() { + local sample_index=0 + local started="$SECONDS" + + emit_header + + while (( SECONDS - started <= duration )); do + local timestamp + timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + + mapfile -t pids < <( + for proc in /proc/[0-9]*; do + pid="${proc##*/}" + cmdline="$(read_cmdline "$pid")" + if [[ -n "$cmdline" && "$cmdline" =~ $pattern ]]; then + printf '%s\n' "$pid" + fi + done + ) + + if [[ "${#pids[@]}" -eq 0 ]]; then + emit_row "$timestamp" "$sample_index" "" "no_process_match" "" "" "" "" + else + local pid + for pid in "${pids[@]}"; do + if [[ ! -d "/proc/$pid" ]]; then + continue + fi + emit_row \ + "$timestamp" \ + "$sample_index" \ + "$pid" \ + "$(read_comm "$pid")" \ + "$(read_cpu_percent "$pid")" \ + "$(read_status_kib "$pid" VmRSS)" \ + "$(read_pss_kib "$pid")" \ + "$(read_cmdline "$pid")" + done + fi + + sample_index=$((sample_index + 1)) + if (( SECONDS - started >= duration )); then + break + fi + sleep "$interval" + done +} + +if [[ -n "$output" ]]; then + mkdir -p "$(dirname "$output")" + run_sampler > "$output" +else + run_sampler +fi diff --git a/contributions/compute-benchmark/xbattlax/templates/compute_bom.csv b/contributions/compute-benchmark/xbattlax/templates/compute_bom.csv new file mode 100644 index 0000000..9746a1a --- /dev/null +++ b/contributions/compute-benchmark/xbattlax/templates/compute_bom.csv @@ -0,0 +1,7 @@ +hardware_profile,role,part,sku_or_module,quantity,unit_price,currency,regional_source_url,stock_status,power_notes,cooling_notes,storage_or_carrier_notes,notes +CM4_4GB,compute,Compute Module 4,,,,,,,,,, +CM5_4GB,compute,Compute Module 5,,,,,,,,,, +Pi4_4GB,compute,Raspberry Pi 4 4GB,,,,,,,,,, +TwoGB_stretch,compute,2GB SBC or module,,,,,,,,,, +Consumer_MCU,safety_controller,STM32G070RBT6,,,,,,,,,, +Educational_ESP32,micro_ros_bridge,ESP32 board,,,,,,,,,, diff --git a/contributions/compute-benchmark/xbattlax/templates/run_matrix.csv b/contributions/compute-benchmark/xbattlax/templates/run_matrix.csv new file mode 100644 index 0000000..c99e8b0 --- /dev/null +++ b/contributions/compute-benchmark/xbattlax/templates/run_matrix.csv @@ -0,0 +1,8 @@ +run_id,date_utc,git_sha,hardware_profile,ram_gb,ros_distro,rmw,scenario,node_strategy,lidar_hz,scan_dropping,command_or_launch,expected_duration_s,notes +baseline_idle,,,,,jazzy,,ros_graph_idle,python_cpp_baseline,5,no,,60, +slam_5hz,,,,,jazzy,,slam_mapping,python_cpp_baseline,5,no,,300, +nav_known_map,,,,,jazzy,,nav2_known_map,python_cpp_baseline,5,no,,300, +recovery_idle,,,,,jazzy,,recovery_safety_idle,python_cpp_baseline,5,no,,60, +recovery_burst,,,,,jazzy,,recovery_safety_event_burst,python_cpp_baseline,5,no,,60, +composable_idle,,,,,jazzy,,ros_graph_idle,composable_nodes,5,no,,60, +rust_spike_late_summer,,,,,jazzy,,selected_custom_node,rust_rclrs_optional,5,no,,60, diff --git a/contributions/dock-cycle/README.md b/contributions/dock-cycle/README.md new file mode 100644 index 0000000..0f50991 --- /dev/null +++ b/contributions/dock-cycle/README.md @@ -0,0 +1,73 @@ +# Dock Cycle: undock, dock, recharge & station services (ROS2 package) + +Everything around the charging/service *dock*. The robot must *undock* at the +start of a job, *return to the dock* (on completion or on low battery), *dock +precisely* to make charge contact, trigger *dock station services* (recharge, +and — where the dock supports them — auto-empty and mop-wash), and *find the dock +when it is lost* and relocalization has already failed. Because the physical robot +isn't built yet, this is a *Gazebo simulation*; it is later re-validated on +hardware in the [live-robot-bringup RFC](../live-robot-bringup). + +> *Status — ready to start work.* No need to wait for OOMWOO hardware — develop it in the +> Gazebo sim ([urdf-gazebo-sim](../urdf-gazebo-sim)) or on the real +> [placeholder Proscenic M6 Pro](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/). +> Say so in the [discussions](https://github.com/makerspet/oomwoo/discussions) so we can coordinate. + +# Important References +- [nav-localize RFC](../nav-localize) — localization + Nav2; the find-the-dock fallback is invoked when its relocalization fails. +- [clean-and-map RFC](../clean-and-map) and [cleaning-jobs RFC](../cleaning-jobs) — trigger return-to-dock for recharge / auto-empty / mop-wash mid-job. +- [urdf-gazebo-sim RFC](../urdf-gazebo-sim) — robot URDF and Gazebo world(s) to model the dock in. +- [ROS2 software interfaces](../../docs/SOFTWARE_INTERFACES.md) — shared topic/action/service contract for simulation-first modules. +- Model a *generic basic charging dock* (charge contacts + a detectable marker). Exact dock geometry is TBD — the old teardown reference vacuum is no longer used. +- [OOMWOO ROS2 development](https://github.com/makerspet/oomwoo-install) — build OOMWOO ROS2 Docker image(s) with your packages. +- Nav2 docking (`opennav_docking`) is a good starting point for precise approach. +- [Project discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- [Discord server](https://discord.gg/3y2JKz5T25) + +# Request for Contribution - Instructions + +- model the dock in Gazebo + - add charge contacts and a *detectable marker* for the final approach (IR beacon, fiducial/AprilTag, or a reflective-intensity pattern the LiDAR can see) + - model a *battery* (drain during cleaning, charge curve while docked) so low-battery behavior can be tested + - model a *generic basic dock* (contacts + a detectable marker); exact geometry TBD + - post in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) to let everyone know you're working on it, and post your progress +- undocking + - safely back / drive out of the dock to a known start pose, then hand off to cleaning +- return-to-dock + - navigate to the dock vicinity, triggered by *job complete* OR *low battery / full bin / mop needs washing* mid-job (coordinate with [cleaning-jobs](../cleaning-jobs)) +- precise docking + - final approach using the dock marker; align to the charge contacts; *confirm charging started* +- dock station services + - once docked, trigger and wait for *recharge*, and — where the dock supports them — *auto-empty* and *mop-wash*; report completion so the job can resume +- find-the-dock-when-lost + - fallback used when [nav-localize](../nav-localize) relocalization has *failed*: run a search pattern to reacquire the dock marker, dock, and re-establish pose from the known dock location +- test it well + - start from *various poses and battery levels*; verify undock → clean → return → dock → service → resume + - *kidnap* the robot so relocalization fails and verify find-the-dock recovers it +- regression tests (headless, CI-friendly) + - docking success rate and final alignment error + - find-the-dock success rate from random lost poses + - low-battery return-then-resume completes without losing the job +- submit a PR (pull request) to `contributions/dock-cycle//` + - link to ROS2 package(s) and the dock model / world additions + - instructions, documentation - how to install, run, configure, troubleshoot, test results + - videos of docking, find-the-dock, and low-battery return-and-resume + - announce your submission in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- iterate with review +- TBD, expect the RFC to evolve + +## Acceptance criteria + +Objective, measurable. Examples: +- Robot *undocks* reliably to a known start pose +- Robot *returns and docks precisely*, making charge contact, from various poses (alignment within a stated tolerance) +- *Station services* (recharge; auto-empty / mop-wash where modeled) are triggered, completed, and reported +- *Low-battery mid-job* triggers return-to-dock and the job resumes after charging +- *Find-the-dock-when-lost* recovers a kidnapped robot after relocalization fails +- *Regression tests* pass (dock success, alignment error, find-the-dock success), runnable headless in CI +- Documented and reliably reproducible by someone else +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple +attempts are welcome and useful even if not selected — modules are swappable, and +a non-selected design is still a valid learning exercise and a fallback. diff --git a/contributions/dust-bin/README.md b/contributions/dust-bin/README.md new file mode 100644 index 0000000..bda0af0 --- /dev/null +++ b/contributions/dust-bin/README.md @@ -0,0 +1,33 @@ +# Dust Bin (mechanical module) + +> *On hold.* The project moved *away from the old teardown reference vacuum*, so the +> dust bin must be designed around the *sourced parts + a forthcoming 3D reference design* +> (not yet sketched). This module resumes once the key parts are sourced and the design is +> sketched. *Useful now:* [source-3d-models](../source-3d-models) and [part-specs](../part-specs). + +A removable dust bin that collects dust, holds an air filter, and receives air from the +vacuum fan. The bin inlet mates with a gasket; the lid latch is spring-loaded. + +# Request for Contribution — Instructions (resumes when off hold) + +When this reopens, design a 3D-printable removable dust bin that: +- mates (sealed) with the *vacuum-fan inlet* and the chassis +- holds a *common, replaceable filter* — standardise on an abundant filter size so builders + can rebuy anywhere (see [design research](../../README.md#design-research)) +- has a reliable *spring latch*; easy to remove, insert, and empty +- is 3D-printable (assume PETG); split to fit ~20 × 25 cm, 20 cm height +- ignore mop functionality for now +- submit STEP + native CAD + 3MF/STL + a sub-component BoM + docs/photos to + `contributions/dust-bin//` + +## Acceptance criteria (when it resumes) + +- *Sealed* airflow (no leaks), easy to empty, reliable latch +- Fits the (forthcoming) chassis + fan interfaces without forcing changes to other modules +- Uses a common, sourceable filter size +- Documented; STEP + native CAD source provided +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple attempts +are welcome and useful even if not selected — modules are swappable, and a non-selected +design is still a valid learning exercise and a fallback. diff --git a/contributions/floor-care/README.md b/contributions/floor-care/README.md new file mode 100644 index 0000000..aa5376e --- /dev/null +++ b/contributions/floor-care/README.md @@ -0,0 +1,69 @@ +# Floor-Surface Handling & Edge Cleaning (ROS2 package) + +Clean *better* by adapting to the floor. This package adds *wall following* (to +clean tight against walls), *carpet-edge following*, recognizing *carpet vs +hardwood*, and *lifting / lowering the mop* accordingly (never wet a carpet). +Because the physical robot isn't built yet, this is a *Gazebo simulation* — you +model the surfaces and the surface sensor in Gazebo; it is later re-validated on +hardware in the [live-robot-bringup RFC](../live-robot-bringup). + +> *Status — ready to start work.* No need to wait for OOMWOO hardware — develop it in the +> Gazebo sim ([urdf-gazebo-sim](../urdf-gazebo-sim)) or on the real +> [placeholder Proscenic M6 Pro](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/). +> Say so in the [discussions](https://github.com/makerspet/oomwoo/discussions) so we can coordinate. + +# Important References +- [clean-and-map RFC](../clean-and-map) — coverage cleaning that this refines at edges and surface transitions. +- [urdf-gazebo-sim RFC](../urdf-gazebo-sim) — robot URDF; this package likely needs a *surface sensor* and a *mop lift/lower actuator* modeled. +- [ROS2 software interfaces](../../docs/SOFTWARE_INTERFACES.md) — shared topic/action/service contract for simulation-first modules. +- [OOMWOO ROS2 development](https://github.com/makerspet/oomwoo-install) — build OOMWOO ROS2 Docker image(s) with your packages. +- [Project discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- [Discord server](https://discord.gg/3y2JKz5T25) + +> Note: this package may need new sim sensors/actuators (surface sensor, mop +> actuator). Add them inside your submission for now and propose folding the +> stable ones back into [urdf-gazebo-sim](../urdf-gazebo-sim) later — please don't +> rewrite that RFC. + +# Request for Contribution - Instructions + +- *wall following* + - detect walls from the LiDAR and drive a controlled offset along them so edges get cleaned, not just the room interior + - integrate with [clean-and-map](../clean-and-map) coverage so edges *and* interior are both covered, without re-cleaning everything + - post in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) to let everyone know you're working on it, and post your progress +- *carpet-edge following* + - detect carpet boundaries and follow the edge to clean along them +- *surface recognition (carpet vs hardwood)* + - model a plausible sensor in sim (wheel current / IMU vibration / a dedicated downward sensor) and *publish the surface type* +- *mop lift / lower* + - actuate the mop *up on carpet, down on hardwood*; model the actuator in Gazebo; *never wet a carpet* + - coordinate with [cleaning-jobs](../cleaning-jobs) for mop-related job constraints +- test it well + - build worlds with *mixed carpet + hardwood* regions and walls + - verify edge coverage, correct surface classification, and correct mop state at every transition +- regression tests (headless, CI-friendly) + - surface-classification accuracy + - edge-coverage percentage (walls / carpet edges actually cleaned) + - *zero carpet-wetting events* (mop never down on carpet) +- submit a PR (pull request) to `contributions/floor-care//` + - link to ROS2 package(s) and any sim sensor/actuator + world additions + - instructions, documentation - how to install, run, configure, troubleshoot, test results + - videos of wall/edge following and mop lift/lower at surface transitions + - announce your submission in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- iterate with review +- TBD, expect the RFC to evolve + +## Acceptance criteria + +Objective, measurable. Examples: +- *Wall / carpet-edge following* cleans tight along edges and integrates with coverage (high edge-coverage %) +- *Surface recognition* classifies carpet vs hardwood accurately +- *Mop lift/lower* tracks surface type correctly with *zero carpet-wetting events* +- *Regression tests* pass (classification accuracy, edge coverage, no carpet-wetting), runnable headless in CI +- Works in at least one *mixed carpet + hardwood* world +- Documented and reliably reproducible by someone else +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple +attempts are welcome and useful even if not selected — modules are swappable, and +a non-selected design is still a valid learning exercise and a fallback. diff --git a/contributions/io-pcb/README.md b/contributions/io-pcb/README.md new file mode 100644 index 0000000..cff2649 --- /dev/null +++ b/contributions/io-pcb/README.md @@ -0,0 +1,153 @@ +# I/O + Motor-Driver PCB (hardware / KiCad) + +The custom board that connects every OOMWOO motor and sensor to the SBC: an STM32 MCU, +motor drivers, sensor front-ends, and battery charging on one PCB. The MCU runs firmware / +micro-ROS and talks to the Raspberry Pi 5 SBC over a serial / USB link. + +> *Design basis:* OOMWOO v1 uses a *separate Raspberry Pi 5* as the SBC (ROS2, Nav2, SLAM), +> so this board is a *pure I/O + power board* — no application processor on it. There is a +> starting-point reference schematic (an RK3562 + STM32 combined design), but the on-board +> Rockchip SoC and its whole subsystem are *removed*; only the STM32 I/O side is kept and +> converted to KiCad for review. + +# References + +- *Starting-point schematic (PDF)* — + [makerspet/oomwoo-io-board](https://github.com/makerspet/oomwoo-io-board/blob/main/oomwoo-io-board-RK3562-schematic.pdf). + An RK3562 + STM32G070 reference (Apache-2.0, *unvalidated* — a starting point, not a proven + design). Trim it down as described below. +- *Drive-wheel connector pinout* — + [AlieksieievYurii vacuum-cleaner motherboard schematic](https://raw.githubusercontent.com/AlieksieievYurii/vacuum-cleaner/2bd7cf7f9af3ae9040373f667bab83e2e57c26b7/motherboard/circuit-pcb/SCHEMATIC_motherboard.svg) + shows the drive-wheel assembly connectors: *JST PH2.0, 6-pin* — + + | Pin | Signal | + |---|---| + | 1 | MOT+ | + | 2 | MOT- | + | 3 | HALL_SPEED | + | 4 | HALL_DIR | + | 5 | +5V | + | 6 | GND | + + Verify against the *sourced* Roborock-family wheel module before layout — a submission in + [part-specs](../part-specs) describes a variant with extra wheel-drop / limit-switch pins, + so confirm the pin count and pinout on a physical module. +- [part-specs](../part-specs) — connector pinouts, encoder PPR, and datasheets for the sourced parts. +- [design-document.md](../../docs/design-document.md) — the I/O-board section (MCU, pin budget, + offloading the fan to an external ESC, etc.). +- [BOM.md](../../BOM.md) — the sourced parts this board must drive. +- [Project discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) · [Discord](https://discord.gg/3y2JKz5T25) + +# Request for Contribution — Instructions + +Deliver a *KiCad schematic* derived from the reference PDF, trimmed to the I/O side and +updated for OOMWOO, then *hold for review before PCB layout*. + +- *Remove the Rockchip subsystem entirely* — the RK3562 SoC and everything that exists only + to support it: *LPDDR4 DRAM, eMMC, the SoC PMIC / SoC-specific power rails (VCCIO, PMU / OSC + / PLL), the DDR PHY, the USB / PCIe PHY, and the MIPI camera interface*. Heavy compute and + the camera live on the Raspberry Pi 5 SBC, not this board. +- *Remove the WiFi / BT module* (AP6256) — WiFi/BT is provided by the SBC. +- *Keep and convert to KiCad the I/O side:* + - *STM32G070* MCU + support (clock, decoupling, debug / boot, the serial / USB link to the SBC) + - *motor drivers* — drive wheels (×2), main brush, side brush, water pump, mop lift / spin + (if fitted); put the *suction fan on an external ESC* (one PWM line) to save MCU pins + - *sensor front-ends* — cliff / anti-fall IR, docking IR + bumper, side-proximity IR, bumper + switches, IMU, LiDAR interface, multizone ToF, ultrasonic carpet sensor + - *battery charging + protection / BMS*, and the board power rails that feed the STM32 / + sensors / motors (keep these; remove only the SoC-specific rails) + - speaker + amp, mic, buttons, LEDs +- *Move the battery from 3S to 4S* — OOMWOO targets *~14.8 V* (see [BOM.md](../../BOM.md)). The + reference appears to be 3S; update the pack, the charge-IC configuration, the protection, and + any cell-count-dependent dividers / thresholds to *4S*. +- *Wire the drive-wheel connectors* to the JST PH2.0 6-pin pinout above (verified against the + sourced module). +- assume battery Xiaomi/Roborock/Dreame BRR-2P4S-5200 +- *Convert the kept design to KiCad* (from Altium); keep a clean, readable, hierarchical schematic. +- *Hold here for review.* Deliver the trimmed, 4S, KiCad *schematic* and stop — the maintainer + reviews before anyone starts PCB layout / manufacturing. +- *Submit* a PR to `contributions/io-pcb//` with the KiCad project, a short + sub-BoM, and notes; announce it in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=). +- iterate with review +- TBD, expect the RFC to evolve + +## Acceptance criteria (schematic-hold milestone) + +- The *entire Rockchip subsystem* (SoC, DRAM, eMMC, PMIC / VCCIO / PMU / PLL, DDR + USB/PCIe + PHY, MIPI camera) and the *WiFi / BT module* are removed. +- The *kept blocks* (STM32G070, motor drivers, sensor front-ends, battery charging, audio, + buttons / LEDs, SBC link) are present, correct, and complete in *KiCad*. +- Battery as specified +- Drive-wheel connectors match the referenced pinout (JST PH2.0 6-pin), reconciled with part-specs. +- Delivered as a buildable *KiCad project*; *ERC clean*; a sub-BoM and short design notes included. +- Stops at the reviewed *schematic* — no PCB layout yet. +- Documented and reproducible. +- TBD, expect criteria to evolve. + +The maintainer intends to *accelerate* this module and may commission a contributor to do it; +community submissions are still welcome and reviewed the same way. The maintainer selects among +compliant candidates using these criteria — multiple attempts are welcome and useful even if +not selected. + +## Appendix A. Tentative MCU GPIO list + +1. Power source current sense (analog in) +2. VBat sense (analog in) +3. Main fan sense (analog in) +4. anti-fall left up sensor (analog in because IR sensors are analog) +5. anti-fall left down sensor (analog in) +6. anti-fall right up sensor (analog in) +7. anti-fall right down sensor (analog in) +8. wheel motor left driver in1 (digital output) +9. wheel motor left driver in2 (digital output) +10. wheel motor left driver encoder (digital input) +11. wheel motor right driver encoder (digital input) +12. Power button (digital input) +13. CPU (e.g. Raspberry Pi) power on/off (digital output) +14. STM32 SWDIO +15. STM32 SWCLK +16. Vacuum power on/off (digital output) +17. Wheel motor right current sense (analog in) +18. Wheel motor left current sense (analog in) +19. Main brush motor current sense (analog in) +20. IMU SPI SCLK (digital out) +21. IMU SPI MISO +22. IMU SPI MOSI +23. IMU SPI CS +24. Wheel motor right driver in1 (digital out) +25. Motors power enable (digital out) +26. Wheel motor right driver in2 (digital out) +27. Water pump sense (analog in) +28. Side brush left front motor sense (analog in) +29. Side brush right front motor sense (analog in) +30. CPU reset (e.g. Raspberry Pi) +31. Dock IR sensor 1 (analog in) +32. Dock IR sensor 2 (analog in) +33. Water pump motor PWM (digital out) +34. Main brush motor PWM (digital out) +35. Lidar motor PWM (digital out) +36. Bumper switch 1 (digital in) +37. UART1 TX +38. UART RX +39. Side brush motor right PWM (digital out) +40. Side brush motor left PWM (digital out) +41. Power LED on/off (digital out) +42. Home LED on/off (digital out) +43. Home button (digital in) +44. Battery charge sense (digital in) +45. Charge status (digital out) +46. Bumper switch 1 (digital in) +47. Bumper switch 2 (digital in) +48. Test/program +49. Test/program +50. Main fan motor PWM (digital out) +51. Main fan motor current sense (analog in) +52. IMU interrupt 2 (digital in) +53. IMU interrupt 1 (digital in) +54. IMU FSYNC (digital in) +55. Side proximity IR sensor left (analog in) +56. Side proximity IR sensor right (analog in) +57. Side proximity IR LED left PWM (digital out) +58. Side proximity IR LED right PWM (digital out) +59. Wheel drop sensor left (digital in) +60. Wheel drop sensor right (digital in) diff --git a/contributions/live-robot-bringup/README.md b/contributions/live-robot-bringup/README.md new file mode 100644 index 0000000..8bc78e4 --- /dev/null +++ b/contributions/live-robot-bringup/README.md @@ -0,0 +1,65 @@ +# Live Robot Bring-up & Validation (ROS2 integration) + +Take the behaviors validated in *simulation* and run them on a *real robot +vacuum*. Connect an off-the-shelf vacuum to ROS2 following the Proscenic M6 Pro +tutorial, then *re-run the acceptance tests from the simulation RFCs on +hardware*. This is the *single home for live validation* across all behaviors, +so the many sim contributors aren't blocked on hardware and the bring-up / bridge +cost is paid once. + +> *Note:* the Proscenic / 3irobotix CRL-200S here is an *interim real-hardware test mule* +> for the ROS2 software stack — it is *not* the OOMWOO hardware design (which is built from +> sourced Roborock/Dreame/Xiaomi parts). This RFC eventually re-runs the same tests on real +> OOMWOO hardware. + +> *Status — ready to start work.* The bring-up itself is doable now — connect the +> [placeholder Proscenic M6 Pro to ROS2](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/) +> with the [bridge](https://github.com/remakeai/vacuum_ros2_bridge). Re-running each behavior's +> acceptance tests follows as those behaviors land, but doing the bring-up / bridge now unblocks +> everyone else's hardware testing. + +# Important References +- [Connect a robot vacuum (Proscenic M6 Pro) to ROS2 — tutorial](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/) — the primary how-to for getting a real vacuum onto ROS2. +- [remakeai/vacuum_ros2_bridge](https://github.com/remakeai/vacuum_ros2_bridge) — ROS2 bridge for a 3irobotix CRL-200-based vacuum (Proscenic), full ROS2 control. +- [codetiger/VacuumTiger](https://github.com/codetiger/VacuumTiger) — 3irobotix CRL-200-based low-level control, reverse engineered. +- The behavior RFCs whose tests you re-run on hardware: [clean-and-map](../clean-and-map), [nav-localize](../nav-localize), [dock-cycle](../dock-cycle), [recovery-safety](../recovery-safety), [floor-care](../floor-care), [cleaning-jobs](../cleaning-jobs). +- [ROS2 software interfaces](../../docs/SOFTWARE_INTERFACES.md) — shared topic/action/service contract that hardware bring-up should validate. +- [OOMWOO ROS2 development](https://github.com/makerspet/oomwoo-install) — build OOMWOO ROS2 Docker image(s) with your packages. +- [Project discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- [Discord server](https://discord.gg/3y2JKz5T25) + +# Request for Contribution - Instructions + +- connect a real vacuum to ROS2 + - follow the [tutorial](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/) on a Proscenic M6 Pro or compatible 3irobotix CRL-200-based vacuum + - bring up the real sensors and actuators on ROS2: LiDAR, odometry, motors, bumper, battery, dock signals + - post in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) to let everyone know you're working on it, and post your progress +- match the simulation interfaces + - map the sim topics / actions to the real robot so the behavior packages run *unchanged* wherever possible; document every gap + - bring-up checklist: teleop, sensor sanity checks, e-stop verified before any autonomous run +- re-run the sim acceptance tests on hardware + - run each behavior's acceptance tests on the real robot: [clean-and-map](../clean-and-map), [nav-localize](../nav-localize), [dock-cycle](../dock-cycle), [recovery-safety](../recovery-safety), [floor-care](../floor-care) (as the hardware allows), [cleaning-jobs](../cleaning-jobs) + - record real-world results; document *sim-to-real gaps*, hardware-specific issues, and tuning changes +- be explicit about coverage + - if a behavior *can't* be tested on a given robot (e.g. no mop or no auto-empty dock), *log it* — do not silently skip it +- submit a PR (pull request) to `contributions/live-robot-bringup//` + - link to the bring-up / bridge package(s) and config + - instructions, documentation - exact hardware, how to connect, run, troubleshoot + - test results per behavior, videos, logs, and a sim-to-real notes write-up + - announce your submission in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- iterate with review +- TBD, expect the RFC to evolve + +## Acceptance criteria + +Objective, measurable. Examples: +- A real vacuum is *connected to ROS2* with LiDAR, odometry, motors, bumper, battery, and dock signals working +- The sim interfaces are matched so behavior packages run on hardware with minimal changes; gaps are documented +- Each simulated behavior is *reproduced on the real robot* — or there is a *documented reason* it couldn't be (no silent skips) +- Real-world test results, videos, and a *sim-to-real gap* write-up are included +- Reproducible by someone else with the same hardware +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple +attempts are welcome and useful even if not selected — modules are swappable, and +a non-selected design is still a valid learning exercise and a fallback. diff --git a/contributions/nav-localize/README.md b/contributions/nav-localize/README.md new file mode 100644 index 0000000..31e0e76 --- /dev/null +++ b/contributions/nav-localize/README.md @@ -0,0 +1,71 @@ +# Localization & Navigation on a Known Map (ROS2 package) + +Once the [first clean](../clean-and-map) has produced a map, the robot must be able +to *operate on that saved map*: localize itself, navigate to any goal with Nav2, +recover its pose when it gets *lost* or is picked up and moved (the *kidnapped +robot* problem), and *resume an unfinished map*. Because the physical robot isn't +built yet, this is a *Gazebo simulation*; it is later re-validated on hardware in +the [live-robot-bringup RFC](../live-robot-bringup). + +> *Status — ready to start work.* No need to wait for OOMWOO hardware — develop it in the +> Gazebo sim ([urdf-gazebo-sim](../urdf-gazebo-sim)) or on the real +> [placeholder Proscenic M6 Pro](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/). +> Say so in the [discussions](https://github.com/makerspet/oomwoo/discussions) so we can coordinate. + +# Important References +- [clean-and-map RFC](../clean-and-map) — produces the saved/partial map this package consumes. +- [urdf-gazebo-sim RFC](../urdf-gazebo-sim) — robot URDF, Gazebo world(s), bumper. +- [ROS2 software interfaces](../../docs/SOFTWARE_INTERFACES.md) — shared topic/action/service contract for simulation-first modules. +- [Gazebo + Nav2 simulation tutorial](https://makerspet.com/blog/tutorial-map-navigate-ros2-robot-in-simulation/) — baseline diff-drive + LiDAR robot in a Gazebo world. +- [OOMWOO ROS2 development](https://github.com/makerspet/oomwoo-install) — build OOMWOO ROS2 Docker image(s) with your packages. +- Nav2 (navigation), AMCL (localization), and slam_toolbox (localization mode / serialized session continue) are the expected building blocks. +- [Project discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- [Discord server](https://discord.gg/3y2JKz5T25) + +# Request for Contribution - Instructions + +- reproduce the baseline first + - load a map saved by [clean-and-map](../clean-and-map) and bring up Nav2 + a localizer (AMCL or slam_toolbox localization mode) on the [urdf-gazebo-sim](../urdf-gazebo-sim) robot + - post in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) to let everyone know you're working on it, and post your progress +- localization on a known map + - *global initial localization at startup* without being given a pose (e.g. AMCL global init / scan-matching) — the robot figures out where it is on the saved map + - track pose reliably during navigation; expose a localization-confidence signal (covariance / scan-match score) +- navigation + - Nav2 navigate-to-pose and navigate-through-poses to arbitrary goals on the saved map + - obey dynamic obstacles via the local costmap +- lost / kidnapped recovery + - *detect* when localization confidence drops (low score, high covariance, or a detected pickup/kidnap) + - *relocalize*: rotate in place and/or drive to gather scans until the pose re-converges + - define a clear *"relocalized" success condition*, and what happens if it *fails* — hand off to the [dock-cycle](../dock-cycle) *find-the-dock-when-lost* fallback +- resume an unfinished map + - load a *partial / serialized SLAM session* (e.g. slam_toolbox serialization) and *continue mapping where it left off*, merging newly seen areas into the existing map without corrupting it +- test it well + - start from *many initial poses*, including a wrong or unknown initial pose + - *kidnap* the robot mid-run (teleport it in sim) and verify it recovers + - resume from several partial maps and verify the merged map is correct +- regression tests (headless, CI-friendly) + - relocalization success rate from random poses + - navigation success rate to random reachable goals + - map-resume correctness (resumed + continued map matches a from-scratch map of the same world) +- submit a PR (pull request) to `contributions/nav-localize//` + - link to ROS2 package(s) + - instructions, documentation - how to install, run, configure, troubleshoot, test results + - videos of relocalization-when-lost and map-resume runs + - announce your submission in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- iterate with review +- TBD, expect the RFC to evolve + +## Acceptance criteria + +Objective, measurable. Examples: +- On a saved map, from an *unknown initial pose*, the robot performs global localization and converges to the correct pose +- Nav2 navigation reaches arbitrary reachable goals reliably, avoiding dynamic obstacles +- When *lost / kidnapped*, the robot detects it, relocalizes, and resumes — or cleanly hands off to the find-the-dock fallback when relocalization fails +- An *unfinished map* can be loaded and mapping continued, producing a complete, uncorrupted map +- *Regression tests* pass and verify relocalization, navigation, and map-resume, runnable headless in CI +- Documented and reliably reproducible by someone else +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple +attempts are welcome and useful even if not selected — modules are swappable, and +a non-selected design is still a valid learning exercise and a fallback. diff --git a/contributions/part-specs/OsakaTX/README.md b/contributions/part-specs/OsakaTX/README.md new file mode 100644 index 0000000..b0b23b1 --- /dev/null +++ b/contributions/part-specs/OsakaTX/README.md @@ -0,0 +1,345 @@ +# Part Specs — Compiled Datasheets & Specifications + +> **Contributor:** OsakaTX +> **Status:** Initial compilation — datasheets and specifications found via online research +> **Methodology:** Web research from manufacturer datasheets, public SDKs, open-source reverse-engineering projects, and Aliexpress listings + +This document compiles the electrical and mechanical specifications found for key BOM +parts. Each section covers what was found and what is still missing. + +--- + +## 1. Drive Wheel Assembly (Roborock S-family) + +### Part Identification + +The Roborock S4/S5/S5-Max/S6/S7-family drive wheel module is a complete unit: +gearmotor + incremental encoder + rubber tire + suspension + wheel-drop switch. +See BOM.md for the full Aliexpress search links. + +### Motor Specs (Nidec 20N704RC70 — "Motor for Wheel") + +Sourced from the [Nidec Robot Cleaner Motor catalog PDF](https://file.elecfans.com/web1/M00/CC/89/o4YBAF-ZOBKAQBvyADDMAglsvTw020.pdf): + +| Parameter | Value | +|---|---| +| **Rated voltage** | 14.4 V | +| **No-load speed** | 16,800 rpm | +| **No-load current** | 0.4 A | +| **Rated speed** | 13,200 rpm | +| **Rated current** | 1.4 A | +| **Maximum power** | 13.8 W | +| **Noise (SPL)** | 45 dB(A) | +| **Status** | ⚠️ "In development" per Nidec catalog — model may have changed in production | + +**Note:** The actual motor used in production Roborock wheels may differ from the +catalog entry above. The wheel module includes a **gearbox** (ratio not published) +that reduces this to the final wheel RPM. The encoder is mounted after the gearbox. + +### Wheel Dimensions (from open PR #10 — alvarosamudio's URDF) + +| Dimension | Value | +|---|---| +| **Wheel diameter** | 0.065 m (65 mm) | +| **Robot body diameter** | 0.33 m (330 mm) | + +### Encoder + +- **Type:** Incremental quadrature encoder (magnetic or optical, likely magnetic for dust resistance) +- **Known fact:** Connected to GD32F103 MCU timer inputs (quadrature decoder mode) +- **PPR:** ❔ **NOT FOUND** — needs physical disassembly and measurement or reverse-engineering +- The GD32F103VCT6 has hardware timers capable of quadrature decoding +- The 16-pin J25/J26 connectors carry encoder + cliff + bumper + misc signals + +### Connector (Wheel Module to Main Board) + +- **Connector type:** J25 (left wheel) / J26 (right wheel) — 16-pin, 1mm pitch SHD (shielded) connector +- **Known pins:** 2-4 pins for encoder A/B channels, 2 pins for wheel-drop sensor, remaining for other sensors +- **Full pinout:** ❔ **NOT CONFIRMED** — needs PCB tracing or oscilloscope probing + +### Wheel-Drop Sensor + +- **Model:** ❔ **NOT FOUND** — likely a microswitch or Hall-effect sensor within the wheel suspension assembly +- **Pinout:** ❔ **NOT CONFIRMED** + +### What We Still Need + +| Item | Status | +|---|---| +| Encoder type (magnetic vs optical) | ❔ Unknown | +| Encoder PPR (pulses per revolution) | ❔ Unknown — **critical for odometry** | +| Gearbox ratio | ❔ Unknown | +| Wheel module connector full pinout | ❔ Unknown | +| Wheel-drop sensor model + pinout | ❔ Unknown | +| Cable length | ❔ Unknown | +| Module weight | ❔ Unknown | +| Actual motor model in the production module | ❔ Needs teardown verification | + +### References + +- [Nidec Robot Cleaner Motor Product Introduction PDF](https://file.elecfans.com/web1/M00/CC/89/o4YBAF-ZOBKAQBvyADDMAglsvTw020.pdf) +- [codetiger/VacuumRobot — Mainboard Component Diagram](https://github.com/codetiger/VacuumRobot/blob/main/Research/Motherboard/Component_Diagram.md) +- [codetiger/VacuumRobot — Connection Evidence](https://github.com/codetiger/VacuumRobot/blob/main/Research/Motherboard/Connection_Evidence.md) +- [codetiger/VacuumTiger — Custom firmware implementing the protocol](https://github.com/codetiger/VacuumTiger) + +--- + +## 2. Suction Fan / Blower + +### Nidec 20N-series Blower Units (full assemblies) + +From the [Nidec catalog PDF](https://file.elecfans.com/web1/M00/CC/89/o4YBAF-ZOBKAQBvyADDMAglsvTw020.pdf): + +#### Standard Blower Units + +| Parameter | 20N183L010 A | 20N704R310 / R500 | 20N704S310 | 22N709Q140 | +|---|---|---|---|---| +| Rated voltage | 12 V | 14.4 V | 14.4 V | 14.4 V | +| Rated speed | 15,000 rpm | 17,200 rpm | 15,100 rpm | 22,500 rpm | +| Rated current | 1.7 A | 2.2 A | 1.6 A | 4.5 A | +| Input power | 20 W | 31.7 W | 23 W | 64.8 W | +| Max static pressure | 1.8 kPa | 2.6 kPa | 3.0 kPa | 4.0 kPa | +| Max air volume | 0.63 m³/min | 0.78 m³/min | 0.63 m³/min | 0.99 m³/min | +| Noise | 67 dB(A) | 70 dB(A) | 68 dB(A) | 75 dB(A) | +| Fixing device | With | With | Without | Without | + +#### Nidec BLDC Bare Motors (for Blower — without impeller housing) + +| Parameter | 20N704K500 B | 20N704P110 A | 22N709P230 D | 35N048P010 | +|---|---|---|---|---| +| Rated voltage | 12 V | 13 V | 14.4 V | 18 V | +| No-load speed | 16,500 rpm | 23,000 rpm | 29,000 rpm | 28,800 rpm | +| No-load current | 0.4 A | 0.6 A | 0.6 A | 0.8 A | +| Rated speed | 13,500 rpm | 16,700 rpm | 24,800 rpm | 25,900 rpm | +| Rated current | 1.4 A | 1.75 A | 2.8 A | 7.1 A | +| Max power | 15.9 W | 21.2 W | 58.4 W | 74.6 W | +| Noise | 40 dB(A) | 50 dB(A) | 65 dB(A) | 60 dB(A) | + +### Fan Control Interface (BLDC bare motors) + +From the catalog, the Nidec Smart_20N series BLDC motors support: + +| Feature | Details | +|---|---| +| **Control** | PWM input for speed control | +| **Feedback** | FG (frequency generator / tachometer) output | +| **Special** | 22N709P230 D additionally has SS (soft-start) | +| **Drive** | 3-phase BLDC — requires external driver/controller | +| **Voltage range** | 5–24 V (Smart_20N series datasheet) | +| **Mass** | 25–30 g (bare motor) | + +### Driving the Fans + +- The open-source [ripinteer/fan_protector](https://github.com/ripinteer/fan_protector) project is a reference for driving/protecting these BLDC blowers +- The [jniebuhr/roborock-pcb](https://github.com/jniebuhr/roborock-pcb) project has a custom PCB for driving Roborock fans (Nidec-based), including connector pinout +- Connector on the Roborock CPAP board: JST XH 3-pin 2.54mm for motor, JST XH 2-pin 2.54mm for fan + +### What We Still Need + +| Item | Status | +|---|---| +| Full suction-assembly-level datasheet (including impeller housing) | ❔ Missing (we have bare motor specs) | +| Specific connector model + pinout for each assembly variant | ❔ Unknown | +| Cable length(s) | ❔ Unknown | +| Signal waveforms (PWM, FG) | ❔ Unknown | +| Weight of full assembly | ❔ Unknown | + +### References + +- [Nidec Robot Cleaner Motor PD](https://file.elecfans.com/web1/M00/CC/89/o4YBAF-ZOBKAQBvyADDMAglsvTw020.pdf) +- [Nidec Smart_20N series product page](https://www.nidec.com/en/product/search/category/B101/M102/S100/NCJ-20N-Type-3/) +- [jniebuhr/roborock-pcb — custom PCB with pinout](https://github.com/jniebuhr/roborock-pcb) +- [ripinteer/fan_protector — BLDC driver reference](https://github.com/ripinteer/fan_protector) + +--- + +## 3. LiDAR — 3irobotix CRL-200S / Delta-2D + +### Basic Specs + +| Parameter | Value | +|---|---| +| **Model** | 3irobotix CRL-200S (same platform as Delta-2D) | +| **Type** | Laser triangulation (not ToF) | +| **Range** | 0.13–8 m @ 100% reflectivity | +| **Accuracy** | < 1% @ 5m | +| **Scan rate** | 4–10 Hz (configurable via motor voltage) | +| **Points/sec** | 2–5 KHz | +| **Wavelength** | 780 nm | +| **Laser class** | Class 1 | +| **Max ambient** | 1K lux | +| **Weight** | ~175 g | +| **Retail** | ~$28–40 | +| **Life** | Estimated 1,500+ hours | +| **Power** | 0.35A idle, 0.37A ranging @ 5V (~1.75 W) + motor ~0.1A | + +### Interface + +| Parameter | Value | +|---|---| +| **Connector** | JST PH 2.0mm 5-pin | +| **Data** | UART 115200 baud, 8N1 | +| **Protocol** | Proprietary 3irobotix — 8-byte header + variable payload | +| **Commands** | 0xAE (health), 0xAD (measurement data) | +| **Distance multiplier** | 0.25 mm per LSB | + +#### Pinout (J17 on 3irobotix CRL-200S mainboard) + +| Pin | Function | Direction | Notes | +|---|---|---|---| +| 1 | Motor+ | Power | 5V for rotation | +| 2 | Motor- | Power | GND | +| 3 | TX | Output | LiDAR → MCU (UART RX) | +| 4 | RX | Input | MCU → LiDAR (UART TX) | +| 5 | GND | Power | Ground | + +### Motor Control + +| Parameter | Value | +|---|---| +| **Motor voltage range** | 2.2–3.8 V (for valid ranging) | +| **RPM range** | ~240–420 RPM | +| **Motor drive** | Direct voltage control (external) or H-bridge / PWM | +| **Ranging fails below** | ~2.2 V motor (below 240 RPM) | +| **Ranging fails above** | ~4.0 V motor (above 460 RPM) | +| **Angular resolution** | 0.77° at 240 RPM → 1.41° at 420 RPM | + +### Data Format (from notblackmagic.com reverse engineering) + +Start byte `0xAA`, followed by: +- 2-byte packet type +- Data length +- Angle + distance pairs (each 2 bytes) +- Checksum byte + +### References + +- [kaiaai/awesome-2d-lidars — comparison table & pinout](https://github.com/kaiaai/awesome-2d-lidars) +- [kaiaai/LDS — Arduino LiDAR library supporting CRL-200S / Delta-2D](https://github.com/kaiaai/LDS) +- [notblackmagic.com — full reverse engineering of Delta-2G (same protocol)](https://notblackmagic.com/bitsnpieces/lidar-modules/) +- [codetiger/VacuumRobot — LiDAR protocol decoding](https://github.com/codetiger/VacuumRobot) +- [3irobotix Delta-2B SDK](https://github.com/CWRU-AutonomousVehiclesLab/Delta-2B-Lidar-SDK) +- Delta-1A protocol doc (shared with 2A/2B/2G/2D): see notblackmagic.com above + +--- + +## 4. VL53L7CX — Multizone Time-of-Flight Sensor + +### Basic Specs + +| Parameter | Value | +|---|---| +| **Type** | Time-of-F Flight, 8×8 multizone (64 zones) | +| **Field of View** | 90° diagonal (65° × 65° typical) | +| **Range** | Up to 350 cm (varies by target reflectivity and ambient) | +| **I²C address** | 0x52 (default) — configurable | +| **I²C speed** | Up to 1 MHz (fast mode+) | +| **Supply** | 2.8 V (AVDD), 1.8 V (IOVDD) or 2.8 V (optional) | +| **Package** | 4.4 × 2.4 × 1.0 mm (LGA-12) | + +### Pinout + +| Pin | Name | Function | +|---|---|---| +| 1 | AVDD | Power (2.8 V) | +| 2 | GND | Ground | +| 3 | GPIO1 | Interrupt output (programmable) | +| 4 | LPn | Low power mode control | +| 5 | I2C_RST | I²C interface reset (active high) | +| 6 | SCL | I²C clock | +| 7 | SDA | I²C data | +| 8 | IOVDD | I/O voltage (1.8 V or 2.8 V) | + +### Interface + +- **I²C** up to 1 MHz +- Requires external pull-up resistors on SCL/SDA +- Interrupt pin (GPIO1) for data-ready signaling +- LPn pin for low-power mode control + +### References + +- [ST VL53L7CX Datasheet](https://www.st.com/resource/en/datasheet/vl53l7cx.pdf) +- [STM32duino VL53L7CX Arduino library](https://github.com/stm32duino/VL53L7CX) +- [UM3038 — User guide for using VL53L7CX](https://www.pololu.com/file/0J1993/um3038-a-guide-to-using-the-vl53l7cx-timeofflight-multizone-ranging-sensor-with-90-fov-stmicroelectronics.pdf) + +--- + +## 5. IMU — MPU-6050 (common choice, architecture TBD) + +### Basic Specs + +| Parameter | Value | +|---|---| +| **Type** | 6-axis (3-axis gyro + 3-axis accelerometer) | +| **Gyro range** | ±250, ±500, ±1000, ±2000 °/s (programmable) | +| **Accel range** | ±2g, ±4g, ±8g, ±16g (programmable) | +| **I²C address** | 0x68 (AD0 low) or 0x69 (AD0 high) | +| **I²C speed** | Up to 400 kHz | +| **Supply** | 2.375–3.46 V | +| **Package** | 4×4×0.9 mm QFN-24 | + +### Pinout (QFN-24) + +| Pin | Name | Function | +|---|---|---| +| 8 | SCL | I²C clock | +| 9 | SDA | I²C data | +| 12 | AD0 | I²C address select | +| 20 | INT | Interrupt output | +| 6 | VDD | Power (2.4–3.5 V) | +| 18 | VLOGIC | Logic reference (1.8V±5% or VDD) | +| 7, 10, 13, 19, 21 | GND | Ground | + +### References + +- [MPU-6000/6050 Register Map & Descriptions](https://cdn.sparkfun.com/datasheets/Sensors/Accelerometers/RM-MPU-6000A.pdf) +- [MPU-6050 Product Specification v3.4](https://www.cdiweb.com/datasheets/invensense/mpu-6050_datasheet_v3%204.pdf) + +--- + +## 6. IR Cliff / Proximity Sensors — TCRT5000 (typical) + +### Basic Specs + +| Parameter | Value | +|---|---| +| **Type** | Reflective optical sensor (IR LED + phototransistor) | +| **Operating voltage** | LED: 1.2–1.5 V forward (typical), Phototransistor: up to 30 V | +| **Detection range** | 0.2–15 mm (for reliable cliff detection) | +| **Output** | Phototransistor collector (open collector — needs pull-up resistor) | +| **Package** | 4-pin DIP (standard leaded package) | + +### Pinout + +| Pin | Name | Function | +|---|---|---| +| 1 | A (Anode) | IR LED anode | +| 2 | C (Cathode) | IR LED cathode | +| 3 | C (Collector) | Phototransistor collector (output) | +| 4 | E (Emitter) | Phototransistor emitter (GND) | + +### Typical Circuit + +- IR LED: driven through a current-limiting resistor (e.g., 100–220 Ω at 3.3–5 V) +- Phototransistor: collector pulled up to MCU voltage (3.3 V) via a 10–47 kΩ resistor +- Output read as analog voltage (ADC) or digital threshold (comparator) + +### References + +- [TCRT5000 datasheet (components101)](https://components101.com/sensors/tcrt5000-ir-sensor-pinout-datasheet) +- [TCRT5000 guide (Utmel)](https://www.utmel.com/components/tcrt5000-ir-sensor-datasheet-pinout-and-circuit?id=697) + +--- + +## Summary of Found vs Missing + +| Part | Documentation Found | Critical Gaps | +|---|---|---| +| **Drive wheel** | Motor electrical specs (Nidec 20N704RC70), wheel diameter (65mm), connector type (16-pin SHD) | ❌ Encoder PPR, ❌ gearbox ratio, ❌ full connector pinout, ❌ wheel-drop sensor model | +| **Suction fan** | Complete Nidec catalog specs for many models, PWM/FG control, connector types (JST XH) | ❌ Impeller housing geometry, ❌ assembly weight, ❌ cable lengths | +| **LiDAR (CRL-200S)** | ✅ Full specs, pinout, protocol, RPM/voltage relationship, power consumption | Minor: exact motor model inside, formal manufacturer datasheet | +| **VL53L7CX ToF** | ✅ Full datasheet, I²C interface, pinout, FOV, range | None — standard ST component | +| **IMU (MPU-6050)** | ✅ Full datasheet, I²C interface, pinout | None — standard InvenSense component | +| **IR cliff sensor** | ✅ TCRT5000 specs, circuit, pinout | None — standard sensor, subject to final BOM choice | +| **Caster wheel** | ❌ Nothing found | Everything — model, dimensions, mounting, weight, any embedded sensor | diff --git a/contributions/part-specs/README.md b/contributions/part-specs/README.md new file mode 100644 index 0000000..8e66b88 --- /dev/null +++ b/contributions/part-specs/README.md @@ -0,0 +1,99 @@ +# Procure Part Specs & Datasheets for Sourced Components + +We've already sourced candidate parts (see [BOM.md](../../BOM.md)). To design the I/O +board and firmware, *drive the motors and fans*, and build accurate mounts, we need each +part's detailed *electrical + mechanical specs* — pinouts, voltages, currents, encoder +PPR, torque, waveforms, etc. Vendors rarely publish these for vacuum sub-assemblies, so +contributors will *find datasheets, ask vendors, or safely reverse-engineer* them. + +This is the *electrical/mechanical data* companion to +[source-3d-models](../source-3d-models) (which covers the *geometry*). No robotics +background needed — a multimeter, patience, and (for reverse-engineering) an oscilloscope go far. + +Please check [BOM.md](../../BOM.md) and the [sourcing follow-along blog post](https://makerspet.com/blog/how-to-source-bom-for-oomwoo-open-source-vacuum-robot/) +for datasheets/specs already found. + +## What we need, per part + +### Drive wheel assembly (Roborock-family — see BOM) +- motor model; motor/assembly *datasheet* (if any) +- *encoder type + PPR* (pulses per revolution) +- *gearbox ratio*; *wheel diameter* +- rated + max motor *voltage*, *current* (no-load & stall), *torque* +- max / rated wheel *speed* +- *cable length(s)*; *connector models* (both ends); *full connector + motor pinouts* +- *wheel-drop sensor* model + pinout (these modules include one) +- signal *waveforms* (encoder channels, motor drive) +- assembly *weight* + +### Suction fan / blower (several options sourced — see BOM) +- fan/motor model; *datasheet* +- rated *voltage, current, RPM*; *airflow* + *static pressure (Pa)* +- *how to drive it* — BLDC driver + control interface (PWM / tach / hall / 3-phase), + soft-start / protection behaviour +- *connector model(s) + pinout*; cable length +- signal *waveforms* +- *weight* + +### Caster / universal wheel assembly (Roomba-family — see BOM) +- model, dimensions, mounting, any embedded sensor, weight, datasheet + +## Already found vs still missing + +*Found* +- *Fan datasheet (some options):* https://file.elecfans.com/web1/M00/CC/89/o4YBAF-ZOBKAQBvyADDMAglsvTw020.pdf +- *Nidec BLDC motors* (candidate fan motors — *bare motors, not the full suction assemblies*): + [NCJ-20N Type-3](https://www.nidec.com/en/product/search/category/B101/M102/S100/NCJ-20N-Type-3/), + [NCJ-20N Type-4](https://www.nidec.com/en/product/search/category/B101/M102/S100/NCJ-20N-Type-4/) +- *Driving the fans:* [ripinteer/fan_protector](https://github.com/ripinteer/fan_protector) + — reference for driving / protecting the BLDC blower (from earlier project research) + +*Still missing (help wanted)* +- Connector *pinouts* + cable lengths for wheels, fans, caster +- *Encoder type + PPR*, gearbox ratios, torque/current under load +- *Wheel-drop sensor* model + pinout +- Signal *waveforms* (encoder, motor/fan drive) +- Suction-*assembly*-level datasheets (we have some bare motors, not the assemblies) +- Weights; caster specs + +## Reverse-engineering — only if specs can't be found, and SAFELY + +If a spec isn't published, reverse-engineer it by opening an existing vacuum and probing. +*Safety first:* +- Don't do it unless you're qualified, experienced. +- Opening a vacuum usually *voids the warranty* and *can damage it* — accept that risk knowingly. +- *Secure / prop up the vacuum* so it can't scoot off the table or bench when the wheels or + fan spin during testing (clamp it, or raise the wheels off the surface). +- Respect the *Li-ion battery* — don't short, pierce, or stress the pack; disconnect where sensible. +- Mind *pinch points and spinning parts* — keep fingers/hair clear of the impeller and brushes. +- Any *mains-connected* testing (e.g. a dock) — extra caution; isolate. +- Use a *multimeter + oscilloscope* to capture voltages, currents, and waveforms; trace and + *label connector pinouts*; photograph everything. + +*Legal:* +- By performing any work for this project including reverse engineering you agree to + - wave liability, indemnify this project, the legal entity behind it (Remake AI Statutory Trust) and and its founder + - contribute your work and results thereof, if any, as open-source, to be published under Apache 2.0 license + +## Submit + +A PR to `contributions/part-specs///`: +- a spec sheet (markdown table) with everything you found +- any datasheets (PDF) and source links +- photos of connectors + labelled pinouts +- waveform captures where reverse-engineered +- provenance — which vendor / model / revision the data is from +- announce it in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) + +## Acceptance criteria + +- Spec sheet for a part (or a clearly-scoped subset) +- Datasheets / links where found + - pinouts + waveforms where reverse-engineered when appropriate +- Provenance stated (part / vendor / revision) +- Verifiable by someone else with the same part +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple attempts +are welcome and useful even if not selected — a non-selected spec sheet is still a valid +reference and a fallback. diff --git a/contributions/part-specs/Scowt/DriveWheel.md b/contributions/part-specs/Scowt/DriveWheel.md new file mode 100644 index 0000000..6ff8723 --- /dev/null +++ b/contributions/part-specs/Scowt/DriveWheel.md @@ -0,0 +1,28 @@ +> ⚠️ **Tentative — not yet confirmed on a physical PCB.** The pinout, cable length, and connector +> below are educated guesses from photos and references. **Verify before wiring** — a wrong +> motor/encoder pinout can damage hardware. Corrections welcome (open a PR or issue). + +## Drive Wheel pinout and cable length: ## + - Pinout: + - pin 1: Limit switch, grey + - pin 2: Limit switch, grey + - pin 3: Encoder 5v, orange + - pin 4: Encoder Signal, blue + - pin 5: Encoder Ground, brown + - pin 6: Motor power, black + - pin 7: Motor power, red + - Note: Some uncertainty around the encoder wires; I agreed with https://electronics.stackexchange.com/questions/549640/how-to-find-pinout-of-dc-geared-motor-with-encoder but do not have a pcb to hand to confirm. The comments there suggest the encoder is hall effect, but am not certain this is true. + - Cable length: + - 250mm ( Estimate Only - based on examining photos, please confirm with physical article ) + - Connector: + - JST, might be XH but not certain. + +Links: +https://electronics.stackexchange.com/questions/549640/how-to-find-pinout-of-dc-geared-motor-with-encoder +https://www.reddit.com/r/Roborock/comments/1t4akoj/new_wheel_for_roborock_s5v/ +(used google translate to determine wire colour) +https://drive.google.com/file/d/1xLM9X-zjDowNAcrZBPqK-h6_eV-Fou7R/edit (disassembly of roborock vacuum; red and black wires clearly largest traces at 2:00, pin 1 marking also visible on connector) + +## Evidence + +![Roborock S4/S5 wheel module connector closeup](WM_V2_closeup.png) \ No newline at end of file diff --git a/contributions/part-specs/Scowt/WM_V2_closeup.png b/contributions/part-specs/Scowt/WM_V2_closeup.png new file mode 100644 index 0000000..7dac73d Binary files /dev/null and b/contributions/part-specs/Scowt/WM_V2_closeup.png differ diff --git a/contributions/recovery-safety/README.md b/contributions/recovery-safety/README.md new file mode 100644 index 0000000..fc32909 --- /dev/null +++ b/contributions/recovery-safety/README.md @@ -0,0 +1,71 @@ +# Recovery Behaviors & Safety (ROS2 package) + +Keep the robot *unstuck and safe*. This package provides a *recovery ladder* +(back up, wiggle, shake free, rotate, nudge, clear costmap, try an alternate path), +*escalation* when a recovery attempt fails, and a final *pause-and-alert* state +when everything fails. It also covers *safety*: cliff / wheel-drop / pickup +detection, an e-stop, and *status / error reporting* so a human knows what +happened. Because the physical robot isn't built yet, this is a *Gazebo +simulation*; it is later re-validated on hardware in the +[live-robot-bringup RFC](../live-robot-bringup). + +> *Status — ready to start work.* No need to wait for OOMWOO hardware — develop it in the +> Gazebo sim ([urdf-gazebo-sim](../urdf-gazebo-sim)) or on the real +> [placeholder Proscenic M6 Pro](https://makerspet.com/blog/tutorial-connect-robot-vacuum-cleaner-to-ros-2-proscenic-m6-pro/). +> Say so in the [discussions](https://github.com/makerspet/oomwoo/discussions) so we can coordinate. + +# Important References +- [clean-and-map RFC](../clean-and-map) and [urdf-gazebo-sim RFC](../urdf-gazebo-sim) — the *bumper* (left / right / front) and any cliff / wheel-drop sensors this package reacts to. +- [nav-localize RFC](../nav-localize) — pickup / kidnap detection ties into relocalization. +- [ROS2 software interfaces](../../docs/SOFTWARE_INTERFACES.md) — shared topic/action/service contract for simulation-first modules. +- [OOMWOO ROS2 development](https://github.com/makerspet/oomwoo-install) — build OOMWOO ROS2 Docker image(s) with your packages. +- Nav2's behavior/recovery server is a good starting point for composing recoveries. +- [Project discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- [Discord server](https://discord.gg/3y2JKz5T25) + +# Request for Contribution - Instructions + +- implement a *recovery ladder* + - a configurable, ordered set of behaviors, e.g. *back up*, *wiggle / shake free*, *rotate in place*, *clear the costmap*, *nudge*, *try an alternate path* + - pick the right recovery for the situation (wedged, bumper-jammed, no valid path, localization lost) + - post in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) to let everyone know you're working on it, and post your progress +- *escalation* + - if behavior *N* fails, try *N+1*; track attempts per situation so the robot doesn't repeat a recovery forever +- *pause-and-alert* + - when the whole ladder is exhausted, stop safely, publish a clear *error / status*, and wait for a human or a resume command — *never thrash* +- *safety sensors* + - detect *cliffs*, *wheel drop*, *pickup / kidnap* (hand pickup detection to [nav-localize](../nav-localize)), and bumper jams; respond by stopping motors / entering a safe state +- *e-stop* + - a software emergency stop that brings the robot to a safe state immediately +- *status & error reporting* + - a structured robot-state / error topic, human-readable, suitable for Home Assistant, so a person knows *why* the robot paused +- test it well + - induce wedged / stuck situations in sim: trap the robot, drop a dynamic obstacle onto it, place it at a cliff edge, lift it + - verify the ladder runs, escalates, then pauses-and-reports; verify it never thrashes indefinitely +- regression tests (headless, CI-friendly) + - recovery success rate across induced stuck scenarios + - guaranteed termination — the robot always reaches *recovered* or *paused-and-reported*, never an infinite loop + - safety responses fire on cliff / wheel-drop / pickup / e-stop +- submit a PR (pull request) to `contributions/recovery-safety//` + - link to ROS2 package(s) + - instructions, documentation - how to install, run, configure, troubleshoot, test results + - videos of recovery, escalation, and pause-and-alert + - announce your submission in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- iterate with review +- TBD, expect the RFC to evolve + +## Acceptance criteria + +Objective, measurable. Examples: +- A configurable *recovery ladder* runs the right behaviors for the situation and frees the robot in the large majority of induced stuck scenarios +- Recoveries *escalate* and are bounded — the robot never repeats a failed recovery forever +- When recovery is impossible, the robot *pauses safely and reports a clear error*, then resumes on command +- *Safety*: cliff, wheel-drop, pickup, and e-stop all bring the robot to a safe state +- *Status / error reporting* is structured and Home-Assistant-friendly +- *Regression tests* pass, including a guaranteed-termination check, runnable headless in CI +- Documented and reliably reproducible by someone else +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple +attempts are welcome and useful even if not selected — modules are swappable, and +a non-selected design is still a valid learning exercise and a fallback. diff --git a/contributions/recovery-safety/xbattlax/README.md b/contributions/recovery-safety/xbattlax/README.md new file mode 100644 index 0000000..53c6378 --- /dev/null +++ b/contributions/recovery-safety/xbattlax/README.md @@ -0,0 +1,117 @@ +# Recovery Safety Prototype by xbattlax + +This contribution provides a first ROS2 recovery-and-safety package for the +OOMWOO recovery-safety RFC. It is deliberately small, deterministic, and +simulation-first: + +- a bounded recovery ladder for bumper, wedged, no-path, and lost-localization + situations +- immediate safe stop for e-stop, cliff, wheel-drop, and pickup events +- pause-and-alert when the ladder is exhausted +- a structured JSON status topic suitable for later Home Assistant integration +- unit tests for guaranteed termination and safety responses + +The core ladder lives in pure Python, so it can be regression-tested without a +running ROS graph. The ROS2 node is a thin adapter around that core. + +## Package + +`oomwoo_recovery_safety` + +Location: + +```text +contributions/recovery-safety/xbattlax/oomwoo_recovery_safety +``` + +## Interfaces + +### Subscribed topics + +| Topic | Type | Purpose | +|---|---|---| +| `/bumper_left` | `ros_gz_interfaces/msg/Contacts` | Trigger left-bumper recovery after filtering ground-plane contacts. | +| `/bumper_right` | `ros_gz_interfaces/msg/Contacts` | Trigger right-bumper recovery after filtering ground-plane contacts. | +| `/oomwoo/recovery/event` | `std_msgs/msg/String` | Manual/test trigger. Payload examples: `wedged`, `no_valid_path`, `localization_lost`, `bumper_front`. | +| `/oomwoo/recovery/behavior_result` | `std_msgs/msg/String` | Optional external result for the current behavior: `succeeded`, `failed`, or JSON `{"outcome":"succeeded"}`. | +| `/oomwoo/safety/e_stop` | `std_msgs/msg/Bool` | Immediate non-recoverable pause. | +| `/oomwoo/safety/cliff` | `std_msgs/msg/Bool` | Immediate safety pause. | +| `/oomwoo/safety/wheel_drop` | `std_msgs/msg/Bool` | Immediate safety pause. | +| `/oomwoo/safety/pickup` | `std_msgs/msg/Bool` | Immediate safety pause for pickup/kidnap handoff. | +| `/oomwoo/recovery/reset` | `std_msgs/msg/Bool` | Reset a paused/recovered controller when true. | + +### Published topics + +| Topic | Type | Purpose | +|---|---|---| +| `/cmd_vel` | `geometry_msgs/msg/Twist` | Short bounded motion commands for recovery. | +| `/oomwoo/status` | `std_msgs/msg/String` | JSON status with `state`, `reason_code`, `message`, `recoverable`, `source`, and recovery metadata. | +| `/oomwoo/recovery/command` | `std_msgs/msg/String` | JSON command for non-motion actions such as `clear_costmap`. | + +## Build + +From the OOMWOO ROS2 container: + +```bash +source /opt/ros/jazzy/setup.bash +cd /workspace +colcon build \ + --base-paths contributions/recovery-safety/xbattlax/oomwoo_recovery_safety \ + --packages-select oomwoo_recovery_safety +``` + +## Test + +```bash +source /opt/ros/jazzy/setup.bash +cd /workspace +colcon test \ + --base-paths contributions/recovery-safety/xbattlax/oomwoo_recovery_safety \ + --packages-select oomwoo_recovery_safety +colcon test-result --verbose +``` + +The tests cover: + +- bounded escalation to pause-and-alert +- successful recovery stopping the ladder +- e-stop and safety events entering safe pause +- ignored duplicate triggers while already recovering +- JSON status shape + +## Run + +```bash +source /opt/ros/jazzy/setup.bash +source install/setup.bash +ros2 launch oomwoo_recovery_safety recovery_safety.launch.py +``` + +Manual trigger example: + +```bash +ros2 topic pub --once /oomwoo/recovery/event std_msgs/msg/String "{data: bumper_front}" +``` + +Mark the current behavior as successful: + +```bash +ros2 topic pub --once /oomwoo/recovery/behavior_result std_msgs/msg/String "{data: succeeded}" +``` + +Trigger e-stop: + +```bash +ros2 topic pub --once /oomwoo/safety/e_stop std_msgs/msg/Bool "{data: true}" +``` + +## Current limitations + +- This is a first integration scaffold, not a full Nav2 behavior-server plugin. +- `clear_costmap` is published as an intent on `/oomwoo/recovery/command`; a + future adapter should call Nav2 costmap clear services directly. +- Success detection is external for now. If no `succeeded` result arrives before + a behavior's timeout, the node escalates to the next behavior and eventually + pauses. +- Cliff, wheel-drop, and pickup are represented as boolean topics until the + hardware/simulation message contract is finalized. diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/launch/recovery_safety.launch.py b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/launch/recovery_safety.launch.py new file mode 100644 index 0000000..71901ab --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/launch/recovery_safety.launch.py @@ -0,0 +1,15 @@ +from launch import LaunchDescription +from launch_ros.actions import Node + + +def generate_launch_description(): + return LaunchDescription( + [ + Node( + package="oomwoo_recovery_safety", + executable="recovery_safety_node", + name="recovery_safety", + output="screen", + ) + ] + ) diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/__init__.py b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/__init__.py new file mode 100644 index 0000000..9153532 --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/__init__.py @@ -0,0 +1 @@ +"""OOMWOO recovery-safety prototype package.""" diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/core.py b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/core.py new file mode 100644 index 0000000..d50f1f8 --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/core.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +from dataclasses import asdict, dataclass +from enum import Enum +import json +from typing import Mapping + + +class Situation(str, Enum): + BUMPER_LEFT = "bumper_left" + BUMPER_RIGHT = "bumper_right" + BUMPER_FRONT = "bumper_front" + WEDGED = "wedged" + NO_VALID_PATH = "no_valid_path" + LOCALIZATION_LOST = "localization_lost" + CLIFF = "cliff" + WHEEL_DROP = "wheel_drop" + PICKUP = "pickup" + E_STOP = "e_stop" + + +class ControllerState(str, Enum): + IDLE = "idle" + RECOVERING = "recovering" + RECOVERED = "recovered" + PAUSED = "paused" + + +class DecisionKind(str, Enum): + START_STEP = "start_step" + STATUS_ONLY = "status_only" + IGNORED = "ignored" + + +@dataclass(frozen=True) +class RecoveryStep: + name: str + command: str + duration_sec: float + linear_x: float = 0.0 + angular_z: float = 0.0 + + +@dataclass(frozen=True) +class RecoveryStatus: + state: str + reason_code: str + message: str + recoverable: bool + source: str = "oomwoo_recovery_safety" + situation: str | None = None + behavior: str | None = None + step_index: int | None = None + ladder_length: int | None = None + + def to_json(self) -> str: + return json.dumps(asdict(self), sort_keys=True) + + +@dataclass(frozen=True) +class Decision: + kind: DecisionKind + status: RecoveryStatus + step: RecoveryStep | None = None + + +SAFETY_SITUATIONS = { + Situation.CLIFF, + Situation.WHEEL_DROP, + Situation.PICKUP, + Situation.E_STOP, +} + + +DEFAULT_LADDERS: Mapping[Situation, tuple[RecoveryStep, ...]] = { + Situation.BUMPER_LEFT: ( + RecoveryStep("back_up", "twist", 0.8, linear_x=-0.12), + RecoveryStep("rotate_away_from_left_bumper", "twist", 1.0, linear_x=-0.06, angular_z=-0.55), + RecoveryStep("wiggle_free", "twist", 0.7, linear_x=-0.04, angular_z=0.85), + RecoveryStep("clear_costmap", "clear_costmap", 0.1), + ), + Situation.BUMPER_RIGHT: ( + RecoveryStep("back_up", "twist", 0.8, linear_x=-0.12), + RecoveryStep("rotate_away_from_right_bumper", "twist", 1.0, linear_x=-0.06, angular_z=0.55), + RecoveryStep("wiggle_free", "twist", 0.7, linear_x=-0.04, angular_z=-0.85), + RecoveryStep("clear_costmap", "clear_costmap", 0.1), + ), + Situation.BUMPER_FRONT: ( + RecoveryStep("back_up", "twist", 0.9, linear_x=-0.14), + RecoveryStep("rotate_left", "twist", 0.8, angular_z=0.6), + RecoveryStep("rotate_right", "twist", 0.8, angular_z=-0.6), + RecoveryStep("clear_costmap", "clear_costmap", 0.1), + ), + Situation.WEDGED: ( + RecoveryStep("back_up", "twist", 1.0, linear_x=-0.12), + RecoveryStep("wiggle_left", "twist", 0.6, linear_x=-0.04, angular_z=0.9), + RecoveryStep("wiggle_right", "twist", 0.6, linear_x=-0.04, angular_z=-0.9), + RecoveryStep("rotate_in_place", "twist", 1.2, angular_z=0.7), + RecoveryStep("clear_costmap", "clear_costmap", 0.1), + ), + Situation.NO_VALID_PATH: ( + RecoveryStep("clear_costmap", "clear_costmap", 0.1), + RecoveryStep("nudge_reverse", "twist", 0.6, linear_x=-0.08), + RecoveryStep("rotate_in_place", "twist", 1.0, angular_z=0.6), + ), + Situation.LOCALIZATION_LOST: ( + RecoveryStep("stop_and_wait", "stop", 0.1), + RecoveryStep("rotate_to_collect_scans", "twist", 1.5, angular_z=0.45), + ), +} + + +class RecoveryController: + def __init__(self, ladders: Mapping[Situation, tuple[RecoveryStep, ...]] | None = None): + self._ladders = dict(ladders or DEFAULT_LADDERS) + self._state = ControllerState.IDLE + self._situation: Situation | None = None + self._step_index = 0 + self._current_step: RecoveryStep | None = None + self._last_status = self._make_status("READY", "Recovery controller ready", True) + + @property + def state(self) -> ControllerState: + return self._state + + @property + def last_status(self) -> RecoveryStatus: + return self._last_status + + def trigger(self, situation: Situation | str) -> Decision: + parsed = self._parse_situation(situation) + + if parsed in SAFETY_SITUATIONS: + return self._pause( + parsed, + reason_code=self._safety_reason(parsed), + message=f"Safety event {parsed.value} paused the robot", + recoverable=False, + ) + + if self._state == ControllerState.RECOVERING: + status = self._make_status( + "RECOVERY_ALREADY_ACTIVE", + f"Ignoring {parsed.value}; already recovering from {self._situation.value}", + True, + ) + self._last_status = status + return Decision(DecisionKind.IGNORED, status) + + if self._state == ControllerState.PAUSED: + status = self._make_status( + "RECOVERY_PAUSED", + f"Ignoring {parsed.value}; controller is paused and needs reset", + self._last_status.recoverable, + ) + self._last_status = status + return Decision(DecisionKind.IGNORED, status) + + ladder = self._ladders.get(parsed) + if not ladder: + return self._pause( + parsed, + reason_code="NO_RECOVERY_LADDER", + message=f"No recovery ladder configured for {parsed.value}", + recoverable=True, + ) + + self._state = ControllerState.RECOVERING + self._situation = parsed + self._step_index = 0 + self._current_step = ladder[0] + status = self._make_status( + "RECOVERY_STARTED", + f"Starting recovery step {self._current_step.name}", + True, + ) + self._last_status = status + return Decision(DecisionKind.START_STEP, status, self._current_step) + + def step_succeeded(self) -> Decision: + if self._state != ControllerState.RECOVERING: + status = self._make_status("NO_ACTIVE_RECOVERY", "No active recovery to complete", True) + self._last_status = status + return Decision(DecisionKind.IGNORED, status) + + self._state = ControllerState.RECOVERED + self._current_step = None + status = self._make_status("RECOVERED", "Recovery succeeded", True) + self._last_status = status + return Decision(DecisionKind.STATUS_ONLY, status) + + def step_failed(self, detail: str = "step failed") -> Decision: + if self._state != ControllerState.RECOVERING or self._situation is None: + status = self._make_status("NO_ACTIVE_RECOVERY", "No active recovery to fail", True) + self._last_status = status + return Decision(DecisionKind.IGNORED, status) + + ladder = self._ladders[self._situation] + next_index = self._step_index + 1 + if next_index >= len(ladder): + return self._pause( + self._situation, + reason_code="RECOVERY_EXHAUSTED", + message=f"Recovery ladder exhausted after {detail}", + recoverable=True, + ) + + self._step_index = next_index + self._current_step = ladder[next_index] + status = self._make_status( + "RECOVERY_ESCALATED", + f"Escalating after {detail}; starting {self._current_step.name}", + True, + ) + self._last_status = status + return Decision(DecisionKind.START_STEP, status, self._current_step) + + def reset(self) -> Decision: + self._state = ControllerState.IDLE + self._situation = None + self._step_index = 0 + self._current_step = None + status = self._make_status("READY", "Recovery controller reset", True) + self._last_status = status + return Decision(DecisionKind.STATUS_ONLY, status) + + def _pause( + self, + situation: Situation, + *, + reason_code: str, + message: str, + recoverable: bool, + ) -> Decision: + self._state = ControllerState.PAUSED + self._situation = situation + self._current_step = None + status = self._make_status(reason_code, message, recoverable) + self._last_status = status + return Decision(DecisionKind.STATUS_ONLY, status) + + def _make_status(self, reason_code: str, message: str, recoverable: bool) -> RecoveryStatus: + ladder_length = None + if self._situation in self._ladders: + ladder_length = len(self._ladders[self._situation]) + + return RecoveryStatus( + state=self._state.value, + reason_code=reason_code, + message=message, + recoverable=recoverable, + situation=self._situation.value if self._situation else None, + behavior=self._current_step.name if self._current_step else None, + step_index=self._step_index if self._current_step else None, + ladder_length=ladder_length, + ) + + @staticmethod + def _parse_situation(situation: Situation | str) -> Situation: + if isinstance(situation, Situation): + return situation + try: + return Situation(str(situation).strip().lower()) + except ValueError as exc: + raise ValueError(f"Unknown recovery situation: {situation}") from exc + + @staticmethod + def _safety_reason(situation: Situation) -> str: + if situation == Situation.E_STOP: + return "E_STOP" + return f"SAFETY_{situation.value.upper()}" diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/recovery_node.py b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/recovery_node.py new file mode 100644 index 0000000..1fbe6ea --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/oomwoo_recovery_safety/recovery_node.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import json +from time import monotonic + +from geometry_msgs.msg import Twist +import rclpy +from rclpy.executors import ExternalShutdownException +from rclpy.exceptions import ROSInterruptException +from rclpy.node import Node +from ros_gz_interfaces.msg import Contacts +from std_msgs.msg import Bool, String + +from oomwoo_recovery_safety.core import Decision, DecisionKind, RecoveryController, Situation + + +class RecoverySafetyNode(Node): + def __init__(self): + super().__init__("recovery_safety") + self._controller = RecoveryController() + self._active_deadline: float | None = None + + self._cmd_pub = self.create_publisher(Twist, "cmd_vel", 10) + self._status_pub = self.create_publisher(String, "oomwoo/status", 10) + self._command_pub = self.create_publisher(String, "oomwoo/recovery/command", 10) + + self.create_subscription(Contacts, "bumper_left", self._bumper_left_cb, 10) + self.create_subscription(Contacts, "bumper_right", self._bumper_right_cb, 10) + self.create_subscription(String, "oomwoo/recovery/event", self._event_cb, 10) + self.create_subscription(String, "oomwoo/recovery/behavior_result", self._behavior_result_cb, 10) + self.create_subscription(Bool, "oomwoo/safety/e_stop", self._e_stop_cb, 10) + self.create_subscription(Bool, "oomwoo/safety/cliff", self._cliff_cb, 10) + self.create_subscription(Bool, "oomwoo/safety/wheel_drop", self._wheel_drop_cb, 10) + self.create_subscription(Bool, "oomwoo/safety/pickup", self._pickup_cb, 10) + self.create_subscription(Bool, "oomwoo/recovery/reset", self._reset_cb, 10) + + self.create_timer(0.05, self._timer_cb) + self._publish_status(self._controller.last_status) + + def _bumper_left_cb(self, msg: Contacts): + if self._has_real_contact(msg): + self._execute(self._controller.trigger(Situation.BUMPER_LEFT)) + + def _bumper_right_cb(self, msg: Contacts): + if self._has_real_contact(msg): + self._execute(self._controller.trigger(Situation.BUMPER_RIGHT)) + + def _event_cb(self, msg: String): + try: + self._execute(self._controller.trigger(msg.data)) + except ValueError as exc: + self.get_logger().warn(str(exc)) + + def _behavior_result_cb(self, msg: String): + outcome = self._parse_outcome(msg.data) + if outcome == "succeeded": + self._stop_motion() + self._active_deadline = None + self._execute(self._controller.step_succeeded()) + elif outcome == "failed": + self._stop_motion() + self._active_deadline = None + self._execute(self._controller.step_failed("external failure result")) + else: + self.get_logger().warn(f"Ignoring unknown behavior outcome: {msg.data}") + + def _e_stop_cb(self, msg: Bool): + if msg.data: + self._stop_motion() + self._active_deadline = None + self._execute(self._controller.trigger(Situation.E_STOP)) + + def _cliff_cb(self, msg: Bool): + if msg.data: + self._stop_motion() + self._active_deadline = None + self._execute(self._controller.trigger(Situation.CLIFF)) + + def _wheel_drop_cb(self, msg: Bool): + if msg.data: + self._stop_motion() + self._active_deadline = None + self._execute(self._controller.trigger(Situation.WHEEL_DROP)) + + def _pickup_cb(self, msg: Bool): + if msg.data: + self._stop_motion() + self._active_deadline = None + self._execute(self._controller.trigger(Situation.PICKUP)) + + def _reset_cb(self, msg: Bool): + if msg.data: + self._stop_motion() + self._active_deadline = None + self._execute(self._controller.reset()) + + def _timer_cb(self): + if self._active_deadline is None or monotonic() < self._active_deadline: + return + + self._stop_motion() + self._active_deadline = None + self._execute(self._controller.step_failed("behavior timeout")) + + def _execute(self, decision: Decision): + self._publish_status(decision.status) + if decision.kind != DecisionKind.START_STEP or decision.step is None: + return + + step = decision.step + if step.command == "twist": + twist = Twist() + twist.linear.x = step.linear_x + twist.angular.z = step.angular_z + self._cmd_pub.publish(twist) + elif step.command == "stop": + self._stop_motion() + else: + self._publish_command(step.command, step.name) + + self._active_deadline = monotonic() + step.duration_sec + + def _publish_status(self, status): + self._status_pub.publish(String(data=status.to_json())) + + def _publish_command(self, command: str, behavior: str): + payload = { + "command": command, + "behavior": behavior, + "source": "oomwoo_recovery_safety", + } + self._command_pub.publish(String(data=json.dumps(payload, sort_keys=True))) + + def _stop_motion(self): + self._cmd_pub.publish(Twist()) + + @staticmethod + def _has_real_contact(msg: Contacts) -> bool: + for contact in msg.contacts: + names = {contact.collision1.name, contact.collision2.name} + if not any("ground_plane" in name.split("::") for name in names): + return True + return False + + @staticmethod + def _parse_outcome(raw: str) -> str: + value = raw.strip().lower() + if value.startswith("{"): + try: + value = str(json.loads(raw).get("outcome", "")).strip().lower() + except json.JSONDecodeError: + return "" + return value + + +def main(args=None): + rclpy.init(args=args) + node = RecoverySafetyNode() + try: + rclpy.spin(node) + except (KeyboardInterrupt, ExternalShutdownException, ROSInterruptException): + pass + except Exception as exc: + if "context is not valid" not in str(exc): + raise + finally: + node.destroy_node() + try: + if rclpy.ok(): + rclpy.shutdown() + except Exception as exc: + if "rcl_shutdown already called" not in str(exc): + raise diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/package.xml b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/package.xml new file mode 100644 index 0000000..4d713f9 --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/package.xml @@ -0,0 +1,21 @@ + + + + oomwoo_recovery_safety + 0.1.0 + Bounded recovery ladder and safety pause node for OOMWOO. + + xbattlax + Apache-2.0 + + geometry_msgs + rclpy + ros_gz_interfaces + std_msgs + + pytest + + + ament_python + + diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/resource/oomwoo_recovery_safety b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/resource/oomwoo_recovery_safety new file mode 100644 index 0000000..e4e2076 --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/resource/oomwoo_recovery_safety @@ -0,0 +1 @@ +oomwoo_recovery_safety diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/setup.cfg b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/setup.cfg new file mode 100644 index 0000000..7ae25f5 --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/oomwoo_recovery_safety +[install] +install_scripts=$base/lib/oomwoo_recovery_safety diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/setup.py b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/setup.py new file mode 100644 index 0000000..beafb44 --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/setup.py @@ -0,0 +1,27 @@ +from setuptools import find_packages, setup + + +package_name = "oomwoo_recovery_safety" + +setup( + name=package_name, + version="0.1.0", + packages=find_packages(exclude=["test"]), + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + ("share/" + package_name + "/launch", ["launch/recovery_safety.launch.py"]), + ], + install_requires=["setuptools"], + zip_safe=True, + maintainer="xbattlax", + maintainer_email="xbattlax@gmail.com", + description="Bounded recovery ladder and safety pause node for OOMWOO.", + license="Apache-2.0", + tests_require=["pytest"], + entry_points={ + "console_scripts": [ + "recovery_safety_node = oomwoo_recovery_safety.recovery_node:main", + ], + }, +) diff --git a/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/test/test_recovery_controller.py b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/test/test_recovery_controller.py new file mode 100644 index 0000000..03477fd --- /dev/null +++ b/contributions/recovery-safety/xbattlax/oomwoo_recovery_safety/test/test_recovery_controller.py @@ -0,0 +1,109 @@ +import json + +import pytest + +from oomwoo_recovery_safety.core import ControllerState, DecisionKind, RecoveryController, Situation + + +def test_bumper_recovery_escalates_and_terminates(): + controller = RecoveryController() + + decision = controller.trigger(Situation.BUMPER_LEFT) + assert decision.kind == DecisionKind.START_STEP + assert decision.step.name == "back_up" + + seen = [decision.step.name] + for _ in range(10): + decision = controller.step_failed("test failure") + if decision.kind == DecisionKind.START_STEP: + seen.append(decision.step.name) + continue + break + + assert seen == [ + "back_up", + "rotate_away_from_left_bumper", + "wiggle_free", + "clear_costmap", + ] + assert controller.state == ControllerState.PAUSED + assert controller.last_status.reason_code == "RECOVERY_EXHAUSTED" + assert controller.last_status.recoverable is True + + +def test_success_stops_ladder(): + controller = RecoveryController() + controller.trigger("bumper_front") + + decision = controller.step_succeeded() + + assert decision.kind == DecisionKind.STATUS_ONLY + assert controller.state == ControllerState.RECOVERED + assert controller.last_status.reason_code == "RECOVERED" + + +@pytest.mark.parametrize( + ("situation", "reason"), + [ + (Situation.E_STOP, "E_STOP"), + (Situation.CLIFF, "SAFETY_CLIFF"), + (Situation.WHEEL_DROP, "SAFETY_WHEEL_DROP"), + (Situation.PICKUP, "SAFETY_PICKUP"), + ], +) +def test_safety_events_pause_immediately(situation, reason): + controller = RecoveryController() + + decision = controller.trigger(situation) + + assert decision.kind == DecisionKind.STATUS_ONLY + assert controller.state == ControllerState.PAUSED + assert controller.last_status.reason_code == reason + assert controller.last_status.recoverable is False + + +def test_duplicate_trigger_is_ignored_while_recovering(): + controller = RecoveryController() + controller.trigger(Situation.BUMPER_RIGHT) + + decision = controller.trigger(Situation.WEDGED) + + assert decision.kind == DecisionKind.IGNORED + assert controller.state == ControllerState.RECOVERING + assert controller.last_status.reason_code == "RECOVERY_ALREADY_ACTIVE" + + +def test_reset_returns_to_idle_after_pause(): + controller = RecoveryController() + controller.trigger(Situation.E_STOP) + + decision = controller.reset() + + assert decision.kind == DecisionKind.STATUS_ONLY + assert controller.state == ControllerState.IDLE + assert controller.last_status.reason_code == "READY" + + +def test_paused_controller_ignores_new_recovery_until_reset(): + controller = RecoveryController() + controller.trigger(Situation.E_STOP) + + decision = controller.trigger(Situation.BUMPER_LEFT) + + assert decision.kind == DecisionKind.IGNORED + assert controller.state == ControllerState.PAUSED + assert controller.last_status.reason_code == "RECOVERY_PAUSED" + + +def test_status_json_shape(): + controller = RecoveryController() + controller.trigger(Situation.NO_VALID_PATH) + + payload = json.loads(controller.last_status.to_json()) + + assert payload["state"] == "recovering" + assert payload["reason_code"] == "RECOVERY_STARTED" + assert payload["recoverable"] is True + assert payload["source"] == "oomwoo_recovery_safety" + assert payload["situation"] == "no_valid_path" + assert payload["behavior"] == "clear_costmap" diff --git a/contributions/source-3d-models/README.md b/contributions/source-3d-models/README.md new file mode 100644 index 0000000..3c4e89f --- /dev/null +++ b/contributions/source-3d-models/README.md @@ -0,0 +1,72 @@ +# Source 3D Models (STEP) for Off-the-Shelf BOM Parts (procurement) + +To design printed mounts and the chassis that *fit real sourced parts*, we need accurate +3D models (*STEP*) of the off-the-shelf components in the [BOM](../../BOM.md) — the +drive-wheel assembly, the suction fan/blower (several candidate models), the caster wheel, +the side-brush motor, and more. Manufacturers rarely publish these, so the community needs +to *obtain, measure, and model* them. Every printed mount and the chassis references this +geometry — see [ARCHITECTURE.md](../../docs/ARCHITECTURE.md) §5.2 (mechanical interface +standard). Missing models block or force guesswork on the mechanical modules. + +This is a great *first contribution* for anyone with CAD skills and calipers — no +robotics or programming needed, and the modules can be modeled *in parallel*. + +## Priority parts (from the [BOM](../../BOM.md)) + +Start with the "ready now" mechanical parts: +- *Drive wheel assembly pair* (e.g. Roborock S5/S50/S55/S6/S7-family modules) +- *Caster / universal wheel* (e.g. Roomba i/j/e/500–900 caster) +- *Suction fan / blower* — model the option(s) we shortlist (e.g. Dreame L10s / Nidec / + Roborock S8-family blowers listed in the BOM) +- *Side-brush motor*, *main-brush assembly*, *water pump*, *battery pack*, etc. + +See [BOM.md](../../BOM.md) for the full list, the exact candidate models, and AliExpress +search links, plus [docs/ali_express_research.md](../../docs/ali_express_research.md) for +sourcing notes. + +# Request for Contribution - Instructions + +- *Pick a part* from the [BOM](../../BOM.md) and *claim it* in + [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) or + [Discord](https://discord.gg/3y2JKz5T25) so two people don't model the same thing. Post progress too. +- *Get an accurate model* — two paths: + 1. *Find an existing STEP* (manufacturer site, GrabCAD, etc.) and *verify it against the + real part* (dimensions match). Fastest — but confirm it's the same model/revision. + 2. *Model it yourself*: order the exact part (AliExpress links are in the BOM), measure + with calipers, optionally use a 3D scanner and build an accurate STEP. +- *Capture the interface-relevant geometry* (external only — internal detail is not needed): + - Overall *bounding envelope* (so it fits the chassis space / height budget) + - *Mounting features*: holes, bosses, clips, bolt pattern — and their exact positions + - *Functional interfaces*: axle/shaft position + axis, wheel contact plane, fan air + inlet/outlet, connector / wire-exit locations + - *Mating faces* where other modules attach + - Note the *key overall dimensions* +- *Record provenance* — the exact vendor + link + which model/revision you measured. Parts + vary between sellers and revisions; say which one this model represents. +- *Submit a PR* to `contributions/source-3d-models///`: + - the *STEP* file (primary deliverable) + native CAD source (Fusion/SolidWorks/etc.) if you modeled it + - a *photo* of the real part (ideally next to your model / with calipers) + - the *source link* (AliExpress/vendor) and *key dimensions* + - notes on tolerances and how you measured + - announce it in [Project Discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +- Iterate with review. +- TBD, expect the RFC to evolve. + +(*) If you have a strong reason to model a different variant than the BOM lists, post your +rationale in [discussions](https://github.com/makerspet/oomwoo/discussions?discussions_q=) first. + +## Acceptance criteria + +Objective, measurable. Examples: +- Accurate *STEP* of the specified part — bounding envelope, mounting features, and + functional interfaces match the measured real part (within a stated tolerance) +- Includes the *exact source link*, *key dimensions*, and a *photo* of the real part + - Please make sure the model is licensed as open-source +- *Verifiable/reproducible* by someone else who buys the same part +- STEP provided (+ native CAD source if self-modeled) +- Documented well enough that a mount designer can build against it with confidence +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple attempts +are welcome and useful even if not selected — models are swappable, and a non-selected +model is still a valid reference and a fallback. diff --git a/contributions/urdf-gazebo-sim/README.md b/contributions/urdf-gazebo-sim/README.md new file mode 100644 index 0000000..473b39b --- /dev/null +++ b/contributions/urdf-gazebo-sim/README.md @@ -0,0 +1,7 @@ +# oomwoo URDF + Gazebo Simulation + +Self-hosted package: [github.com/alvarosamudio/oomwoo_gazebo](https://github.com/alvarosamudio/oomwoo_gazebo) + +ROS2 package with URDF model, Gazebo simulation worlds, bump recovery node, and Nav2/SLAM configuration for the OOMWOO robot vacuum. Built against the [ROS2 Software Interfaces](../../docs/SOFTWARE_INTERFACES.md) contract. + +See the repo above for full documentation, setup instructions, and usage. diff --git a/contributions/vacuum-fan/README.md b/contributions/vacuum-fan/README.md new file mode 100644 index 0000000..b106e7d --- /dev/null +++ b/contributions/vacuum-fan/README.md @@ -0,0 +1,49 @@ +# Blower Fan Assembly (mechanical module) + +> *On hold.* Fan options are *already sourced* — see the suction-fan rows in +> [BOM.md](../../BOM.md) (multiple Pa/price options: Dreame, Nidec, Roborock-family blowers). +> The project has also moved *away from the old teardown reference vacuum* (its parts are +> hard to source vs. big names like Roborock/Dreame/Xiaomi). This module resumes once enough +> components are sourced and a *3D reference design with the key parts* is sketched, so the +> housing can be designed to fit real parts. +> +> *Useful upstream work now:* model the sourced fans in +> [source-3d-models](../source-3d-models), and gather fan specs / how to drive them in +> [part-specs](../part-specs). Those unblock this design. + +The vacuum fan assembly provides air suction to the main brush and streams air into the dust bin. +It consists of: +- a high-speed *BLDC blower motor + impeller* (already sourced — see BOM) +- a blower housing (volute) — to be designed +- a blower exhaust gasket +- blower housing rubber mounts + +> *Design research note:* real-world cleaning does *not* track raw suction (Pa) — +> mid-range sealed motors match flagships. Prioritise a *well-sealed* airflow path +> (no leaks at the bin / fan / brush seams) and a good brush over chasing maximum Pa. +> See [design research](../../README.md#design-research). + +# Request for Contribution — Instructions (resumes when off hold) + +When this reopens, the task is to design a *3D-printable volute housing + gasket around a +sourced BLDC blower* (chosen from the [BOM](../../BOM.md)) that mates cleanly with the dust +bin and the chassis. Design goals: +- 3D-printable (assume PETG); ideally no supports; reliably reproducible +- sliced part fits ~20 × 25 cm, 20 cm height (split if needed) +- *well-sealed* airflow path (no leaks); mates with the dust bin + chassis +- low vibration; secure motor mounting +- gasket: find an off-the-shelf one, else print TPU +- submit STEP + native CAD + 3MF/STL + a sub-component BoM + docs/photos/video to + `contributions/vacuum-fan//` + +## Acceptance criteria (when it resumes) + +- Fits the (forthcoming) chassis + dust-bin interfaces without forcing changes to other modules +- Well-sealed, reasonably quiet, low-vibration, strong suction with the chosen sourced blower +- 3D-printable and reliably reproducible by someone else +- Documented; STEP + native CAD source provided +- TBD, expect criteria to evolve + +The maintainer selects among compliant candidates using these criteria. Multiple attempts +are welcome and useful even if not selected — modules are swappable, and a non-selected +design is still a valid learning exercise and a fallback. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..21e651a --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,236 @@ +# OOMWOO Architecture Brief + +> *Status: DRAFT / skeleton.* This document defines the system so that modules +> can be built in parallel without colliding. Sections marked *TBD* are the +> gating decisions; until they are filled in, hardware modules that must fit +> together cannot be finalized. Treat the interface specs as the contract every +> module agrees to. + +## 1. Purpose/Goal and scope + +OOMWOO is an open-source, 3D-printed, ROS2-based home robot vacuum with 2D LiDAR +and Home Assistant support. It is designed to be *built from scratch by +the community*, module by module, clean well and to double as an affordable ROS2 +development and learning platform. + +*North star (not MVP):* OOMWOO is also the reference hardware for a broader +robot application platform. Architectural boundaries (especially the app layer in +§6) are drawn with that future in mind, but the MVP below deliberately excludes it. + +## 2. Design principles + +- *Open and swappable.* Every module has a defined interface. Any compliant + implementation can replace another. No module depends on the internals of + another, only on its published interface. +- *Simulation-first.* Software must run in Gazebo before it runs on hardware, + so contributors with no robot can still build and test. +- *Affordable and printable.* Target off-the-shelf parts (a CM4/CM5-class compute + module, common vacuum LiDARs, sourced Roborock/Dreame/Xiaomi motors and wear + parts) and FDM-printable chassis parts. +- *Safety is reviewed, not crowd-trusted.* Battery, charging, and motor-driver + modules pass a maintainer safety review before merge (see §8). +- *Reference-design backed.* A known-working vacuum (see the references in the + [README](../README.md)) anchors the geometry and proves feasibility. + +## 3. System overview + +``` + LiDAR (UART, ~5 Hz) · MIPI camera(s) · IMU · serial audio + | + +-------------v-------------------------------+ + | CPU - CM4 / CM5 (or pin-compatible module) | + | ROS2 · SLAM (slam_toolbox) · Nav2 · behavior | + | educational variant: ESP32-S3 + micro-ROS | + | (SLAM offboard on a dev PC over Wi-Fi) | + +-------------^----------------+---------------+ + serial (cmds/telemetry) + | custom serial protocol + CPU-reset / health GPIO | (NOT micro-ROS) + +-------------+----------------v---------------+ + | MCU - STM32G070 (FreeRTOS, static alloc) | + | motors · encoders · sensors · charging ctrl | + | SAFETY (no Linux/ROS2): bumper/cliff/wheel- | + | drop stop · current limit · CPU watchdog | + +-------------+--------------------+-----------+ + | | + +----------+----+ +--------+---------+ + | L/R drive | | suction fan, | + | wheels, brush | | bumper, cliff, | + | | | IR, wheel-drop | + +---------------+ +------------------+ + + Power: off-the-shelf 4S2P Li-ion pack (built-in BMS). + The CPU module + MCU sit on one carrier I/O board. +``` + +> The CPU/MCU split keeps *all hard safety on the MCU*, independent of Linux/ROS2. +> Interfaces are largely decided (see §5); refine as the io-pcb spec settles. + +## 4. Coordinate frames and conventions + +- *TBD:* Define `base_link` origin and orientation (REP-103: x-forward, + y-left, z-up). All mechanical mounting points and URDF frames reference this. +- *TBD:* Define the reference plane (floor contact), robot diameter, and height + envelope. *These two numbers gate every hardware module.* Source these from the *sourced + parts* + a 3D-scanned donor (see [source-3d-models](../contributions/source-3d-models)); + the current baseline is a ~349 mm round body ([oomwoo-one URDF](https://github.com/makerspet/oomwoo-one)). +- Units: millimeters, kilograms, SI. Right-handed frames. Angles in radians. + +## 5. Hardware architecture + +### 5.1 Chassis and reference frame +The chassis is the *integration backbone*. It publishes the mounting interface +every other hardware module targets. Reference geometry (dimensions, wheelbase, +motor specs, mass) comes from the *sourced parts* ([BOM](../BOM.md)) + 3D scans of +a donor vacuum (see [source-3d-models](../contributions/source-3d-models)); the +[oomwoo-one URDF](https://github.com/makerspet/oomwoo-one) carries the current +~349 mm round-body baseline. + +- *TBD:* Overall diameter and height budget. +- *TBD:* Mounting grid / bolt pattern standard (e.g., M3 on a defined pitch). +- *TBD:* Mass budget per module and total target mass. + +### 5.2 Mechanical interface standard (the contract) +Every hardware module's RFC must specify, against this standard: +- Mounting points (bolt pattern, location relative to `base_link`). +- Bounding envelope (max size the module may occupy). +- Mass budget. +- Mating tolerances and print orientation. +- *TBD:* Define the standard connector/fastener set (screw sizes, heat-set + inserts, etc.) so parts from different authors actually mate. + +### 5.3 Electrical interface standard (the contract) +- *Battery:* off-the-shelf pack with a *built-in BMS* — *4S2P Li-ion*, ~14.4 V + nominal, ~5200 mAh / ~75 Wh (OEM BRR-2P4S-5200 class), charged 16.8 V CC/CV with + NTC temperature sense. Chemistry is now decided; see the [BOM](../BOM.md). +- *TBD:* Power rails distributed to modules (VBAT, 5V, 3.3V) and connector types/pinouts. +- *CPU ↔ MCU:* a *custom high-speed serial protocol* (not micro-ROS) carries + commands/telemetry, plus discrete GPIOs (CPU power on/off, and a CPU-reset line + the MCU asserts on missed health packets). The *MCU owns motors and sensors*; the + CPU never drives them directly. +- *Sensors:* bumper/cliff/wheel-drop and analog IR are *MCU-side* (digital in / ADC); + the *LiDAR (UART, ~5 Hz)*, MIPI camera(s), IMU, and serial audio attach to the *CPU*. + +### 5.4 Compute (CPU) and real-time controller (MCU) + +OOMWOO splits compute across two processors — mirroring how consumer vacuums are +built, and, crucially, so that *safety never depends on Linux/ROS2*. + +*CPU (compute module).* The I/O board is a *carrier* that accepts a *Raspberry Pi +Compute Module 4 or 5* and — because the CM4 pinout is a de-facto standard — the +many *pin-compatible alternative modules* (Radxa CM3/CM4, Pine64 SOQuartz, LuckFox +Core3566, …), several with an *NPU* for future on-device vision. The CPU runs +*ROS2, SLAM (slam_toolbox), Nav2, LiDAR processing, and high-level behavior*. CM +modules are low-profile (helps the height budget) and swappable (hackable, cheaper, +NPU options). +- *Minimum target: a 4 GB CM4/CM5 (or Pi 4).* Realistic prior art runs + slam_toolbox + Nav2 onboard a 4 GB Pi 4. Getting the floor to *2 GB* is a goal + (ROS2 composable nodes; selectively rewriting heavy Python nodes in Rust/C++) — + *no guarantee*, to be settled by the compute-benchmark. No 8–16 GB module needed. +- *Cooling:* no dedicated CPU fan — the suction fan's airflow cools the compute + board, as in consumer vacuums. +- The earlier bespoke *RK3562* reference schematic is *dropped* in favour of the + CM4/CM5 carrier. + +*MCU (real-time / safety controller).* A dedicated microcontroller — *tentatively +the STM32G070RBT6* (~56 GPIO incl. 16 ADC channels, ~$1 at JLCPCB, LQFP not BGA) — +owns *motors, encoders, all sensors, battery-charging control, and safety*. Its +role is *fixed*: functionality does not migrate onto the CPU, and CPU work does not +migrate onto the MCU. +- Firmware is *tentatively FreeRTOS* (static allocation, watchdog, guaranteed + reaction times, CE-oriented) speaking a *custom serial protocol — not micro-ROS* + (the tried-and-true consumer-vacuum approach; cf. the reverse-engineered + [3irobotix protocol](https://github.com/codetiger/VacuumRobot)). +- *Hard safety lives here, independent of Linux/ROS2:* the MCU stops all motors on + a *bumper hit, cliff detection, or wheel-drop*, current-limits a *stuck brush*, + and *watchdogs the CPU* — if the CPU's health packets stop, it stops the motors + and can *reset the CPU*. +- Tentative ~60-signal pin budget: see the [io-pcb RFC](../contributions/io-pcb) + appendix (why the MCU needs a high-GPIO part). + +*CPU ↔ MCU link.* A *high-speed serial* channel carries commands/telemetry both +ways, plus discrete GPIOs — notably the *CPU-reset* line the MCU asserts on missed +health packets, and CPU power on/off. Any LiDAR supported by `kaiaai/LDS` / +`lds2d` is interface-compatible. + +### 5.5 Two build profiles + +The same carrier I/O board + MCU supports two swappable compute configurations: + +| | *Consumer / regular* | *Educational / lower-cost* | +|---|---|---| +| CPU-slot module | CM4 / CM5 (or pin-compatible alt) | *ESP32-S3* board in the CM4 form factor | +| Where ROS2 / SLAM runs | *onboard* (ROS2 + slam_toolbox + Nav2) | *offboard* on a local dev PC; ESP32-S3 runs *micro-ROS* | +| Link | self-contained robot | robot ↔ dev PC over *Wi-Fi* | +| Trade-off | plug-and-play for non-experts | cheaper, but Wi-Fi congestion / dead-zones — a learning platform, not a polished consumer product | + +Onboard SLAM is the default for the consumer version *so non-experts can build and +use it* without setting up a separate ROS2 dev machine. The ESP32-S3 can only be +the *CPU-slot* option — it lacks the ~60 GPIO the MCU role needs, so it never +replaces the STM32. + +## 6. Software architecture + +### 6.1 ROS2 graph (MVP) +- Core nodes (MVP): LiDAR driver, base controller (diff-drive), odometry, + teleop, SLAM (manual mapping), TF/URDF publisher. +- *Interface contract:* each software module's RFC declares the ROS2 topics, + services, message types, and parameters it publishes/consumes. Modules depend + on these interfaces, not on each other's code. See + [SOFTWARE_INTERFACES.md](SOFTWARE_INTERFACES.md) for the current draft ROS2 + graph contract. + +### 6.2 Simulation +- Gazebo + URDF, with a set of residential-layout worlds for navigation and + coverage testing. Sim parity is a first-class requirement, not an afterthought. + +### 6.3 Application layer (Phase 2 — north star, NOT in MVP) +A ROS2-agnostic layer that runs third-party apps locally in isolated *Podman* +containers, so app developers need no ROS2 expertise. Documented here only to +keep its boundary clean; *explicitly out of scope for the Aug 31 MVP.* + +## 7. MVP definition (target: 2026-08-31) + +*In scope:* ROS2 on a CM4/CM5-class compute module · LiDAR · manual SLAM/mapping · teleop drive · +3D-printed chassis · Gazebo sim with URDF · evaluation + demo video. No dock, +no autonomous exploration, no Home Assistant, no app layer. + +*Explicit non-goals for MVP:* autonomous coverage, docking, auto-empty, mopping, +Home Assistant, the app platform, accessories. These are later phases. + +*Critical-path ownership:* the maintainer (+ small core) own the chassis, +interface specs, and integration so the MVP does not depend on volunteer delivery +timing. Community modules accelerate and improve the MVP; they do not block it. + +## 8. Safety review gate + +Battery, charging, motor-driver, and mains-adjacent modules require maintainer +safety review before merge. RFCs for these modules must include a hazard note +(over-current, thermal, short, mechanical pinch). + +The battery risk is *reduced* by using an *off-the-shelf 4S2P Li-ion pack with a +built-in BMS* (over-charge / over-discharge / short protection); the review then +focuses on the *16.8 V CC/CV charging path + NTC temperature sense*. *Hard safety +lives on the MCU, never on Linux/ROS2* — it independently stops motors on +bumper/cliff/wheel-drop, current-limits a stuck brush, and watchdog-resets the CPU. + +## 9. Roadmap (phases after MVP) + +1. Rechargeable battery + basic dock + autonomous floor mapping. +2. Home Assistant integration. +3. Application layer (Podman app runtime) + first delightful apps. +4. Accessories and novel apps; integrations (e.g., LeRobot arm). + +## 10. Open questions + +- *Resolved:* the MCU runs a *custom serial protocol (not micro-ROS)*; the CPU runs + *onboard ROS2/SLAM/Nav2*. micro-ROS is used only in the *educational* ESP32-S3 + profile (SLAM offboard on a dev PC). See §5.4–5.5. +- *Resolved:* battery is an off-the-shelf *4S2P Li-ion pack with a built-in BMS*, + charged 16.8 V CC/CV. See §5.3, §8. +- Can OOMWOO's onboard ROS2 stack fit in *2 GB* (composable nodes, selective Rust) + rather than 4 GB? To be answered by the compute-benchmark. +- MCU family: *STM32G070RBT6* is the tentative pick (GPIO/ADC count, ~$1 at JLCPCB, + LQFP) — open to alternatives. +- One hardware-agnostic HAL covering reference vacuum + DIY builds (community idea)? +- Module selection process: who decides which competing implementation wins, and + on what criteria? (See each module's acceptance criteria.) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..65b97d5 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,103 @@ +# Contributing to OOMWOO + +Thanks for your interest. OOMWOO is an open-source robot vacuum you build +yourself, and it's at a very early stage. That's the best time to get involved, +the foundations are still being laid and your input can shape the direction. + +The project is built *module by module* so many people can work in parallel. +Browse the [module list in the README](../README.md#requests-for-contributions), +and see [ARCHITECTURE.md](ARCHITECTURE.md) for how the pieces fit together. + +## Ways to help right now + +You don't need to be a roboticist to contribute: + +- *Ideas and feedback* — open a [Discussion](https://github.com/makerspet/oomwoo/discussions) + about features, design choices, or what would make you build one. +- *Code* — firmware, ROS2 packages, Home Assistant integration. +- *Hardware* — 3D-printable chassis design, mechanical parts, PCB. +- *Documentation* — build guides, wiring diagrams, troubleshooting notes. +- *Testing* — once there's something to build, real-world build reports are gold. +- *Spread the word* — star the repo, share your build, post a demo. + +## Getting started + +1. *Pick a module.* Browse the + [module list in the README](../README.md#requests-for-contributions) and choose a + hardware or software module. Software and simulation modules can start + immediately; hardware modules wait on the interface specs in + [ARCHITECTURE.md](ARCHITECTURE.md). Read the module's `README.md` so you know + the contract. +2. *Start a conversation first.* Claim or ask about the module in its + [Issue](https://github.com/makerspet/oomwoo/issues) or + [Discussion](https://github.com/makerspet/oomwoo/discussions) before writing + code, so we align on the approach and avoid wasted effort. +3. *Build it in your own repo.* For code and simulation modules, develop your + package in your **own public repository** — you own it, version it, and keep the + credit. Build against the ROS2 interface contract in + [SOFTWARE_INTERFACES.md](SOFTWARE_INTERFACES.md) so your work stays interoperable + with other modules. (Docs and small reference material are handled differently — + see below.) +4. *Submit a pointer PR.* Add a link to your repo in the module's entry with a + one-line description. It's small, easy to review, and lets several + implementations of the same module sit side by side. Keep the PR focused. +5. *Iterate in the open.* Modules are swappable — the best implementation surfaces + over time, with the maintainer having the last call. A design that isn't + selected is still a useful fallback. + +## How contributions are structured + +OOMWOO keeps the core small and lets the community grow around it: + +- *Canonical / reference code stays first-party.* [oomwoo-one](https://github.com/makerspet/oomwoo-one) + (robot description + sim), [oomwoo-install](https://github.com/makerspet/oomwoo-install) + (dev environment), and the `kaiaai_*` packages are maintained by the project so the + out-of-the-box build always works. +- *Module implementations (code) live in your repo.* You build a competing + implementation of a module — a sim, a navigation stack, a behavior — in your own + repository and submit a *link*. The project features accepted work from the + module's page, credited to you. When a contribution is *featured*, we pin a + specific commit or tag (and may fork it into the makerspet org) so the reference + build stays reproducible even if the upstream repo moves. +- *Docs, specs and small reference material stay in-tree.* Part specifications, + datasheets, STEP-model sourcing notes, PCB notes, and benchmarks are lightweight + and best kept alongside the project — contribute those under + `contributions///` as files in a PR. + +Why links for code? You keep ownership, credit, and freedom to iterate; the project +stays lean and avoids absorbing third-party code and its licensing; and multiple +implementations of a module can coexist and be compared. The shared +[SOFTWARE_INTERFACES.md](SOFTWARE_INTERFACES.md) contract is what keeps +independently-built modules compatible. + +## Hardware contributions + +For CAD and mechanical work, please include source files (not just exported STLs) +where possible, so others can modify your design. Note the tool and version you +used. If your change affects the bill of materials, mention it in the PR. Each +hardware module must stay within the mechanical/electrical interfaces in +[ARCHITECTURE.md](ARCHITECTURE.md). + +*Safety:* battery, charging, motor-driver, and mains-adjacent modules require a +maintainer safety review before merge. Include a hazard note in your submission. + +## Code style + +Conventions are still being established. For now: keep it simple, readable, and +consistent with the surrounding code. ROS2 packages should follow standard ROS2 +layout and naming. We'll formalize linting and style as the codebase grows. + +## Licensing + +By contributing, you agree that your contributions are licensed under the +project's [Apache License 2.0](../LICENSE). Hardware design files will be released +under an open hardware license (to be finalized); contributions of hardware +files are made on that same open basis. + +## Community and conduct + +Be respectful, helpful, and welcoming. We want OOMWOO to be an easy, friendly +place for makers of every skill level. Harassment or hostility isn't tolerated. + +Questions? Open a [Discussion](https://github.com/makerspet/oomwoo/discussions?discussions_q=) +or join us on [Discord](https://discord.gg/3y2JKz5T25). diff --git a/docs/RFC_BACKLOG.md b/docs/RFC_BACKLOG.md new file mode 100644 index 0000000..fc72a9c --- /dev/null +++ b/docs/RFC_BACKLOG.md @@ -0,0 +1,64 @@ +# RFC Backlog (planned / not-yet-active) + +Planned modules that are *not yet active RFCs*. The RFCs ready to work on *now* live in the +[README status table](../README.md#requests-for-contributions), and each active RFC's full +spec is under [contributions/](../contributions). An item here graduates into an active RFC +(its own `contributions//` folder) once it is unblocked and ready. + +*Phase legend:* `MVP` = targeted for the bare-bones build · `P2` = next phase · +`P3+` = later. *Safety* = requires a maintainer safety review. + +## On hold (have an RFC, paused pending sourced parts + a 3D reference design) + +| Module | RFC | Notes | +|---|---|---| +| Dust bin 3D design | [dust-bin](../contributions/dust-bin) | Design / print / test the dust bin — waits on sourced parts + a 3D design | +| Blower fan assembly | [vacuum-fan](../contributions/vacuum-fan) | Fans already sourced (see BOM); the volute / gasket housing waits on the 3D design | + +## Planned hardware (mechanical design) + +Waits on sourced parts + a 3D reference-design sketch, then becomes an active +`contributions/` RFC. Part *specs* and *STEP models* are already active — see +[part-specs](../contributions/part-specs) and [source-3d-models](../contributions/source-3d-models). +The motor-driver / power PCB and battery charging are now the active +[io-pcb](../contributions/io-pcb) RFC. + +| Module | ID | Phase | Notes | +|---|---|---|---| +| Chassis / base frame (reference) | `hw-chassis` | MVP | Integration backbone; defines the mechanical interface. Maintainer-owned. | +| Drive-wheel mounts (L/R) | `hw-wheel-mount` | MVP | Mount + suspension around the sourced wheel modules. | +| Compute mount (RPi 5) | `hw-compute-mount` | MVP | Mount + airflow for the Pi 5. | +| LiDAR mount | `hw-lidar-mount` | MVP | Centered turret; parametric for other models. | +| Main brush assembly | `hw-main-brush` | MVP | *Tapered rubber anti-tangle roller* + drive. | +| Bumper (mechanical + switches) | `hw-bumper` | MVP | Contact detection. | +| Cliff-sensor mounts | `hw-cliff` | MVP | IR drop detection at edges / stairs. | +| Top cover / shell | `hw-shell` | MVP | Cosmetic + protective; LiDAR clearance. | +| Wiring harness | `hw-harness` | MVP | Connector pinouts per [io-pcb](../contributions/io-pcb) + [part-specs](../contributions/part-specs). | +| Side brush | `hw-side-brush` | P2 | Edge cleaning. | +| Mop module (dual-spinning) | `hw-mop` | P2 | 3D-printed *dual-spinning* pads; skip the self-washing roller. | +| Charging dock (basic) | `hw-dock` | MVP | *Safety.* Contacts + alignment. | +| Charging dock (auto-empty) | `hw-dock` | P2 | | +| Charging dock (mop wash/dry) | `hw-dock` | P2 | | + +## Planned software (later phase) + +Foundational software (URDF, sim, SLAM, Nav2, coverage, teleop, sensors) is already covered by +the active RFCs. Remaining, later-phase work: + +| Module | ID | Phase | Notes | +|---|---|---|---| +| Diagnostics / telemetry | `sw-diagnostics` | P2 | Health, logs. | +| Regression / CI tests | `sw-regression-tests` | P2 | Sim-based tests gating PRs. | +| Home Assistant integration | `sw-homeassistant` | P2 | MQTT / HA entity, map, control. | +| App runtime layer | `sw-app-runtime` | P3+ | ROS2-agnostic app sandbox. North star. | +| Web UI / dashboard | `sw-webui` | P3+ | Local control + map view. | + +## Non-engineering contributions (also wanted) + +| Track | Phase | Notes | +|---|---|---| +| 3D-print validation | MVP | Confirm parts print cleanly on common FDM printers. | +| Real-home testing | MVP/P2 | Build and report on real floors. | +| Docs / build guides | MVP | Turn working modules into step-by-step instructions. | +| Posts / videos / demos | ongoing | Content that grows the project (highly valued). | +| SOTA research | ongoing | Best-in-class prior art per module (part of each RFC). | diff --git a/docs/SOFTWARE_INTERFACES.md b/docs/SOFTWARE_INTERFACES.md new file mode 100644 index 0000000..bf05029 --- /dev/null +++ b/docs/SOFTWARE_INTERFACES.md @@ -0,0 +1,159 @@ +# OOMWOO ROS2 Software Interfaces + +> Status: DRAFT. This is the shared software contract for simulation-first +> contributions. It reflects the current `urdf-gazebo-sim` package and should be +> updated whenever a module needs a new public topic, service, action, frame, or +> parameter. + +## Purpose + +OOMWOO software modules are meant to be swappable. A contributor should be able +to build `clean-and-map`, `nav-localize`, `recovery-safety`, `dock-cycle`, or +`cleaning-jobs` without depending on another module's private implementation. + +This document defines the public ROS2 surface area those modules can share while +the hardware is still evolving. It is not a final hardware API. + +## Naming rules + +- Public names below are shown as root topics, for example `/scan`. Launch files + and node configs may use relative names such as `scan` when they resolve to + the same root topic in the default launch. +- Use REP-103 frames and SI units. +- Prefer standard ROS2/Nav2 message types before adding custom OOMWOO messages. +- If a module introduces a new public interface, document the producer, consumer, + message type, QoS expectation, and failure behavior in its README and update + this file. +- Simulation-only details, such as Gazebo collision entity names, must not leak + into cross-module contracts unless they are explicitly marked as simulation + diagnostics. + +## Frames + +| Frame | Owner | Meaning | +|---|---|---| +| `map` | SLAM/localization | Global map frame used by SLAM, AMCL, Nav2, and saved maps. | +| `odom` | Base odometry | Locally continuous odometry frame. | +| `base_footprint` | Robot description / odometry | Planar base frame for navigation. | +| `base_link` | Robot description | Main robot body frame. Hardware modules should reference this once geometry is frozen. | +| `base_scan` | Robot description | 2D LiDAR frame. | + +Open decision: `base_link` origin, reference plane, robot diameter, and height +envelope are still defined in `ARCHITECTURE.md`. + +## Baseline Topics + +These topics are provided by the current Gazebo simulation or standard Nav2/SLAM +bringup and should be treated as the MVP baseline. + +| Topic | Type | Direction | Producer | Consumers | +|---|---|---|---|---| +| `/cmd_vel` | `geometry_msgs/msg/Twist` | Command | Teleop, Nav2 velocity smoother, recovery nodes | Gazebo diff-drive / base controller | +| `/odom` | `nav_msgs/msg/Odometry` | State | Gazebo odometry / base controller | SLAM, AMCL, Nav2, recovery and job logic | +| `/tf` | `tf2_msgs/msg/TFMessage` | State | Robot state publisher, odometry, SLAM/localization | All pose-aware modules | +| `/joint_states` | `sensor_msgs/msg/JointState` | State | Gazebo joint state publisher / hardware base | Robot state publisher, diagnostics | +| `/scan` | `sensor_msgs/msg/LaserScan` | Sensor | 2D LiDAR / Gazebo LiDAR | SLAM, AMCL, Nav2 costmaps, wall following | +| `/map` | `nav_msgs/msg/OccupancyGrid` | State | SLAM or map server | Nav2, cleaning, zones, visualization | +| `/bumper_left` | `ros_gz_interfaces/msg/Contacts` in Gazebo | Sensor | Gazebo left contact sensor | Recovery, safety, clean-and-map obstacle handling | +| `/bumper_right` | `ros_gz_interfaces/msg/Contacts` in Gazebo | Sensor | Gazebo right contact sensor | Recovery, safety, clean-and-map obstacle handling | + +### Bumper events + +The current simulation publishes raw Gazebo contact messages: + +- Message type: `ros_gz_interfaces/msg/Contacts` +- Contact list field: `contacts` +- Per-contact fields: `collision1`, `collision2`, `positions`, `normals`, + `depths`, `wrenches` + +Consumers should treat `len(msg.contacts) > 0` as a bumper event after filtering +out ground-plane contacts. Do not read a `collisions` field; that belongs to the +single-contact message type and is not what the bridge publishes. + +Hardware may eventually replace raw Gazebo contacts with a normalized bumper +message. Until that decision is made, module submissions should isolate the +Gazebo-specific parsing behind a small adapter. + +## Nav2 Interfaces + +Modules should reuse Nav2 actions and servers where possible. + +| Interface | Type | Typical consumer | +|---|---|---| +| `/navigate_to_pose` | `nav2_msgs/action/NavigateToPose` | `nav-localize`, `cleaning-jobs`, `dock-cycle` | +| `/navigate_through_poses` | `nav2_msgs/action/NavigateThroughPoses` | Coverage, room jobs, dock approach | +| Nav2 behavior server | `spin`, `backup`, `drive_on_heading`, `wait` behaviors | Recovery and local fallback logic | +| Costmaps | Nav2 local/global costmap topics | Obstacle handling, zones, diagnostics | +| Map saver | Nav2 map saver service/CLI | `clean-and-map`, `nav-localize` | + +If a module needs to command motion directly, it must define how it arbitrates +with Nav2 and recovery nodes so two nodes do not fight over `/cmd_vel`. + +## Module Contracts + +| Module | Inputs | Outputs / public behavior | +|---|---|---| +| `urdf-gazebo-sim` | `/cmd_vel` | Publishes `/scan`, `/odom`, `/tf`, `/joint_states`, `/bumper_left`, `/bumper_right`; provides worlds and robot description. | +| `clean-and-map` | `/scan`, `/odom`, `/tf`, bumper events, optional Nav2 actions | Drives first-pass coverage, produces a complete map, defines a done condition, saves map artifacts. | +| `nav-localize` | Saved map, `/scan`, `/odom`, `/tf`, Nav2 bringup | Provides known-map navigation, relocalization, and map-resume behavior. | +| `recovery-safety` | Bumper events, future cliff/wheel-drop/pickup/e-stop signals, Nav2 failures | Stops or gates motion, runs bounded recoveries, publishes clear pause/error status. | +| `floor-care` | `/scan`, map/coverage context, future surface sensor | Provides wall/edge following, surface classification, and mop actuator decisions. | +| `cleaning-jobs` | Saved map, zones, coverage progress, battery/bin/mop status, Nav2 actions | Provides start/pause/resume/cancel/status job behavior suitable for a future Home Assistant layer. | +| `dock-cycle` | Nav2/localization, dock marker, battery/service state | Provides undock, return-to-dock, precise docking, recharge/service completion, and find-dock fallback. | +| `live-robot-bringup` | Hardware drivers, same logical topics | Validates that hardware exposes the same public interfaces as the simulation. | + +## Status and Errors + +The final robot status API is still open. Until it is selected, modules that +need status reporting should document: + +- `state`: short machine-readable state, for example `cleaning`, `recovering`, + `paused`, `docked`, or `error` +- `reason_code`: stable machine-readable reason, for example `BUMPER_STUCK`, + `LOCALIZATION_LOST`, or `LOW_BATTERY` +- `message`: human-readable explanation +- `recoverable`: whether a resume command is expected to work +- `source`: module name that produced the status + +Open decision: choose the transport and type for cross-module status, likely a +standard diagnostic message or a small OOMWOO-specific message package. + +## QoS and Parameters + +- `use_sim_time` should be true in simulation launch files. +- Sensor streams such as `/scan` should use sensor-data QoS where configurable. +- `/map` and saved-map metadata should be available to late joiners where the + producer supports transient-local durability. +- Command topics should use small queues and should fail safe: stale commands + must not keep the robot moving. + +## Validation Checklist + +A module submission that depends on the MVP simulation should document how to +check the interfaces it uses. At minimum: + +```bash +ros2 topic list +ros2 topic echo /scan --once +ros2 topic echo /odom --once +ros2 topic echo /bumper_left +ros2 topic echo /bumper_right +ros2 run tf2_tools view_frames +``` + +For Nav2-based modules, also document how to send a `NavigateToPose` goal and +how to confirm `/cmd_vel` arbitration is safe. + +## Miscellaneous + +- PR your packages into the official distribution [makerspet/oomwoo-install](https://github.com/makerspet/oomwoo-install) +- put your ROS2 packages under `/ros_ws/src` (don't create another colcon workspace under `~/`) +- follow conventions of being able to select a robot package using `kaia config robot.model oomwoo_one`, see [tutorial](https://makerspet.com/blog/simulate-oomwoo-one-robot-vacuum-in-gazebo-with-ros-2/) + +## Open Decisions + +- Final hardware bumper/cliff/wheel-drop message shape. +- Robot-wide status/error message type and topic. +- Battery, dust-bin, mop, and dock service state interfaces. +- Localization-confidence interface for relocalization and kidnap detection. +- Job action/service API for start, pause, resume, cancel, and status. diff --git a/docs/blog/oomwoo-one-first-ros2-package.md b/docs/blog/oomwoo-one-first-ros2-package.md new file mode 100644 index 0000000..7c6f92e --- /dev/null +++ b/docs/blog/oomwoo-one-first-ros2-package.md @@ -0,0 +1,169 @@ +# Write Your First OOMWOO ROS 2 Package: Cover the Floor While Mapping + +> *Draft for makerspet.com (WordPress / Gutenberg).* Post 2 of 2. Builds on Post 1 +> (*Simulate the oomwoo-one Robot Vacuum in Gazebo with ROS 2*). Here you'll write a small +> *pure-ROS 2* package that drives `oomwoo-one` on a coverage path *while* SLAM maps the +> room, and launch it all with one `ros2 launch`. + +This is the "hello world" of developing for OOMWOO. It's deliberately simple — a reactive +"drive forward, turn when blocked" coverage that, combined with SLAM, maps and roughly covers +a room. Proper boustrophedon coverage is the [clean-and-map +RFC](https://github.com/makerspet/oomwoo/tree/main/contributions/clean-and-map); this teaches +you the mechanics of writing and launching an OOMWOO node. + +## Prerequisites + +The dev environment from *Post 1* (the `makerspet/oomwoo:jazzy-dev` container running, with +`kaia config robot.model oomwoo_one` set). Everything below runs *inside the container*. + +## 1. Create a ROS 2 package + +``` +mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src +ros2 pkg create --build-type ament_python oomwoo_coverage --dependencies rclpy sensor_msgs geometry_msgs +``` + +## 2. Write the coverage node + +Create `~/ros2_ws/src/oomwoo_coverage/oomwoo_coverage/coverage_node.py`: + +```python +import math +import rclpy +from rclpy.node import Node +from sensor_msgs.msg import LaserScan +from geometry_msgs.msg import Twist + + +class Coverage(Node): + """Drive forward; when the path ahead is blocked, turn until it clears. + Combined with SLAM, this bounces around a room and maps it.""" + + def __init__(self): + super().__init__('coverage') + self.pub = self.create_publisher(Twist, 'cmd_vel', 10) + self.create_subscription(LaserScan, 'scan', self.on_scan, 10) + self.clear = True + self.turn_dir = 1.0 + self.create_timer(0.1, self.tick) # 10 Hz control loop + + def on_scan(self, msg): + n = len(msg.ranges) + if n == 0: + return + # Check the forward +/- 25 deg sector (forward = scan index 0 for this LiDAR; + # adjust 'center' if your scan's zero points elsewhere). + sector = int(math.radians(25) / msg.angle_increment) + idxs = [(i) % n for i in range(-sector, sector + 1)] + fwd = [msg.ranges[i] for i in idxs + if msg.range_min < msg.ranges[i] < msg.range_max] + was_clear = self.clear + self.clear = (min(fwd) > 0.35) if fwd else True + if was_clear and not self.clear: + self.turn_dir *= -1.0 # alternate turn direction each time we hit something + + def tick(self): + cmd = Twist() + if self.clear: + cmd.linear.x = 0.20 # m/s forward + else: + cmd.angular.z = 0.8 * self.turn_dir # rad/s turn until clear + self.pub.publish(cmd) + + +def main(): + rclpy.init() + node = Coverage() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + node.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() +``` + +## 3. Register the node + +In `~/ros2_ws/src/oomwoo_coverage/setup.py`, add the console script: + +```python + entry_points={ + 'console_scripts': [ + 'coverage = oomwoo_coverage.coverage_node:main', + ], + }, +``` + +## 4. Add a launch file (sim + SLAM + your node, in one command) + +Create `~/ros2_ws/src/oomwoo_coverage/launch/coverage.launch.py`: + +```python +import os +from launch import LaunchDescription +from launch.actions import IncludeLaunchDescription, TimerAction +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch_ros.actions import Node +from ament_index_python.packages import get_package_share_directory + + +def generate_launch_description(): + gazebo = IncludeLaunchDescription(PythonLaunchDescriptionSource( + os.path.join(get_package_share_directory('kaiaai_gazebo'), + 'launch', 'world.launch.py'))) + + nav = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(get_package_share_directory('kaiaai_bringup'), + 'launch', 'navigation.launch.py')), + launch_arguments={'use_sim_time': 'true', 'slam': 'True'}.items()) + + coverage = Node(package='oomwoo_coverage', executable='coverage', + name='coverage', parameters=[{'use_sim_time': True}]) + + # give Gazebo + SLAM ~12 s to come up before the robot starts moving + return LaunchDescription([gazebo, nav, TimerAction(period=12.0, actions=[coverage])]) +``` + +Reference the launch folder in `setup.py` `data_files` so it installs: +```python + (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')), +``` +(add `import os` and `from glob import glob` at the top of `setup.py`). + +## 5. Build and run + +``` +cd ~/ros2_ws +colcon build --packages-select oomwoo_coverage +source install/setup.bash +ros2 launch oomwoo_coverage coverage.launch.py +``` + +Gazebo and RViz open, SLAM starts, and after ~12 s oomwoo-one begins driving itself — +forward until it's blocked, then turning — while the map fills in. Open RViz +(`ros2 launch kaiaai_bringup monitor_robot.launch.py use_sim_time:=true` in another shell) to +watch the map grow. + +## 6. Save the map + +``` +ros2 run nav2_map_server map_saver_cli -f ~/maps/map +``` + +## What you just learned + +- created a ROS 2 package, wrote a node that *subscribes to `/scan` and publishes `/cmd_vel`*, +- combined *your code + SLAM* in a single `ros2 launch`, +- produced a real map from an autonomous run. + +That's the whole loop of developing for OOMWOO. From here, the natural next step is *real +coverage path planning* (boustrophedon, wall-following, frontier exploration) — which is +exactly the [clean-and-map RFC](https://github.com/makerspet/oomwoo/tree/main/contributions/clean-and-map). +Pick it up (or another module) from the [Requests for +Contributions](https://github.com/makerspet/oomwoo#requests-for-contributions), and come build +with us on [Discord](https://discord.gg/3y2JKz5T25). diff --git a/docs/blog/oomwoo-one-simulate-in-gazebo.md b/docs/blog/oomwoo-one-simulate-in-gazebo.md new file mode 100644 index 0000000..2e3ccf2 --- /dev/null +++ b/docs/blog/oomwoo-one-simulate-in-gazebo.md @@ -0,0 +1,127 @@ +# Simulate the oomwoo-one Robot Vacuum in Gazebo with ROS 2 + +> *Draft for makerspet.com (WordPress / Gutenberg).* Post 1 of 2: set up the OOMWOO +> software dev environment and drive `oomwoo-one` in simulation — no robot required. +> (Post 2 teaches you to write your own OOMWOO code.) + +[OOMWOO](https://github.com/makerspet/oomwoo) is an open-source robot vacuum you build +yourself. *oomwoo-one* is the first model. This tutorial gets its ROS 2 simulation running +in *Gazebo* so you can develop mapping, navigation, and cleaning behaviours with *no +hardware* — everything runs in Docker on *Ubuntu or Windows*. + +You'll get: SLAM mapping, autonomous Nav2 navigation, manual driving, and bumper sensors — +the same interfaces the real robot will expose. + +## Prerequisites + +- *Docker* — Docker Desktop on Windows/macOS, Docker Engine on Linux. +- *An X server* for the GUI windows (Gazebo, RViz): + - *Windows:* [VcXsrv](https://sourceforge.net/projects/vcxsrv/) (XLaunch). + - *Linux:* native X — nothing to install. +- No physical robot. + +## 1. Start the X server (Windows only) + +Launch *XLaunch* (from VcXsrv) and accept the defaults *except*: + +> On the *"Display settings"* page, set *Display number = `0`* (not `-1`). +> The Docker container connects to `host.docker.internal:0.0`, so the display number *must +> be 0* or no GUI windows will appear. + +Also tick *"Disable access control"* on the "Extra settings" page so the container can +connect. Finish the wizard — a tiny X icon appears in your tray. + +On *Linux*, instead allow local Docker to reach your X server: +```bash +xhost +local:docker +``` + +## 2. Pull the OOMWOO Docker image + +``` +docker pull makerspet/oomwoo:jazzy-dev +``` + +## 3. Start the container + +*Windows (PowerShell):* +```powershell +docker run --name makerspet -it --rm -v c:\maps:/root/maps -p 8888:8888/udp -p 5555:5555/udp -e DISPLAY=host.docker.internal:0.0 -e LIBGL_ALWAYS_INDIRECT=0 --add-host=host.docker.internal:host-gateway makerspet/oomwoo:jazzy-dev +``` +(`DISPLAY=...:0.0` matches the XLaunch *display 0* from step 1.) + +*Ubuntu / Linux:* +```bash +docker run --name makerspet -it --rm -v ~/maps:/root/maps -p 8888:8888/udp -p 5555:5555/udp -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix --network host makerspet/oomwoo:jazzy-dev +``` + +Need more terminals into the same container? Open another PowerShell/terminal and run: +``` +docker exec -it makerspet bash +``` + +## 4. Select the oomwoo-one model + +Inside the container: +``` +kaia config robot.model oomwoo_one +``` + +## 5. Launch the Gazebo world + +``` +ros2 launch kaiaai_gazebo world.launch.py +``` +A Gazebo window opens with oomwoo-one in a living-room world. + +## 6. Start SLAM mapping + +In a new container shell (`docker exec -it makerspet bash`): +``` +ros2 launch kaiaai_bringup navigation.launch.py use_sim_time:=true slam:=True +``` + +## 7. Open the RViz monitor + +``` +ros2 launch kaiaai_bringup monitor_robot.launch.py use_sim_time:=true +``` +Watch the map build as the robot moves. + +## 8. Drive it manually + +``` +ros2 run kaiaai_teleop teleop_keyboard +``` +Use the keyboard to drive oomwoo-one around and fill in the map. + +## 9. Autonomous navigation + +In RViz, click *"Nav2 Goal"* and click-drag a destination — oomwoo-one plans a path and +drives there on its own. + +## 10. Check the bumper sensors + +``` +ros2 topic echo /bumper_left +ros2 topic echo /bumper_right +``` +Drive into a wall and watch the left/right contact events fire. + +## 11. Save your map + +``` +ros2 run nav2_map_server map_saver_cli -f ~/maps/map +``` +On Windows the map lands in `c:\maps`; on Linux in `~/maps`. + +## What's next + +You now have a full oomwoo-one simulation: SLAM, Nav2, teleop, and bumpers, exactly the +interfaces the real robot exposes. In *Post 2* you'll write your *first OOMWOO ROS 2 +package* — a node that drives a coverage path *while* mapping — and launch it with +`ros2 launch`. + +Want to help build OOMWOO? Grab a module from the [Requests for +Contributions](https://github.com/makerspet/oomwoo#requests-for-contributions) or say hi on +[Discord](https://discord.gg/3y2JKz5T25). diff --git a/docs/design-document.md b/docs/design-document.md new file mode 100644 index 0000000..e66b798 --- /dev/null +++ b/docs/design-document.md @@ -0,0 +1,332 @@ +# OOMWOO Design Document + +> *Working design doc.* This accumulates research-backed design decisions before +> they're split into per-module [RFCs](../contributions) and the [RFC backlog](RFC_BACKLOG.md), and/or migrated into +> the [README](../README.md). A condensed version of §1 already lives in the +> README's [Design research](../README.md#design-research) section. Expect this to +> grow as remaining components are researched. + +--- + +## 1. Design research — what makes vacuum users happy + +We reviewed the 2025–2026 consumer robot vacuum landscape (global + China-sourceable +brands, budget → flagship) across RTINGS, VacuumWars, Reddit r/robotvacuums, and +reviewer blogs, then adversarially fact-checked the key claims. The point: copy the +solutions that correlate with happy users, skip the ones that need commercial scale. + +### Suction — a sourcing problem, not an engineering one +Real-world cleaning does *not* track advertised suction (Pa). ~$500 mid-tier +models beat flagships in pickup tests. A moderate *sealed* sourced motor + a good +brush + a *tight airflow seal* matches flagship cleaning — *no custom-molded +impeller needed.* Airflow sealing (bin/fan/brush seams) matters more than raw Pa. +[(source)](https://www.thesmarthomehookup.com/the-best-300-600-robot-vacuums-they-beat-the-flagships/) + +### Navigation & "never gets stuck" — the #1 user pain, hardest to replicate +Best obstacle avoidance comes from *sensor fusion* (LiDAR + a floor-level 3D-ToF +or RGB camera + AI object recognition), not any single sensor. LiDAR is structurally +*blind below its ~10 cm turret*, which is exactly why robots eat cables and socks. +The Eufy Omni S2 ($1,599) was the only model in one test to pass all 24 obstacles — +and it has the full vision stack. *Never-stuck is commercial-scale.* +- *For OOMWOO:* v1 leans on the *bumper* for low/LiDAR-invisible obstacles (this + is already how the [clean-and-map RFC](../contributions/clean-and-map) handles it). + Camera + AI vision avoidance is a *later / experimental* goal, not an MVP promise. + Don't position OOMWOO as out-navigating commercial flagships; position it as an + open platform to *experiment* with navigation and vision. +[(source)](https://vacuumwars.com/best-robot-vacuums-with-obstacle-avoidance/) + +### Brush — anti-tangle is what users notice +Rubber beats bristle, and a *tapered, one-side-mounted roller* resists hair-wrap +best (hair tangling is a top complaint). Easy to 3D-print or source compatible. + +### Mop — dual-spinning is the replicable sweet spot +Performance ladder: flat drag pad (worst) → *dual spinning pads* (mid) → +self-washing roller (best). But the roller mop's "better stains / less residual +water" edge was *refuted* under fact-checking — it's overstated. A 3D-printed +*dual-spinning* mop is competitive and DIY-able; *skip the self-washing roller* +(and its multifunction wash/dry dock) for now. +[(source)](https://vacuumwars.com/robot-vacuum-mop-systems/) + +### Dock — basic is DIY-able, full-service is not +A *basic charging dock* is well within reach (print the housing; source contacts + +adapter + IR beacon). *Auto-empty / mop-wash / hot-dry all-in-one docks* are +commercial-scale — defer, or use an off-the-shelf corded vac for emptying. + +### Cloud-free / local control — the real differentiator +[Valetudo](https://github.com/Hypfer/Valetudo) gives cloud-free MQTT/REST local +control across ~10 brands. *Dreame* is the most rootable (≈16 models) and the +safest donor to study. Cloud-free local operation is OOMWOO's positioning advantage. + +### Well-loved models worth studying +Eufy Omni S2 (obstacle avoidance), Narwal Flow (roller mop), Ecovacs Deebot T90 Pro +Omni (~$499 all-rounder), Dreame X40 Ultra (dual-spinning mop; Dreame = best donor). + +> *Caveats:* the top-level dimensions (suction-decoupling, sensor-fusion, mop +> ladder) are primary-source and verified. Per-model rankings are *directional*, +> from single-run reviewer tests. Rootability is *per hardware revision* — +> re-verify any specific donor unit before buying to root. + +--- + +## 2. Print vs source strategy + +*Rule of thumb: print geometry, source mechanisms and wear items.* Anything with a +gearbox, encoder, rubber compound, spring, pump, or bearing is precision you can buy +for a few dollars; anything custom-shaped that mates with the OOMWOO chassis, print. + +| Component | Source or 3D print | Why / how | +|---|---|---| +| *Driving wheel assemblies* | *Source (whole module)* | Complete drive modules (gearmotor + encoder + suspension + rubber tire). 3D print at most an adapter bracket. Why? Requires advanced skill - possibly SLA for gearbox, FDM TPU tire. The #1 "don't print it" part. | +| *Universal / caster wheel* | *Print* or source wheel/ball caster. | Likely a simple passive swivel, possibly TPU. | +| *Side brush assembly* | *Hybrid* | Source brush + small gearmotor; print the mount. Fit a *common replaceable brush*. Fixed (not extendable) for v1. | +| *Main brush* | *Source / hybrid* | Tapered rubber anti-tangle roller in a *common wear-part size*; source compatible, or print core + rubber. | +| *Bumper* | *Hybrid* | Print floating shroud; source lever microswitches + return springs. | +| *Dust bin / water tank* | *Print body + source guts* | Print custom body (mates airflow); source filter (common HEPA size), gasket (or TPU print), latch spring. Water tank adds a sourced *pump + solenoid valve + tubing*. | +| *Mop lift* | *Hybrid (P2)* | Print cam/linkage; source a small *servo or geared motor*. | +| Mop disposable cloths | *Source* | Source (easier) or DIY sew. | +| Mop dryer | *Source* | Source the mop dryer. | +| *Enclosure / top shell* | *Print* | Custom cosmetic/structural; no off-the-shelf equivalent. Design for splitting to fit common print beds. | +| *Dock (basic charge)* | *Print housing + source contacts* | Source *pogo pins / spring contacts / magnets* (magnets can carry [10 A](https://xdaforums.com/t/home-made-pogo-pin-charging-dock.2019847/)) + wall adapter + IR-beacon LEDs. Plenty of [DIY precedent](https://www.instructables.com/Roamer-the-Self-Charging-Companion-Robot/). | +| *Auto-empty dock* | *3D print enclosure + source bin/fan* | Needs its own fan + bin (commercial-scale). Off-the-shelf corded vac bolted to a printed dock is the DIY path. | +| *Mop dock* | *3D print enclosure + source water tanks/hookups* | Needs its own fan + bin (commercial-scale). Off-the-shelf corded vac bolted to a printed dock is the DIY path. | +| Battery, LiDAR, motors, PCB, fasteners, bearings, gaskets | *Source* | Standard sourced parts (custom PCB + LiDAR aside). | +| Single Board Computer | *Source* | Raspberry Pi 5 4GB or better for first model. | +| Input/Output board | *Custom* | No DIY-vacuum I/O PCBs I'm aware of. I'll design a custom PCB for sensors and motor drivers. | +| Cameras, sensors, LiDAR | *Source* | Color + distance cameras for top-tier obstacle avoidance. IR cliff, side proximity sensors. Ultrasonic carpet sensor. | + +*Sourcing strategy:* deliberately spec sourced wear parts (brushes, filters, wheel +modules) in *common, abundant sizes* so users buy cheap "universal" / Roomba-style +replacements anywhere. A selling feature *and* less inventory to stock. + +--- + +## 3. Feature decisions + +### 3.1 Extendable side brush — *fixed for v1* +The extendable arm (e.g. Roborock FlexiArm) is a *genuinely loved* feature — best +corner-cleaning scores and strong reviews +[(source)](https://vacuumwars.com/vacuum-wars-best-robot-vacuums/). But it's a +mechanically complex actuated mechanism. A well-placed *fixed* side brush captures +most of the corner benefit at a fraction of the complexity. Extendable is a great +*P2+ community mod*, not an MVP requirement. + +### 3.2 Body shape — *round* +D-shape cleans corners/edges better, but the real-world advantage is "smaller than +most people expect," and a well-engineered round robot with good side brushes +performs as well or better in most homes +[(source)](https://www.ecovacs.com/us/blog/round-vs-square-robot-vacuum). Round wins +for a DIY/printable platform: +1. Simpler to design, print, and seal. +2. Navigates better — rotates in place, backs out the way it came (D-shape + complicates every nav/recovery RFC). +3. Natural fit for the spinning 2D LiDAR turret. +4. Matches the round teardown reference we're porting. +5. Corners are better solved by the side brush (later extendable) than by body shape. + +*Decision: round body + fixed side brush now, extendable side brush later.* + +--- + +## 4. Compute (SBC) + +*v1: Raspberry Pi 5 (4 GB).* Chosen to onboard the large Raspberry Pi community — +the biggest contributor wedge. Runs ROS2 + LiDAR SLAM + Nav2 comfortably. No on-board NPU. + +- *ML vision option — Hailo AI HAT.* Add the RPi M.2 AI Kit (Hailo-8L, ~13 TOPS) for + real-time camera obstacle detection: keeps the entire RPi ecosystem advantage *and* + gains an NPU. *Check the board-stack height* — a vacuum is only ~10 cm tall and + the M.2 HAT adds Z. Evaluate alternatives (Coral USB/M.2 accelerator, or lighter + CPU-only models) against the height budget. +- *Later: Rockchip RK3588 / RK3576.* Orange Pi 5, Radxa Rock 5B, Banana Pi CM5 — + built-in *6 TOPS NPU*, RKNN toolkit, YOLO ~65 ms/image. Cheaper and integrated, but + driver / ROS2 / kernel maturity *lags RPi*, so it's a *later* move, not the + community-onboarding one. + +--- + +## 5. I/O board (custom, JLCPCB) + +No off-the-shelf DIY-vacuum I/O board exists, so we design one. It carries an *MCU +running micro-ROS* that talks to the SBC over a fast serial / USB link: the SBC does +ROS2 / SLAM / nav / vision; the board does real-time motor + sensor I/O. + +*MCU:* STM32G070RBT6 (LQFP64, ~$0.93 @ 100 pcs) — cheap, lots of peripherals. +*Pin-budget risk:* the full peripheral set below is a lot for 64 pins. Do a +pin-allocation spreadsheet before committing. Offload the *BLDC fan to an external +ESC* (1 PWM pin instead of in-MCU FOC) to save pins and complexity. If still tight, +consider the STM32G0B1RET6 (same family, more peripherals/RAM) or a 100-pin part. + +*LiDAR (3irobotix CRL-200S):* wires to the I/O board. The board *drives the LiDAR +motor* (MOSFET + closed-loop speed control using the LiDAR's RPM feedback) and *passes +raw LiDAR data through the MCU to the SBC* over the fast serial link. *Confirm the MCU +UART bandwidth handles the CRL-200S data rate.* + +Cross-checked peripheral list (your running list + `[+]` = additions to consider): + +*Sensors* +- 2D LiDAR (CRL-200S) — motor-driven + data passthrough to SBC +- IR cliff / proximity / docking +- Bumper micro-switches (+ optional optical/IR bumper) +- Wheel encoders +- Carpet ultrasonic sensor +- Color camera → *connects to the SBC's CSI/USB, not this board* +- VL53L7CX distance sensor (I²C) +- Water tank level (full/empty) +- Dust bin present / lid open-closed +- Battery level / fuel gauge +- Dock-connected / charge sense +- IMU +- `[+]` Wheel-drop / lift sensor (robot picked up) — distinct from cliff + +*Motor drivers* +- Main wheels ×2 +- Main brush +- Side brush(es) +- `[+]` *Mop pad spin motor(s)* — for dual-spinning pads (you listed mop *lift* but not *spin*) +- Mop lift servo +- Water pump (low-side MOSFET) +- Vacuum fan (BLDC) — via *external ESC* (recommended) +- LiDAR motor (MOSFET, closed-loop) + +*Power* +- Battery connector +- Charging circuit (charge-controller IC) +- Dock contacts +- BMS interface +- Current sense (per-rail and/or per-motor for stall / tangle detection) +- DC-DC converters (5 V for SBC, 3.3 V logic, motor rails) +- Power on/off button + soft-latch power circuit +- `[+]` Protection: reverse-polarity, over-current fuse / eFuse, inrush limiting + +*Audio / UI* +- Speaker amp + connector (audio in from the SBC) +- Mic placeholder +- Power / status LEDs +- Buttons (power, dock, clean) +- `[+]` Buzzer (cheap fallback if the speaker amp is deferred) + +*Host link:* USB or high-speed UART between MCU and SBC, carrying micro-ROS *and* +the LiDAR passthrough — confirm bandwidth covers both. + +> *Scope note:* the *wash/dry dock has its own controller* (ESP32 + WiFi) for its +> pumps / heater / fan / water-level. Those are *not* on the robot I/O board. + +--- + +## 6. Electrical / sensor BoM sketch + +Rough *robot* BoM at prototype / low-qty China-sourcing prices (compresses at volume). +Excludes the dock. + +| Item | Qty | ~USD | Notes | +|---|---|---|---| +| Drive wheel modules | 2 | 12–27 | sourced complete module | +| Caster wheel | 1 | 0–3 | print or ball caster | +| Suction blower (BLDC) | 1 | 8–20 | sealed sourced motor | +| Main brush + motor | 1 | 5–12 | tapered rubber roller | +| Side brush + motor | 1–2 | 3–8 | | +| Mop spin motor(s) + pads | 1–2 | 6–15 | mopping models | +| Water pump + valve + tubing | 1 | 4–10 | mopping models | +| Mop lift servo | 1 | 2–6 | mopping models | +| Battery pack (~14.8 V Li-ion) + BMS | 1 | 15–30 | safety review | +| LiDAR (CRL-200S / LDS) | 1 | 30–40 | your cost | +| VL53L7CX ToF | 1 | 8–15 | obstacle detection | +| Color camera | 1 | 5–15 | to SBC | +| IMU | 1 | 2–5 | | +| IR cliff / proximity | 3–4 | 3–8 | | +| Bumper micro-switches | 2–3 | 1–3 | | +| Ultrasonic carpet sensor | 1 | 2–5 | | +| Speaker + amp, mic, LEDs, buttons | — | 3–8 | | +| Custom I/O PCB (JLCPCB assembled, low qty) | 1 | 15–40 | | +| Wiring, connectors, fasteners, magnets, gaskets, filter | — | 12–25 | | +| Printed parts (filament) | — | 5–15 | | +| *Robot subtotal (sourced parts)* | | *~$130–270* | excludes SBC | +| Raspberry Pi 5 4 GB | 1 | ~60 | | +| Hailo AI HAT (optional, premium vision) | 1 | ~70 | | + +--- + +## 7. Water system + +*Robot:* an onboard *clean-water tank* (printed) → *solenoid diaphragm pump* (what +commercial units use) → mop head. For spinning pads, dirty water stays in the pads until +the dock washes them. + +*Dock tiers (where the water complexity lives):* +- *Auto-empty:* dock fan + bin/bag suck the robot's dustbin out through a sealed port. + The off-the-shelf-corded-vac approach works here. +- *Wash + dry:* *clean tank + dirty tank* (printed) + *two pumps* + a wash + tray/roller + a *hot-air blower (heater + fan)*. Wash *must* include hot-air dry — + wash-without-dry breeds mildew/odor. Needs its *own ESP32 + WiFi controller*. +- *Plumbing hookup:* offer as an *option* on the premium dock (pump + supply/drain + lines + valve); default to tank-based (direct plumbing needs pro install). Skip exotic + water-recycling (e.g. silver-ion distillation). +- DIY parts are cheap/common: 12 V diaphragm or peristaltic pumps, solenoid valves, + silicone tubing, float / capacitive level sensors. + +--- + +## 8. Budget target + +*Target: ~$100–200 sourced parts + Raspberry Pi 5 4 GB*, aiming at the capability of a +mid-range ($500–600) commercial vacuum. + +- *Verdict: realistic for the mechanicals + core sensors* at the low–mid end, and it + compresses at volume. But *mopping + premium obstacle detection* (ToF + color camera + + an NPU accelerator) are exactly the line items that push toward / past $200 — the + Hailo HAT alone is ~$70. So $100–200 holds for a capable vacuum; "premium vision now" + wants a +$70-ish allowance (or defer the NPU to the Rockchip generation). +- *Sanity check vs commercial:* a $500–600 LiDAR vacuum has roughly a *$120–180 FOB + BoM at 5,000 MOQ*. Our low-qty numbers are coherent — we trade *no tooling/molds* + (3D print) for *higher per-part cost* (no volume). +- *Set builder expectations honestly:* total DIY spend (kit + RPi 5 + LiDAR + margin) + lands *above* a commercial unit's BoM. The value proposition is *openness, local + control, and hackability — not beating Roborock on price.* + +--- + +## 9. Product lineup — shared base, three dock tiers + +One robot base, three dock tiers, released in order: +1. *Basic charging dock* (first release — simplest, MVP-aligned). +2. *Auto-empty dock* (dust). +3. *Auto-empty + mop-wash + hot-dry dock* (the full-service tier). + +Notes: +- Mirrors how commercial brands segment (same robot, dock tiers) and fits the + swappable-module philosophy. +- The *mop hardware (spinning pads + onboard water tank) is an add-on module* on the + shared base — only the mopping models carry it; the base is otherwise constant. +- The *wash+dry dock is almost its own mini-product* (pumps, heater, tanks, ESP32). + Correctly the last and hardest deliverable after the robot itself. +- Skip a wash-*only* dock — drying is mandatory (odor). + +--- + +## 10. Obstacle detection strategy (premium hardware + community ML) + +Targets the #1 user pain (getting stuck / eating cables). Strategy: *premium hardware +now, ML maturity via community contribution over time.* + +- *Sensors:* *VL53L7CX* multizone ToF (8×8, *90° FoV*, ~350 cm — wider FoV than the + L5CX's 63°, better coverage) *+ color camera.* The ToF alone detects the low / small / + cliff objects the *LiDAR is blind to*, so v1 gets real "doesn't eat cables" value + *before* any ML matures — de-risks the feature. +- *ML:* the *camera + model* is where the NPU matters (Hailo HAT now, Rockchip NPU + later); real-time RGB detection won't run well on the RPi 5 CPU. +- *Community-labeled household-obstacle dataset* is a legitimately novel contribution + track — every contributor's home is training data. Treat it as a first-class + contribution track alongside the RFCs; it doubles as community-building. + +--- + +## 11. Still to research / open decisions + +- *Battery:* chemistry (Li-ion 3S/4S?), specific cells + BMS, charge profile — *safety review*. +- *BLDC fan ESC:* select an off-the-shelf ESC vs in-MCU drive. +- *Filter:* pick a common, abundant filter size as the interface standard. +- *Gasket sourcing* vs TPU printing for airflow seals. +- *Dock docking signal:* IR beacon protocol / fiducial / reflective marker. +- *Fastener / heat-set insert standard* (ties to ARCHITECTURE §5.2). +- *Confirm:* MCU pin budget, host-link bandwidth (micro-ROS + LiDAR passthrough), Hailo stack height. +- Remaining mechanical parts not yet covered above.