chore: establish Enuxia Vacuum project identity
This commit is contained in:
@@ -0,0 +1,667 @@
|
||||
# AGENTS.md
|
||||
|
||||
# Enuxia Vacuum — Repository Guidance
|
||||
|
||||
This repository is maintained as **Enuxia Vacuum**, an independent open-source fork of
|
||||
[VacuumTiger](https://github.com/codetiger/VacuumTiger).
|
||||
|
||||
The upstream project provided the original software foundation. Enuxia Vacuum preserves
|
||||
that history and attribution while developing its own technical direction, architecture
|
||||
and roadmap.
|
||||
|
||||
This file is the main repository guidance for coding agents and automated contributors.
|
||||
|
||||
---
|
||||
|
||||
## 1. Project intent
|
||||
|
||||
Enuxia Vacuum is a local-first robotics stack for autonomous vacuum robots.
|
||||
|
||||
The project is not intended to merely reproduce proprietary factory behavior. Its goal is
|
||||
to provide an understandable, modifiable and self-hostable robotics platform with:
|
||||
|
||||
- low-level hardware control;
|
||||
- local telemetry;
|
||||
- mapping and localization;
|
||||
- robust autonomous navigation;
|
||||
- cleaning coverage planning;
|
||||
- docking;
|
||||
- local APIs;
|
||||
- engineering diagnostics;
|
||||
- future user-facing PWA / Android clients.
|
||||
|
||||
The primary development platform is currently the **Proscenic M6 Pro / 3irobotix CRL-200S**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Upstream provenance
|
||||
|
||||
Enuxia Vacuum is derived from the open-source VacuumTiger project.
|
||||
|
||||
Preserve:
|
||||
|
||||
- upstream Git history;
|
||||
- copyright notices;
|
||||
- applicable license notices;
|
||||
- attribution for upstream-originated code;
|
||||
- component-specific licensing metadata.
|
||||
|
||||
Do not rewrite history to imply that upstream code was originally authored by Enuxia.
|
||||
|
||||
When making changes, Git history should remain the authoritative record of authorship and
|
||||
evolution.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
The project is being progressively reorganized around clear subsystem boundaries.
|
||||
|
||||
```text
|
||||
User applications
|
||||
PWA / Android / diagnostics
|
||||
|
|
||||
v
|
||||
Mission Manager
|
||||
map / clean / goto / dock
|
||||
|
|
||||
+-------------------+
|
||||
| |
|
||||
v v
|
||||
Explorer Cleaning Planner
|
||||
\ /
|
||||
\ /
|
||||
v v
|
||||
Global Goal
|
||||
|
|
||||
v
|
||||
Global Planner
|
||||
Theta*
|
||||
|
|
||||
v
|
||||
Global Path
|
||||
|
|
||||
v
|
||||
Layered Local Costmap
|
||||
|
|
||||
v
|
||||
Local Planner
|
||||
DWA
|
||||
|
|
||||
v
|
||||
Velocity Commands
|
||||
|
|
||||
v
|
||||
SangamIO
|
||||
|
|
||||
v
|
||||
Robot
|
||||
```
|
||||
|
||||
### Main responsibilities
|
||||
|
||||
#### SangamIO
|
||||
|
||||
Hardware abstraction and robot-specific drivers.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- MCU protocol handling;
|
||||
- LiDAR communication;
|
||||
- sensor telemetry;
|
||||
- drive control;
|
||||
- vacuum and brush actuators;
|
||||
- platform initialization;
|
||||
- robot-independent command/telemetry interface.
|
||||
|
||||
Higher-level code must not depend directly on CRL-200S / GD32 protocol details when the
|
||||
same information can be obtained through SangamIO.
|
||||
|
||||
#### DhruvaSLAM
|
||||
|
||||
Mapping and localization backend.
|
||||
|
||||
Responsibilities include:
|
||||
|
||||
- odometry;
|
||||
- LiDAR preprocessing;
|
||||
- scan matching;
|
||||
- occupancy-grid mapping;
|
||||
- submaps;
|
||||
- keyframes;
|
||||
- loop closure;
|
||||
- pose-graph optimization;
|
||||
- map persistence and publication.
|
||||
|
||||
The current repository also contains legacy navigation/exploration logic in DhruvaSLAM.
|
||||
That logic is a baseline, not the target architecture.
|
||||
|
||||
#### Navigation V2
|
||||
|
||||
Navigation V2 is the next major architecture.
|
||||
|
||||
It should separate:
|
||||
|
||||
- global planning;
|
||||
- local planning;
|
||||
- obstacle handling;
|
||||
- progress detection;
|
||||
- stuck detection;
|
||||
- recovery;
|
||||
- exploration.
|
||||
|
||||
New navigation development should move toward this architecture rather than increasing
|
||||
coupling inside the legacy navigator.
|
||||
|
||||
#### Drishti
|
||||
|
||||
Engineering and diagnostic UI.
|
||||
|
||||
It is intended for:
|
||||
|
||||
- robot telemetry;
|
||||
- LiDAR visualization;
|
||||
- SLAM visualization;
|
||||
- navigation debugging;
|
||||
- actuator testing;
|
||||
- development diagnostics.
|
||||
|
||||
Drishti is not the final end-user application.
|
||||
|
||||
---
|
||||
|
||||
## 4. Core architecture rules
|
||||
|
||||
### Navigation is not SLAM
|
||||
|
||||
SLAM answers:
|
||||
|
||||
> Where is the robot, and what does the environment look like?
|
||||
|
||||
Navigation answers:
|
||||
|
||||
> How should the robot move safely from its current pose to a requested goal?
|
||||
|
||||
Do not merge navigation policy into the SLAM core unless a true mapping/localization
|
||||
dependency requires it.
|
||||
|
||||
### Exploration is not navigation
|
||||
|
||||
Exploration chooses **where to go next**.
|
||||
|
||||
Navigation decides **how to get there**.
|
||||
|
||||
The Explorer should provide a goal or approach pose to Navigation and receive a result
|
||||
such as:
|
||||
|
||||
```text
|
||||
Reached
|
||||
Unreachable
|
||||
Blocked
|
||||
Cancelled
|
||||
SafetyStop
|
||||
```
|
||||
|
||||
Navigation should not need to understand frontier semantics.
|
||||
|
||||
### Global planning is not local control
|
||||
|
||||
The global planner produces a path through the known map.
|
||||
|
||||
The local planner is responsible for:
|
||||
|
||||
- short-horizon trajectory selection;
|
||||
- dynamic obstacle response;
|
||||
- collision checking;
|
||||
- robot footprint;
|
||||
- clearance;
|
||||
- immediate velocity commands.
|
||||
|
||||
Do not use a global A* / Theta* planner as a direct wheel controller.
|
||||
|
||||
### Mapping is not cleaning
|
||||
|
||||
Mapping discovers and models the environment.
|
||||
|
||||
Cleaning uses an existing map to cover floor space efficiently.
|
||||
|
||||
Future cleaning should use room segmentation and coverage planning rather than frontier
|
||||
exploration as the cleaning strategy.
|
||||
|
||||
---
|
||||
|
||||
## 5. Navigation V2 direction
|
||||
|
||||
Target flow:
|
||||
|
||||
```text
|
||||
Goal
|
||||
|
|
||||
v
|
||||
Global Planner (Theta*)
|
||||
|
|
||||
v
|
||||
Global Path
|
||||
|
|
||||
v
|
||||
Layered Local Costmap
|
||||
|
|
||||
v
|
||||
Local Planner (DWA)
|
||||
|
|
||||
v
|
||||
cmd_vel
|
||||
```
|
||||
|
||||
The local costmap should eventually combine:
|
||||
|
||||
```text
|
||||
Static SLAM map
|
||||
Live LiDAR obstacles
|
||||
Bumper/contact observations
|
||||
Cliff observations
|
||||
Virtual walls / no-go zones
|
||||
Robot footprint
|
||||
Inflation / clearance costs
|
||||
```
|
||||
|
||||
### Recovery
|
||||
|
||||
Recovery must be based on **measured robot motion**, not only elapsed time.
|
||||
|
||||
Avoid logic equivalent to:
|
||||
|
||||
```text
|
||||
reverse for 1.5 seconds
|
||||
then assume recovery succeeded
|
||||
```
|
||||
|
||||
Prefer:
|
||||
|
||||
```text
|
||||
contact
|
||||
-> stop
|
||||
-> record obstacle/contact
|
||||
-> backup
|
||||
-> confirm actual encoder/pose displacement
|
||||
-> choose clear direction
|
||||
-> rotate
|
||||
-> verify clearance
|
||||
-> resume / replan
|
||||
```
|
||||
|
||||
Repeated bumper hits in the same small region should influence future local navigation
|
||||
instead of causing the robot to retry the same approach indefinitely.
|
||||
|
||||
---
|
||||
|
||||
## 6. CRL-200S / M6 Pro hardware facts
|
||||
|
||||
Current validated development platform:
|
||||
|
||||
```text
|
||||
Robot Proscenic M6 Pro / 3irobotix CRL-200S
|
||||
SoC Allwinner A33
|
||||
MCU GD32F103
|
||||
LiDAR Delta-2D
|
||||
MCU UART /dev/ttyS3
|
||||
LiDAR UART /dev/ttyS1
|
||||
```
|
||||
|
||||
Validated Enuxia fork work includes:
|
||||
|
||||
- GD32 cold-boot initialization fixes;
|
||||
- corrected Delta-2D angular decoding;
|
||||
- stable full-revolution LiDAR scans;
|
||||
- hardware telemetry;
|
||||
- drive and actuator control;
|
||||
- bumper persistence into SLAM state;
|
||||
- SLAM pose-graph / submap corrections;
|
||||
- navigation and frontier failure handling improvements.
|
||||
|
||||
Do not regress these areas when restructuring the project.
|
||||
|
||||
---
|
||||
|
||||
## 7. Hardware ownership and process safety
|
||||
|
||||
On hardware where the factory controller and SangamIO compete for the same MCU/UART or
|
||||
actuators, they must not run simultaneously.
|
||||
|
||||
For the CRL-200S development setup:
|
||||
|
||||
- SangamIO is the active hardware daemon;
|
||||
- the original factory hardware controller must remain inactive during SangamIO use.
|
||||
|
||||
Never introduce code or documentation that casually starts both hardware stacks.
|
||||
|
||||
---
|
||||
|
||||
## 8. Physical safety
|
||||
|
||||
This repository controls physical machinery.
|
||||
|
||||
For motion-related work:
|
||||
|
||||
- assume experimental code can fail;
|
||||
- supervise tests;
|
||||
- keep a reliable stop path available;
|
||||
- test away from stairs and drops;
|
||||
- do not disable cliff protection for convenience;
|
||||
- distinguish bumper recovery from cliff recovery;
|
||||
- do not blindly reverse after a cliff event;
|
||||
- fail safely when motion feedback disagrees with commanded motion.
|
||||
|
||||
Safety-critical behavior should be explicit and testable.
|
||||
|
||||
---
|
||||
|
||||
## 9. Development style
|
||||
|
||||
Prefer small, reviewable changes.
|
||||
|
||||
Do not mix unrelated changes such as:
|
||||
|
||||
- hardware protocol changes;
|
||||
- SLAM changes;
|
||||
- navigation tuning;
|
||||
- UI changes;
|
||||
|
||||
inside the same commit unless the change genuinely spans those layers.
|
||||
|
||||
### Recommended workflow
|
||||
|
||||
```text
|
||||
1. reproduce a problem
|
||||
2. identify one hypothesis
|
||||
3. change one subsystem
|
||||
4. build / test
|
||||
5. run a supervised hardware test if required
|
||||
6. inspect logs
|
||||
7. commit the validated change
|
||||
```
|
||||
|
||||
Avoid broad parameter tuning while fixing a logic bug.
|
||||
|
||||
---
|
||||
|
||||
## 10. Rust checks
|
||||
|
||||
For Rust components, prefer running:
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
cargo test
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Use `cargo clippy` when appropriate.
|
||||
|
||||
Do not silence warnings by default if the warning points to a real design issue.
|
||||
|
||||
---
|
||||
|
||||
## 11. Public repository hygiene
|
||||
|
||||
This repository is intended to be public.
|
||||
|
||||
Never commit:
|
||||
|
||||
- Wi-Fi credentials;
|
||||
- passwords;
|
||||
- private keys;
|
||||
- API tokens;
|
||||
- private infrastructure credentials;
|
||||
- NAND dumps;
|
||||
- device partitions containing secrets;
|
||||
- deployment-specific secrets;
|
||||
- private maps or logs containing sensitive data;
|
||||
- generated binaries unless intentionally published as release artifacts.
|
||||
|
||||
Prefer example configuration files over real deployment configuration.
|
||||
|
||||
Use placeholders such as:
|
||||
|
||||
```text
|
||||
192.0.2.10
|
||||
robot.local
|
||||
server.local
|
||||
```
|
||||
|
||||
instead of private infrastructure addresses in public documentation.
|
||||
|
||||
---
|
||||
|
||||
## 12. Git and upstream workflow
|
||||
|
||||
Recommended remote model:
|
||||
|
||||
```text
|
||||
origin -> Enuxia public repository
|
||||
upstream -> codetiger/VacuumTiger
|
||||
```
|
||||
|
||||
Do not force-push rewritten upstream history into the public Enuxia repository without a
|
||||
very strong reason.
|
||||
|
||||
When useful, upstream changes may be reviewed and selectively integrated.
|
||||
|
||||
Do not blindly overwrite Enuxia-modified files with upstream versions.
|
||||
|
||||
---
|
||||
|
||||
## 13. Legacy navigator policy
|
||||
|
||||
The current Dhruva navigation stack has been useful for validation and debugging, but it
|
||||
has known architectural limits.
|
||||
|
||||
Do not treat the current `Navigator` behavior as the long-term design.
|
||||
|
||||
In particular, avoid adding another local patch solely to preserve a design where:
|
||||
|
||||
```text
|
||||
frontier
|
||||
-> global path
|
||||
-> direct waypoint controller
|
||||
-> timed escape
|
||||
-> global replan
|
||||
```
|
||||
|
||||
New work should move toward Navigation V2:
|
||||
|
||||
```text
|
||||
goal
|
||||
-> global planner
|
||||
-> local costmap
|
||||
-> local planner
|
||||
-> measured recovery
|
||||
```
|
||||
|
||||
A legacy fix is still acceptable when required for:
|
||||
|
||||
- safety;
|
||||
- data integrity;
|
||||
- regression prevention;
|
||||
- preserving the validated baseline;
|
||||
- enabling migration to Navigation V2.
|
||||
|
||||
---
|
||||
|
||||
## 14. SLAM invariants
|
||||
|
||||
Be careful when changing pose-graph or submap code.
|
||||
|
||||
Previously observed failure class:
|
||||
|
||||
```text
|
||||
pose graph corrects keyframes/current pose
|
||||
but submap origins remain in old geometry
|
||||
```
|
||||
|
||||
This can make the corrected robot pose inconsistent with the rebuilt occupancy grid.
|
||||
|
||||
Any future optimization change must consider:
|
||||
|
||||
- current pose;
|
||||
- keyframes;
|
||||
- submap origins;
|
||||
- global map invalidation/rebuild;
|
||||
- active navigation path invalidation.
|
||||
|
||||
Do not update only one representation of geometry.
|
||||
|
||||
---
|
||||
|
||||
## 15. Bumper observations
|
||||
|
||||
Bumper contacts are useful perception data.
|
||||
|
||||
A contact should not exist only as a transient navigation event.
|
||||
|
||||
Where appropriate:
|
||||
|
||||
- record contact position;
|
||||
- persist it into the active local mapping representation;
|
||||
- expose it to local obstacle avoidance;
|
||||
- deduplicate repeated nearby contacts;
|
||||
- allow repeated contact history to influence navigation decisions.
|
||||
|
||||
Do not let a global-map rebuild silently erase meaningful contact observations.
|
||||
|
||||
---
|
||||
|
||||
## 16. Unknown space
|
||||
|
||||
Current design intent:
|
||||
|
||||
```text
|
||||
UNKNOWN = not traversable by the robot center
|
||||
```
|
||||
|
||||
But UNKNOWN should not necessarily be treated identically to a confirmed physical
|
||||
obstacle for every inflation operation.
|
||||
|
||||
Keep the distinction between:
|
||||
|
||||
- unknown;
|
||||
- free;
|
||||
- occupied / physical obstacle.
|
||||
|
||||
This distinction is important for frontier approach planning.
|
||||
|
||||
---
|
||||
|
||||
## 17. Exploration targets
|
||||
|
||||
A frontier cell is not necessarily a valid robot-center goal.
|
||||
|
||||
Prefer:
|
||||
|
||||
```text
|
||||
unknown region
|
||||
|
|
||||
frontier boundary
|
||||
|
|
||||
safe approach pose in known free space
|
||||
```
|
||||
|
||||
The approach pose may include orientation toward the unknown region.
|
||||
|
||||
Avoid sending the robot center directly onto unknown cells.
|
||||
|
||||
---
|
||||
|
||||
## 18. Testing philosophy
|
||||
|
||||
A robotics feature is not considered validated because it worked once.
|
||||
|
||||
Create repeatable physical scenarios.
|
||||
|
||||
Important test cases include:
|
||||
|
||||
### Bed / furniture leg
|
||||
|
||||
Expected:
|
||||
|
||||
- obstacle detected;
|
||||
- no repeated charging into the same obstacle;
|
||||
- local avoidance or clean abandonment.
|
||||
|
||||
### Soft low obstacle
|
||||
|
||||
Expected:
|
||||
|
||||
- bumper observation persists;
|
||||
- measured backup;
|
||||
- repeated contact strengthens avoidance.
|
||||
|
||||
### Rug / traction problem
|
||||
|
||||
Expected:
|
||||
|
||||
- commanded motion without encoder progress is recognized;
|
||||
- recovery does not loop forever.
|
||||
|
||||
### Narrow passage
|
||||
|
||||
Expected:
|
||||
|
||||
- robot footprint respected;
|
||||
- sufficient clearance.
|
||||
|
||||
### Dynamic obstacle
|
||||
|
||||
Expected:
|
||||
|
||||
- local planner stops or avoids;
|
||||
- global replan only when necessary.
|
||||
|
||||
### Cliff
|
||||
|
||||
Expected:
|
||||
|
||||
- immediate stop;
|
||||
- no unsafe blind reverse;
|
||||
- persistent forbidden region where appropriate.
|
||||
|
||||
---
|
||||
|
||||
## 19. Public communication
|
||||
|
||||
Describe Enuxia Vacuum as:
|
||||
|
||||
> An independent open-source fork of VacuumTiger developed by Enuxia.
|
||||
|
||||
Do not describe the repository as if it were created from scratch.
|
||||
|
||||
At the same time, do not frame Enuxia Vacuum as a passive mirror.
|
||||
|
||||
The project now has its own:
|
||||
|
||||
- validated hardware baseline;
|
||||
- technical decisions;
|
||||
- architecture;
|
||||
- development roadmap;
|
||||
- navigation redesign;
|
||||
- future cleaning and application layers.
|
||||
|
||||
Use `ATTRIBUTION.md` for provenance details.
|
||||
|
||||
---
|
||||
|
||||
## 20. Before submitting a change
|
||||
|
||||
Check:
|
||||
|
||||
```text
|
||||
[ ] Does this change respect subsystem boundaries?
|
||||
[ ] Does it preserve hardware safety?
|
||||
[ ] Does it expose secrets or private infrastructure?
|
||||
[ ] Does it regress validated M6 Pro behavior?
|
||||
[ ] Does it add another legacy navigation workaround unnecessarily?
|
||||
[ ] Are tests/builds passing?
|
||||
[ ] Is the commit focused?
|
||||
[ ] Does public documentation remain accurate?
|
||||
```
|
||||
|
||||
When in doubt, prefer a smaller change with a measurable test.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Attribution and fork history
|
||||
|
||||
## Enuxia Vacuum
|
||||
|
||||
Enuxia Vacuum is an independent derivative project maintained by Enuxia.
|
||||
|
||||
The project is based on the open-source repository:
|
||||
|
||||
- **Project:** VacuumTiger
|
||||
- **Upstream:** https://github.com/codetiger/VacuumTiger
|
||||
- **Upstream root copyright:** Copyright (c) 2024 Harishankar Narayanan
|
||||
|
||||
The complete Git history is intentionally preserved so that the origin of upstream code and subsequent Enuxia work remain auditable.
|
||||
|
||||
## What comes from upstream?
|
||||
|
||||
The fork began with the VacuumTiger codebase, including major components such as:
|
||||
|
||||
- SangamIO;
|
||||
- CRL-200S hardware support;
|
||||
- DhruvaSLAM;
|
||||
- DhruvaNav;
|
||||
- Drishti;
|
||||
- VastuSLAM;
|
||||
- protocol reverse-engineering tools;
|
||||
- related tests and documentation.
|
||||
|
||||
Exact authorship of individual files and changes should be determined from Git history.
|
||||
|
||||
## Enuxia development
|
||||
|
||||
The Enuxia fork adds and continues to develop work including, among other changes:
|
||||
|
||||
- real-hardware validation on the Proscenic M6 Pro / CRL-200S;
|
||||
- GD32 cold-boot initialization fixes;
|
||||
- Delta-2D LiDAR packet decoding corrections;
|
||||
- SLAM and submap correction work;
|
||||
- persistent bumper observations;
|
||||
- navigation and exploration fixes;
|
||||
- a new navigation architecture;
|
||||
- future local planning, recovery, cleaning, docking and application layers.
|
||||
|
||||
Git history is the authoritative record of which changes were introduced after the fork.
|
||||
|
||||
## Licensing
|
||||
|
||||
Do not remove upstream copyright or licensing notices.
|
||||
|
||||
The upstream repository contains components whose metadata references MIT, Apache-2.0, or dual MIT/Apache-2.0 licensing. The root LICENSE inherited from VacuumTiger is MIT.
|
||||
|
||||
Each component must continue to comply with the license applicable to its source code and dependencies.
|
||||
|
||||
This document is intended to preserve technical provenance. It is not legal advice.
|
||||
@@ -1,450 +1,97 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
This repository is maintained as **Enuxia Vacuum**, an independent open-source fork of
|
||||
VacuumTiger.
|
||||
|
||||
## Project Overview
|
||||
## Read this first
|
||||
|
||||
VacuumTiger is an open-source robotic vacuum firmware project for the CRL-200S hardware platform. The project consists of three main components:
|
||||
Before making changes, read:
|
||||
|
||||
1. **SangamIO**: A daemon providing hardware abstraction and real-time control via TCP/UDP streaming
|
||||
2. **VastuSLAM**: A high-performance 2D SLAM library for robot vacuum cleaners
|
||||
3. **Drishti**: Python visualization and control client for monitoring and commanding the robot
|
||||
- [`AGENTS.md`](AGENTS.md) — authoritative development and architecture guidance
|
||||
- [`README.md`](README.md) — public project overview
|
||||
- [`ATTRIBUTION.md`](ATTRIBUTION.md) — upstream provenance and licensing context
|
||||
|
||||
**Target Platform**: Embedded Linux (ARM) - Allwinner A33 running Tina Linux
|
||||
**Primary Language**: Rust (edition 2024) for SangamIO, Python 3.8+ for Drishti
|
||||
**Cross-compilation Target**: `armv7-unknown-linux-musleabihf`
|
||||
**Architecture**: Daemon-based with UDP/TCP streaming (not a library)
|
||||
`AGENTS.md` is the source of truth for repository-level coding-agent instructions.
|
||||
|
||||
## System Architecture
|
||||
---
|
||||
|
||||
### Overview
|
||||
## Important project rules
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Client Applications (SLAM/Drishti) │
|
||||
└─────────────┬───────────────┬───────────────────┘
|
||||
│ UDP 5555 │ TCP 5555
|
||||
│ (sensors) │ (commands)
|
||||
┌─────────────▼───────────────▼───────────────────┐
|
||||
│ SangamIO Daemon (Robot) │
|
||||
│ • Telemetry streaming @ 110Hz (UDP unicast) │
|
||||
│ • Lidar streaming @ 5Hz (360° scans) │
|
||||
│ • Command processing (TCP, reliable) │
|
||||
│ • Real-time control loop (20ms heartbeat) │
|
||||
└─────────────┬───────────────┬───────────────────┘
|
||||
│ /dev/ttyS3 │ /dev/ttyS1
|
||||
┌──────▼─────────┐ ┌──▼────────────┐
|
||||
│ GD32F103 MCU │ │ Delta-2D Lidar│
|
||||
│ (Motor Control)│ │ (360° Scan) │
|
||||
└────────────────┘ └───────────────┘
|
||||
Do not assume the current upstream VacuumTiger architecture is the target architecture.
|
||||
|
||||
Enuxia Vacuum has its own roadmap, including a Navigation V2 redesign with:
|
||||
|
||||
```text
|
||||
Global planner
|
||||
↓
|
||||
Layered local costmap
|
||||
↓
|
||||
DWA local planner
|
||||
↓
|
||||
Progress / stuck detector
|
||||
↓
|
||||
Recovery manager
|
||||
```
|
||||
|
||||
### Key Design Principles
|
||||
The current Dhruva navigator is a validated baseline, not the long-term design.
|
||||
|
||||
1. **Daemon, Not Library**: SangamIO runs as a standalone service, not imported as a library
|
||||
2. **Concrete Over Abstract**: Direct implementations instead of trait objects (except LidarDriver)
|
||||
3. **Lock-Free Streaming**: Uses lock-free queues for sensor data flow
|
||||
4. **Real-Time Guarantees**: OS threads for heartbeat (20ms) instead of async
|
||||
5. **Hybrid Protocol**: UDP for sensor streaming (low latency), TCP for commands (reliable)
|
||||
Do not keep adding workaround logic to the legacy navigator when the work belongs in
|
||||
Navigation V2.
|
||||
|
||||
## Build and Deployment
|
||||
---
|
||||
|
||||
### Development (Host Machine)
|
||||
## Hardware safety
|
||||
|
||||
This code controls physical hardware.
|
||||
|
||||
When touching robot motion, hardware drivers or recovery behavior:
|
||||
|
||||
- preserve cliff and bumper protections;
|
||||
- never assume a timed motion actually occurred;
|
||||
- use encoder/pose feedback for recovery;
|
||||
- do not run competing hardware controllers simultaneously;
|
||||
- keep tests small and supervised;
|
||||
- fail safely.
|
||||
|
||||
---
|
||||
|
||||
## Public repository hygiene
|
||||
|
||||
Never add:
|
||||
|
||||
- credentials;
|
||||
- private keys;
|
||||
- tokens;
|
||||
- private IP-specific deployment configuration;
|
||||
- NAND dumps;
|
||||
- private maps;
|
||||
- sensitive logs.
|
||||
|
||||
Use example values in documentation and configuration samples.
|
||||
|
||||
---
|
||||
|
||||
## Development approach
|
||||
|
||||
Prefer:
|
||||
|
||||
```text
|
||||
one problem
|
||||
→ one hypothesis
|
||||
→ one focused change
|
||||
→ build/test
|
||||
→ supervised runtime validation if needed
|
||||
→ commit
|
||||
```
|
||||
|
||||
For Rust components, normally run:
|
||||
|
||||
```bash
|
||||
# Build daemon
|
||||
cd sangam-io
|
||||
cargo build --release
|
||||
|
||||
# Run with configuration
|
||||
cargo run --release -- --config sangamio.toml
|
||||
|
||||
# Run with verbose logging
|
||||
RUST_LOG=debug cargo run --release
|
||||
```
|
||||
|
||||
### Production (ARM Robot)
|
||||
|
||||
```bash
|
||||
# Add ARM target
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
|
||||
# Build for ARM (produces static binary with musl)
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf
|
||||
|
||||
# Strip debug symbols (reduces size ~40%)
|
||||
arm-linux-gnueabihf-strip target/armv7-unknown-linux-musleabihf/release/sangam-io
|
||||
```
|
||||
|
||||
### Deployment to Robot
|
||||
|
||||
**IMPORTANT**: The robot monitor auto-restarts killed processes. Rename binaries to prevent this.
|
||||
|
||||
**SSH Credentials**: `root@vacuum`, password: `vacuum@123`
|
||||
|
||||
**Complete Deployment**:
|
||||
|
||||
```bash
|
||||
# Build ARM binary
|
||||
cd sangam-io
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf
|
||||
|
||||
# Deploy binary (device lacks sftp-server, use cat over SSH)
|
||||
cat target/armv7-unknown-linux-musleabihf/release/sangam-io | \
|
||||
sshpass -p "vacuum@123" ssh root@vacuum "cat > /usr/sbin/sangamio && chmod +x /usr/sbin/sangamio"
|
||||
|
||||
# Deploy configuration
|
||||
sshpass -p "vacuum@123" ssh root@vacuum "cat > /etc/sangamio.toml" < sangamio.toml
|
||||
|
||||
# Disable original firmware (rename to prevent auto-restart)
|
||||
sshpass -p "vacuum@123" ssh root@vacuum "mv /usr/sbin/AuxCtrl /usr/sbin/AuxCtrl.bak && killall -9 AuxCtrl"
|
||||
|
||||
# Run daemon
|
||||
sshpass -p "vacuum@123" ssh root@vacuum "RUST_LOG=info /usr/sbin/sangamio"
|
||||
```
|
||||
|
||||
**CRITICAL**: Always overwrite `/usr/sbin/sangamio` directly. Do NOT write to `/tmp` folder.
|
||||
|
||||
## Network Protocol
|
||||
|
||||
SangamIO uses a hybrid UDP/TCP architecture:
|
||||
- **UDP** for sensor streaming (low latency, no head-of-line blocking)
|
||||
- **TCP** for commands (reliable delivery)
|
||||
|
||||
Both use the same port (5555) for simpler configuration.
|
||||
|
||||
### Message Format
|
||||
|
||||
All messages use length-prefixed framing with Protobuf payloads:
|
||||
|
||||
```
|
||||
+------------------+------------------+
|
||||
| Length (4 bytes) | Protobuf Message |
|
||||
| (big-endian) | (binary) |
|
||||
+------------------+------------------+
|
||||
```
|
||||
|
||||
### Proto Schema
|
||||
|
||||
See `sangam-io/proto/sangamio.proto` for SangamIO messages (sensors, commands).
|
||||
|
||||
### Topics (UDP Streaming)
|
||||
|
||||
**Outbound (Daemon → Client via UDP)**:
|
||||
- `sensors/sensor_status`: Sensor data @ 110Hz (all 13 sensors)
|
||||
- `sensors/device_version`: Version info (one-time)
|
||||
- `sensors/lidar`: LidarScan @ 5Hz (point cloud)
|
||||
|
||||
**Inbound (Client → Daemon via TCP)**:
|
||||
- `command`: RobotCommand (SetVelocity, Stop, actuator control, etc.)
|
||||
|
||||
### Client Registration
|
||||
|
||||
UDP uses unicast (not broadcast). Clients are registered automatically:
|
||||
1. Client connects TCP to port 5555
|
||||
2. Server records client IP address
|
||||
3. UDP packets are sent to client on same port
|
||||
4. When TCP disconnects, UDP streaming stops
|
||||
|
||||
### Example Python Client
|
||||
|
||||
```python
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
from proto import sangamio_pb2
|
||||
|
||||
ROBOT_IP = '192.168.68.101'
|
||||
PORT = 5555
|
||||
|
||||
# TCP for commands
|
||||
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
tcp_sock.connect((ROBOT_IP, PORT))
|
||||
|
||||
# UDP for sensor data (same port)
|
||||
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
udp_sock.bind(('0.0.0.0', PORT))
|
||||
|
||||
def receive_sensors():
|
||||
while True:
|
||||
data, addr = udp_sock.recvfrom(4096)
|
||||
length = struct.unpack('>I', data[:4])[0]
|
||||
msg = sangamio_pb2.Message()
|
||||
msg.ParseFromString(data[4:4+length])
|
||||
|
||||
if msg.topic == 'sensors/sensor_status':
|
||||
sg = msg.sensor_group
|
||||
print(f"Battery: {sg.values['battery_level'].u8_val}%")
|
||||
|
||||
# Start receiving in background
|
||||
threading.Thread(target=receive_sensors, daemon=True).start()
|
||||
|
||||
# Send commands via TCP
|
||||
def send_command(cmd):
|
||||
payload = cmd.SerializeToString()
|
||||
tcp_sock.send(struct.pack('>I', len(payload)) + payload)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `sangamio.toml`:
|
||||
|
||||
```toml
|
||||
[device]
|
||||
type = "crl200s"
|
||||
name = "CRL-200S Vacuum Robot"
|
||||
|
||||
[device.hardware]
|
||||
gd32_port = "/dev/ttyS3"
|
||||
lidar_port = "/dev/ttyS1"
|
||||
|
||||
[network]
|
||||
bind_address = "0.0.0.0:5555"
|
||||
```
|
||||
|
||||
## Module Organization
|
||||
|
||||
```
|
||||
VacuumTiger/
|
||||
├── sangam-io/ # Hardware abstraction daemon
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs # Entry point, TCP/UDP listener
|
||||
│ │ ├── config.rs # TOML configuration loading
|
||||
│ │ ├── error.rs # Error types
|
||||
│ │ ├── core/ # Core abstractions
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── driver.rs # DeviceDriver trait
|
||||
│ │ │ └── types.rs # SensorValue, Command types
|
||||
│ │ ├── devices/ # Hardware drivers
|
||||
│ │ │ ├── mod.rs # Device factory (crl200s, mock)
|
||||
│ │ │ ├── crl200s/ # Real hardware driver
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── constants.rs # Offsets, flags, timing
|
||||
│ │ │ │ ├── COMMANDS.md # GD32 command reference
|
||||
│ │ │ │ ├── SENSORSTATUS.md # Sensor packet docs
|
||||
│ │ │ │ ├── gd32/ # Motor controller driver
|
||||
│ │ │ │ │ ├── mod.rs # Driver orchestration
|
||||
│ │ │ │ │ ├── protocol.rs # RxPacket, PacketReader
|
||||
│ │ │ │ │ ├── packet.rs # TxPacket, checksum
|
||||
│ │ │ │ │ ├── heartbeat.rs # 20ms watchdog loop
|
||||
│ │ │ │ │ ├── reader.rs # Status parsing
|
||||
│ │ │ │ │ ├── commands.rs # Command handlers
|
||||
│ │ │ │ │ └── state.rs # Shared atomic state
|
||||
│ │ │ │ └── delta2d/ # 3iRobotix lidar
|
||||
│ │ │ │ ├── mod.rs # LidarDriver impl
|
||||
│ │ │ │ └── protocol.rs # Packet parsing
|
||||
│ │ │ └── mock/ # Simulation driver (--features mock)
|
||||
│ │ │ ├── mod.rs # MockDriver, simulation loop
|
||||
│ │ │ ├── config.rs # SimulationConfig structs
|
||||
│ │ │ ├── physics.rs # Differential drive kinematics
|
||||
│ │ │ ├── lidar_sim.rs # Ray-casting lidar
|
||||
│ │ │ ├── imu_sim.rs # IMU with noise
|
||||
│ │ │ ├── encoder_sim.rs # Encoder with slip
|
||||
│ │ │ ├── sensor_sim.rs # Bumpers, cliffs
|
||||
│ │ │ ├── map_loader.rs # PGM+YAML loader
|
||||
│ │ │ └── noise.rs # Noise generator
|
||||
│ │ └── streaming/ # Network communication
|
||||
│ │ ├── mod.rs # Re-exports
|
||||
│ │ ├── wire.rs # Protobuf serialization
|
||||
│ │ ├── udp_publisher.rs # UDP sensor streaming
|
||||
│ │ └── tcp_receiver.rs # TCP command handler
|
||||
│ ├── proto/ # Protobuf schema
|
||||
│ │ └── sangamio.proto # Message definitions
|
||||
│ ├── maps/ # Example simulation maps
|
||||
│ ├── docs/ # Documentation
|
||||
│ │ └── mock-device-guide.md
|
||||
│ ├── sangamio.toml # Hardware configuration
|
||||
│ └── mock.toml # Simulation configuration
|
||||
│
|
||||
├── vastu-slam/ # High-performance 2D SLAM library
|
||||
│ ├── src/
|
||||
│ │ ├── lib.rs # Library entry point
|
||||
│ │ ├── core/ # Foundation types (Pose2D, WorldPoint, GridCoord, sensors)
|
||||
│ │ ├── grid/ # Occupancy grid storage (SoA layout, log-odds updates)
|
||||
│ │ ├── matching/ # Scan matching (correlative, branch-bound, loop closure)
|
||||
│ │ ├── submap/ # Submap manager, pose graph, multi-submap matching
|
||||
│ │ ├── config/ # YAML configuration loading
|
||||
│ │ ├── io/ # Persistence (.vastu, .pgm, .svg formats)
|
||||
│ │ ├── modes/ # Operation modes (localization-only)
|
||||
│ │ └── evaluation/ # Cartographer-style evaluation metrics
|
||||
│ ├── configs/ # YAML configuration files
|
||||
│ ├── maps/ # Test maps (PGM format)
|
||||
│ ├── scenarios/ # Test scenarios (YAML)
|
||||
│ ├── benches/ # Performance benchmarks
|
||||
│ └── tests/ # Integration tests
|
||||
│
|
||||
└── drishti/ # Python visualization client
|
||||
├── drishti.py # Console client
|
||||
├── drishti_ui.py # GUI application
|
||||
└── ui/ # PyQt widgets
|
||||
├── main_window.py
|
||||
├── processors/ # Data processing
|
||||
├── threads/ # Background workers
|
||||
└── widgets/ # UI components
|
||||
```
|
||||
|
||||
## Critical Hardware Constraints
|
||||
|
||||
### GD32F103 Heartbeat
|
||||
- **Requirement**: CMD=0x66 heartbeat every 20-50ms
|
||||
- **Consequence**: Motors stop if heartbeat missed
|
||||
- **Implementation**: Dedicated OS thread, not async
|
||||
- **Recovery**: 5-second re-initialization sequence
|
||||
|
||||
### Serial Port Exclusivity
|
||||
- **Ports**: `/dev/ttyS3` (GD32), `/dev/ttyS1` (lidar)
|
||||
- **Issue**: Only one process can open port
|
||||
- **Solution**: Kill/rename AuxCtrl before running
|
||||
|
||||
### Initialization Sequence
|
||||
- **GD32 Wake**: CMD=0x08 repeated for up to 5 seconds
|
||||
- **Required**: Must complete before any commands work
|
||||
- **Automatic**: Driver handles this internally
|
||||
|
||||
## Protocol Specifications
|
||||
|
||||
### GD32 Protocol
|
||||
- **Format**: `[0xFA 0xFB] [LEN] [CMD] [PAYLOAD] [CRC]`
|
||||
- **CRC**: 16-bit big-endian word sum checksum
|
||||
- **Status**: CMD=0x15, 96 bytes @ 110Hz
|
||||
- **Location**: `src/devices/crl200s/gd32/protocol.rs`
|
||||
|
||||
### Delta-2D Protocol
|
||||
- **Format**: Variable-length with 8-byte headers
|
||||
- **Data**: Angle (0.01° units), distance (0.25mm units)
|
||||
- **Rate**: ~5Hz complete scans
|
||||
- **Location**: `src/devices/crl200s/delta2d/protocol.rs`
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
- **Binary Size**: ~350KB statically linked
|
||||
- **Memory Usage**: <10MB RSS
|
||||
- **CPU Usage**: <1% on Allwinner A33
|
||||
- **Latency**: <25ms command-to-action
|
||||
- **Streaming Rates**:
|
||||
- Telemetry: 110Hz (every GD32 packet)
|
||||
- Lidar: 5Hz (native scan rate)
|
||||
- Commands: On-demand
|
||||
|
||||
## Thread Model
|
||||
|
||||
| Thread | Purpose | Timing | Priority |
|
||||
|--------|---------|--------|----------|
|
||||
| Main | Initialization | - | Normal |
|
||||
| GD32 Heartbeat | Safety watchdog | 20ms ± 2ms | High |
|
||||
| GD32 Reader | Parse status | Continuous | Normal |
|
||||
| Lidar Reader | Parse scans | Continuous | Normal |
|
||||
| TCP Publisher | Broadcast data | On-demand | Normal |
|
||||
| TCP Receiver | Handle commands | On-demand | Normal |
|
||||
|
||||
## Safety and Error Handling
|
||||
|
||||
- **Never use `unwrap()` or `panic!()`** in production code
|
||||
- **All errors through `sangam_io::Error` enum**
|
||||
- **Motors stop on any error condition**
|
||||
- **Heartbeat continues even if commands fail**
|
||||
- **Use `Result<T>` for fallible operations**
|
||||
|
||||
## Testing and Debugging
|
||||
|
||||
### Local Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
cargo fmt --check
|
||||
cargo test
|
||||
|
||||
# Virtual serial ports
|
||||
socat -d -d pty,raw,echo=0 pty,raw,echo=0
|
||||
|
||||
# Run with PTY
|
||||
RUST_LOG=debug cargo run -- --config test.toml
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
### Hardware Testing
|
||||
Do not make broad refactors or mass renames unless explicitly requested.
|
||||
|
||||
```bash
|
||||
# Deploy and run
|
||||
./deploy.sh
|
||||
|
||||
# Monitor logs
|
||||
ssh root@vacuum "journalctl -u sangamio -f"
|
||||
|
||||
# Check resources
|
||||
ssh root@vacuum "top -p $(pgrep sangamio)"
|
||||
```
|
||||
|
||||
## Drishti Visualization
|
||||
|
||||
### GUI Application
|
||||
|
||||
```bash
|
||||
cd drishti
|
||||
source venv/bin/activate
|
||||
python drishti_ui.py --robot 192.168.68.101
|
||||
```
|
||||
|
||||
Features:
|
||||
- Full-screen robot diagram with physically-correct layout
|
||||
- Real-time sensor overlays (all 13 sensors)
|
||||
- Bumper/cliff highlighting on trigger
|
||||
- Wheel encoder tick counters
|
||||
- Battery/charging status indicator
|
||||
- Start/dock button states
|
||||
|
||||
### Console Client (Legacy)
|
||||
|
||||
```bash
|
||||
python drishti.py --robot 192.168.68.101 --verbose
|
||||
```
|
||||
|
||||
## Common Pitfalls and Solutions
|
||||
|
||||
1. **AuxCtrl Auto-Restart**: Must rename binary, not just kill process
|
||||
2. **Serial Port Access**: Check `/dev/ttyS3` for GD32, not `/dev/ttyS1`
|
||||
3. **No SCP Support**: Use `cat | ssh` for file transfer
|
||||
4. **Unit Confusion**: Always use meters, m/s, radians in API
|
||||
5. **Blocking I/O**: Serial has timeouts, don't assume immediate response
|
||||
|
||||
## Version History
|
||||
|
||||
### SangamIO
|
||||
- **v0.3.0**: UDP/TCP unified port architecture, mock device simulation
|
||||
- **v0.2.0**: Daemon architecture, performance optimizations
|
||||
- **v0.1.0**: Initial release with basic functionality
|
||||
|
||||
### VastuSLAM
|
||||
- **v0.1.0**: High-performance 2D SLAM library with scan matching, submap architecture, and loop closure
|
||||
|
||||
## Documentation References
|
||||
|
||||
- **sangam-io/README.md**: SangamIO daemon documentation
|
||||
- **sangam-io/COMMANDS.md**: GD32 command reference with TCP API
|
||||
- **sangam-io/SENSORSTATUS.md**: Sensor packet byte layout
|
||||
- **vastu-slam/README.md**: VastuSLAM library documentation
|
||||
- **vastu-slam/src/*/README.md**: Module-specific documentation (core, grid, matching, submap, config, io, modes, evaluation)
|
||||
- **drishti/README.md**: Python client documentation
|
||||
- **protocol-mitm/README.md**: MITM reverse engineering tools
|
||||
|
||||
## Future Roadmap
|
||||
|
||||
### Planned Features
|
||||
- WebSocket interface for browser monitoring
|
||||
- ROS2 bridge for ecosystem integration
|
||||
- Additional lidar models (RPLIDAR, Hokuyo)
|
||||
- Multi-robot coordination support
|
||||
|
||||
### Explicitly Not Goals
|
||||
- Generic HAL for all hardware
|
||||
- Plugin system (static compilation preferred)
|
||||
- Remote/cloud operation (LAN-only)
|
||||
- AI/ML integration (keep in higher layers)
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
When modifying this codebase:
|
||||
|
||||
1. **Maintain simplicity**: Avoid unnecessary abstractions
|
||||
2. **Prioritize reliability**: Real-time constraints matter
|
||||
3. **Document changes**: Update relevant documentation
|
||||
4. **Test on hardware**: Virtual tests insufficient
|
||||
5. **Preserve safety**: Never bypass heartbeat mechanism
|
||||
Preserve upstream attribution and license notices.
|
||||
|
||||
@@ -1,246 +1,400 @@
|
||||
# VacuumTiger
|
||||
# Enuxia Vacuum
|
||||
|
||||
**Open-source firmware for hackable vacuum robots**
|
||||
**Open, local-first robotics stack for autonomous vacuum robots.**
|
||||
|
||||
VacuumTiger is a modular, configuration-driven firmware stack that makes it easy to build autonomous vacuum robots. Define your sensors and actuators in TOML, and the system handles everything else—from real-time control to network streaming.
|
||||
Enuxia Vacuum is an independent open-source fork of [VacuumTiger](https://github.com/codetiger/VacuumTiger), now developed by **Enuxia** as a platform for fully local robot control, mapping, navigation and cleaning.
|
||||
|
||||
## Why VacuumTiger?
|
||||
The project started from a practical goal: take commodity robot-vacuum hardware and turn it into a robot that can be understood, modified, self-hosted and improved without depending on a proprietary cloud.
|
||||
|
||||
- **Configurable**: Define sensors, actuators, and hardware in a single TOML file
|
||||
- **Extensible**: Add new robot platforms by implementing one trait
|
||||
- **Generic Protocol**: TCP streaming works with any SLAM application
|
||||
- **Real-time**: 110Hz sensor updates, <25ms command latency
|
||||
- **Hackable**: Small codebase (~4K lines Rust), no dependencies on proprietary SDKs
|
||||
Today, our primary development platform is the **Proscenic M6 Pro / 3irobotix CRL-200S**.
|
||||
|
||||
## How It Works
|
||||
> The long-term goal is not merely to reproduce the factory firmware.
|
||||
> It is to build a cleaner, more modular and more capable robotics stack around the hardware.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ SLAM Application (Planned) │
|
||||
│ Reads sangamio.toml to discover capabilities │
|
||||
└─────────────┬───────────────────────────────────┘
|
||||
│ TCP Protocol (Generic)
|
||||
┌─────────────▼───────────────────────────────────┐
|
||||
│ SangamIO Daemon │
|
||||
│ Configuration-driven hardware abstraction │
|
||||
│ • Streams sensor groups defined in config │
|
||||
│ • Routes commands to configured actuators │
|
||||
└─────────────┬───────────────────────────────────┘
|
||||
│ DeviceDriver Trait
|
||||
┌─────────────▼───────────────────────────────────┐
|
||||
│ Robot-Specific Driver (e.g., CRL-200S) │
|
||||
│ Implements protocol for specific hardware │
|
||||
└─────────────────────────────────────────────────┘
|
||||
---
|
||||
|
||||
## Why Enuxia Vacuum?
|
||||
|
||||
Most consumer robot vacuums are closed systems:
|
||||
|
||||
- proprietary firmware;
|
||||
- vendor-controlled cloud services;
|
||||
- undocumented hardware protocols;
|
||||
- limited local APIs;
|
||||
- difficult long-term maintenance;
|
||||
- little room for experimentation.
|
||||
|
||||
Enuxia Vacuum takes the opposite approach:
|
||||
|
||||
- **local-first** — the robot can operate without a vendor cloud;
|
||||
- **open** — hardware protocols and software architecture are inspectable;
|
||||
- **modular** — hardware access, SLAM, navigation and UI are separated;
|
||||
- **self-hostable** — high-level computation can run on your own Linux infrastructure;
|
||||
- **hardware-aware** — the stack is built around real sensors, actuators and failure modes;
|
||||
- **hackable** — the project is intended to be understood and extended.
|
||||
|
||||
---
|
||||
|
||||
## Current status
|
||||
|
||||
The CRL-200S / M6 Pro platform is already functional as a development robot.
|
||||
|
||||
| Area | Status |
|
||||
|---|---|
|
||||
| Root / recovery access | ✅ Validated |
|
||||
| Low-level hardware control | ✅ Validated |
|
||||
| Wheel encoders | ✅ Validated |
|
||||
| IMU | ✅ Validated |
|
||||
| Bumpers | ✅ Validated |
|
||||
| Cliff sensors | ✅ Validated |
|
||||
| Battery / charging telemetry | ✅ Validated |
|
||||
| Vacuum / brushes | ✅ Validated |
|
||||
| Delta-2D LiDAR | ✅ Decoded and corrected |
|
||||
| GD32 cold-boot initialization | ✅ Fixed |
|
||||
| Local sensor streaming | ✅ Operational |
|
||||
| 2D SLAM | 🟡 Functional, still being consolidated |
|
||||
| Global navigation | 🟡 Existing baseline |
|
||||
| Navigation V2 | 🚧 In development |
|
||||
| Local obstacle avoidance | 🚧 Planned / in development |
|
||||
| Cleaning coverage planner | 📋 Planned |
|
||||
| Docking | 📋 Planned |
|
||||
| Local API | 📋 Planned |
|
||||
| PWA / Android application | 📋 Planned |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
The current architecture keeps the hardware-facing code on the robot while allowing heavier robotics workloads to run on a Linux machine on the local network.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ User layer │
|
||||
│ PWA / Android / diagnostics │
|
||||
└──────────────────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Mission Manager │
|
||||
│ map / clean / goto / dock │
|
||||
└──────────────────────┬───────────────────────┘
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ Exploration │ │ Cleaning Planner │
|
||||
│ frontier / info │ │ coverage / rooms │
|
||||
└────────┬─────────┘ └────────┬─────────┘
|
||||
└──────────────┬─────────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Global Planner │
|
||||
│ Theta* │
|
||||
└────────┬────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Local Planner │
|
||||
│ DWA │
|
||||
└────────┬────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ SangamIO │
|
||||
│ hardware layer │
|
||||
└────────┬────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Robot │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Configuration-Driven Design
|
||||
A layered local costmap is planned between perception and the local planner:
|
||||
|
||||
Everything is defined in `sangamio.toml`:
|
||||
|
||||
```toml
|
||||
[device]
|
||||
type = "crl200s"
|
||||
name = "CRL-200S Vacuum Robot"
|
||||
|
||||
[device.hardware]
|
||||
gd32_port = "/dev/ttyS3"
|
||||
lidar_port = "/dev/ttyS1"
|
||||
heartbeat_interval_ms = 20
|
||||
|
||||
[network]
|
||||
bind_address = "0.0.0.0:5555"
|
||||
```text
|
||||
SLAM static map
|
||||
Live LiDAR
|
||||
Bumper contacts
|
||||
Cliff sensors
|
||||
No-go zones
|
||||
Robot footprint
|
||||
│
|
||||
▼
|
||||
Layered Costmap
|
||||
│
|
||||
▼
|
||||
Local Planner
|
||||
```
|
||||
|
||||
**SLAM applications read this same config** to discover what sensors and actuators are available, enabling truly portable navigation algorithms.
|
||||
---
|
||||
|
||||
## Generic TCP Protocol
|
||||
## Repository layout
|
||||
|
||||
The protocol is robot-agnostic. Any client that speaks the wire format can control any VacuumTiger robot.
|
||||
|
||||
### Message Format
|
||||
|
||||
```
|
||||
[4-byte length (big-endian)][Protobuf payload]
|
||||
```text
|
||||
enuxia-vacuum/
|
||||
├── sangam-io/ # Hardware abstraction and robot drivers
|
||||
├── dhruva-slam/ # SLAM, mapping and current navigation stack
|
||||
├── dhruva-nav/ # Upstream navigation research / reusable building blocks
|
||||
├── drishti/ # Engineering and diagnostic UI
|
||||
├── vastu-slam/ # SLAM library
|
||||
├── protocol-mitm/ # Reverse-engineering / protocol tooling
|
||||
└── slam-test/ # Integration and algorithm tests
|
||||
```
|
||||
|
||||
All communication uses Protocol Buffers for efficient binary serialization. See `sangam-io/proto/sangamio.proto` and `dhruva-slam/proto/dhruva.proto` for schema definitions.
|
||||
The internal component names are intentionally preserved during the first stages of the fork. Renaming them all at once would make upstream comparison and regression analysis unnecessarily difficult.
|
||||
|
||||
### Component Actions
|
||||
---
|
||||
|
||||
| Action | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `Enable` | Activate component | `{"type": "Enable"}` |
|
||||
| `Disable` | Deactivate component | `{"type": "Disable"}` |
|
||||
| `Reset` | Emergency stop / factory reset | `{"type": "Reset"}` |
|
||||
| `Configure` | Set parameters (velocity, speed) | `{"type": "Configure", "config": {...}}` |
|
||||
## Primary hardware: Proscenic M6 Pro / CRL-200S
|
||||
|
||||
### Supported Components
|
||||
The current development target uses:
|
||||
|
||||
| Component | Enable | Disable | Configure |
|
||||
|-----------|--------|---------|-----------|
|
||||
| `drive` | Nav mode | Stop | `{linear, angular}` or `{left, right}` |
|
||||
| `vacuum` | 100% | Off | `{speed: U8}` |
|
||||
| `main_brush` | 100% | Off | `{speed: U8}` |
|
||||
| `side_brush` | 100% | Off | `{speed: U8}` |
|
||||
| `lidar` | Power on | Power off | - |
|
||||
| `led` | - | - | `{state: U8}` |
|
||||
- **SoC:** Allwinner A33
|
||||
- **MCU:** GD32F103
|
||||
- **LiDAR:** Delta-2D
|
||||
- **MCU UART:** `/dev/ttyS3`
|
||||
- **LiDAR UART:** `/dev/ttyS1`
|
||||
- **Linux:** Tina Linux / OpenWrt based userspace
|
||||
|
||||
## Adding a New Robot Platform
|
||||
The hardware driver exposes the robot through SangamIO, which provides a robot-agnostic control and telemetry interface to higher-level software.
|
||||
|
||||
VacuumTiger is designed to support any robot hardware. Here's how to add a new platform:
|
||||
### Validated CRL-200S work
|
||||
|
||||
### 1. Create Configuration
|
||||
The Enuxia fork includes additional work validated on real M6 Pro hardware, including:
|
||||
|
||||
```toml
|
||||
[device]
|
||||
type = "my_robot"
|
||||
- GD32 initialization reliable after cold boot;
|
||||
- corrected Delta-2D angular decoding;
|
||||
- stable full-revolution LiDAR scans;
|
||||
- wheel / bumper / cliff / battery telemetry;
|
||||
- drive, vacuum and brush control;
|
||||
- persistent bumper observations in SLAM;
|
||||
- SLAM pose-graph and submap correction work;
|
||||
- exploration and navigation failure handling improvements.
|
||||
|
||||
[device.hardware]
|
||||
motor_port = "/dev/ttyUSB0"
|
||||
heartbeat_interval_ms = 20
|
||||
---
|
||||
|
||||
[network]
|
||||
bind_address = "0.0.0.0:5555"
|
||||
## SangamIO
|
||||
|
||||
[`sangam-io/`](sangam-io/) is the hardware abstraction layer.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- robot-specific protocol handling;
|
||||
- sensor acquisition;
|
||||
- wheel commands;
|
||||
- actuators;
|
||||
- LiDAR control;
|
||||
- telemetry streaming;
|
||||
- hardware-independent commands for upper layers.
|
||||
|
||||
High-level navigation code should not need to understand the GD32 protocol directly.
|
||||
|
||||
See [sangam-io/README.md](sangam-io/README.md).
|
||||
|
||||
---
|
||||
|
||||
## DhruvaSLAM
|
||||
|
||||
[`dhruva-slam/`](dhruva-slam/) provides the current mapping and localization stack.
|
||||
|
||||
It includes:
|
||||
|
||||
- wheel odometry;
|
||||
- LiDAR preprocessing;
|
||||
- scan matching;
|
||||
- occupancy-grid mapping;
|
||||
- submaps;
|
||||
- loop closure;
|
||||
- pose graph optimization;
|
||||
- map streaming;
|
||||
- the current navigation/exploration baseline.
|
||||
|
||||
The existing navigator is considered a **baseline**, not the final architecture.
|
||||
|
||||
See [dhruva-slam/README.md](dhruva-slam/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Navigation V2
|
||||
|
||||
Navigation is the next major Enuxia development milestone.
|
||||
|
||||
The current architecture couples global planning, frontier exploration and recovery too tightly. Navigation V2 will separate these responsibilities.
|
||||
|
||||
Target design:
|
||||
|
||||
```text
|
||||
Mission
|
||||
↓
|
||||
Goal
|
||||
↓
|
||||
Global planner (Theta*)
|
||||
↓
|
||||
Global path
|
||||
↓
|
||||
Local costmap
|
||||
↓
|
||||
Local planner (DWA)
|
||||
↓
|
||||
Velocity commands
|
||||
↓
|
||||
Progress / stuck detector
|
||||
↓
|
||||
Recovery manager
|
||||
```
|
||||
|
||||
### 2. Implement DeviceDriver Trait
|
||||
### Goals
|
||||
|
||||
```rust
|
||||
// src/devices/my_robot/mod.rs
|
||||
pub struct MyRobotDriver { /* ... */ }
|
||||
Navigation V2 is intended to provide:
|
||||
|
||||
impl DeviceDriver for MyRobotDriver {
|
||||
fn initialize(
|
||||
&mut self,
|
||||
sensor_data: HashMap<String, Arc<Mutex<SensorGroupData>>>
|
||||
) -> Result<()> {
|
||||
// Open serial ports
|
||||
// Spawn reader threads
|
||||
// Update sensor_data in real-time
|
||||
}
|
||||
- real local obstacle avoidance;
|
||||
- footprint-aware collision checking;
|
||||
- dynamic obstacle handling;
|
||||
- measured, encoder-based recovery;
|
||||
- repeated-contact memory;
|
||||
- robust stuck detection;
|
||||
- independent global and local planning;
|
||||
- clean separation between exploration and navigation.
|
||||
|
||||
fn send_command(&mut self, cmd: Command) -> Result<()> {
|
||||
match cmd {
|
||||
Command::ComponentControl { id, action } => {
|
||||
// Handle drive, vacuum, lidar, etc.
|
||||
}
|
||||
Command::Shutdown => { /* ... */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
A bumper contact must become information, not simply trigger a timed reverse command.
|
||||
|
||||
---
|
||||
|
||||
## Mapping vs cleaning
|
||||
|
||||
Mapping and cleaning are intentionally treated as two separate problems.
|
||||
|
||||
### Mapping
|
||||
|
||||
```text
|
||||
unknown space
|
||||
↓
|
||||
frontier detection
|
||||
↓
|
||||
safe approach poses
|
||||
↓
|
||||
navigation
|
||||
↓
|
||||
map expansion
|
||||
```
|
||||
|
||||
### 3. Register in Factory
|
||||
### Cleaning
|
||||
|
||||
```rust
|
||||
// src/devices/mod.rs
|
||||
pub fn create_device(config: &Config) -> Result<Box<dyn DeviceDriver>> {
|
||||
match config.device.device_type.as_str() {
|
||||
"crl200s" => Ok(Box::new(CRL200SDriver::new(...))),
|
||||
"my_robot" => Ok(Box::new(MyRobotDriver::new(...))),
|
||||
_ => Err(Error::UnknownDevice(...)),
|
||||
}
|
||||
}
|
||||
Once a stable map exists:
|
||||
|
||||
```text
|
||||
saved map
|
||||
↓
|
||||
room segmentation
|
||||
↓
|
||||
coverage planning
|
||||
↓
|
||||
cleaning lanes
|
||||
↓
|
||||
local obstacle avoidance
|
||||
↓
|
||||
return to dock
|
||||
```
|
||||
|
||||
That's it! Your new robot works with any existing SLAM application.
|
||||
Future cleaning modes may include:
|
||||
|
||||
## Repository Structure
|
||||
- whole-home cleaning;
|
||||
- room cleaning;
|
||||
- zone cleaning;
|
||||
- spot cleaning;
|
||||
- multiple passes;
|
||||
- no-go / no-mop areas;
|
||||
- multi-floor maps.
|
||||
|
||||
```
|
||||
VacuumTiger/
|
||||
├── sangam-io/ # Hardware abstraction daemon (Rust)
|
||||
│ ├── src/
|
||||
│ │ ├── core/ # DeviceDriver trait, SensorValue types
|
||||
│ │ ├── devices/ # Robot-specific implementations
|
||||
│ │ │ └── crl200s/ # CRL-200S driver (GD32 + Delta-2D)
|
||||
│ │ └── streaming/ # TCP protocol, Protobuf wire format
|
||||
│ ├── proto/ # Protobuf schema definitions
|
||||
│ ├── sangamio.toml # Robot configuration
|
||||
│ ├── COMMANDS.md # GD32 command reference
|
||||
│ └── SENSORSTATUS.md # Sensor packet documentation
|
||||
├── dhruva-slam/ # 2D SLAM daemon (Rust)
|
||||
│ ├── src/
|
||||
│ │ ├── core/ # Foundation types (Pose2D, PointCloud2D)
|
||||
│ │ ├── sensors/ # Odometry fusion, lidar preprocessing
|
||||
│ │ ├── algorithms/ # Scan matching, mapping, loop closure
|
||||
│ │ ├── engine/ # SLAM orchestration, pose graph
|
||||
│ │ └── io/ # SangamIO client, TCP streaming
|
||||
│ └── proto/ # SLAM message definitions
|
||||
├── drishti/ # Diagnostic visualization (Python)
|
||||
│ ├── drishti_ui.py # PyQt GUI with sensor overlays
|
||||
│ ├── drishti.py # Console client
|
||||
│ └── ui/ # PyQt widgets and processors
|
||||
├── slam-test/ # SLAM integration test framework
|
||||
└── protocol-mitm/ # Reverse engineering tools
|
||||
```
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Building
|
||||
|
||||
### Build SangamIO
|
||||
### SangamIO
|
||||
|
||||
```bash
|
||||
# Install Rust with ARM target
|
||||
cd sangam-io
|
||||
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
|
||||
# Build for robot
|
||||
cd sangam-io
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf
|
||||
cargo build --release \
|
||||
--target armv7-unknown-linux-musleabihf
|
||||
```
|
||||
|
||||
### Deploy to Robot
|
||||
Depending on your toolchain, an ARM cross-linker may be required.
|
||||
|
||||
### DhruvaSLAM
|
||||
|
||||
```bash
|
||||
# Copy binary to robot
|
||||
cat target/armv7-unknown-linux-musleabihf/release/sangam-io | \
|
||||
ssh root@vacuum "cat > /usr/sbin/sangamio && chmod +x /usr/sbin/sangamio"
|
||||
cd dhruva-slam
|
||||
|
||||
# Run daemon
|
||||
ssh root@vacuum "RUST_LOG=info /usr/sbin/sangamio"
|
||||
cargo test
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
### Connect with Drishti
|
||||
### Drishti
|
||||
|
||||
```bash
|
||||
cd drishti
|
||||
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
pip install -r requirements.txt
|
||||
python drishti_ui.py --robot 192.168.68.101
|
||||
python drishti_ui.py --help
|
||||
```
|
||||
|
||||

|
||||
---
|
||||
|
||||
## Project Status
|
||||
## Safety
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| SangamIO Daemon | ✅ Complete | Config-driven, ~350KB binary |
|
||||
| CRL-200S Driver | ✅ Verified | GD32 motor controller + Delta-2D lidar |
|
||||
| TCP Protocol | ✅ Complete | Protobuf binary streaming |
|
||||
| Drishti UI | ✅ Complete | Real-time sensor visualization |
|
||||
| DhruvaSLAM | ✅ Complete | 2D SLAM with multi-algorithm support |
|
||||
| SLAM Integration Tests | ✅ Complete | Round-trip test framework |
|
||||
| Additional Platforms | 📋 Planned | Roomba, Turtlebot, etc. |
|
||||
This project controls physical hardware.
|
||||
|
||||
## Contributing
|
||||
During development:
|
||||
|
||||
Help us build the best open-source vacuum robot firmware:
|
||||
- supervise all motion tests;
|
||||
- keep a reliable way to stop the robot;
|
||||
- test away from stairs and drops;
|
||||
- do not bypass cliff protection;
|
||||
- do not run the factory hardware controller and SangamIO at the same time on platforms where they compete for the same hardware;
|
||||
- expect experimental navigation code to fail.
|
||||
|
||||
- **New platforms**: Implement DeviceDriver for your robot
|
||||
- **SLAM**: Build navigation algorithms using the generic protocol
|
||||
- **Sensors**: Add support for new lidar models, IMUs
|
||||
- **Documentation**: Improve guides and examples
|
||||
Enuxia Vacuum is research and development software. It is not currently a safety-certified robotic system.
|
||||
|
||||
## Safety Notice
|
||||
---
|
||||
|
||||
⚠️ This firmware controls physical hardware. Always:
|
||||
- Test in a safe, controlled environment
|
||||
- Keep emergency stop accessible
|
||||
- Monitor battery voltage during operation
|
||||
## Project origin
|
||||
|
||||
Enuxia Vacuum did **not** start from a blank repository.
|
||||
|
||||
It is derived from the open-source **VacuumTiger** project:
|
||||
|
||||
- upstream repository: `codetiger/VacuumTiger`
|
||||
- original root copyright notice: Copyright © 2024 Harishankar Narayanan
|
||||
- upstream history is intentionally preserved in this repository
|
||||
|
||||
The fork retains upstream code, history and applicable copyright/license notices while developing an independent technical direction under Enuxia.
|
||||
|
||||
We are grateful to the upstream project for the reverse-engineering and software foundation that made this work possible.
|
||||
|
||||
See [ATTRIBUTION.md](ATTRIBUTION.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0. See [LICENSE](LICENSE) for details.
|
||||
This repository is a derivative work containing code from multiple upstream components.
|
||||
|
||||
## Links
|
||||
The root `LICENSE` inherited from VacuumTiger is MIT. Some subprojects also declare MIT, Apache-2.0, or dual MIT/Apache-2.0 licensing in their own metadata.
|
||||
|
||||
- [Original Research](https://github.com/codetiger/VacuumRobot)
|
||||
**Applicable upstream notices and component-specific licenses must be preserved.**
|
||||
|
||||
Before packaging or redistributing binaries, review the license metadata of the components you are distributing.
|
||||
|
||||
---
|
||||
|
||||
## Enuxia
|
||||
|
||||
Enuxia develops local-first systems that connect software, infrastructure and physical hardware.
|
||||
|
||||
This project follows the same principle:
|
||||
|
||||
> **The machine should remain understandable, controllable and useful without depending on somebody else's cloud.**
|
||||
|
||||
+107
-386
@@ -1,425 +1,146 @@
|
||||
# DhruvaSLAM
|
||||
# DhruvaSLAM — Enuxia Vacuum mapping stack
|
||||
|
||||
Production-ready 2D SLAM (Simultaneous Localization and Mapping) implementation in Rust for robotic vacuum cleaners, targeting the Allwinner A33 embedded platform.
|
||||
DhruvaSLAM is the current SLAM, mapping and robot-state backend used by Enuxia Vacuum.
|
||||
|
||||
## Overview
|
||||
It originates from the upstream [VacuumTiger](https://github.com/codetiger/VacuumTiger) project and is being adapted by Enuxia around real-world CRL-200S / Proscenic M6 Pro testing.
|
||||
|
||||
DhruvaSLAM is a standalone SLAM daemon that:
|
||||
1. Connects to [SangamIO](../sangam-io/README.md) daemon for sensor data (encoders, gyro, lidar)
|
||||
2. Computes odometry from wheel encoders with IMU fusion
|
||||
3. Performs scan-to-map matching for pose correction
|
||||
4. Builds and maintains an occupancy grid map
|
||||
5. Publishes corrected poses and maps over TCP for visualization
|
||||
The current codebase is a **validated development baseline**, not the final navigation architecture.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DhruvaSLAM Daemon │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
|
||||
│ │ Odometry │ │ SLAM │ │ TCP Publisher │ │
|
||||
│ │ Pipeline │ │ Engine │ │ (Port 5557) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ Encoders ──►│ │ Lidar ─────►│ │ ──► Pose @ 50Hz │ │
|
||||
│ │ Gyro ──────►│ │ Odometry ──►│ │ ──► Map @ 1Hz │ │
|
||||
│ │ │ │ │ │ ──► Scan @ 5Hz │ │
|
||||
│ └─────────────┘ └─────────────┘ └──────────────────────┘ │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│ TCP 5555 (Protobuf)
|
||||
┌──────────▼──────────┐
|
||||
│ SangamIO Daemon │
|
||||
│ (Robot Hardware) │
|
||||
└─────────────────────┘
|
||||
---
|
||||
|
||||
## Responsibilities
|
||||
|
||||
DhruvaSLAM currently handles:
|
||||
|
||||
- wheel odometry;
|
||||
- LiDAR preprocessing;
|
||||
- scan matching;
|
||||
- occupancy-grid mapping;
|
||||
- submaps;
|
||||
- keyframes;
|
||||
- loop closure;
|
||||
- pose-graph optimization;
|
||||
- map persistence;
|
||||
- telemetry / map streaming;
|
||||
- exploration and navigation baseline logic.
|
||||
|
||||
```text
|
||||
SangamIO
|
||||
│
|
||||
├── wheel / sensors
|
||||
└── LiDAR
|
||||
│
|
||||
▼
|
||||
DhruvaSLAM
|
||||
│
|
||||
┌────┴─────┐
|
||||
▼ ▼
|
||||
Pose Map
|
||||
│ │
|
||||
└────┬─────┘
|
||||
▼
|
||||
Navigation / diagnostics
|
||||
```
|
||||
|
||||
### Key Features
|
||||
---
|
||||
|
||||
- **Configurable Odometry** - Wheel-only, Complementary, ESKF, or Mahony fusion
|
||||
- **Multiple Scan Matchers** - ICP, P2L-ICP, Correlative, Multi-Resolution, Hybrid
|
||||
- **Occupancy Grid Mapping** - Log-odds representation with Bresenham ray tracing
|
||||
- **Feature Extraction** - Line and corner detection from occupancy maps
|
||||
- **Loop Closure** - LiDAR-IRIS binary descriptors with sparse CG optimization
|
||||
- **Bag Recording/Playback** - Record sensor data for offline development
|
||||
## Enuxia baseline work
|
||||
|
||||
### Specifications
|
||||
The Enuxia M6 Pro baseline contains a set of fixes developed from real robot runs, including:
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Language | Rust 2024 edition |
|
||||
| Target | ARM Cortex-A7 (Allwinner A33) |
|
||||
| Input | 110Hz sensors, 5Hz lidar |
|
||||
| Output | 50Hz pose, 1Hz map, 5Hz scan |
|
||||
| Memory | < 100MB for SLAM |
|
||||
- configurable minimum LiDAR scan size;
|
||||
- full initial 360° scan before autonomous exploration;
|
||||
- corrected treatment of unknown space during planning;
|
||||
- navigation invalidation after SLAM graph corrections;
|
||||
- submap-origin corrections after pose-graph optimization;
|
||||
- persistent bumper obstacles;
|
||||
- improved frontier failure tracking;
|
||||
- protection against repeated failed frontier selections;
|
||||
- safer stuck/failure termination.
|
||||
|
||||
## Architecture
|
||||
These changes improved the baseline substantially, but they also exposed architectural limitations in the current navigator.
|
||||
|
||||
DhruvaSLAM is organized as a binary crate (not a library) with 5 logical layers:
|
||||
---
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ main.rs │ ← Entry point
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ io/ │ ← Infrastructure
|
||||
│ (sangam_client, streaming) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ engine/ │ ← Orchestration
|
||||
│ (slam, graph optimization) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ algorithms/ │ ← Core algorithms
|
||||
│ (matching, mapping, localization, descriptors) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ sensors/ │ ← Sensor processing
|
||||
│ (odometry, preprocessing, calibration) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ core/ │ ← Foundation
|
||||
│ (types, math, simd) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
## Navigation status
|
||||
|
||||
The existing navigation code is intentionally considered **legacy/baseline navigation**.
|
||||
|
||||
The next Enuxia milestone is Navigation V2.
|
||||
|
||||
### Navigation V2 target
|
||||
|
||||
```text
|
||||
Goal
|
||||
↓
|
||||
Global planner
|
||||
Theta*
|
||||
↓
|
||||
Global path
|
||||
↓
|
||||
Layered local costmap
|
||||
↓
|
||||
DWA local planner
|
||||
↓
|
||||
cmd_vel
|
||||
↓
|
||||
SangamIO
|
||||
```
|
||||
|
||||
See individual module READMEs for details:
|
||||
- [core/](src/core/README.md) - Foundation types (Pose2D, PointCloud2D, LaserScan) and SIMD
|
||||
- [sensors/](src/sensors/README.md) - Odometry fusion and lidar preprocessing
|
||||
- [algorithms/](src/algorithms/README.md) - Matching, mapping, localization, descriptors
|
||||
- [engine/](src/engine/README.md) - SLAM orchestration, keyframes, pose graph
|
||||
- [io/](src/io/README.md) - SangamIO client, TCP streaming
|
||||
Navigation V2 will also introduce:
|
||||
|
||||
## Building
|
||||
- footprint-aware collision checking;
|
||||
- measured progress detection;
|
||||
- encoder-based stuck detection;
|
||||
- contact-memory from bumpers;
|
||||
- recovery based on actual displacement rather than timers;
|
||||
- clean separation between navigation and exploration.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Build for development
|
||||
cargo build
|
||||
|
||||
# Build optimized release
|
||||
cargo test
|
||||
cargo build --release
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Check without building
|
||||
cargo check
|
||||
```
|
||||
|
||||
### Cross-Compilation (ARM)
|
||||
Run with an explicit config:
|
||||
|
||||
```bash
|
||||
# Add ARM target
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
|
||||
# Build for robot
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf
|
||||
RUST_LOG=info \
|
||||
./target/release/dhruva-slam \
|
||||
--config /path/to/dhruva-slam.toml
|
||||
```
|
||||
|
||||
## Usage
|
||||
Do not commit deployment-specific network addresses or credentials into the public repository.
|
||||
|
||||
### Running the SLAM Daemon
|
||||
---
|
||||
|
||||
```bash
|
||||
# With default config (looks for dhruva-slam.toml)
|
||||
cargo run --release
|
||||
## Development philosophy
|
||||
|
||||
# With explicit config file
|
||||
cargo run --release -- --config dhruva-slam.toml
|
||||
SLAM answers:
|
||||
|
||||
# With command-line overrides
|
||||
cargo run --release -- --sangam 192.168.68.101:5555 --port 5557
|
||||
> Where is the robot, and what does the world look like?
|
||||
|
||||
# With debug logging
|
||||
RUST_LOG=debug cargo run --release
|
||||
```
|
||||
Navigation answers:
|
||||
|
||||
**Arguments:**
|
||||
| Argument | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `-c, --config` | Configuration file | `dhruva-slam.toml` |
|
||||
> How should the robot move safely from here to there?
|
||||
|
||||
## Configuration
|
||||
Exploration answers:
|
||||
|
||||
Edit `dhruva-slam.toml`:
|
||||
> Where should the robot go next to learn more about the environment?
|
||||
|
||||
```toml
|
||||
[source]
|
||||
sangam_address = "192.168.68.101:5555"
|
||||
Cleaning answers:
|
||||
|
||||
[output]
|
||||
bind_port = 5557
|
||||
odometry_rate_hz = 50.0
|
||||
map_rate_hz = 1.0
|
||||
feature_rate_hz = 0.2
|
||||
> How should the known floor be covered efficiently?
|
||||
|
||||
[slam]
|
||||
min_range = 0.15 # Lidar min range (m)
|
||||
max_range = 8.0 # Lidar max range (m)
|
||||
Those responsibilities are being separated progressively in the Enuxia fork.
|
||||
|
||||
[lidar]
|
||||
mounting_x = -0.0936 # Lidar X offset from robot center (m)
|
||||
mounting_y = 0.0
|
||||
optical_offset = 0.0258 # Optical center offset (m)
|
||||
angle_offset = 0.0 # Angular calibration (rad)
|
||||
---
|
||||
|
||||
[odometry]
|
||||
algorithm = "mahony" # wheel, complementary, eskf, mahony
|
||||
ticks_per_meter = 4464.0 # Encoder calibration
|
||||
wheel_base = 0.233 # Distance between wheels (m)
|
||||
## Origin
|
||||
|
||||
[matcher]
|
||||
algorithm = "multi_res" # icp, p2l, correlative, multi_res, hybrid_icp, hybrid_p2l
|
||||
DhruvaSLAM originates from VacuumTiger.
|
||||
|
||||
[loop_closure]
|
||||
enabled = true
|
||||
detection_interval = 5 # Check every N keyframes
|
||||
min_keyframe_gap = 20 # Skip recent poses
|
||||
optimization_threshold = 3 # Optimize after N loop closures
|
||||
|
||||
[optimization]
|
||||
solver = "sparse_cg" # dense_cholesky, sparse_cg
|
||||
max_iterations = 50
|
||||
tolerance = 1e-6
|
||||
```
|
||||
|
||||
### Key Parameters
|
||||
|
||||
| Parameter | Description | Typical Value |
|
||||
|-----------|-------------|---------------|
|
||||
| `odometry.algorithm` | Fusion algorithm | `mahony` (recommended) |
|
||||
| `odometry.ticks_per_meter` | Encoder ticks per meter | 4464 (CRL-200S) |
|
||||
| `odometry.wheel_base` | Distance between wheel centers | 0.233m |
|
||||
| `matcher.algorithm` | Scan matching algorithm | `multi_res` (recommended) |
|
||||
| `slam.min_range` | Minimum valid lidar range | 0.15m |
|
||||
| `slam.max_range` | Maximum valid lidar range | 8.0m |
|
||||
|
||||
## TCP Output Protocol
|
||||
|
||||
The daemon publishes data on the configured port (default 5557) using length-prefixed Protobuf:
|
||||
|
||||
```
|
||||
┌──────────────────┬─────────────────────┐
|
||||
│ Length (4 bytes) │ Protobuf Payload │
|
||||
│ Big-endian u32 │ (binary) │
|
||||
└──────────────────┴─────────────────────┘
|
||||
```
|
||||
|
||||
See `proto/dhruva.proto` for the complete schema.
|
||||
|
||||
### Published Messages
|
||||
|
||||
| Message Type | Rate | Description |
|
||||
|--------------|------|-------------|
|
||||
| `OdometryMessage` | 50Hz | Corrected pose (x, y, theta) |
|
||||
| `SlamMapMessage` | 1Hz | Occupancy grid map (base64) |
|
||||
| `SlamScanMessage` | 5Hz | Processed lidar scan with pose |
|
||||
| `FeatureMessage` | 0.2Hz | Extracted lines and corners |
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
dhruva-slam/
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, config, main loop
|
||||
│ │
|
||||
│ ├── core/ # Foundation layer
|
||||
│ │ ├── types/ # Pose2D, Point2D, PointCloud2D, LaserScan
|
||||
│ │ ├── math.rs # Angle normalization, interpolation
|
||||
│ │ └── simd/ # SIMD vector types (Float4)
|
||||
│ │
|
||||
│ ├── sensors/ # Sensor processing
|
||||
│ │ ├── calibration.rs # Gyro bias estimation
|
||||
│ │ ├── odometry/ # Wheel, Complementary, ESKF, Mahony
|
||||
│ │ └── preprocessing/ # Range filter, downsampler, outlier removal
|
||||
│ │
|
||||
│ ├── algorithms/ # Core algorithms
|
||||
│ │ ├── matching/ # ICP, P2L-ICP, Correlative, Multi-Resolution
|
||||
│ │ ├── mapping/ # OccupancyGrid, RayTracer, MapIntegrator
|
||||
│ │ ├── planning/ # A* path planning
|
||||
│ │ └── descriptors/ # LiDAR-IRIS for loop closure
|
||||
│ │
|
||||
│ ├── engine/ # SLAM orchestration
|
||||
│ │ ├── slam/ # OnlineSlam, Keyframe, Submap, Recovery
|
||||
│ │ └── graph/ # PoseGraph, LoopDetector, SparseCG Optimizer
|
||||
│ │
|
||||
│ ├── io/ # I/O infrastructure
|
||||
│ │ ├── sangam_client.rs # SangamIO TCP/UDP client
|
||||
│ │ ├── streaming/ # TCP/UDP publishers
|
||||
│ │ ├── map_manager.rs # Map persistence
|
||||
│ │ └── motion_controller.rs # Velocity commands
|
||||
│ │
|
||||
│ ├── state/ # Multi-threaded state management
|
||||
│ │ ├── shared.rs # Shared atomic state
|
||||
│ │ └── commands.rs # Command processing
|
||||
│ │
|
||||
│ ├── threads/ # Multi-threaded daemon architecture
|
||||
│ │ ├── slam_thread.rs # Real-time SLAM processor
|
||||
│ │ ├── publisher_thread.rs # Data stream publisher
|
||||
│ │ ├── exploration_thread.rs # Autonomous exploration
|
||||
│ │ └── navigation_thread.rs # Goal-based navigation
|
||||
│ │
|
||||
│ ├── exploration/ # Frontier-based exploration
|
||||
│ │ ├── frontier.rs # Frontier detection
|
||||
│ │ └── strategy.rs # Selection strategies
|
||||
│ │
|
||||
│ └── navigation/ # Path planning and execution
|
||||
│ ├── navigator.rs # Navigation orchestrator
|
||||
│ ├── path.rs # Path data structures
|
||||
│ ├── state.rs # Navigation state machine
|
||||
│ ├── target.rs # Goal management
|
||||
│ └── features/ # Frontier, clearance, mapping features
|
||||
│
|
||||
├── proto/ # Protobuf schemas
|
||||
│ └── dhruva.proto # Output message definitions
|
||||
│
|
||||
├── results/ # Benchmark results
|
||||
├── docs/ # Design documents
|
||||
│
|
||||
├── Cargo.toml # Dependencies
|
||||
└── dhruva-slam.toml # Runtime configuration
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
SangamIO (110Hz sensors, 5Hz lidar)
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────┐
|
||||
│ SangamClient │
|
||||
│ • TCP connection (Protobuf) │
|
||||
│ • Message deserialization │
|
||||
└───────────────────┬───────────────────┘
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐
|
||||
│ Odometry │ │ Preprocess │
|
||||
│ Pipeline │ │ + SLAM │
|
||||
│ │ │ │
|
||||
│ • Wheel odom │ │ • Range filter│
|
||||
│ • IMU fusion │─────►│ • Downsample │
|
||||
│ (Mahony/ESKF) │ │ • Match to map│
|
||||
│ │ │ • Update map │
|
||||
└───────┬───────┘ └───────┬───────┘
|
||||
│ │
|
||||
└──────────┬───────────┘
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ TCP Publisher │
|
||||
│ • Pose @ 50Hz │
|
||||
│ • Map @ 1Hz │
|
||||
│ • Scan @ 5Hz │
|
||||
│ • Features │
|
||||
└──────────────────┘
|
||||
│
|
||||
▼
|
||||
Visualization Client
|
||||
(Drishti)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run specific test
|
||||
cargo test test_wheel_odometry
|
||||
|
||||
# Run with output
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
## Algorithm Selection Guide
|
||||
|
||||
### Odometry Algorithms
|
||||
|
||||
| Algorithm | Use Case | CPU |
|
||||
|-----------|----------|-----|
|
||||
| `wheel` | Testing, baseline | Very Low |
|
||||
| `complementary` | Simple fusion | Low |
|
||||
| `eskf` | Production with uncertainty | Medium |
|
||||
| `mahony` | **Recommended** - auto gyro calibration | Low |
|
||||
|
||||
### Scan Matchers
|
||||
|
||||
| Algorithm | Use Case | CPU | Accuracy |
|
||||
|-----------|----------|-----|----------|
|
||||
| `icp` | Small displacements | Low | Good |
|
||||
| `p2l` | Structured environments (walls) | Medium | Very Good |
|
||||
| `correlative` | Large initial errors | High | Good |
|
||||
| `multi_res` | **Recommended** - balanced | Medium | Good |
|
||||
| `hybrid_icp` | Correlative + ICP refinement | High | Very Good |
|
||||
| `hybrid_p2l` | Correlative + P2L refinement | High | Excellent |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Crate | Purpose |
|
||||
|-------|---------|
|
||||
| `prost` | Protobuf serialization |
|
||||
| `serde` | Configuration serialization |
|
||||
| `postcard` | Bag file format |
|
||||
| `kiddo` | K-d tree for nearest neighbor |
|
||||
| `basic_toml` | Config parsing |
|
||||
| `env_logger` | Logging |
|
||||
| `ctrlc` | Signal handling |
|
||||
| `approx` | Floating-point comparison in tests |
|
||||
|
||||
## Design Notes
|
||||
|
||||
### Design Principles
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| **Binary, Not Library** | Standalone daemon, not imported as a library |
|
||||
| **Runtime Configuration** | Algorithm selection via TOML config |
|
||||
| **Embedded-First** | Optimized for Allwinner A33 (ARM Cortex-A7) |
|
||||
| **Testability** | Components unit-testable with mock data |
|
||||
|
||||
### Platform Considerations (Allwinner A33)
|
||||
|
||||
**Target specs:** Quad-core ARM Cortex-A7 @ 1.2 GHz, 512MB RAM, NEON SIMD
|
||||
|
||||
**Performance Budget (5 Hz SLAM target):**
|
||||
|
||||
| Component | Budget | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Preprocessing | 5 ms | Range filter + downsample to 180 pts |
|
||||
| Odometry Fusion | 1 ms | Mahony AHRS |
|
||||
| Scan Matching | 50 ms | Multi-resolution correlative |
|
||||
| Map Update | 10 ms | Log-odds with ray tracing |
|
||||
| Feature Extraction | 20 ms | Line/corner detection |
|
||||
|
||||
**Memory Budget (< 100 MB):**
|
||||
|
||||
| Component | Allocation |
|
||||
|-----------|------------|
|
||||
| Occupancy Grid | 40 MB |
|
||||
| Scan Buffers | 10 MB |
|
||||
| K-d Trees | 10 MB |
|
||||
| Working Memory | 40 MB |
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- Loop closure detection is implemented but not fully integrated into pose correction
|
||||
- Background graph optimization runs in publisher thread (could be separate thread for large maps)
|
||||
|
||||
### Future Work
|
||||
|
||||
See [IMPROVEMENTS.md](IMPROVEMENTS.md) for detailed roadmap:
|
||||
- Full loop closure integration with pose graph correction
|
||||
- Background optimization thread
|
||||
- Multi-resolution map representation
|
||||
- GICP scan matching
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [IMPROVEMENTS.md](IMPROVEMENTS.md) - Technical improvement roadmap
|
||||
- [docs/LOOP_CLOSURE_PROPOSAL.md](docs/LOOP_CLOSURE_PROPOSAL.md) - LiDAR-IRIS implementation plan
|
||||
|
||||
## License
|
||||
|
||||
See the root [LICENSE](../LICENSE) file.
|
||||
Enuxia preserves upstream history and licensing while developing the M6 Pro integration and future navigation architecture independently.
|
||||
|
||||
+69
-401
@@ -1,427 +1,95 @@
|
||||
# Drishti - Robot Diagnostic Tool
|
||||
# Drishti — Enuxia Vacuum engineering console
|
||||
|
||||
PyQt5-based real-time visualization and control client for the CRL-200S robotic vacuum with two operating modes:
|
||||
Drishti is the engineering and diagnostic interface used during development of Enuxia Vacuum.
|
||||
|
||||
- **Sensor Mode**: Connect to SangamIO for raw sensor visualization (robot view)
|
||||
- **SLAM Mode**: Connect to DhruvaSLAM for map visualization and autonomous navigation
|
||||
It provides real-time visibility into the robot and SLAM stack.
|
||||
|
||||
Drishti originates from the upstream [VacuumTiger](https://github.com/codetiger/VacuumTiger) project and remains a development tool in the Enuxia fork.
|
||||
|
||||
It is **not intended to be the final consumer application**.
|
||||
|
||||
---
|
||||
|
||||
## Modes
|
||||
|
||||
### Sensor mode
|
||||
|
||||
Connects directly to SangamIO for:
|
||||
|
||||
- sensor telemetry;
|
||||
- LiDAR visualization;
|
||||
- wheel/encoder data;
|
||||
- bumper and cliff state;
|
||||
- battery information;
|
||||
- actuator testing;
|
||||
- low-level diagnostics.
|
||||
|
||||
### SLAM mode
|
||||
|
||||
Connects to DhruvaSLAM for:
|
||||
|
||||
- occupancy-grid visualization;
|
||||
- robot pose;
|
||||
- LiDAR overlay;
|
||||
- navigation paths;
|
||||
- mapping commands;
|
||||
- mapping and navigation diagnostics.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd drishti
|
||||
python -m venv venv
|
||||
source venv/bin/activate # or venv\Scripts\activate on Windows
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `protobuf>=4.0` - Protocol buffer messaging
|
||||
- `PyQt5>=5.15.0` - GUI framework
|
||||
- `pyqtgraph>=0.13.0` - High-performance plotting/visualization
|
||||
- `PyOpenGL>=3.1.6` - 3D graphics via OpenGL
|
||||
- `numpy>=1.21.0` - Numerical computing
|
||||
|
||||
## Usage
|
||||
|
||||
### Sensor Mode
|
||||
|
||||
Connect to SangamIO for raw sensor data visualization:
|
||||
Show available CLI options:
|
||||
|
||||
```bash
|
||||
python drishti_ui.py sensor --robot 192.168.68.101
|
||||
python drishti_ui.py --help
|
||||
```
|
||||
|
||||
Options:
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `--robot` | 192.168.68.101 | SangamIO hostname or IP |
|
||||
| `--port` | 5555 | SangamIO port (TCP+UDP) |
|
||||
| `--log-raw-packets` | disabled | Log raw GD32 status packets |
|
||||
| `--verbose` | disabled | Enable debug logging |
|
||||
---
|
||||
|
||||
### SLAM Mode
|
||||
## Role in Enuxia Vacuum
|
||||
|
||||
Connect to DhruvaSLAM for SLAM visualization and control:
|
||||
Drishti should remain an engineering console.
|
||||
|
||||
```bash
|
||||
python drishti_ui.py slam --host localhost
|
||||
The future user-facing interface is expected to communicate with the local Enuxia Vacuum backend through a stable API/WebSocket layer.
|
||||
|
||||
```text
|
||||
Drishti
|
||||
│
|
||||
└── engineering/debug
|
||||
|
||||
PWA / Android
|
||||
│
|
||||
└── user application
|
||||
│
|
||||
▼
|
||||
Enuxia Vacuum API
|
||||
│
|
||||
▼
|
||||
missions / mapping / cleaning
|
||||
```
|
||||
|
||||
Options:
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `--host` | localhost | DhruvaSLAM hostname |
|
||||
| `--port` | 5557 | DhruvaSLAM port (TCP+UDP) |
|
||||
| `--command-port` | same as --port | Optional separate command port |
|
||||
| `--verbose` | disabled | Enable debug logging |
|
||||
---
|
||||
|
||||
### Console Demo (Legacy)
|
||||
## Known development caveats
|
||||
|
||||
```bash
|
||||
python drishti.py --robot 192.168.68.101 --verbose
|
||||
```
|
||||
UI state should not be treated as the sole source of truth during development.
|
||||
|
||||
## Features
|
||||
Some current mapping/navigation states still require consolidation in the backend state model.
|
||||
|
||||
### Sensor Mode
|
||||
Physical motion tests must always be supervised.
|
||||
|
||||
**3D Visualization:**
|
||||
- Full-screen OpenGL wireframe robot model
|
||||
- Animated wheels (spin based on encoder data)
|
||||
- Animated lidar turret (spins when enabled)
|
||||
- Animated brushes (main and side, when spinning)
|
||||
- Animated vacuum fan (when active)
|
||||
- Bumper indicators (green normal, red triggered)
|
||||
- Cliff sensor indicators (4 pairs)
|
||||
- Battery bar with charging animation
|
||||
- IMU orientation overlay (roll/pitch/yaw)
|
||||
- Multiple camera presets (iso, top, front, side)
|
||||
- Auto-rotate camera when idle
|
||||
---
|
||||
|
||||
**Hardware Control:**
|
||||
- Drive motor enable/disable with speed control
|
||||
- Lidar enable/disable with scan counter
|
||||
- Vacuum pump speed 0-100%
|
||||
- Main brush speed 0-100%
|
||||
- Side brush speed 0-100%
|
||||
- Emergency stop (immediate watchdog reset)
|
||||
## Origin
|
||||
|
||||
**Sensor Display:**
|
||||
- Battery level + charging state
|
||||
- Bumper state (left/right)
|
||||
- Cliff sensors (4 pairs)
|
||||
- Wheel encoder ticks
|
||||
- Lidar range and quality
|
||||
- IMU gyro/accelerometer data
|
||||
- Motor current, voltage status
|
||||
- Error codes and diagnostics
|
||||
Drishti originates from VacuumTiger.
|
||||
|
||||

|
||||
|
||||
### SLAM Mode
|
||||
|
||||
**Mapping:**
|
||||
- Start/stop mapping sessions
|
||||
- Clear current map
|
||||
- Save maps to persistent storage
|
||||
- List all saved maps with metadata (size, creation time, area)
|
||||
- Enable/rename/delete saved maps
|
||||
|
||||
**Visualization:**
|
||||
- Real-time occupancy grid (10Hz update)
|
||||
- Robot pose display (x, y, heading)
|
||||
- Trajectory trail (2000-point history)
|
||||
- Current lidar scan overlay
|
||||
- Navigation path visualization
|
||||
- Goal marker display
|
||||
- Grid overlay with axis labels
|
||||
|
||||
**Navigation:**
|
||||
- Set goal via Ctrl+Click on map
|
||||
- Visualize planned path (waypoints)
|
||||
- Track navigation progress (distance remaining, ETA)
|
||||
- Cancel active navigation goal
|
||||
|
||||
**Camera Control:**
|
||||
- Auto-follow mode (centers on robot)
|
||||
- Manual pan (click+drag)
|
||||
- Manual zoom (mouse wheel)
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
### Sensor Mode
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `M` | Toggle drive motor (enable/disable) |
|
||||
| `L` | Toggle lidar |
|
||||
| `V` | Toggle vacuum pump |
|
||||
| `B` | Toggle main brush |
|
||||
| `S` | Toggle side brush |
|
||||
| `↑` | Drive forward |
|
||||
| `↓` | Drive backward |
|
||||
| `←` | Turn left |
|
||||
| `→` | Turn right |
|
||||
| `↑+←` | Drive forward while turning left |
|
||||
| `↑+→` | Drive forward while turning right |
|
||||
| `↓+←` | Drive backward while turning left |
|
||||
| `↓+→` | Drive backward while turning right |
|
||||
| `Space` | Emergency stop (immediate reset) |
|
||||
| `Esc` | Quit application |
|
||||
|
||||
### SLAM Mode
|
||||
|
||||
| Key/Action | Description |
|
||||
|------------|-------------|
|
||||
| `Ctrl+Click` | Set navigation goal on map |
|
||||
| Mouse wheel | Zoom map in/out |
|
||||
| Click+drag | Pan map view |
|
||||
| `Esc` | Quit application |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
drishti/
|
||||
├── drishti_ui.py # Entry point (dual-mode launcher)
|
||||
├── drishti.py # Legacy console client
|
||||
├── debug_proto.py # Proto message debug tool
|
||||
├── requirements.txt # Python dependencies
|
||||
├── proto/ # Protocol buffer definitions
|
||||
│ ├── sangamio_pb2.py # SangamIO protobuf (generated)
|
||||
│ └── dhruva_pb2.py # DhruvaSLAM protobuf (generated)
|
||||
└── ui/
|
||||
├── main_window.py # Sensor mode main window
|
||||
├── slam_main_window.py # SLAM mode main window
|
||||
├── threads/
|
||||
│ ├── telemetry_thread.py # SangamIO data (TCP+UDP)
|
||||
│ ├── slam_thread.py # DhruvaSLAM data (TCP)
|
||||
│ └── odometry_thread.py # Legacy SLAM odometry
|
||||
├── widgets/
|
||||
│ ├── robot_3d_view.py # 3D wireframe visualization
|
||||
│ ├── robot_3d_geometry.py # 3D geometry primitives
|
||||
│ ├── lidar_scan_overlay.py # Polar lidar display
|
||||
│ ├── slam_view.py # Map + trajectory view
|
||||
│ ├── control_panel.py # Sensor mode controls
|
||||
│ ├── slam_control_panel.py # SLAM mode controls
|
||||
│ ├── navigation_panel.py # Navigation status
|
||||
│ ├── diagnostics_panel.py # Timing diagnostics
|
||||
│ ├── compact_actuator.py # Actuator controls
|
||||
│ └── collapsible_group.py # Collapsible sections
|
||||
└── processors/
|
||||
└── imu_processor.py # Mahony AHRS filter
|
||||
```
|
||||
|
||||
### Thread Model
|
||||
|
||||
**Sensor Mode (TelemetryThread):**
|
||||
1. Connects via TCP (port 5555) for registration
|
||||
2. Receives UDP unicast sensor data (port 5555)
|
||||
3. TCP connection kept alive for sending commands
|
||||
4. Emits: `sensor_data_received(dict)`, `connection_status(bool, str)`
|
||||
|
||||
**SLAM Mode (SlamThread):**
|
||||
1. Connects via TCP (port 5557, unified architecture)
|
||||
2. Two separate connections: streaming (receive-only) and commands (request/response)
|
||||
3. Emits: `robot_status_received`, `sensor_status_received`, `current_map_received`, `map_list_received`, `navigation_status_received`
|
||||
|
||||
## Network Protocol
|
||||
|
||||
### Sensor Mode (SangamIO)
|
||||
|
||||
Uses hybrid UDP/TCP architecture on same port (5555):
|
||||
- **UDP**: Sensor streaming (low latency, no head-of-line blocking)
|
||||
- **TCP**: Commands (reliable delivery)
|
||||
|
||||
**Client Registration:**
|
||||
1. Client connects TCP to port 5555
|
||||
2. Server records client IP address
|
||||
3. UDP packets sent to client on same port
|
||||
4. When TCP disconnects, UDP streaming stops
|
||||
|
||||
**Message Format:** Length-prefixed Protobuf
|
||||
```
|
||||
[4-byte length (big-endian)][Protobuf payload]
|
||||
```
|
||||
|
||||
**UDP Topics (Outbound @ 110Hz):**
|
||||
- `sensors/sensor_status`: All 13 sensors
|
||||
- `sensors/device_version`: Version info (one-time)
|
||||
- `sensors/lidar`: LidarScan @ 5Hz
|
||||
|
||||
**TCP Commands (Inbound):**
|
||||
- `ComponentControl`: Enable/disable/configure components
|
||||
- `ProtocolSync`: Synchronization
|
||||
- `Shutdown`: Graceful shutdown
|
||||
|
||||
**Sensor Data Available:**
|
||||
| Sensor | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `is_charging` | bool | Charging state |
|
||||
| `battery_level` | u8 | Battery percentage (0-100) |
|
||||
| `bumper_left` | bool | Left bumper triggered |
|
||||
| `bumper_right` | bool | Right bumper triggered |
|
||||
| `cliff_left` | bool | Left cliff detected |
|
||||
| `cliff_center` | bool | Center cliff detected |
|
||||
| `cliff_right` | bool | Right cliff detected |
|
||||
| `wheel_left` | u32 | Left encoder ticks |
|
||||
| `wheel_right` | u32 | Right encoder ticks |
|
||||
| `lidar_speed` | u16 | Lidar RPM (0 when disabled) |
|
||||
| `gyro_x/y/z` | i16 | Gyroscope (0.1°/s units) |
|
||||
| `tilt_x/y/z` | i16 | Accelerometer (mG units) |
|
||||
|
||||
**Command Examples:**
|
||||
|
||||
Enable drive motor:
|
||||
```python
|
||||
cmd = sangamio_pb2.Command()
|
||||
cmd.component_control.id = "drive"
|
||||
cmd.component_control.action.type = ComponentAction.ENABLE
|
||||
```
|
||||
|
||||
Set velocity:
|
||||
```python
|
||||
cmd.component_control.id = "drive"
|
||||
cmd.component_control.action.type = ComponentAction.CONFIGURE
|
||||
cmd.component_control.action.config["linear"].f32_val = 0.2
|
||||
cmd.component_control.action.config["angular"].f32_val = 0.0
|
||||
```
|
||||
|
||||
### SLAM Mode (DhruvaSLAM)
|
||||
|
||||
Uses unified port architecture (default: 5557) for both streaming and commands.
|
||||
|
||||
**Protocol:** Length-prefixed Protobuf (see `dhruva-slam/proto/dhruva.proto`)
|
||||
|
||||
**DhruvaStream (Inbound):**
|
||||
| Message | Rate | Content |
|
||||
|---------|------|---------|
|
||||
| `robot_status` | 10Hz | Pose, state, battery, mapping progress |
|
||||
| `sensor_status` | 10Hz | Lidar, encoders, IMU |
|
||||
| `current_map` | 1Hz | Occupancy grid |
|
||||
| `map_list` | on change | Saved maps |
|
||||
| `navigation_status` | 5Hz | Path, goal, progress |
|
||||
|
||||
**DhruvaCommand (Outbound):**
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `StartMapping(map_name)` | Begin SLAM session |
|
||||
| `StopMapping(save)` | End session, optionally save |
|
||||
| `ClearMap()` | Discard current map |
|
||||
| `EnableMap(map_id)` | Load saved map |
|
||||
| `RenameMap(map_id, new_name)` | Rename saved map |
|
||||
| `DeleteMap(map_id)` | Delete saved map |
|
||||
| `SetGoal(x, y, theta)` | Set navigation goal |
|
||||
| `CancelGoal()` | Stop navigation |
|
||||
| `EmergencyStop()` | Halt all motion |
|
||||
|
||||
**Occupancy Grid Encoding:**
|
||||
| Value | Meaning | Display Color |
|
||||
|-------|---------|---------------|
|
||||
| 0 | Free space | Light gray |
|
||||
| 100 | Occupied | White |
|
||||
| 255 | Unknown | Dark gray |
|
||||
|
||||
## IMU Processing
|
||||
|
||||
The `imu_processor.py` implements a **Mahony AHRS filter** with quaternion-based orientation estimation.
|
||||
|
||||
**Calibration:**
|
||||
- Collects bias samples for ~3 seconds (330 samples @ 110Hz)
|
||||
- Computes average gyro bias to subtract from measurements
|
||||
- Returns zeros during calibration phase
|
||||
|
||||
**Filter Parameters:**
|
||||
- `KP = 5.0`: Proportional gain for fast settling
|
||||
- `KI = 0.1`: Integral gain for bias correction
|
||||
|
||||
**Output:** Roll, pitch, yaw in degrees (ROS REP-103 convention)
|
||||
|
||||
## Coordinate Conventions
|
||||
|
||||
All coordinates use **ROS REP-103** standard:
|
||||
|
||||
```
|
||||
+X (forward)
|
||||
↑
|
||||
|
|
||||
←------+------→ +Y (left)
|
||||
|
|
||||
+Z (up, out of page)
|
||||
|
||||
Angles: 0 rad = +X direction, increases counter-clockwise
|
||||
```
|
||||
|
||||
- **Roll**: Rotation around X (left/right tilt)
|
||||
- **Pitch**: Rotation around Y (nose up/down)
|
||||
- **Yaw**: Rotation around Z (heading, CCW positive)
|
||||
|
||||
## Debug Utilities
|
||||
|
||||
### debug_proto.py
|
||||
|
||||
Capture and analyze DhruvaSLAM proto messages:
|
||||
|
||||
```bash
|
||||
python debug_proto.py
|
||||
```
|
||||
|
||||
- Connects to localhost:5557
|
||||
- Sends StartMapping command
|
||||
- Analyzes received proto messages
|
||||
- Saves raw bytes to `/tmp/proto_message.bin`
|
||||
- Useful for debugging protocol issues
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sensor Mode
|
||||
|
||||
**No data received:**
|
||||
- Verify SangamIO is running: `ssh root@vacuum "ps aux | grep sangam"`
|
||||
- Check network: `ping 192.168.68.101`
|
||||
- Verify port 5555 is accessible (both TCP and UDP)
|
||||
- Check firewall allows UDP on port 5555
|
||||
|
||||
**Connection refused:**
|
||||
- Robot may not be running SangamIO
|
||||
- AuxCtrl may still be running (rename it first)
|
||||
|
||||
**Drive not responding:**
|
||||
- Make sure motor is enabled first (press `M`)
|
||||
- Check status bar for "Drive ENABLED"
|
||||
- Verify no error codes displayed
|
||||
|
||||
**3D view not rendering:**
|
||||
- Ensure PyOpenGL is installed
|
||||
- Check OpenGL drivers are up to date
|
||||
|
||||
### SLAM Mode
|
||||
|
||||
**No map data:**
|
||||
- Verify DhruvaSLAM is running: `ps aux | grep dhruva`
|
||||
- Check connection status in UI status bar
|
||||
- Start mapping session using control panel
|
||||
|
||||
**Parse errors:**
|
||||
- Failed proto messages saved to `/tmp/failed_proto.bin`
|
||||
- Use `debug_proto.py` to analyze protocol issues
|
||||
|
||||
**Navigation not working:**
|
||||
- Ensure a map is loaded (Enable Map)
|
||||
- Robot must be localized first
|
||||
- Check localization confidence indicator
|
||||
|
||||
**Map not updating:**
|
||||
- Verify mapping is active (state = "Mapping")
|
||||
- Check trajectory is being drawn
|
||||
- Large maps may have slower update rate
|
||||
|
||||
## UI Components
|
||||
|
||||
### Control Panel (Sensor Mode)
|
||||
|
||||
Collapsible sections:
|
||||
- **Motion Control**: Drive motor enable + speed slider
|
||||
- **Cleaning Systems**: Vacuum, main brush, side brush
|
||||
- **Sensors**: Lidar toggle, cliff calibration
|
||||
- **System**: LED control, dustbox status
|
||||
- **Calibration**: IMU, compass calibration
|
||||
- **Power Management**: Voltage displays
|
||||
|
||||
### SLAM Control Panel
|
||||
|
||||
- **Status Display**: State, active map, pose, confidence
|
||||
- **Mapping Controls**: Start/Stop/Clear/Emergency Stop
|
||||
- **Map Management**: Enable/Rename/Delete
|
||||
- **Map List**: Scrollable list with metadata
|
||||
|
||||
### Navigation Panel
|
||||
|
||||
- Current state (Idle, Planning, Navigating, Goal Reached)
|
||||
- Target description
|
||||
- Distance remaining + ETA
|
||||
- Progress bar
|
||||
- Waypoint counter
|
||||
- Cancel Navigation button
|
||||
Enuxia preserves the upstream history and applicable notices while continuing development as part of Enuxia Vacuum.
|
||||
|
||||
+95
-620
@@ -1,646 +1,121 @@
|
||||
# SangamIO
|
||||
# SangamIO — Enuxia Vacuum hardware layer
|
||||
|
||||
Hardware abstraction daemon for robotic vacuum platforms, providing real-time sensor streaming via UDP and command control via TCP.
|
||||
SangamIO is the hardware abstraction daemon used by Enuxia Vacuum.
|
||||
|
||||
Supported hardware:
|
||||
It runs close to the robot hardware and translates platform-specific protocols into a stable interface for SLAM, navigation, diagnostics and future mission-control software.
|
||||
|
||||
- CRL-200S
|
||||
- Roborock S5 Max (rooted firmware `4.1.2_1668`)
|
||||
This component originates from the upstream [VacuumTiger](https://github.com/codetiger/VacuumTiger) project and is being extended and validated by Enuxia on real robotic vacuum hardware.
|
||||
|
||||
## Overview
|
||||
---
|
||||
|
||||
SangamIO acts as a bridge between high-level clients (SLAM, visualization) and low-level hardware (GD32 motor controller, Delta-2D lidar). It runs as a standalone daemon on the robot, streaming sensor data at 110Hz via UDP and accepting commands over TCP.
|
||||
## Role
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Client Applications (SLAM/Drishti) │
|
||||
└─────────────┬───────────────┬───────────────────┘
|
||||
│ UDP 5555 │ TCP 5555
|
||||
│ (sensors) │ (commands)
|
||||
┌─────────────▼───────────────▼───────────────────┐
|
||||
│ SangamIO Daemon (Robot) │
|
||||
│ • Telemetry streaming @ 110Hz (UDP unicast) │
|
||||
│ • Lidar streaming @ 5Hz (360° scans) │
|
||||
│ • Command processing (TCP, reliable) │
|
||||
│ • Real-time control loop (20ms heartbeat) │
|
||||
└─────────────┬───────────────┬───────────────────┘
|
||||
│ /dev/ttyS3 │ /dev/ttyS1
|
||||
┌──────▼─────────┐ ┌──▼────────────┐
|
||||
│ GD32F103 MCU │ │ Delta-2D Lidar│
|
||||
│ (Motor Control)│ │ (360° Scan) │
|
||||
└────────────────┘ └───────────────┘
|
||||
```text
|
||||
Navigation / SLAM / diagnostics
|
||||
│
|
||||
▼
|
||||
SangamIO API
|
||||
│
|
||||
┌───────┴────────┐
|
||||
▼ ▼
|
||||
Robot MCU LiDAR
|
||||
/ actuators / sensors
|
||||
```
|
||||
|
||||
### Key Specifications
|
||||
SangamIO owns the hardware-specific details so higher-level software does not have to.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Language | Rust 2024 edition |
|
||||
| Version | 0.3.0 |
|
||||
| Target | ARM (armv7-unknown-linux-musleabihf) or host (simulation) |
|
||||
| Binary Size | ~350KB (statically linked) |
|
||||
| Memory Usage | <10MB RSS |
|
||||
| CPU Usage | <1% on Allwinner A33 |
|
||||
Responsibilities include:
|
||||
|
||||
## Building
|
||||
- low-level robot protocol handling;
|
||||
- sensor telemetry;
|
||||
- wheel control;
|
||||
- vacuum and brush control;
|
||||
- LiDAR control;
|
||||
- platform-specific initialization;
|
||||
- TCP command transport;
|
||||
- UDP sensor streaming.
|
||||
|
||||
### Development (Host Machine)
|
||||
---
|
||||
|
||||
## Current Enuxia development platform
|
||||
|
||||
### Proscenic M6 Pro / 3irobotix CRL-200S
|
||||
|
||||
Validated hardware:
|
||||
|
||||
- Allwinner A33 Linux SoC;
|
||||
- GD32F103 motor/control MCU;
|
||||
- Delta-2D LiDAR;
|
||||
- wheel encoders;
|
||||
- IMU;
|
||||
- left/right bumpers;
|
||||
- cliff sensors;
|
||||
- battery and charging telemetry;
|
||||
- cleaning actuators.
|
||||
|
||||
Relevant device nodes:
|
||||
|
||||
```text
|
||||
/dev/ttyS3 GD32 MCU
|
||||
/dev/ttyS1 Delta-2D LiDAR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enuxia CRL-200S work
|
||||
|
||||
The fork contains additional changes validated on physical M6 Pro hardware.
|
||||
|
||||
### GD32 cold-boot initialization
|
||||
|
||||
The original initialization path was not always sufficient after a true power cycle.
|
||||
|
||||
The Enuxia baseline contains a revised initialization/wake sequence that restores continuous MCU telemetry after cold boot.
|
||||
|
||||
### Delta-2D packet decoding
|
||||
|
||||
The Delta-2D parser was corrected after validating the packet layout on real hardware.
|
||||
|
||||
The measurement payload uses:
|
||||
|
||||
```text
|
||||
byte 0 motor speed
|
||||
byte 1..2 offset angle
|
||||
byte 3..4 start angle
|
||||
byte 5.. 3-byte samples
|
||||
```
|
||||
|
||||
Angular sampling is derived from the scan structure rather than treating the offset-angle field as the per-sample increment.
|
||||
|
||||
This produces stable full-revolution scans on the M6 Pro.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Build for real hardware
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf
|
||||
|
||||
# Build for simulation (mock device)
|
||||
cargo build --release --features mock
|
||||
|
||||
# Run with configuration file
|
||||
cargo run --release --features mock -- mock.toml
|
||||
cargo run --release --features mock -- --config mock.toml
|
||||
|
||||
# Enable verbose logging
|
||||
RUST_LOG=debug cargo run --release --features mock
|
||||
|
||||
# Run tests
|
||||
cargo test --features mock
|
||||
```
|
||||
|
||||
### Production (ARM Robot)
|
||||
|
||||
```bash
|
||||
# Add ARM target (one-time)
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
|
||||
# Build for ARM (real hardware)
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf
|
||||
|
||||
# Strip debug symbols (reduces size ~40%)
|
||||
arm-linux-gnueabihf-strip \
|
||||
target/armv7-unknown-linux-musleabihf/release/sangam-io
|
||||
cargo build --release \
|
||||
--target armv7-unknown-linux-musleabihf
|
||||
```
|
||||
|
||||
The Roborock S5 Max stock image uses glibc rather than musl:
|
||||
A suitable ARM cross-linker may be required depending on the host system.
|
||||
|
||||
```bash
|
||||
rustup target add armv7-unknown-linux-gnueabihf
|
||||
cargo build --release --target armv7-unknown-linux-gnueabihf
|
||||
```
|
||||
---
|
||||
|
||||
The S5 Max sequence helper is firmware-specific. Its current offset is for
|
||||
firmware `4.1.2_1668` and `libuart_api` Build ID
|
||||
`8d639ff160b8766b7bb87e848e9b0a9c0ff12a08`; re-derive the offset before using
|
||||
it with another firmware build.
|
||||
## Safety
|
||||
|
||||
### Feature Flags
|
||||
SangamIO can directly control wheels and cleaning actuators.
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| (default) | Real hardware support only |
|
||||
| `mock` | Enable mock device simulation |
|
||||
Do not run two hardware controllers simultaneously if both attempt to own the same MCU/UARTs.
|
||||
|
||||
## Deployment
|
||||
Always supervise real-hardware tests and keep a reliable stop path available.
|
||||
|
||||
**SSH**: `root@vacuum` (see project docs for credentials)
|
||||
---
|
||||
|
||||
```bash
|
||||
# Deploy binary (device lacks sftp-server, use cat over SSH)
|
||||
cat target/armv7-unknown-linux-musleabihf/release/sangam-io | \
|
||||
ssh root@vacuum "cat > /usr/sbin/sangamio && chmod +x /usr/sbin/sangamio"
|
||||
## Origin
|
||||
|
||||
# Deploy configuration
|
||||
ssh root@vacuum "cat > /etc/sangamio.toml" < sangamio.toml
|
||||
SangamIO originates from the VacuumTiger project.
|
||||
|
||||
# Disable original firmware (rename to prevent auto-restart)
|
||||
ssh root@vacuum "mv /usr/sbin/AuxCtrl /usr/sbin/AuxCtrl.bak && killall -9 AuxCtrl"
|
||||
|
||||
# Run daemon
|
||||
ssh root@vacuum "RUST_LOG=info /usr/sbin/sangamio"
|
||||
```
|
||||
|
||||
> **Important**: Always overwrite `/usr/sbin/sangamio` directly. The robot monitor auto-restarts processes, so renaming AuxCtrl prevents conflicts.
|
||||
|
||||
### Roborock S5 Max live takeover
|
||||
|
||||
The S5 Max driver must exclusively own `/dev/ttyS2`, `/dev/ttyS1`,
|
||||
`/dev/lds_motor`, and `/dev/watchdog`. The stock `rr_loader` and `WatchDoge`
|
||||
processes therefore cannot run alongside SangamIO. The MCU also rejects stale
|
||||
transmit sequence numbers, so a live handoff must capture the last stock
|
||||
sequence while `rr_loader` is frozen.
|
||||
|
||||
The procedure below was validated on rooted firmware `4.1.2_1668`. Keep a
|
||||
second terminal open. A forced reboot is the recovery path and restores the
|
||||
stock stack because the `/etc/inittab` override is a temporary bind mount.
|
||||
|
||||
Build and upload the daemon, helper, and base configuration:
|
||||
|
||||
```bash
|
||||
TARGET=armv7-unknown-linux-gnueabihf
|
||||
cargo build --release --target "$TARGET" \
|
||||
--bin sangam-io --bin roborock_s5max_sequence
|
||||
scp -O \
|
||||
"target/$TARGET/release/sangam-io" \
|
||||
"target/$TARGET/release/roborock_s5max_sequence" \
|
||||
roborock-s5max.toml root@vacuum:/mnt/data/
|
||||
```
|
||||
|
||||
Then perform the handoff as root on the robot:
|
||||
|
||||
```sh
|
||||
BIN=/mnt/data/sangam-io
|
||||
SEQ=/mnt/data/roborock_s5max_sequence
|
||||
BASE=/mnt/data/roborock-s5max.toml
|
||||
LIVE=/tmp/roborock-s5max-live.toml
|
||||
|
||||
# Every exit after this point restores the stock stack by rebooting.
|
||||
recover()
|
||||
{
|
||||
trap - EXIT HUP INT TERM
|
||||
sync
|
||||
/sbin/reboot -f
|
||||
}
|
||||
trap recover EXIT HUP INT TERM
|
||||
|
||||
chmod 755 "$BIN" "$SEQ"
|
||||
WD_PIDS="$(pidof WatchDoge)"
|
||||
RR_PIDS="$(pidof rr_loader)"
|
||||
|
||||
# Select the rr_loader instance that owns the MCU UART.
|
||||
RR_PID=""
|
||||
for pid in $RR_PIDS; do
|
||||
for fd in /proc/$pid/fd/*; do
|
||||
if [ "$(readlink "$fd" 2>/dev/null)" = /dev/ttyS2 ]; then
|
||||
RR_PID="$pid"
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
test -n "$WD_PIDS" && test -n "$RR_PID" || exit 1
|
||||
|
||||
# Prevent init from immediately respawning WatchDoge during takeover.
|
||||
grep -v WatchDoge /etc/inittab > /tmp/inittab.no-watchdog
|
||||
mount --bind /tmp/inittab.no-watchdog /etc/inittab
|
||||
kill -HUP 1
|
||||
sleep 1
|
||||
|
||||
# Freeze every process that can race the handoff, then read the sequence.
|
||||
kill -STOP $WD_PIDS $RR_PIDS
|
||||
kill -STOP $(pidof RoboController AppProxy 2>/dev/null) 2>/dev/null
|
||||
LAST_SEQUENCE="$($SEQ "$RR_PID")" || exit 1
|
||||
echo "Captured stock MCU sequence: $LAST_SEQUENCE"
|
||||
|
||||
# Let TERM cleanup release the watchdog, UARTs, and LDS motor.
|
||||
kill -TERM $WD_PIDS $RR_PIDS
|
||||
kill -CONT $WD_PIDS $RR_PIDS
|
||||
sleep 2
|
||||
for pid in $WD_PIDS $RR_PIDS; do
|
||||
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid"
|
||||
done
|
||||
|
||||
sed "s/^# last_stock_sequence = 0/last_stock_sequence = $LAST_SEQUENCE/" \
|
||||
"$BASE" > "$LIVE"
|
||||
grep '^last_stock_sequence' "$LIVE" >/dev/null || exit 1
|
||||
RUST_LOG=info "$BIN" --config "$LIVE"
|
||||
```
|
||||
|
||||
For the default read-only configuration, leave `allow_actuation = false`.
|
||||
This provides MCU telemetry only: starting the LDS requires charger and
|
||||
subsystem-control writes, so lidar remains disabled behind the actuation gate.
|
||||
Set `allow_actuation = true` only after reviewing the safety limits and only
|
||||
when a command client will control the robot.
|
||||
|
||||
To stop SangamIO, send `SIGINT` from the second terminal. Its ordered shutdown
|
||||
stops wheels, cleaning motors, and the LDS; the shell's exit trap then reboots
|
||||
the robot and restores all stock processes:
|
||||
|
||||
```sh
|
||||
kill -INT $(pidof sangam-io)
|
||||
```
|
||||
|
||||
If the daemon or SSH session fails, use `/sbin/reboot -f` from the second
|
||||
terminal. If SSH is unavailable, the hardware watchdog should reboot the robot
|
||||
after its 16-second timeout. Do not try to restart only part of the stock stack
|
||||
after a takeover.
|
||||
|
||||
## Configuration
|
||||
|
||||
For the Roborock S5 Max, start with `roborock-s5max.toml`. Actuation is
|
||||
disabled by default and live takeover must continue the stock MCU transmit
|
||||
sequence using `roborock_s5max_sequence`.
|
||||
|
||||
The S5 Max publishes its detailed native values alongside the established
|
||||
`sensor_status` keys expected by Dhruva clients:
|
||||
|
||||
| Common key | S5 Max source |
|
||||
|------------|---------------|
|
||||
| `wheel_left`, `wheel_right` | Low 16 bits of the signed 32-bit monotonic odometry counters; wrapping deltas remain valid |
|
||||
| `gyro_x`, `gyro_y`, `gyro_z` | MCU angular rates converted from rad/s to signed milliradians/s |
|
||||
| `bumper_left`, `bumper_right` | Conservative aliases: both become true when either raw bumper channel is active |
|
||||
| `cliff_left_side`, `cliff_left_front`, `cliff_right_front`, `cliff_right_side` | Conservative aliases: all become true when any raw cliff channel is active |
|
||||
| `battery_voltage`, `battery_level` | MCU BMS voltage and state-of-charge report |
|
||||
| `is_dock_connected`, `is_charging` | MCU dock-state report |
|
||||
| `dustbox_attached` | MCU dustbin presence report |
|
||||
|
||||
The conservative bumper and cliff aliases avoid understating a safety event;
|
||||
their physical channel ordering has not yet been proven. Exact values remain
|
||||
available as `bumper_raw` and `cliff_raw`. Native signed wheel counters remain
|
||||
available as `wheel_left_ticks` and `wheel_right_ticks`.
|
||||
|
||||
The `c0` drive command fields are mean encoder ticks per 20 ms and body yaw in
|
||||
radians per second. This was established from stock captures (forced turns use
|
||||
exactly `pi/2`, while the measured yaw rate settles near `1.57`) and exercised
|
||||
on hardware by the bounded wheel-deadman test and autonomous mapping runs.
|
||||
|
||||
Edit `sangamio.toml`:
|
||||
|
||||
```toml
|
||||
[device]
|
||||
type = "crl200s"
|
||||
name = "CRL-200S Vacuum Robot"
|
||||
|
||||
[device.hardware]
|
||||
gd32_port = "/dev/ttyS3"
|
||||
lidar_port = "/dev/ttyS1"
|
||||
heartbeat_interval_ms = 20
|
||||
|
||||
# Coordinate frame transforms (ROS REP-103)
|
||||
[device.hardware.frame_transforms.lidar]
|
||||
scale = -1.0 # Convert CW to CCW
|
||||
offset = 3.14159265 # Rotate 180° (lidar mounted backward)
|
||||
|
||||
[device.hardware.frame_transforms.imu_gyro]
|
||||
x = [2, 1] # output_x (Roll) = input_z
|
||||
y = [1, 1] # output_y (Pitch) = input_y
|
||||
z = [0, -1] # output_z (Yaw) = input_x * -1
|
||||
|
||||
[network]
|
||||
bind_address = "0.0.0.0:5555"
|
||||
```
|
||||
|
||||
| Parameter | Description | Valid Values |
|
||||
|-----------|-------------|--------------|
|
||||
| `gd32_port` | Motor controller serial port | Device path |
|
||||
| `lidar_port` | Lidar sensor serial port | Device path |
|
||||
| `heartbeat_interval_ms` | Safety heartbeat interval | **20-50ms only** |
|
||||
| `bind_address` | TCP server bind address | `host:port` |
|
||||
| `frame_transforms` | Coordinate transforms (optional) | See below |
|
||||
|
||||
> **Critical**: The GD32 has a hardware watchdog requiring heartbeats every 20-50ms. Values outside this range will cause motors to stop.
|
||||
|
||||
### Coordinate Frame Transforms
|
||||
|
||||
SangamIO transforms raw sensor data to **ROS REP-103** convention:
|
||||
- **X = forward** (direction robot drives)
|
||||
- **Y = left** (port side)
|
||||
- **Z = up**
|
||||
- **Angles = counter-clockwise (CCW) positive**
|
||||
|
||||
All transforms default to identity (no change) if not specified. The CRL-200S requires transforms because:
|
||||
- **Lidar**: Mounted backward (0° = rear), clockwise angles
|
||||
- **IMU**: Non-standard axis mapping (gyro_x = yaw, not roll)
|
||||
|
||||
| Transform | Type | Formula | CRL-200S Value |
|
||||
|-----------|------|---------|----------------|
|
||||
| `lidar` | AffineTransform1D | `out = scale * in + offset` | `scale=-1, offset=π` |
|
||||
| `imu_gyro` | AxisTransform3D | `[source_axis, sign]` | Remap + flip yaw |
|
||||
| `imu_accel` | AxisTransform3D | `[source_axis, sign]` | Identity (default)
|
||||
|
||||
## Network Protocol
|
||||
|
||||
SangamIO uses a hybrid UDP/TCP architecture for optimal performance:
|
||||
|
||||
| Channel | Protocol | Purpose | Why |
|
||||
|---------|----------|---------|-----|
|
||||
| Sensor data | UDP unicast | Low-latency streaming | No head-of-line blocking |
|
||||
| Commands | TCP | Reliable control | Guaranteed delivery |
|
||||
|
||||
### Message Format
|
||||
|
||||
Both UDP and TCP use length-prefixed Protobuf framing:
|
||||
|
||||
```
|
||||
┌──────────────────┬─────────────────────┐
|
||||
│ Length (4 bytes) │ Protobuf Payload │
|
||||
│ Big-endian u32 │ (binary) │
|
||||
└──────────────────┴─────────────────────┘
|
||||
```
|
||||
|
||||
See `proto/sangamio.proto` for the complete schema.
|
||||
|
||||
### Topics (UDP Streaming)
|
||||
|
||||
| Topic | Rate | Size | Description |
|
||||
|-------|------|------|-------------|
|
||||
| `sensors/sensor_status` | 110Hz | ~150B | Encoders, IMU, bumpers, cliffs, battery |
|
||||
| `sensors/lidar` | 5Hz | ~2KB | 360° point cloud (angle, distance, quality) |
|
||||
| `sensors/device_version` | Once | ~50B | Firmware version info |
|
||||
|
||||
### Commands (TCP)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `ComponentControl::drive` | Set linear/angular velocity |
|
||||
| `ComponentControl::lidar` | Enable/disable lidar motor |
|
||||
| `ComponentControl::vacuum` | Control vacuum motor (0-100%) |
|
||||
| `ComponentControl::*_brush` | Control brushes (0-100%) |
|
||||
| `Shutdown` | Graceful daemon shutdown |
|
||||
|
||||
### Client Registration
|
||||
|
||||
UDP uses unicast (not broadcast). Clients are registered automatically:
|
||||
|
||||
1. Client connects TCP to port 5555
|
||||
2. Server records client IP address
|
||||
3. UDP packets are sent to client on same port
|
||||
4. When TCP disconnects, UDP streaming stops
|
||||
|
||||
### Python Client Example
|
||||
|
||||
```python
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
from proto import sangamio_pb2
|
||||
|
||||
ROBOT_IP = '192.168.68.101'
|
||||
PORT = 5555
|
||||
|
||||
# TCP for commands
|
||||
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
tcp_sock.connect((ROBOT_IP, PORT))
|
||||
|
||||
# UDP for sensor data (same port)
|
||||
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
udp_sock.bind(('0.0.0.0', PORT))
|
||||
|
||||
def receive_sensors():
|
||||
while True:
|
||||
data, addr = udp_sock.recvfrom(4096)
|
||||
length = struct.unpack('>I', data[:4])[0]
|
||||
msg = sangamio_pb2.Message()
|
||||
msg.ParseFromString(data[4:4+length])
|
||||
|
||||
if msg.topic == 'sensors/sensor_status':
|
||||
sg = msg.sensor_group
|
||||
print(f"Battery: {sg.values['battery_level'].u8_val}%")
|
||||
|
||||
# Start receiving in background
|
||||
threading.Thread(target=receive_sensors, daemon=True).start()
|
||||
|
||||
# Send commands via TCP
|
||||
def send_command(cmd):
|
||||
payload = cmd.SerializeToString()
|
||||
tcp_sock.send(struct.pack('>I', len(payload)) + payload)
|
||||
```
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
sangam-io/
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, TCP/UDP listener
|
||||
│ ├── config.rs # TOML configuration loading
|
||||
│ ├── error.rs # Error types
|
||||
│ │
|
||||
│ ├── core/
|
||||
│ │ ├── driver.rs # DeviceDriver trait
|
||||
│ │ └── types.rs # SensorValue, Command types
|
||||
│ │
|
||||
│ ├── devices/
|
||||
│ │ ├── mod.rs # Device factory (crl200s, mock)
|
||||
│ │ ├── crl200s/ # Real hardware driver
|
||||
│ │ │ ├── mod.rs # CRL200S orchestrator
|
||||
│ │ │ ├── constants.rs # Hardware constants
|
||||
│ │ │ ├── COMMANDS.md # GD32 command reference
|
||||
│ │ │ ├── SENSORSTATUS.md # Sensor packet docs
|
||||
│ │ │ ├── gd32/ # Motor controller driver
|
||||
│ │ │ │ ├── mod.rs # Driver core
|
||||
│ │ │ │ ├── commands.rs # Command handlers
|
||||
│ │ │ │ ├── heartbeat.rs # 20ms watchdog
|
||||
│ │ │ │ ├── reader.rs # Status parsing
|
||||
│ │ │ │ ├── packet.rs # Packet encoding
|
||||
│ │ │ │ ├── protocol.rs # Packet framing
|
||||
│ │ │ │ └── state.rs # Atomic state
|
||||
│ │ │ └── delta2d/ # Lidar driver
|
||||
│ │ │ ├── mod.rs # Driver core
|
||||
│ │ │ └── protocol.rs # Packet parsing
|
||||
│ │ │
|
||||
│ │ └── mock/ # Simulation driver (--features mock)
|
||||
│ │ ├── mod.rs # MockDriver, simulation loop
|
||||
│ │ ├── config.rs # SimulationConfig structs
|
||||
│ │ ├── physics.rs # Differential drive kinematics
|
||||
│ │ ├── lidar_sim.rs # Ray-casting lidar
|
||||
│ │ ├── imu_sim.rs # IMU with noise
|
||||
│ │ ├── encoder_sim.rs # Encoder with slip
|
||||
│ │ ├── sensor_sim.rs # Bumpers, cliffs
|
||||
│ │ ├── map_loader.rs # PGM+YAML loader
|
||||
│ │ └── noise.rs # Noise generator
|
||||
│ │
|
||||
│ └── streaming/
|
||||
│ ├── mod.rs # Streaming module docs
|
||||
│ ├── wire.rs # Protobuf serialization
|
||||
│ ├── udp_publisher.rs # UDP sensor streaming
|
||||
│ └── tcp_receiver.rs # TCP command handling
|
||||
│
|
||||
├── proto/
|
||||
│ └── sangamio.proto # Protobuf schema
|
||||
├── maps/ # Example simulation maps
|
||||
│ ├── example.yaml # Map metadata
|
||||
│ └── example.pgm # Occupancy grid
|
||||
├── sangamio.toml # Hardware configuration
|
||||
└── mock.toml # Simulation configuration
|
||||
```
|
||||
|
||||
## Supported Components
|
||||
|
||||
Commands use a unified `ComponentControl` interface:
|
||||
|
||||
| Component | Enable | Disable | Configure |
|
||||
|-----------|--------|---------|-----------|
|
||||
| `drive` | Mode 0x02 | Stop + Mode 0x00 | `linear`, `angular` (m/s, rad/s) |
|
||||
| `vacuum` | 100% | 0% | `speed` (0-100%) |
|
||||
| `main_brush` | 100% (S5 Max: stock 71%) | 0% | `speed` (0-100%) |
|
||||
| `side_brush` | 100% (S5 Max: stock 30%) | 0% | `speed` (0-100%) |
|
||||
| `water_pump` | 100% | 0% | `speed` (0-100%) |
|
||||
| `lidar` | Power on | Power off | - |
|
||||
| `led` | - | - | `state` (0-18) |
|
||||
|
||||
## Sensors
|
||||
|
||||
The `sensor_status` group includes:
|
||||
|
||||
| Sensor | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `bumper_left`, `bumper_right` | Bool | Bumper contact |
|
||||
| `cliff_left_side`, `cliff_left_front` | Bool | Left cliff sensors |
|
||||
| `cliff_right_front`, `cliff_right_side` | Bool | Right cliff sensors |
|
||||
| `is_charging` | Bool | Charging state |
|
||||
| `battery_voltage` | F32 | Battery voltage (V) |
|
||||
| `battery_level` | U8 | Estimated charge (0-100%) |
|
||||
| `wheel_left`, `wheel_right` | U16 | Encoder ticks |
|
||||
| `gyro_x`, `gyro_y`, `gyro_z` | I16 | Angular velocity (Roll, Pitch, Yaw after transform) |
|
||||
| `accel_x`, `accel_y`, `accel_z` | I16 | Acceleration (raw units) |
|
||||
| `tilt_x`, `tilt_y`, `tilt_z` | I16 | LP-filtered gravity vector |
|
||||
| `start_button`, `dock_button` | U16 | Button press states |
|
||||
| `dustbox_attached` | Bool | Dustbox present |
|
||||
| `is_dock_connected` | Bool | On charging dock |
|
||||
|
||||
> **Note**: IMU values are in ROS REP-103 frame after applying `frame_transforms`.
|
||||
|
||||
## Thread Model
|
||||
|
||||
### Real Hardware (crl200s)
|
||||
|
||||
| Thread | Purpose | Timing |
|
||||
|--------|---------|--------|
|
||||
| Main | TCP listener, client accept | - |
|
||||
| GD32 Heartbeat | Safety watchdog | 20ms ±2ms |
|
||||
| GD32 Reader | Status parsing → channel | Continuous |
|
||||
| Lidar Reader | Scan accumulation | Continuous |
|
||||
| UDP Publisher | Sensor streaming | <1ms loop |
|
||||
| TCP Receiver | Per-client commands | On-demand |
|
||||
|
||||
### Simulation (mock)
|
||||
|
||||
| Thread | Purpose | Timing |
|
||||
|--------|---------|--------|
|
||||
| Main | TCP listener, client accept | - |
|
||||
| Simulation Loop | Physics + sensors | 9ms (110Hz) |
|
||||
| UDP Publisher | Sensor streaming | <1ms loop |
|
||||
| TCP Receiver | Per-client commands | On-demand |
|
||||
|
||||
## Hardware Constraints
|
||||
|
||||
- **Heartbeat**: GD32 requires CMD=0x66 every 20-50ms or motors stop
|
||||
- **Serial Exclusivity**: Only one process can open `/dev/ttyS3` or `/dev/ttyS1`
|
||||
- **Initialization**: GD32 requires ~5 second wake sequence at boot
|
||||
- **Baud Rate**: Both serial ports run at 115200
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Sensor latency | ~20ms |
|
||||
| Command latency | ~30ms |
|
||||
| Protobuf bandwidth | ~95KB/s |
|
||||
|
||||
## Mock Device Simulation
|
||||
|
||||
The mock device driver enables algorithm development and testing without physical hardware.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Build with mock feature
|
||||
cargo build --release --features mock
|
||||
|
||||
# Run simulation
|
||||
cargo run --release --features mock -- mock.toml
|
||||
```
|
||||
|
||||
### Map Format (ROS Standard)
|
||||
|
||||
Maps use PGM + YAML format compatible with ROS Navigation:
|
||||
|
||||
**maps/example.yaml:**
|
||||
```yaml
|
||||
image: example.pgm # Grayscale occupancy grid
|
||||
resolution: 0.02 # meters per pixel
|
||||
origin: [-5.0, -5.0, 0.0] # [x, y, yaw] of bottom-left
|
||||
occupied_thresh: 0.65 # Darker = occupied
|
||||
cliff_mask: cliffs.pgm # Optional cliff layer
|
||||
```
|
||||
|
||||
**PGM pixel values:**
|
||||
- White (255) = Free space
|
||||
- Black (0) = Wall/obstacle
|
||||
- Gray (205) = Unknown
|
||||
|
||||
### Configuration
|
||||
|
||||
**mock.toml:**
|
||||
```toml
|
||||
[device]
|
||||
type = "mock"
|
||||
name = "Mock CRL-200S"
|
||||
|
||||
[device.simulation]
|
||||
map_file = "maps/example.yaml"
|
||||
start_x = 1.5 # meters
|
||||
start_y = 3.5 # meters
|
||||
start_theta = 0.0 # radians
|
||||
speed_factor = 1.0 # 2.0 = 2x speed
|
||||
random_seed = 42 # 0 = random each run
|
||||
|
||||
[device.simulation.robot]
|
||||
wheel_base = 0.233 # meters
|
||||
ticks_per_meter = 4464.0
|
||||
collision_mode = "stop" # "stop", "slide", "passthrough"
|
||||
|
||||
[device.simulation.lidar]
|
||||
num_rays = 360
|
||||
scan_rate_hz = 5.0
|
||||
max_range = 8.0
|
||||
|
||||
[device.simulation.lidar.noise]
|
||||
range_stddev = 0.005 # 5mm noise
|
||||
miss_rate = 0.01 # 1% invalid readings
|
||||
|
||||
[network]
|
||||
bind_address = "0.0.0.0:5555"
|
||||
```
|
||||
|
||||
### Simulated Components
|
||||
|
||||
| Component | Method | Configurability |
|
||||
|-----------|--------|-----------------|
|
||||
| Lidar | Ray-casting | Noise, miss rate, quality |
|
||||
| Encoders | Kinematics | Slip, quantization |
|
||||
| IMU | Physics-based | Per-axis noise, drift |
|
||||
| Bumpers | Collision detect | Zone angles, trigger distance |
|
||||
| Cliffs | Mask lookup | Sensor positions |
|
||||
| Battery | Fixed values | Voltage, level, charging |
|
||||
|
||||
### Speed Factor
|
||||
|
||||
Accelerate simulation for faster testing:
|
||||
|
||||
| Factor | sensor_status | lidar | Wall time for 1 min sim |
|
||||
|--------|---------------|-------|-------------------------|
|
||||
| 1.0 | 110 Hz | 5 Hz | 60 sec |
|
||||
| 2.0 | 220 Hz | 10 Hz | 30 sec |
|
||||
| 5.0 | 550 Hz | 25 Hz | 12 sec |
|
||||
|
||||
### Creating Maps
|
||||
|
||||
1. **GIMP/Photoshop**: Draw black (walls) on white (free space)
|
||||
2. **ROS map_server**: Save maps from SLAM runs
|
||||
3. **Cartographer**: Export as PGM
|
||||
4. Save as 8-bit grayscale PGM with YAML metadata
|
||||
|
||||
## Documentation
|
||||
|
||||
### CRL-200S Hardware
|
||||
- [src/devices/crl200s/COMMANDS.md](src/devices/crl200s/COMMANDS.md) - GD32 command reference (27 commands)
|
||||
- [src/devices/crl200s/SENSORSTATUS.md](src/devices/crl200s/SENSORSTATUS.md) - Status packet byte layout (96 bytes)
|
||||
|
||||
### Mock Device Simulation
|
||||
- [docs/mock-device-guide.md](docs/mock-device-guide.md) - Mock device user guide
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
# Enable debug logging
|
||||
RUST_LOG=debug ./sangamio
|
||||
|
||||
# Test with virtual serial ports
|
||||
socat -d -d pty,raw,echo=0 pty,raw,echo=0
|
||||
|
||||
# Monitor on robot
|
||||
ssh root@vacuum "journalctl -u sangamio -f"
|
||||
|
||||
# Check resource usage
|
||||
ssh root@vacuum "top -p $(pgrep sangamio)"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
See the root [LICENSE](../LICENSE) file.
|
||||
Enuxia maintains this fork as part of the broader Enuxia Vacuum robotics stack. Upstream history and applicable license notices are preserved.
|
||||
|
||||
Reference in New Issue
Block a user