Improve documentation for device driver extensibility

Update CLAUDE.md, module docs, and READMEs to reflect current
architecture and provide clearer guidance for adding new device
implementations. Key changes:

- Add comprehensive DeviceDriver trait documentation with examples
- Document module structure and factory pattern in devices/mod.rs
- Update file path references to reflect crl200s/ organization
- Change configuration example from TOML to JSON format
- Fix minor documentation inconsistencies across READMEs
This commit is contained in:
codetiger
2025-12-01 10:17:09 +05:30
parent 62c9a99c00
commit 0ab78b6380
11 changed files with 187 additions and 84 deletions
+51 -43
View File
@@ -174,23 +174,23 @@ while True:
## Configuration
Edit `sangamio.toml`:
Edit `hardware.json`:
```toml
[hardware]
gd32_port = "/dev/ttyS3" # Motor controller
lidar_port = "/dev/ttyS1" # Delta-2D lidar
[robot]
max_ticks_per_sec = 3000.0 # Calibration constant
[streaming]
tcp_pub_address = "0.0.0.0:5555" # Telemetry output
tcp_cmd_address = "0.0.0.0:5556" # Command input
[logging]
level = "info"
output = "stdout"
```json
{
"device": {
"type": "crl200s",
"name": "CRL-200S Vacuum Robot",
"hardware": {
"gd32_port": "/dev/ttyS3",
"lidar_port": "/dev/ttyS1"
}
},
"network": {
"tcp_pub_address": "0.0.0.0:5555",
"tcp_cmd_address": "0.0.0.0:5556"
}
}
```
## Module Organization
@@ -200,37 +200,47 @@ VacuumTiger/
├── sangam-io/ # Hardware abstraction daemon
│ ├── src/
│ │ ├── main.rs # Entry point, arg parsing
│ │ ├── app.rs # Application orchestration
│ │ ├── config.rs # Configuration structures
│ │ ├── error.rs # Error types
│ │ ├── serial_io.rs # Serial transport layer
│ │ ├── core/ # Core abstractions
│ │ │ ├── mod.rs
│ │ │ ├── driver.rs # DeviceDriver trait
│ │ │ └── types.rs # SensorValue, Command types
│ │ ├── devices/ # Hardware drivers
│ │ │ ├── mod.rs # DeviceDriver trait
│ │ │ ├── crl200s/ # CRL-200S robot platform
│ │ │ │ ├── gd32/ # Motor controller driver
│ │ │ │ │ ├── mod.rs # Driver orchestration
│ │ │ │ │ ├── protocol.rs # Packet encoding/decoding
│ │ │ │ │ ├── heartbeat.rs # 20ms watchdog loop
│ │ │ │ │ ├── reader.rs # Status parsing
│ │ │ │ │ ├── commands.rs # Command handlers
│ │ │ │ │ ├── actuators.rs # Actuator control
│ │ │ │ │ └── state.rs # Shared atomic state
│ │ │ │ └── delta2d/ # 3iRobotix lidar
│ │ │ │ ├── mod.rs # LidarDriver impl
│ │ │ │ └── protocol.rs # Packet parsing
│ │ │ └── constants.rs # Hardware constants
│ │ │ ├── mod.rs # Device factory
│ │ │ └── crl200s/ # CRL-200S robot platform
│ │ │ ├── mod.rs
│ │ │ ├── constants.rs # Offsets, flags, timing
│ │ │ ├── gd32/ # Motor controller driver
│ │ │ │ ├── mod.rs # Driver orchestration
│ │ │ │ ├── protocol.rs # RxPacket, PacketReader
│ │ │ │ ├── packet.rs # TxPacket, checksum
│ │ │ │ ├── ring_buffer.rs # Zero-copy ring buffer
│ │ │ │ ├── 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
│ │ └── streaming/ # TCP communication
│ │ ├── mod.rs # Re-exports
│ │ ├── messages.rs # Protocol definitions
│ │ ├── wire.rs # Serialization layer
│ │ ├── tcp_publisher.rs # Outbound stream
│ │ └── tcp_receiver.rs # Command handler
│ └── hardware.json # Configuration file
│ ├── hardware.json # Configuration file
│ ├── COMMANDS.md # GD32 command reference
│ └── SENSORSTATUS.md # Sensor packet documentation
│
└── drishti/ # Python visualization client
├── drishti.py # Console client
├── drishti_ui.py # GUI application
└── ui/ # Web interface
└── ui/ # PyQt widgets
├── main_window.py
├── processors/ # Data processing
├── threads/ # Background workers
└── widgets/ # UI components
```
## Critical Hardware Constraints
@@ -257,13 +267,13 @@ VacuumTiger/
- **Format**: `[0xFA 0xFB] [LEN] [CMD] [PAYLOAD] [CRC]`
- **CRC**: 16-bit big-endian word sum checksum
- **Status**: CMD=0x15, 96 bytes @ 500Hz
- **Location**: `src/devices/gd32/protocol.rs`
- **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/delta2d/protocol.rs`
- **Location**: `src/devices/crl200s/delta2d/protocol.rs`
## Performance Characteristics
@@ -362,13 +372,11 @@ python drishti.py --robot 192.168.68.101 --verbose
## Documentation References
- **sangam-io/README.md**: Quick start and overview
- **sangam-io/ARCHITECTURE.md**: Design decisions and extensibility
- **sangam-io/PROTOCOL.md**: TCP message specifications
- **sangam-io/DEPLOYMENT.md**: Production installation guide
- **sangam-io/CHANGELOG.md**: Version history and changes
- **sangam-io/COMMANDS.md**: GD32 command reference with TCP API
- **sangam-io/SENSORSTATUS.md**: Sensor packet byte layout
- **drishti/README.md**: Python client documentation
- **drishti/UI_README.md**: GUI application guide
- **protocol-mitm/README.md**: MITM reverse engineering tools
- **protocol-mitm/docs/**: Protocol discovery documentation
## Future Roadmap
@@ -390,6 +398,6 @@ When modifying this codebase:
1. **Maintain simplicity**: Avoid unnecessary abstractions
2. **Prioritize reliability**: Real-time constraints matter
3. **Document changes**: Update CHANGELOG.md
3. **Document changes**: Update relevant documentation
4. **Test on hardware**: Virtual tests insufficient
5. **Preserve safety**: Never bypass heartbeat mechanism
+8 -5
View File
@@ -10,7 +10,7 @@ VacuumTiger is a modular, configuration-driven firmware stack that makes it easy
- **Extensible**: Add new robot platforms by implementing one trait
- **Generic Protocol**: TCP streaming works with any SLAM application
- **Real-time**: 500Hz sensor updates, <25ms command latency
- **Hackable**: Small codebase (~3K lines Rust), no dependencies on proprietary SDKs
- **Hackable**: Small codebase (~4K lines Rust), no dependencies on proprietary SDKs
## How It Works
@@ -223,14 +223,17 @@ That's it! Your new robot works with any existing SLAM application.
VacuumTiger/
├── sangam-io/ # Hardware abstraction daemon (Rust)
│ ├── src/
│ │ ├── core/ # DeviceDriver trait, sensor types
│ │ ├── core/ # DeviceDriver trait, SensorValue types
│ │ ├── devices/ # Robot-specific implementations
│ │ │ └── crl200s/ # CRL-200S driver (GD32 + Delta-2D)
│ │ └── streaming/ # TCP protocol, wire format
│ └── hardware.json # Robot configuration
│ ├── hardware.json # Robot configuration
│ ├── COMMANDS.md # GD32 command reference
│ └── SENSORSTATUS.md # Sensor packet documentation
├── drishti/ # Diagnostic visualization (Python)
│ ├── drishti_ui.py # GUI with sensor overlays
│ └── drishti.py # Console client
│ ├── drishti_ui.py # PyQt GUI with sensor overlays
│ ├── drishti.py # Console client
│ └── ui/ # PyQt widgets and processors
└── protocol-mitm/ # Reverse engineering tools
```
+1 -1
View File
@@ -1,6 +1,6 @@
# Drishti - Robot Diagnostic Tool
Simple monitoring tool for testing sensor diagnosis and TCP protocol communication with SangamIO.
PyQt-based monitoring tool for testing sensor diagnosis and TCP protocol communication with SangamIO.
## Installation
+1 -1
View File
@@ -4,7 +4,7 @@ Man-in-the-Middle (MITM) toolset for reverse-engineering proprietary serial prot
## Overview
This toolset enables transparent serial port interception using PTY-based forwarding, allowing capture of bidirectional communication without modifying the original firmware or hardware. The captured protocol was successfully reverse-engineered and implemented in the [SangamIO library](../sangam-io/).
This toolset enables transparent serial port interception using PTY-based forwarding, allowing capture of bidirectional communication without modifying the original firmware or hardware. The captured protocol was successfully reverse-engineered and implemented in the [SangamIO daemon](../sangam-io/).
### Key Features
@@ -43,7 +43,7 @@ Packet Format: [0xFA 0xFB] [0x04] [0x65] [MODE] [CRC]
**Purpose**: Enables navigation subsystems including lidar control. Without switching to mode 0x02, the GD32 firmware ignores CMD=0xA2, CMD=0x97, and CMD=0x71.
**Implementation** (sangam-io/src/devices/gd32/mod.rs:462):
**Implementation** (sangam-io/src/devices/crl200s/gd32/mod.rs):
```rust
pub fn set_motor_mode(&mut self, mode: u8) -> Result<()> {
log::info!("GD32: Setting motor mode: 0x{:02X}", mode);
@@ -66,7 +66,7 @@ Packet Format: [0xFA 0xFB] [0x07] [0xA2] [0x10 0x0E 0x00 0x00] [CRC]
**Note**: This command was discovered by analyzing MITM logs. In the original AuxCtrl firmware, CMD=0xA2 and CMD=0x97 are sent in a single write operation, suggesting they're tightly coupled.
**Implementation** (sangam-io/src/devices/gd32/mod.rs:472):
**Implementation** (sangam-io/src/devices/crl200s/gd32/mod.rs):
```rust
pub fn send_lidar_prep(&mut self) -> Result<()> {
log::info!("GD32: Sending lidar preparation command");
@@ -91,7 +91,7 @@ Packet Format: [0xFA 0xFB] [0x04] [0x97] [POWER] [CRC]
- CMD=0x97 [0x01] → GD32 sets `/sys/class/gpio/gpio233/value` = 1
- CMD=0x97 [0x00] → GD32 sets `/sys/class/gpio/gpio233/value` = 0
**Implementation** (sangam-io/src/devices/gd32/mod.rs:482):
**Implementation** (sangam-io/src/devices/crl200s/gd32/mod.rs):
```rust
pub fn set_lidar_power(&mut self, enable: bool) -> Result<()> {
log::info!("GD32: Setting lidar power: {}", enable);
@@ -112,7 +112,7 @@ Packet Format: [0xFA 0xFB] [0x07] [0x71] [PWM 4 bytes little-endian] [CRC]
- CRC: 16-bit big-endian word sum
```
**Implementation** (sangam-io/src/devices/gd32/mod.rs:492):
**Implementation** (sangam-io/src/devices/crl200s/gd32/mod.rs):
```rust
pub fn set_lidar_pwm(&mut self, pwm_percent: i32) -> Result<()> {
log::info!("GD32: Setting lidar PWM: {}%", pwm_percent);
@@ -126,8 +126,6 @@ pub fn set_lidar_pwm(&mut self, pwm_percent: i32) -> Result<()> {
### Full Working Example
From `sangam-io/examples/test_lidar_scenario.rs`:
```rust
// Step 1: Initialize GD32 motor controller
let gd32_transport = SerialTransport::open("/dev/ttyS3", 115200)?;
@@ -227,13 +225,12 @@ gd32.set_lidar_power(false)?; // Disable power
| Component | Status | Location |
|-----------|--------|----------|
| CMD=0x65 (MotorMode) | ✅ Implemented | `src/devices/gd32/protocol.rs:54` |
| CMD=0xA2 (LidarPrep) | ✅ Implemented | `src/devices/gd32/protocol.rs:42` |
| CMD=0x97 (LidarPower) | ✅ Implemented | `src/devices/gd32/protocol.rs:99` |
| CMD=0x71 (LidarPWM) | ✅ Implemented | `src/devices/gd32/protocol.rs:102` |
| Heartbeat thread | ✅ Working | `src/devices/gd32/heartbeat.rs` |
| Delta-2D driver | ✅ Working | `src/devices/delta2d/mod.rs` |
| Test scenario | ✅ Verified | `examples/test_lidar_scenario.rs` |
| CMD=0x65 (MotorMode) | ✅ Implemented | `src/devices/crl200s/gd32/commands.rs` |
| CMD=0xA2 (LidarPrep) | ✅ Implemented | `src/devices/crl200s/gd32/commands.rs` |
| CMD=0x97 (LidarPower) | ✅ Implemented | `src/devices/crl200s/gd32/commands.rs` |
| CMD=0x71 (LidarPWM) | ✅ Implemented | `src/devices/crl200s/gd32/commands.rs` |
| Heartbeat thread | ✅ Working | `src/devices/crl200s/gd32/heartbeat.rs` |
| Delta-2D driver | ✅ Working | `src/devices/crl200s/delta2d/mod.rs` |
## Troubleshooting
@@ -263,14 +260,14 @@ gd32.set_lidar_power(false)?; // Disable power
## References
### Source Code
- **Protocol Implementation**: `sangam-io/src/devices/gd32/protocol.rs`
- **GD32 Driver**: `sangam-io/src/devices/gd32/mod.rs`
- **Test Example**: `sangam-io/examples/test_lidar_scenario.rs`
- **Protocol Implementation**: `sangam-io/src/devices/crl200s/gd32/protocol.rs`
- **GD32 Driver**: `sangam-io/src/devices/crl200s/gd32/mod.rs`
- **Commands**: `sangam-io/src/devices/crl200s/gd32/commands.rs`
### Documentation
- **MITM Logging Guide**: `protocol-mitm/docs/MITM_LOGGING_GUIDE.md`
- **GD32 Protocol Spec**: See protocol.rs source code for complete specification
- **Project Guide**: `sangam-io/GUIDE.md`
- **GD32 Command Reference**: `sangam-io/COMMANDS.md`
- **Sensor Status Packet**: `sangam-io/SENSORSTATUS.md`
### MITM Logs
- **Successful Capture**: `protocol-mitm/logs/mitm_gpio/cleaning_session_with_gpio.log`
+52 -9
View File
@@ -1,21 +1,64 @@
//! DeviceDriver trait definition
//! DeviceDriver trait - the core abstraction for adding new robot hardware.
//!
//! To add a new device, implement this trait and register it in [`crate::devices::create_device`].
//! See [`crate::devices::crl200s::CRL200SDriver`] for a complete implementation example.
use crate::core::types::{Command, SensorGroupData};
use crate::error::Result;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
/// Device driver trait for hardware abstraction
/// Hardware abstraction trait for robot devices.
///
/// # Lifecycle
/// 1. Created via [`crate::devices::create_device`] based on config
/// 2. [`initialize`](Self::initialize) called once at daemon startup
/// 3. [`send_command`](Self::send_command) called for each inbound TCP command
/// 4. `Drop` triggers cleanup when daemon shuts down
///
/// # Threading
/// Drivers typically spawn internal threads for:
/// - **Heartbeat**: Safety-critical timing loop (e.g., motor watchdog)
/// - **Reader**: Continuous sensor data parsing
///
/// Use `Arc<Mutex<>>` for state shared across threads, `Arc<AtomicBool>` for shutdown signals.
///
/// # Example Structure
/// ```ignore
/// pub struct MyDeviceDriver {
/// port: Arc<Mutex<Box<dyn SerialPort>>>, // Shared serial access
/// shutdown: Arc<AtomicBool>, // Thread shutdown signal
/// reader_handle: Option<JoinHandle<()>>, // Thread handle for cleanup
/// }
///
/// impl DeviceDriver for MyDeviceDriver {
/// fn initialize(&mut self) -> Result<HashMap<String, Arc<Mutex<SensorGroupData>>>> {
/// // 1. Create sensor groups
/// // 2. Spawn reader/heartbeat threads
/// // 3. Return sensor map for TCP streaming
/// }
/// fn send_command(&mut self, cmd: Command) -> Result<()> {
/// // Handle ComponentControl commands
/// }
/// }
///
/// impl Drop for MyDeviceDriver {
/// fn drop(&mut self) {
/// self.shutdown.store(true, Ordering::Relaxed);
/// // Join threads...
/// }
/// }
/// ```
pub trait DeviceDriver: Send {
/// Initialize hardware and return sensor data groups
/// Initialize hardware and start background threads.
///
/// The driver should:
/// 1. Create sensor data groups for its sensors
/// 2. Open serial ports
/// 3. Start internal threads (heartbeat, readers)
/// 4. Return the HashMap of sensor groups to be used by TCP publisher
/// Returns sensor groups keyed by group ID (e.g., "gd32_status", "lidar").
/// The TCP publisher uses these to stream sensor data to clients.
fn initialize(&mut self) -> Result<HashMap<String, Arc<Mutex<SensorGroupData>>>>;
/// Send command to hardware
/// Process a command from TCP clients.
///
/// Most commands are [`Command::ComponentControl`] targeting specific components.
/// Return `Err` for invalid commands; the error is logged but doesn't stop the daemon.
fn send_command(&mut self, cmd: Command) -> Result<()>;
}
+4 -1
View File
@@ -1,4 +1,7 @@
//! Core module - driver trait and types
//! Core abstractions for device drivers.
//!
//! - [`driver::DeviceDriver`]: Trait to implement for new hardware
//! - [`types`]: Sensor values, commands, and data structures
pub mod driver;
pub mod types;
+6 -1
View File
@@ -1,4 +1,9 @@
//! Core data types for SangamIO
//! Core data types for sensors, commands, and device communication.
//!
//! Key types for device implementers:
//! - [`SensorGroupData`]: Container for sensor values, updated by driver threads
//! - [`Command`]: Inbound commands from TCP clients (mainly [`Command::ComponentControl`])
//! - [`SensorValue`]: Typed sensor values for the `values` HashMap
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
+7 -1
View File
@@ -78,13 +78,19 @@ use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
/// GD32 driver managing heartbeat and sensor reading
/// GD32 motor controller driver with heartbeat and reader threads.
pub struct GD32Driver {
/// Serial port shared between heartbeat (write) and reader (read) threads
port: Arc<Mutex<Box<dyn SerialPort>>>,
/// Heartbeat interval from config (typically 20ms)
heartbeat_interval_ms: u64,
/// Shutdown signal - set to true to stop all threads
shutdown: Arc<AtomicBool>,
/// Heartbeat thread handle - joined on shutdown
heartbeat_handle: Option<JoinHandle<()>>,
/// Reader thread handle - joined on shutdown
reader_handle: Option<JoinHandle<()>>,
/// Component states (velocity, brushes, etc.) - atomic for lockless heartbeat reads
component_state: Arc<ComponentState>,
}
+13 -2
View File
@@ -1,4 +1,13 @@
//! CRL-200S Vacuum Robot device driver
//! CRL-200S Vacuum Robot driver - reference implementation for new devices.
//!
//! This driver manages two subcomponents:
//! - **GD32**: Motor controller (wheels, brushes, vacuum) via `/dev/ttyS3`
//! - **Delta2D**: Lidar sensor via `/dev/ttyS1`
//!
//! # Sensor Groups Published
//! - `sensor_status`: Bumpers, cliffs, encoders, IMU @ 500Hz
//! - `device_version`: Firmware version (one-time)
//! - `lidar`: Point cloud @ 5Hz
pub mod constants;
pub mod delta2d;
@@ -13,10 +22,12 @@ use gd32::GD32Driver;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
/// CRL-200S device driver
/// CRL-200S device driver coordinating GD32 motor controller and Delta2D lidar.
pub struct CRL200SDriver {
config: DeviceConfig,
/// Motor controller - `None` before `initialize()`, `Some` after
gd32: Option<GD32Driver>,
/// Lidar driver - `None` before `initialize()`, `Some` after
lidar: Option<Delta2DDriver>,
}
+29 -2
View File
@@ -1,4 +1,26 @@
//! Device implementations
//! Device driver implementations and factory.
//!
//! # Adding a New Device
//!
//! 1. **Create module**: `src/devices/my_device/mod.rs`
//! 2. **Implement trait**: `impl DeviceDriver for MyDeviceDriver`
//! 3. **Register here**: Add match arm in [`create_device`]
//!
//! ## Module Structure
//! ```text
//! devices/my_device/
//! ├── mod.rs # MyDeviceDriver struct + DeviceDriver impl
//! ├── protocol.rs # Packet parsing (if serial)
//! └── commands.rs # Command handlers (optional)
//! ```
//!
//! ## Key Patterns (from CRL200S)
//! - Use `Arc<Mutex<>>` for serial port shared across threads
//! - Use `Arc<AtomicBool>` for shutdown signal
//! - Implement `Drop` to join spawned threads
//! - Return sensor groups from `initialize()` for TCP streaming
//!
//! See [`crl200s::CRL200SDriver`] for a complete example.
pub mod crl200s;
@@ -7,7 +29,12 @@ use crate::core::driver::DeviceDriver;
use crate::error::{Error, Result};
use crl200s::CRL200SDriver;
/// Create a device driver based on configuration
/// Device factory - creates driver based on `device.type` in config.
///
/// To add a new device type, add a match arm here:
/// ```ignore
/// "my_device" => Ok(Box::new(MyDeviceDriver::new(config.device.clone())?)),
/// ```
pub fn create_device(config: &Config) -> Result<Box<dyn DeviceDriver>> {
match config.device.device_type.as_str() {
"crl200s" => {