chore: establish Enuxia Vacuum project identity

This commit is contained in:
Julien Denizot
2026-10-03 18:09:28 +02:00
parent b22643d128
commit c95d942fc4
7 changed files with 1400 additions and 2015 deletions
+667
View File
@@ -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.
+53
View File
@@ -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.
+77 -430
View File
@@ -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.
+332 -178
View File
@@ -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
```
![Drishti UI](drishti/drishti.png)
---
## 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
View File
@@ -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
View File
@@ -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.
![Drishti UI](drishti.png)
### 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
View File
@@ -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.