Add protocol MITM tools and implement verified GD32/lidar initialization sequences

This commit adds a complete protocol reverse-engineering toolkit and rewrites
the GD32/lidar drivers based on captured real-world AuxCtrl behavior:

**Protocol MITM Infrastructure** (protocol-mitm/):
- Serial port interception logger in Rust
- Robot-side automation scripts for capturing sessions
- Development tools for log analysis and packet parsing
- Complete session captures showing initialization sequences

**GD32 Driver Improvements**:
- Implement exact AuxCtrl initialization sequence from MITM analysis:
  * CMD=0x0C init sync, 0x8D LED, 0x07 version, 0x65 motor type, 0x71 telemetry
  * Precise timing: LED burst at T+245ms, wake at T+253ms, brushes at T+280ms
  * Heartbeat starts at T+289ms with proper telemetry queries
- Add CMD=0x06 wake command every ~250ms (matches AuxCtrl frequency)
- Enhanced logging for TX/RX packet debugging
- Wait for STATUS_DATA confirmation after init

**Delta-2D Lidar Driver Improvements**:
- Fix packet parsing: wait for complete payload+CRC before reading
- Add detailed logging of bytes available and packet structure
- Implement proper data arrival detection (matches Python reference)

**Example Program Updates** (test_lidar_scenario.rs):
- Complete lidar power-on sequence: mode switch → prep → power → PWM
- Proper initialization delays (1.4s stabilization matches AuxCtrl)
- Graceful shutdown with PWM ramp-down before power-off

**Documentation & Security**:
- Replace hardcoded passwords with $ROBOT_PASSWORD environment variable
- Update all deployment guides with MITM tool references
- Add protocol-mitm/ section to README

**Build Optimizations** (Cargo.toml):
- Enable LTO, strip symbols, single codegen unit, panic=abort
- Reduces binary size significantly for embedded deployment

**Cleanup**:
- Remove obsolete serial_dump.rs (replaced by protocol-mitm tools)

This work enables reliable GD32 and lidar operation by following the exact
command sequences and timing used by the original AuxCtrl firmware.
This commit is contained in:
codetiger
2025-11-02 17:25:07 +05:30
parent 842e179596
commit 71b3163197
35 changed files with 395930 additions and 295 deletions
+5 -5
View File
@@ -215,27 +215,27 @@ assert_eq!(mock.get_written(), expected_command_bytes);
2. **Deploy binary** (SCP doesn't work - device lacks sftp-server):
```bash
cat ../target/armv7-unknown-linux-musleabihf/release/examples/test_lidar_scenario | \
sshpass -p 'vacuum@123' ssh root@vacuum "cat > /tmp/test_lidar && chmod +x /tmp/test_lidar"
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "cat > /tmp/test_lidar && chmod +x /tmp/test_lidar"
```
3. **Disable AuxCtrl** (rename to prevent auto-restart):
```bash
sshpass -p 'vacuum@123' ssh root@vacuum "mv /usr/sbin/AuxCtrl /usr/sbin/AuxCtrl.bak && killall -9 AuxCtrl"
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "mv /usr/sbin/AuxCtrl /usr/sbin/AuxCtrl.bak && killall -9 AuxCtrl"
```
4. **Run test with logging**:
```bash
sshpass -p 'vacuum@123' ssh root@vacuum "RUST_LOG=debug /tmp/test_lidar"
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "RUST_LOG=debug /tmp/test_lidar"
```
5. **CRITICAL: Restore AuxCtrl** after testing:
```bash
sshpass -p 'vacuum@123' ssh root@vacuum "mv /usr/sbin/AuxCtrl.bak /usr/sbin/AuxCtrl"
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "mv /usr/sbin/AuxCtrl.bak /usr/sbin/AuxCtrl"
```
**One-Command Test** (with auto-restore on failure):
```bash
sshpass -p 'vacuum@123' ssh root@vacuum "
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "
mv /usr/sbin/AuxCtrl /usr/sbin/AuxCtrl.bak && \
killall -9 AuxCtrl 2>/dev/null; \
RUST_LOG=debug /tmp/test_lidar; \
+9 -1
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = [
members = [ "protocol-mitm",
"sangam-io",
]
@@ -37,3 +37,11 @@ serialport = "4.5"
# Testing
criterion = "0.5"
[profile.release]
debug = false # Exclude debug symbols
strip = "symbols" # Exclude the rest of the symbols
# opt-level = "z" # Did not use this, but it equals C++'s optimize for size (O3?)
lto = true # Link time optimization, not sure what this does but it helps reduce the size
codegen-units = 1 # Cargo specifies 16 parallel codegen units for release builds. This improves compile times, but prevents some optimizations.
panic = "abort" # when Rust code encounters a situation when it must call panic!(), it unwinds the stack and produces a helpful backtrace. The unwinding code, however, does require extra binary size. rustc can be instructed to abort immediately rather than unwind, which removes the need for this extra unwinding code.
+8
View File
@@ -21,6 +21,12 @@ VacuumTiger/
│ ├── examples/ # Working robot control examples
│ ├── GUIDE.md # Deployment and development guide
│ └── REFERENCE.md # Technical reference
├── protocol-mitm/ # Protocol reverse-engineering tools
│ ├── src/ # Serial MITM logger (Rust)
│ ├── scripts/ # Robot-side automation scripts
│ ├── tools/ # Development machine tools
│ ├── logs/ # Captured protocol sessions
│ └── docs/ # MITM logging documentation
├── docs/ # Protocol documentation
│ ├── GD32_PROTOCOL.md
│ └── HARDWARE_REFERENCE.md
@@ -116,6 +122,7 @@ See **[examples/test_lidar_scenario.rs](sangam-io/examples/test_lidar_scenario.r
| GD32 Driver | ✅ Verified | Automatic heartbeat (20ms) |
| Delta-2D Lidar | ✅ Verified | Full packet parsing |
| Examples | ✅ Complete | test_lidar_scenario |
| Protocol MITM Tools | ✅ Complete | Serial interception & logging |
| Documentation | ✅ Complete | GUIDE.md + REFERENCE.md |
| Additional Controllers | 📋 Planned | STM32, ESP32 |
| Custom Firmware | 📋 Planned | GD32 firmware development |
@@ -139,6 +146,7 @@ See **[docs/HARDWARE_REFERENCE.md](docs/HARDWARE_REFERENCE.md)** for specificati
- **[docs/GD32_PROTOCOL.md](docs/GD32_PROTOCOL.md)** - Complete GD32 communication spec
- **[docs/HARDWARE_REFERENCE.md](docs/HARDWARE_REFERENCE.md)** - Component specs, pinouts, BOM
- **[protocol-mitm/](protocol-mitm/)** - MITM tools for protocol reverse-engineering
## Contributing
+6 -6
View File
@@ -12,7 +12,7 @@ scp binary root@vacuum:/tmp/test
**Solution**: Use `cat` over SSH instead
```bash
cat binary | sshpass -p 'vacuum@123' ssh root@vacuum "cat > /tmp/test && chmod +x /tmp/test"
cat binary | sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "cat > /tmp/test && chmod +x /tmp/test"
```
This works because:
@@ -51,22 +51,22 @@ cargo build --release --example test_lidar_scenario --features="std,gd32,lidar"
# Step 2: Deploy binary
cat ../target/armv7-unknown-linux-musleabihf/release/examples/test_lidar_scenario | \
sshpass -p 'vacuum@123' ssh root@vacuum "cat > /tmp/test_lidar && chmod +x /tmp/test_lidar"
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "cat > /tmp/test_lidar && chmod +x /tmp/test_lidar"
# Step 3: Disable AuxCtrl
sshpass -p 'vacuum@123' ssh root@vacuum "mv /usr/sbin/AuxCtrl /usr/sbin/AuxCtrl.bak && killall -9 AuxCtrl"
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "mv /usr/sbin/AuxCtrl /usr/sbin/AuxCtrl.bak && killall -9 AuxCtrl"
# Step 4: Run test
sshpass -p 'vacuum@123' ssh root@vacuum "RUST_LOG=debug /tmp/test_lidar"
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "RUST_LOG=debug /tmp/test_lidar"
# Step 5: Restore original firmware
sshpass -p 'vacuum@123' ssh root@vacuum "mv /usr/sbin/AuxCtrl.bak /usr/sbin/AuxCtrl"
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "mv /usr/sbin/AuxCtrl.bak /usr/sbin/AuxCtrl"
```
### 4. Alternative: One-Command Test (with auto-restore)
```bash
sshpass -p 'vacuum@123' ssh root@vacuum "
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum "
mv /usr/sbin/AuxCtrl /usr/sbin/AuxCtrl.bak && \
killall -9 AuxCtrl 2>/dev/null; \
RUST_LOG=debug /tmp/test_lidar; \
+2
View File
@@ -0,0 +1,2 @@
[target.armv7-unknown-linux-musleabihf]
linker = "armv7-unknown-linux-musleabihf-gcc"
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "protocol_mitm"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
chrono = "0.4"
libc = "0.2"
+185
View File
@@ -0,0 +1,185 @@
# Protocol MITM - GD32 Protocol Reverse Engineering Tools
Man-in-the-Middle (MITM) toolset for reverse-engineering proprietary serial protocols on embedded devices. This project was created to intercept and analyze the communication between the AuxCtrl firmware and the GD32F103 motor controller in CRL-200S vacuum robots.
## 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/).
### Key Features
- **Transparent PTY-based Serial Forwarding** - Creates virtual serial ports that applications connect to normally
- **Bidirectional Packet Logging** - Captures TX/RX with microsecond-precision timestamps
- **GPIO State Monitoring** - Tracks hardware control lines in parallel with serial data
- **Protocol-Aware Decoding** - Automatically identifies and annotates known GD32 commands
- **Run Counter Integration** - Supports multi-session capture campaigns
- **Graceful Shutdown** - Clean signal handling with statistics reporting
## Project Structure
```
protocol-mitm/
├── src/
│ └── main.rs # Serial MITM logger binary
├── scripts/ # Robot-side automation scripts
│ ├── mitm_boot_setup.sh # Initial MITM configuration
│ ├── mitm_cleanup.sh # Remove MITM setup
│ ├── mitm_disable.sh # Temporarily disable MITM
│ ├── mitm_enable.sh # Re-enable MITM
│ ├── mitm_init.sh # Initialize MITM for new session
│ └── mitm_stop.sh # Stop MITM cleanly
├── tools/ # Development machine tools
│ ├── mitm_deploy_all.sh # Deploy MITM to robot
│ ├── mitm_disable.sh # Remote disable
│ ├── mitm_enable.sh # Remote enable
│ ├── mitm_finish_run.sh # Complete capture session
│ ├── mitm_reboot_robot.sh # Remote reboot
│ ├── mitm_retrieve_logs.sh # Download logs
│ └── README.md
├── logs/ # Captured protocol logs
└── docs/ # Documentation
├── MITM_LOGGING_GUIDE.md
└── LIDAR_INITIALIZATION_SEQUENCE.md
```
## Quick Start
### 1. Build for Target Platform
```bash
cargo build --release --target armv7-unknown-linux-musleabihf
```
### 2. Deploy to Robot
```bash
cd tools
./mitm_deploy_all.sh
```
This will:
- Build the MITM binary
- Copy it to the robot
- Deploy all automation scripts
- Initialize the MITM setup
### 3. Enable MITM
```bash
./mitm_enable.sh
```
The robot will reboot with MITM active. AuxCtrl will transparently communicate through the MITM logger.
### 4. Capture Protocol Data
Perform robot operations (navigation, cleaning, return to dock) while MITM logs all serial traffic to `/tmp/mitm_capture_run<N>_<timestamp>.log`.
### 5. Retrieve Logs
```bash
./mitm_retrieve_logs.sh
```
Logs will be downloaded to `logs/mitm_captures_<timestamp>/` on your development machine.
### 6. Finish Session
```bash
./mitm_finish_run.sh
```
This disables MITM, restores normal operation, and optionally retrieves logs.
## How It Works
### PTY-Based Forwarding
1. **Move Real Serial Port**: `/dev/ttyS3` → `/dev/ttyS3_hardware`
2. **Create Virtual Port**: MITM creates PTY at `/tmp/ttyS3_tap`
3. **Symlink**: `/dev/ttyS3` → `/tmp/ttyS3_tap`
4. **Proxy Loop**: Bidirectional forwarding with logging
```
AuxCtrl → /dev/ttyS3 (symlink) → /tmp/ttyS3_tap (PTY slave)
↓
MITM Process
↓
/dev/ttyS3_hardware → GD32
```
### Log Format
```
[timestamp] TX <bytes> bytes
HEX: FA FB 07 A2 10 0E 00 00 B0 10
PKT: CMD=0xA2 LEN=7
(LIDAR_PREP - Preparation command)
[timestamp] GPIO_233: 0 -> 1 (Lidar Power ON)
[timestamp] RX <bytes> bytes
HEX: FA FB 63 15 01 00 00 00 04 00 ...
PKT: CMD=0x15 LEN=99
(STATUS - Sensor/IMU/Battery data)
```
## Results
This MITM approach successfully captured the complete GD32 protocol, including:
- **Lidar Activation Sequence** (CMD=0x65, CMD=0xA2, CMD=0x97, CMD=0x71)
- **Heartbeat Protocol** (CMD=0x66 every 20ms)
- **Status Packets** (CMD=0x15 with 96 bytes of sensor data)
- **Motor Control** (CMD=0x66 with speed parameters)
- **Checksum Algorithm** (16-bit big-endian word sum)
See [LIDAR_INITIALIZATION_SEQUENCE.md](docs/LIDAR_INITIALIZATION_SEQUENCE.md) for detailed findings.
## Requirements
### Robot (CRL-200S)
- Allwinner A33 running Tina Linux
- Root access via SSH
- GD32F103 motor controller on `/dev/ttyS3`
- GPIO 233 for lidar power control
### Development Machine
- Rust 1.70+ with ARM cross-compilation support
- `armv7-unknown-linux-musleabihf` target installed
- SSH access to robot
## Safety Notes
⚠️ **MITM Mode Disables Original Firmware** - AuxCtrl cannot control the robot while MITM is active. Always restore normal operation before leaving the robot unattended.
⚠️ **Boot-Persistent Changes** - MITM setup modifies system files (`/etc/init.d/rcS`). Use `mitm_cleanup.sh` to fully remove.
⚠️ **GPIO Monitoring** - The MITM logger monitors GPIO 233 for lidar power state. Ensure GPIO is exported and accessible.
## Troubleshooting
### "Permission denied" on /dev/ttyS3_hardware
The real serial port wasn't moved. Run `mitm_boot_setup.sh` on the robot.
### AuxCtrl crashes immediately
MITM binary may not be running. Check: `ps aux | grep serial_mitm`
### No log file created
MITM may have failed to start. Check `/tmp/mitm.log` for errors.
### Robot doesn't boot after MITM
MITM setup may have corrupted boot scripts. Use serial console to restore `/etc/init.d/rcS`.
## Related Projects
- **[SangamIO](../sangam-io/)** - Rust library implementing the reverse-engineered GD32 protocol
- **[VacuumTiger](../)** - Complete robotic vacuum control system
## License
Apache-2.0
## Authors
Created by the VacuumTiger project team.
@@ -0,0 +1,308 @@
# Lidar Initialization Sequence - Final Specification
**Successfully Reverse-Engineered and Implemented**
**Date**: November 2, 2025
**Method**: MITM Protocol Capture + Binary Analysis
**Status**: ✅ **WORKING** - Verified on hardware
## Overview
This document describes the complete, verified lidar initialization and control sequence for the GD32F103 motor controller and 3iRobotix Delta-2D lidar. The protocol was successfully reverse-engineered through MITM logging of the AuxCtrl firmware and validated on actual hardware.
## Critical Discovery: 4-Command Sequence
The lidar requires a **precise 4-command sequence** sent to the GD32 motor controller. Missing any command will prevent the lidar from operating.
### Complete Working Sequence
```
1. CMD=0x65 mode=0x02 - Switch to navigation mode (CRITICAL!)
2. CMD=0xA2 - Lidar preparation command
3. CMD=0x97 [0x01] - Enable lidar power (GPIO 233 ON)
4. CMD=0x71 [100] - Set lidar PWM speed to 100%
```
**Timeline**:
- Wait 1.4s after GD32 initialization
- Send all 4 commands in sequence (no delay between commands)
- Wait 2s for motor spin-up
- Lidar is ready for scanning
## Protocol Specification
### CMD=0x65: Motor Mode Switch
**Discovered via**: MITM capture (line 25673 in cleaning session log)
**CRITICAL**: This MUST be sent before any lidar commands work!
```
Packet Format: [0xFA 0xFB] [0x04] [0x65] [MODE] [CRC]
- MODE: 0x00 = initialization, 0x02 = navigation/cleaning mode
- CRC: 16-bit big-endian word sum
```
**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):
```rust
pub fn set_motor_mode(&mut self, mode: u8) -> Result<()> {
log::info!("GD32: Setting motor mode: 0x{:02X}", mode);
Self::send_command(&self.transport, &Gd32Command::MotorType(mode))?;
log::debug!("GD32: Sent CMD=0x65 (MotorType=0x{:02X})", mode);
Ok(())
}
```
### CMD=0xA2: Lidar Preparation
**Discovered via**: MITM capture (line 25682, bundled with CMD=0x97)
**Purpose**: Unknown internal preparation (possibly initializes GPIO or PWM subsystems)
```
Packet Format: [0xFA 0xFB] [0x07] [0xA2] [0x10 0x0E 0x00 0x00] [CRC]
- Payload: Fixed 4-byte sequence [0x10, 0x0E, 0x00, 0x00]
- CRC: 16-bit big-endian word sum
```
**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):
```rust
pub fn send_lidar_prep(&mut self) -> Result<()> {
log::info!("GD32: Sending lidar preparation command");
Self::send_command(&self.transport, &Gd32Command::LidarPrep)?;
log::debug!("GD32: Sent CMD=0xA2 (LidarPrep)");
Ok(())
}
```
### CMD=0x97: Lidar Power Control (GPIO 233)
**Discovered via**: Binary analysis at address 0x00060988
**Purpose**: Controls GPIO 233 (lidar motor power enable)
```
Packet Format: [0xFA 0xFB] [0x04] [0x97] [POWER] [CRC]
- POWER: 0x00 = OFF, 0x01 = ON
- CRC: 16-bit big-endian word sum
```
**GPIO Behavior**:
- 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):
```rust
pub fn set_lidar_power(&mut self, enable: bool) -> Result<()> {
log::info!("GD32: Setting lidar power: {}", enable);
Self::send_command(&self.transport, &Gd32Command::LidarPower(enable))?;
log::debug!("GD32: Sent CMD=0x97 (LidarPower={})", enable);
Ok(())
}
```
### CMD=0x71: Lidar PWM Speed Control
**Discovered via**: Binary analysis at address 0x0003f604
**Purpose**: Sets lidar motor speed (0-100%)
```
Packet Format: [0xFA 0xFB] [0x07] [0x71] [PWM 4 bytes little-endian] [CRC]
- PWM: 0-100 (percentage, sent as u32)
- CRC: 16-bit big-endian word sum
```
**Implementation** (sangam-io/src/devices/gd32/mod.rs:492):
```rust
pub fn set_lidar_pwm(&mut self, pwm_percent: i32) -> Result<()> {
log::info!("GD32: Setting lidar PWM: {}%", pwm_percent);
Self::send_command(&self.transport, &Gd32Command::LidarPWM(pwm_percent))?;
log::debug!("GD32: Sent CMD=0x71 (LidarPWM={}%)", pwm_percent.clamp(0, 100));
Ok(())
}
```
## Complete Initialization Procedure
### 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)?;
let mut gd32 = Gd32Driver::new(gd32_transport)?;
// Heartbeat thread starts automatically (CMD=0x66 every 20ms)
// CRITICAL: Wait 1.4s after init before powering lidar
thread::sleep(Duration::from_millis(1400));
// Step 2: Send complete 4-command sequence
gd32.set_motor_mode(0x02)?; // Switch to navigation mode
gd32.send_lidar_prep()?; // Preparation command
gd32.set_lidar_power(true)?; // Enable GPIO 233
gd32.set_lidar_pwm(100)?; // Set motor speed to 100%
// Wait for motor spin-up
thread::sleep(Duration::from_secs(2));
// Step 3: Initialize Delta-2D lidar
let lidar_transport = SerialTransport::open("/dev/ttyS1", 115200)?;
let mut lidar = Delta2DDriver::new(lidar_transport)?;
// Step 4: Start scanning
lidar.start()?;
// Step 5: Read scan data
while let Some(scan) = lidar.get_scan()? {
// Process scan data...
}
// Step 6: Clean shutdown
lidar.stop()?;
gd32.set_lidar_pwm(0)?; // Stop motor
gd32.set_lidar_power(false)?; // Disable power
```
### Verified Results
**Hardware Test (November 2, 2025)**:
- ✅ Lidar motor spins up correctly
- ✅ GPIO 233 = 1 (confirmed via sysfs)
- ✅ Scan data received on /dev/ttyS1
- ✅ 47 scans captured in 5 seconds
- ✅ 7,755 data points total
- ✅ Average 165 points per scan
- ✅ Scan rate: ~9.4 Hz
## Critical Timing Requirements
| Event | Delay | Reason |
|-------|-------|--------|
| After GD32 init | 1.4s | Matches AuxCtrl timing; allows GD32 firmware to stabilize |
| After power ON | 2.0s | Motor spin-up to stable operating speed (~300 RPM) |
| Between commands | None | All 4 commands sent immediately in sequence |
| Heartbeat interval | 20ms | CMD=0x66 must continue throughout operation |
**Missing heartbeat → Emergency motor stop within 100ms**
## MITM Discovery Process
### Key Log Analysis Points
**Run**: `cleaning_session_with_gpio.log`
**Line 25673** - Navigation mode switch:
```
[timestamp] TX 5 bytes
HEX: FA FB 04 65 02 65 02
PKT: CMD=0x65 LEN=4
```
**Line 25682** - Bundled prep + power commands:
```
[timestamp] TX 17 bytes
HEX: FA FB 07 A2 10 0E 00 00 B0 10 FA FB 04 97 01 97 01
PKT: CMD=0xA2 LEN=7
PKT: CMD=0x97 LEN=4
(LIDAR_POWER - GPIO 233 = ON)
```
**Line 25691** - PWM speed:
```
[timestamp] TX 12 bytes
HEX: FA FB 07 71 64 00 00 00 71 64
PKT: CMD=0x71 LEN=7
(LIDAR_PWM - Speed = 100%)
```
**GPIO State Change**:
```
[timestamp] GPIO_233: 0 -> 1 (Lidar Power ON)
```
## Implementation Status
### SangamIO Library (v0.1.0)
| 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` |
## Troubleshooting
### Lidar Motor Doesn't Spin
**Check List**:
1. ✅ Did you send CMD=0x65 mode=0x02 first?
2. ✅ Did you send all 4 commands in order?
3. ✅ Is heartbeat running? (ps aux | grep AuxCtrl should be stopped)
4. ✅ Did you wait 1.4s after GD32 init?
5. ✅ Is GPIO 233 = 1? (cat /sys/class/gpio/gpio233/value)
### No Scan Data from /dev/ttyS1
**Possible Causes**:
- Motor not spinning (see above)
- Delta-2D lidar not initialized (check serial port open)
- Start command not sent (lidar.start()?)
- Serial port in use by another process
### GPIO 233 Stays at 0
**Root Cause**: CMD=0x65 mode=0x02 not sent, or sent after CMD=0x97
**Fix**: Always send CMD=0x65 mode=0x02 FIRST in the sequence
## 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`
### Documentation
- **MITM Logging Guide**: `protocol-mitm/docs/MITM_LOGGING_GUIDE.md`
- **GD32 Protocol Spec**: `docs/GD32_PROTOCOL.md`
- **Project Guide**: `sangam-io/GUIDE.md`
### MITM Logs
- **Successful Capture**: `protocol-mitm/logs/mitm_gpio/cleaning_session_with_gpio.log`
- **Analysis Tool**: `protocol-mitm/src/main.rs` (serial_mitm binary)
## Appendix: Binary Analysis
### AuxCtrl Function Addresses
| Function | Address | Notes |
|----------|---------|-------|
| `lidarSpeedControl()` | 0x0003f6b8 | Main control loop |
| `controlLidarPwm(int)` | 0x0003f604 | Sends CMD=0x71 |
| `packetLidarPower(bool)` | 0x00060988 | Sends CMD=0x97 |
| `CGpioControl::GpioInit()` | 0x00066ad0 | GPIO initialization |
| GPIO 233 constant | 0x0001296c | Data section |
### Extraction Commands
```bash
# Extract strings
arm-linux-gnueabihf-strings /usr/sbin/AuxCtrl | grep -i lidar
# Disassemble lidar functions
arm-linux-gnueabihf-objdump -d /usr/sbin/AuxCtrl | grep -A 50 "0003f604"
# Find GPIO references
arm-linux-gnueabihf-objdump -s -j .rodata /usr/sbin/AuxCtrl | grep -B2 -A2 "e9 00 00 00"
```
## Version History
- **v1.0** (Nov 2, 2025): Final verified specification with working hardware test
- **v0.2** (Nov 1, 2025): Added CMD=0xA2 after MITM discovery
- **v0.1** (Nov 1, 2025): Initial spec from binary analysis (CMD=0x97, CMD=0x71 only)
+612
View File
@@ -0,0 +1,612 @@
# MITM Logging Guide - Full Vacuum Cycle Capture
Complete guide for capturing AuxCtrl ↔ GD32 communication during vacuum cleaning operations using the automated MITM logging system.
## Overview
This system enables systematic capture of serial protocol data during multiple full vacuum cleaning cycles. It uses a PTY-based MITM proxy that transparently intercepts communication between the robot's AuxCtrl software and the GD32F103 motor controller.
**Key Features:**
- Transparent to AuxCtrl and Monitor watchdog
- Automatic setup on boot
- Run counter for tracking progress (target: 3-5 runs)
- Manual control with helper scripts
- Graceful shutdown with statistics
- Archives logs for analysis
## Architecture
```
Boot Sequence:
1. Robot boots → procd starts
2. _root.sh runs → mitm_boot_setup.sh executes
3. /dev/ttyS3 moved to /dev/ttyS3_hardware
4. serial_mitm starts, creates /tmp/ttyS3_tap (PTY)
5. /dev/ttyS3 symlinked to /tmp/ttyS3_tap
6. Monitor starts → forks AuxCtrl
7. AuxCtrl opens /dev/ttyS3 → transparently uses MITM
Communication Flow:
AuxCtrl → /dev/ttyS3 (symlink) → /tmp/ttyS3_tap (PTY)
→ serial_mitm (logs TX)
→ /dev/ttyS3_hardware (real port)
→ GD32 MCU
GD32 MCU → /dev/ttyS3_hardware
→ serial_mitm (logs RX)
→ /tmp/ttyS3_tap (PTY)
→ AuxCtrl
```
## Prerequisites
### Development Machine
- Rust toolchain with ARM cross-compilation support
- `sshpass` for automated SSH/SCP
- SSH access to robot configured
Install requirements:
```bash
# macOS
brew install sshpass
# Add ARM target
rustup target add armv7-unknown-linux-musleabihf
```
### Robot
- SSH root access with password or key-based authentication
- Set `ROBOT_PASSWORD` environment variable before running scripts if using password auth
- Example: `export ROBOT_PASSWORD=your_password`
- `/mnt/UDISK` partition with ~100MB free space
- `/mnt/UDISK/_root.sh` modified to call `mitm_boot_setup.sh`
## Quick Start (3-5 Capture Runs)
### 1. Deploy Everything
From `protocol-mitm/` directory:
```bash
# Build and deploy binary + scripts to robot
./tools/mitm_deploy_all.sh
```
This will:
- Build `serial_mitm` for ARM
- Deploy binary to `/mnt/UDISK/binary/serial_mitm`
- Deploy all robot-side scripts to `/mnt/UDISK/`
- Verify robot connectivity
### 2. Enable MITM Mode
```bash
./tools/mitm_enable.sh
```
This creates the flag file that activates MITM on next boot.
### 3. Perform Capture Runs (Repeat 3-5 times)
For each run:
```bash
# Step A: Reboot robot
./tools/mitm_reboot_robot.sh
# (Waits for boot, verifies MITM started)
# Step B: Start vacuum cleaning
# → Press physical button on robot
# → Wait for robot to clean small room
# → Wait for robot to return to dock
# Step C: Finish run
./tools/mitm_finish_run.sh
# (Stops logging, increments counter, shows progress)
```
Repeat Steps A-C until you have 3-5 complete runs.
### 4. Retrieve Logs
After all runs are complete:
```bash
./tools/mitm_retrieve_logs.sh
# Downloads all logs to ./logs/mitm_captures_TIMESTAMP/
```
### 5. Disable MITM and Restore
```bash
./tools/mitm_disable.sh
# (Cleans up, archives logs on robot, offers to reboot)
```
Robot is now back to normal operation.
## Detailed Workflow
### Initial Setup (One Time)
#### 1. Verify SSH Configuration
Ensure `~/.ssh/config` contains:
```
Host vacuum
HostName 192.168.68.101 # Your robot's IP
User root
HostKeyAlgorithms +ssh-rsa
PubkeyAcceptedKeyTypes +ssh-rsa
KexAlgorithms +diffie-hellman-group1-sha1
```
Test connection:
```bash
ssh root@vacuum "echo OK"
# Should print "OK" without prompting for password
```
#### 2. Integrate with _root.sh
SSH to robot and edit `/mnt/UDISK/_root.sh`:
```bash
ssh root@vacuum
vi /mnt/UDISK/_root.sh
```
Add this line at the beginning:
```bash
[ -f /mnt/UDISK/mitm_boot_setup.sh ] && /mnt/UDISK/mitm_boot_setup.sh
```
This ensures MITM setup runs early in boot sequence, before Monitor starts.
#### 3. Deploy MITM Infrastructure
```bash
cd protocol-mitm/
./tools/mitm_deploy_all.sh
```
Expected output:
```
Building serial_mitm binary for ARM...
✓ Binary built successfully (520K)
✓ Robot is reachable
✓ Directories created
✓ Binary deployed to /mnt/UDISK/binary/serial_mitm
✓ All scripts deployed
```
### Capture Session
#### Enable MITM Mode
```bash
./tools/mitm_enable.sh 5 # Target 5 runs (default)
```
This creates `/mnt/UDISK/mitm_enabled` and resets run counter to 1.
#### Run 1-5 Capture Loop
**For Run 1:**
```bash
# Reboot to start MITM
./tools/mitm_reboot_robot.sh
```
Expected boot sequence on robot:
```
[MITM Boot] MITM mode ENABLED. Starting setup...
[MITM Boot] Current run: 1
[MITM Boot] Starting MITM proxy (run 1)...
[MITM Boot] ✓ MITM setup complete!
[MITM Boot] Ready for vacuum cleaning cycle.
```
**Start Cleaning:**
- Press the physical CLEAN button on the robot
- Robot should start cleaning normally
- Let it clean a small room (5-10 minutes)
- Wait for robot to return to dock
**Finish Run:**
```bash
./tools/mitm_finish_run.sh
```
Expected output:
```
Stopping MITM logger on robot...
✓ MITM proxy stopped
Capture Statistics:
Run: 1
Log file: mitm_capture_run1_20231101_143022.log
Size: 4.2M
TX packets: 15234
RX packets: 15189
Progress: 1/5 runs complete
```
**Repeat for Runs 2-5:**
Just run the loop again:
```bash
./tools/mitm_reboot_robot.sh # Run 2 starts
# ... press button, wait for dock ...
./tools/mitm_finish_run.sh
./tools/mitm_reboot_robot.sh # Run 3 starts
# ... and so on ...
```
After Run 5:
```
✅ TARGET ACHIEVED: 5 runs completed!
Next steps:
1. Retrieve all logs: ./tools/mitm_retrieve_logs.sh
2. Disable MITM mode: ./tools/mitm_disable.sh
```
#### Retrieve Logs
```bash
./tools/mitm_retrieve_logs.sh
```
Output:
```
Found 5 log file(s)
Downloading logs to: ./logs/mitm_captures_20231101_150322
mitm_capture_run1_20231101_143022.log ... ✓ (4.2M)
mitm_capture_run2_20231101_144156.log ... ✓ (4.3M)
mitm_capture_run3_20231101_145301.log ... ✓ (4.1M)
mitm_capture_run4_20231101_150412.log ... ✓ (4.4M)
mitm_capture_run5_20231101_151523.log ... ✓ (4.2M)
Summary:
Files downloaded: 5
Total size: 21M
Location: ./logs/mitm_captures_20231101_150322
```
#### Disable MITM
```bash
./tools/mitm_disable.sh
```
This will:
- Run cleanup script (kill proxy, restore /dev/ttyS3)
- Remove flag file
- Archive logs on robot
- Offer to reboot
Choose 'y' to reboot and restore normal operation.
## Log Format
Each log file contains timestamped bidirectional packet captures:
```
=== MITM Session Started ===
Run Number: 1
Timestamp: 2023-11-01T14:30:22+00:00
Real port: /dev/ttyS3_hardware
Virtual port: /tmp/ttyS3_tap
===
[1698850222.946188] TX 14 bytes
HEX: FA FB 0B 66 00 00 00 00 00 00 00 00 66 00
PKT: CMD=0x66 LEN=11
(HEARTBEAT - Keepalive packet)
[1698850222.946523] RX 102 bytes
HEX: FA FB 63 15 01 06 00 0F 04 00 00 01 A6 21 ...
PKT: CMD=0x15 LEN=99
(STATUS - Sensor/IMU/Battery data)
...
=== MITM Session Ended ===
End Timestamp: 2023-11-01T14:40:35+00:00
TX Packets: 15234
RX Packets: 15189
TX Bytes: 213276
RX Bytes: 1503711
===
```
**Packet Annotations:**
- **TX**: AuxCtrl → GD32 (commands)
- **RX**: GD32 → AuxCtrl (status/responses)
- **Timestamps**: Microsecond precision
- **CMD annotations**: Common command types decoded
## Troubleshooting
### MITM Proxy Not Starting
**Symptom:** After reboot, `serial_mitm` is not running
**Checks:**
```bash
ssh root@vacuum
# 1. Is flag file present?
ls -l /mnt/UDISK/mitm_enabled
# 2. Is binary executable?
ls -l /mnt/UDISK/binary/serial_mitm
# Should show: -rwxr-xr-x
# 3. Does _root.sh call boot script?
grep mitm_boot_setup /mnt/UDISK/_root.sh
# 4. Check boot log
cat /tmp/serial_mitm.log 2>/dev/null || dmesg | grep MITM
```
**Fix:** Redeploy with `./tools/mitm_deploy_all.sh`
### AuxCtrl Not Responding After MITM Setup
**Symptom:** Robot doesn't respond to button presses, motors don't work
**Cause:** MITM proxy may have crashed, breaking the serial connection
**Recovery:**
```bash
ssh root@vacuum
# Check if proxy is running
ps | grep serial_mitm
# If not running, cleanup and reboot
/mnt/UDISK/mitm_cleanup.sh
reboot
```
### Device Node Already Exists Error
**Symptom:** Boot setup fails with "cannot move /dev/ttyS3: File exists"
**Cause:** Previous MITM session didn't clean up properly
**Fix:**
```bash
ssh root@vacuum
/mnt/UDISK/mitm_cleanup.sh
reboot
```
### Logs Not Being Created
**Symptom:** MITM proxy runs but no log files appear
**Checks:**
```bash
ssh root@vacuum
# Check log directory
ls -lh /mnt/UDISK/log/
# Check disk space
df -h /mnt/UDISK
# Check proxy status
ps aux | grep serial_mitm
```
**Fix:** Ensure `/mnt/UDISK/log/` exists and has space. Run `mitm_deploy_all.sh` to recreate directories.
### Robot Becomes Unreachable
**Symptom:** SSH connection times out, robot doesn't respond
**Cause:** Network issue or robot crashed
**Recovery:**
1. Power cycle robot (unplug, wait 10s, plug back in)
2. Wait 60s for boot
3. Run `./tools/mitm_reboot_robot.sh` to test connectivity
4. If still unreachable, disable MITM physically:
- Power off robot
- On next boot, immediately SSH and run `/mnt/UDISK/mitm_disable.sh`
## File Structure Reference
### On Robot (`/mnt/UDISK/`)
```
/mnt/UDISK/
├── binary/
│ └── serial_mitm # MITM proxy binary (520KB)
├── log/
│ ├── mitm_capture_run1_*.log # Capture logs
│ ├── mitm_capture_run2_*.log
│ └── archive/ # Archived logs
│ └── capture_TIMESTAMP/
├── mitm_enabled # Flag file (presence = MITM active)
├── mitm_run_counter # Current run number (1-5)
├── mitm_boot_setup.sh # Boot-time setup script
├── mitm_stop.sh # Stop logging script
├── mitm_cleanup.sh # Restore normal operation
├── mitm_enable.sh # Enable MITM mode
├── mitm_disable.sh # Disable MITM mode
└── _root.sh # User boot script (calls boot_setup)
```
### On Dev Machine (`protocol-mitm/`)
```
protocol-mitm/
├── src/
│ └── main.rs # MITM proxy source (serial_mitm binary)
├── scripts/ # Robot-side scripts (deployed)
│ ├── mitm_boot_setup.sh
│ ├── mitm_stop.sh
│ ├── mitm_cleanup.sh
│ ├── mitm_enable.sh
│ ├── mitm_disable.sh
│ └── mitm_init.sh
├── tools/ # Dev-side control scripts
│ ├── mitm_deploy_all.sh # Build + deploy everything
│ ├── mitm_enable.sh # Remote enable
│ ├── mitm_reboot_robot.sh # Reboot + wait
│ ├── mitm_finish_run.sh # Stop current run
│ ├── mitm_retrieve_logs.sh # Download logs
│ ├── mitm_disable.sh # Remote disable
│ └── README.md
├── logs/ # Downloaded logs (local)
│ └── mitm_captures_*/
├── docs/
│ ├── MITM_LOGGING_GUIDE.md # This file
│ └── LIDAR_INITIALIZATION_SEQUENCE.md
├── Cargo.toml
└── README.md
```
## Analysis Tips
### Extract Initialization Sequence
```bash
cd logs/mitm_captures_*/
# Find first init packets (CMD=0x08)
grep "CMD=0x08" mitm_capture_run1_*.log | head -20
# Count heartbeat frequency
grep "CMD=0x66" mitm_capture_run1_*.log | head -100
# Extract all unique command types
grep "PKT: CMD=" mitm_capture_run1_*.log | \
awk '{print $3}' | sort | uniq -c
```
### Compare Runs for Consistency
```bash
# Extract command sequences from each run
for log in mitm_capture_run*.log; do
echo "=== $log ==="
grep "PKT: CMD=" "$log" | awk '{print $3}' | uniq -c
done
```
### Timing Analysis
```bash
# Calculate heartbeat intervals
grep "CMD=0x66" mitm_capture_run1_*.log | \
awk -F'[\\[\\]]' '{print $2}' | \
head -100 > heartbeat_timestamps.txt
# Use a script to calculate intervals
python3 -c "
import sys
times = [float(line.strip()) for line in open('heartbeat_timestamps.txt')]
intervals = [times[i+1] - times[i] for i in range(len(times)-1)]
print(f'Avg interval: {sum(intervals)/len(intervals):.6f}s')
print(f'Min: {min(intervals):.6f}s, Max: {max(intervals):.6f}s')
"
```
## Best Practices
1. **Small Room First**: Test with a small room (5-10 min cleaning) to validate setup before longer runs
2. **Check Logs Immediately**: After each run, check log file size before rebooting for next run
3. **Monitor Disk Space**: MITM logs can be 3-5MB each; ensure sufficient space before starting
4. **Retrieve Frequently**: Download logs after 2-3 runs to avoid losing data if robot crashes
5. **Document Anomalies**: Note any unusual robot behavior (stuttering, errors) in log filenames or separate notes
6. **Compare Runs**: Look for consistency across runs to validate protocol understanding
## Advanced Usage
### Change Target Run Count
```bash
./tools/mitm_enable.sh 10 # Capture 10 runs instead of 5
```
### Manual Control (Robot-side)
If you're already SSHed into the robot:
```bash
# Enable manually
/mnt/UDISK/mitm_enable.sh
reboot
# After cleaning
/mnt/UDISK/mitm_stop.sh
# Disable manually
/mnt/UDISK/mitm_disable.sh
```
### Log to External Storage
Modify `MITM_BINARY` path in `mitm_boot_setup.sh` to log to USB drive if mounted at `/mnt/SNN/`:
```bash
LOG_DIR="/mnt/SNN/mitm_logs"
```
### Custom Run Numbers
Start from a specific run:
```bash
ssh root@vacuum "echo 3 > /mnt/UDISK/mitm_run_counter"
```
## Safety Considerations
- **Monitor Watchdog Active**: The Monitor watchdog continues to supervise AuxCtrl. If proxy crashes, motors stop safely
- **Log Rotation**: MITM logs don't auto-rotate; monitor disk space
- **Network Dependency**: Dev-side scripts require robot network access
- **Reboot Required**: Always reboot after disabling MITM to ensure clean state
## Related Documentation
- [LIDAR_INITIALIZATION_SEQUENCE.md](LIDAR_INITIALIZATION_SEQUENCE.md) - Complete lidar protocol specification
- [Protocol-MITM README](../README.md) - Project overview and quick start
- [SangamIO Guide](../../sangam-io/GUIDE.md) - Implementation guide for the protocol library
- [GD32 Protocol Spec](../../docs/GD32_PROTOCOL.md) - Full GD32 protocol documentation
## Support
If you encounter issues:
1. Check this guide's Troubleshooting section
2. Review `src/main.rs` source code for MITM proxy behavior details
3. Check robot logs: `ssh root@vacuum "dmesg | tail -50"`
4. Verify network connectivity: `ping vacuum`
For persistent issues, collect:
- Output of `./tools/mitm_deploy_all.sh`
- Robot boot log: `ssh root@vacuum "dmesg"`
- Process list: `ssh root@vacuum "ps aux | grep -E 'serial_mitm|AuxCtrl|Monitor'"`
- MITM build log: Check `cargo build` output from protocol-mitm directory
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,185 @@
Warning: Permanently added '192.168.68.101' (RSA) to the list of known hosts.
=
=== MITM Session Started ===
Run Number: 1
Timestamp: 2025-11-01T07:40:38.312960922+00:00
Real port: /dev/ttyS3_hardware
Virtual port: /tmp/ttyS3_tap
===
[1761982838.338542] RX 24 bytes
HEX: 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00
[1761982838.481868] TX 144 bytes
HEX: 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40
[1761982839.157491] RX 21 bytes
HEX: FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D
[1761982839.282515] TX 50 bytes
HEX: FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D
[1761982840.244371] RX 3 bytes
HEX: 0D 00 02
[1761982840.558499] TX 6 bytes
HEX: 0D 0A 5E 40 5E 42
[1761982840.684957] RX 3 bytes
HEX: 00 03 19
[1761982840.738085] TX 6 bytes
HEX: 5E 40 5E 43 5E 59
[1761982840.797848] RX 15 bytes
HEX: 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00
[1761982840.939979] TX 26 bytes
HEX: 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40
[1761982841.104438] RX 18 bytes
HEX: 00 00 00 00 00 00 00 00 00 00 03 00 3A 00 0D 00 02 00
[1761982841.282831] TX 35 bytes
HEX: 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 43 5E 40 3A 5E 40 0D 0A 5E 40 5E 42 5E 40
[1761982841.516205] RX 2 bytes
HEX: 00 0F
[1761982841.560962] TX 4 bytes
HEX: 5E 40 5E 4F
[1761982841.618603] RX 24 bytes
HEX: 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00
[1761982841.820464] TX 144 bytes
HEX: 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40
[1761982843.896920] RX 11 bytes
HEX: 01 06 00 0F 04 00 00 01 A6 25 00
[1761982844.043097] TX 18 bytes
HEX: 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40
[1761982844.204002] RX 11 bytes
HEX: 00 00 00 00 00 00 00 00 02 00 36
[1761982844.353607] TX 21 bytes
HEX: 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 42 5E 40 36
[1761982844.540561] RX 11 bytes
HEX: 00 00 00 00 00 00 00 00 00 00 00
[1761982844.695561] TX 22 bytes
HEX: 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40
[1761982844.883696] RX 16 bytes
HEX: 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00
[1761982845.088495] TX 29 bytes
HEX: 5E 40 5E 40 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40
[1761982845.324656] RX 18 bytes
HEX: 00 00 00 00 00 00 00 00 00 03 00 37 00 0C 00 02 00 00
[1761982845.547337] TX 35 bytes
HEX: 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 43 5E 40 37 5E 40 5E 4C 5E 40 5E 42 5E 40 5E 40
[1761982845.825178] RX 19 bytes
HEX: 00 00 00 00 00 00 00 03 00 3B 00 0A 00 02 00 00 00 03 19
[1761982846.053480] TX 37 bytes
HEX: 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 43 5E 40 3B 5E 40 0D 0A 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59
[1761982846.738058] RX 25 bytes
HEX: FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00
[1761982847.313784] TX 52 bytes
HEX: FB 63 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40
[1761982847.738950] RX 15 bytes
HEX: 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00
[1761982847.920181] TX 160 bytes
HEX: 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40 5E 40 5E 40 5E 40
[1761982848.757878] RX 18 bytes
HEX: 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01
[1761982848.871141] TX 30 bytes
HEX: 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41
[1761982848.999939] RX 4 bytes
HEX: 00 00 00 00
[1761982849.043949] TX 8 bytes
HEX: 5E 40 5E 40 5E 40 5E 40
[1761982849.098781] RX 25 bytes
HEX: 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00
[1761982849.245863] TX 146 bytes
HEX: 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40
[1761982851.084140] RX 27 bytes
HEX: 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 35 00
[1761982851.320332] TX 49 bytes
HEX: 5E 40 62 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 43 5E 40 35 5E 40
[1761982851.605280] RX 24 bytes
HEX: 00 00 00 00 00 00 00 00 00 00 00 00 02 00 3A 00 10 00 02 00 00 00 03 19
[1761982851.821347] TX 47 bytes
HEX: 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 42 5E 40 3A 5E 40 5E 50 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59
[1761982852.098756] RX 32 bytes
HEX: FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00
PKT: CMD=0x15 LEN=99
(STATUS - Sensor/IMU/Battery data)
[1761982852.351607] TX 170 bytes
HEX: FA FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40
PKT: CMD=0x08 LEN=99
(INIT - Initialization sequence)
[1761982854.161587] RX 17 bytes
HEX: 00 00 00 00 00 00 00 01 00 03 00 01 00 E6 FF 00 00
[1761982854.310130] TX 32 bytes
HEX: 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 41 5E 40 5E 43 5E 40 5E 41 5E 40 E6 FF 5E 40 5E 40
[1761982854.485534] RX 21 bytes
HEX: 00 00 00 00 00 00 00 00 03 00 37 00 0B 00 02 00 00 00 03 19 02
[1761982854.643389] TX 41 bytes
HEX: 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 43 5E 40 37 5E 40 5E 4B 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59 5E 42
[1761982854.877662] RX 1024 bytes
HEX: FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 01 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 37 00 0F 00 02 00 00 00 03 19 01 00 01 00 00 00 E1 48 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 03 00 02 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 39 00 0F 00 02 00 00 00 03 19 03 00 02 00 FF FF E2 B2 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 3A 00 10 00 02 00 00 00 03 19 03 00 00 00 00 00 E1 4D FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FE FF 03 00 FF FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 3A 00 10 00 02 00 00 00 03 19 01 00 FF FF FE FF E2 BA FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 03 00 02 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 39 00 0F 00 02 00 00 00 03 19 00 00 02 00 FF FF E2 B5 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 03 00 03 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 38 00 0F 00 02 00 00 00 03 19 01 00 03 00 FF FF E2 B3 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FD FF 03 00 02 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 39 00 0E 00 02 00 00 00 03 19 03 00 02 00 FD FF E2 B7 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FE FF 03 00 02 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 3A 00 0E 00 02 00 00 00 03 19 03 00 02 00 FE FF E2 B4 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FE FF 03 00 00 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 39 00 0E 00 02 00 00 00 03 19 00 00 00 00 FE FF E2 BD FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FD FF 03 00 FE FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 3A 00 0E 00 02 00 00 00 03 19 01 00 FE FF FD FF E2 C1 FA FB 63 15
PKT: CMD=0x15 LEN=99
(STATUS - Sensor/IMU/Battery data)
[1761982864.110155] TX 1024 bytes
HEX: FA FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 43 5E 40 5E 41 5E 40 E6 FF 5E 40 5E 40 5E 40 5E 40 62 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 43 5E 40 37 5E 40 5E 4F 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59 5E 41 5E 40 5E 41 5E 40 5E 40 5E 40 E1 48 FA FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 FF FF 5E 43 5E 40 5E 42 5E 40 E6 FF 5E 40 5E 40 5E 40 5E 40 62 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 39 5E 40 5E 4F 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59 5E 43 5E 40 5E 42 5E 40 FF FF E2 B2 FA FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 43 5E 40 5E 40 5E 40 E6 FF 5E 40 5E 40 5E 40 5E 40 62 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 3A 5E 40 5E 50 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59 5E 43 5E 40 5E 40 5E 40 5E 40 5E 40 E1 4D FA FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E
PKT: CMD=0x08 LEN=99
(INIT - Initialization sequence)
[1761982872.942240] RX 1024 bytes
HEX: 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 FD FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 39 00 0E 00 02 00 00 00 03 19 03 00 FD FF 00 00 E1 44 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 03 00 FD FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 39 00 0E 00 02 00 00 00 03 19 03 00 FD FF 01 00 E1 46 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 04 00 FE FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 39 00 0E 00 02 00 00 00 03 19 00 00 FE FF FF FF E2 BE FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 04 00 FF FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 38 00 0E 00 02 00 00 00 03 19 00 00 FF FF 01 00 E1 47 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 04 00 FE FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 37 00 0E 00 02 00 00 00 03 19 02 00 FE FF 01 00 E1 45 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 FF FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 04 00 38 00 0E 00 02 00 00 00 03 19 03 00 FF FF 00 00 E1 48 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 04 00 FF FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 37 00 0F 00 02 00 00 00 03 19 03 00 FF FF FF FF E2 BA FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FE FF 04 00 FF FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 36 00 0F 00 02 00 00 00 03 19 01 00 FF FF FE FF E2 BF FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 01 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 37 00 0E 00 02 00 00 00 03 19 02 00 01 00 00 00 E1 49 FA FB 03 06 00 06 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 04 00 00 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 38 00 0E 00 03 00 00 00 03 19 03 00 00 00 01 00 E1 4C FA FB
[1761982883.344075] TX 1024 bytes
HEX: 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 FE FF 5E 43 5E 40 FF FF E6 FF 5E 40 5E 40 5E 40 5E 40 62 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 3A 5E 40 5E 50 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59 5E 41 5E 40 FF FF FE FF E2 BA FA FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 FF FF 5E 43 5E 40 5E 42 5E 40 E6 FF 5E 40 5E 40 5E 40 5E 40 62 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 39 5E 40 5E 4F 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59 5E 40 5E 40 5E 42 5E 40 FF FF E2 B5 FA FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 FF FF 5E 43 5E 40 5E 43 5E 40 E6 FF 5E 40 5E 40 5E 40 5E 40 62 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 42 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 38 5E 40 5E 4F 5E 40 5E 42 5E 40 5E 40 5E 40 5E 43 5E 59 5E 41 5E 40 5E 43 5E 40 FF FF E2 B3 FA FB 63 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 5E 41 5E 46 5E 40 5E 4F 5E 40 5E 40 5E 41 A6 25 5E 40 5E 40 5E 40 5E 40 5E 40 5E 40 74 5D 5E 41 5E 40 5E 40 5E 40 5E 40 5E 40 48 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08 20 08 08
[1761982889.709712] RX 1024 bytes
HEX: 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 FF FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 38 00 0E 00 03 00 00 00 03 19 04 00 FF FF 00 00 E1 49 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FD FF 04 00 FD FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 37 00 0E 00 03 00 00 00 03 19 05 00 FD FF FD FF E2 C0 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 01 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 37 00 0F 00 02 00 00 00 03 19 04 00 01 00 00 00 E1 4C FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 04 00 03 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 03 00 38 00 0F 00 02 00 00 00 03 19 02 00 03 00 01 00 E1 51 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 02 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 02 00 38 00 0F 00 02 00 00 00 03 19 03 00 02 00 00 00 E1 4D FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 01 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 02 00 39 00 0F 00 02 00 00 00 03 19 02 00 01 00 00 00 E1 4B FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 04 00 00 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 02 00 3A 00 0E 00 02 00 00 00 03 19 01 00 00 00 FF FF E2 B9 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FD FF 04 00 FF FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 02 00 39 00 0E 00 02 00 00 00 03 19 01 00 FF FF FD FF E2 C0 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 FF FF E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 02 00 3A 00 0E 00 02 00 00 00 03 19 01 00 FF FF 00 00 E1 46 FA FB 63 15 01 06 00 0F 04 00 00 01 A6 25 00 00 00 00 00 00 74 5D 01 00 00 00 00 00 48 15 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 E6 FF 00 00 00 00 62 00 00 00 00 00 00 00 42 04 00 00 00 00 00 00 00 00 00 00 00 00 02 00 3A 00 0E 00 02 00 00 00 03 19 00 00 00 00 00 00 E1 47 FA FB 63 15 01 06
=== MITM Session Ended ===
End Timestamp: 2025-11-01T07:41:38.259881158+00:00
TX Packets: 26
RX Packets: 27
TX Bytes: 3360
RX Bytes: 3476
===
+114
View File
@@ -0,0 +1,114 @@
#!/bin/sh
#
# MITM Boot Setup Script
# Runs on robot boot to set up serial port MITM proxy BEFORE Monitor/AuxCtrl start
#
# Installation:
# 1. Copy to /mnt/UDISK/mitm_boot_setup.sh
# 2. Add to /mnt/UDISK/_root.sh:
# [ -f /mnt/UDISK/mitm_boot_setup.sh ] && /mnt/UDISK/mitm_boot_setup.sh
#
# This script should be called early in boot sequence, before robotManager starts
ENABLED_FLAG="/mnt/UDISK/mitm_enabled"
MITM_BINARY="/mnt/UDISK/binary/serial_mitm"
RUN_COUNTER_FILE="/mnt/UDISK/mitm_run_counter"
REAL_PORT="/dev/ttyS3"
HARDWARE_PORT="/dev/ttyS3_hardware"
VIRTUAL_PORT="/tmp/ttyS3_tap"
PID_FILE="/tmp/mitm.pid"
LOG_DIR="/mnt/UDISK/log"
echo "[MITM Boot] Checking if MITM mode is enabled..."
# Check if MITM mode is enabled
if [ ! -f "$ENABLED_FLAG" ]; then
echo "[MITM Boot] MITM mode disabled (no flag file). Skipping setup."
exit 0
fi
echo "[MITM Boot] MITM mode ENABLED. Starting setup..."
# Check if binary exists
if [ ! -f "$MITM_BINARY" ]; then
echo "[MITM Boot] ERROR: MITM binary not found at $MITM_BINARY"
echo "[MITM Boot] Please run mitm_deploy_all.sh from dev machine first"
exit 1
fi
# Create log directory if needed
mkdir -p "$LOG_DIR"
# Read current run counter
if [ -f "$RUN_COUNTER_FILE" ]; then
RUN_NUM=$(cat "$RUN_COUNTER_FILE")
else
RUN_NUM=1
fi
echo "[MITM Boot] Current run: $RUN_NUM"
# Check if real serial port exists
if [ ! -c "$REAL_PORT" ]; then
echo "[MITM Boot] ERROR: Serial port $REAL_PORT not found!"
exit 1
fi
# Check if already set up (in case of restart)
if [ -c "$HARDWARE_PORT" ]; then
echo "[MITM Boot] MITM already set up (found $HARDWARE_PORT)"
else
# Move real port to backup location
echo "[MITM Boot] Moving $REAL_PORT -> $HARDWARE_PORT"
mv "$REAL_PORT" "$HARDWARE_PORT"
if [ $? -ne 0 ]; then
echo "[MITM Boot] ERROR: Failed to move serial port"
exit 1
fi
fi
# Start MITM proxy in background
echo "[MITM Boot] Starting MITM proxy (run $RUN_NUM)..."
$MITM_BINARY $RUN_NUM >/dev/null 2>&1 &
MITM_PID=$!
# Save PID for later cleanup
echo $MITM_PID > "$PID_FILE"
# Wait a moment for PTY to be created
sleep 1
# Check if virtual port was created
if [ ! -L "$VIRTUAL_PORT" ]; then
echo "[MITM Boot] ERROR: Virtual port $VIRTUAL_PORT not created!"
kill $MITM_PID 2>/dev/null
mv "$HARDWARE_PORT" "$REAL_PORT"
exit 1
fi
# Create symlink /dev/ttyS3 -> /tmp/ttyS3_tap
echo "[MITM Boot] Creating symlink $REAL_PORT -> $VIRTUAL_PORT"
ln -sf "$VIRTUAL_PORT" "$REAL_PORT"
if [ $? -ne 0 ]; then
echo "[MITM Boot] ERROR: Failed to create symlink"
kill $MITM_PID 2>/dev/null
mv "$HARDWARE_PORT" "$REAL_PORT"
exit 1
fi
echo "[MITM Boot] ✓ MITM setup complete!"
echo "[MITM Boot] - MITM proxy running (PID: $MITM_PID)"
echo "[MITM Boot] - Real port: $HARDWARE_PORT"
echo "[MITM Boot] - Virtual port: $VIRTUAL_PORT (via PTY)"
echo "[MITM Boot] - Symlink: $REAL_PORT -> $VIRTUAL_PORT"
echo "[MITM Boot] - AuxCtrl will transparently use MITM proxy"
echo "[MITM Boot] - Run number: $RUN_NUM"
echo ""
echo "[MITM Boot] Ready for vacuum cleaning cycle."
echo "[MITM Boot] Press robot button to start cleaning."
echo "[MITM Boot] Run 'mitm_stop.sh' when complete."
echo ""
exit 0
+69
View File
@@ -0,0 +1,69 @@
#!/bin/sh
#
# MITM Cleanup Script
# Restores robot to normal operation (without MITM)
#
# Usage: /mnt/UDISK/mitm_cleanup.sh
#
# WARNING: This will require a reboot to restart AuxCtrl cleanly
REAL_PORT="/dev/ttyS3"
HARDWARE_PORT="/dev/ttyS3_hardware"
VIRTUAL_PORT="/tmp/ttyS3_tap"
PID_FILE="/tmp/mitm.pid"
echo "========================================="
echo " MITM Cleanup - Restore Normal Mode"
echo "========================================="
echo ""
# Stop MITM proxy if running
if [ -f "$PID_FILE" ]; then
MITM_PID=$(cat "$PID_FILE")
if kill -0 $MITM_PID 2>/dev/null; then
echo "Stopping MITM proxy (PID $MITM_PID)..."
kill -TERM $MITM_PID 2>/dev/null
sleep 2
kill -9 $MITM_PID 2>/dev/null
fi
rm "$PID_FILE"
echo "✓ MITM proxy stopped"
fi
# Remove symlink
if [ -L "$REAL_PORT" ]; then
echo "Removing symlink $REAL_PORT..."
rm "$REAL_PORT"
echo "✓ Symlink removed"
fi
# Remove virtual port
if [ -L "$VIRTUAL_PORT" ]; then
echo "Removing virtual port $VIRTUAL_PORT..."
rm "$VIRTUAL_PORT"
echo "✓ Virtual port removed"
fi
# Restore hardware port
if [ -c "$HARDWARE_PORT" ]; then
echo "Restoring $HARDWARE_PORT -> $REAL_PORT..."
mv "$HARDWARE_PORT" "$REAL_PORT"
echo "✓ Real port restored"
else
echo "WARNING: Hardware port $HARDWARE_PORT not found!"
echo "This is expected if cleanup was already run or MITM was never set up."
fi
echo ""
echo "========================================="
echo " Cleanup Complete"
echo "========================================="
echo ""
echo "IMPORTANT: You must reboot the robot for AuxCtrl to work properly."
echo ""
echo "To reboot:"
echo " reboot"
echo ""
echo "Or from dev machine:"
echo " ./tools/mitm_reboot_robot.sh"
echo ""
+85
View File
@@ -0,0 +1,85 @@
#!/bin/sh
#
# MITM Disable Script
# Disables MITM logging mode and archives logs
#
# Usage: /mnt/UDISK/mitm_disable.sh
#
# This removes the flag file and runs cleanup
ENABLED_FLAG="/mnt/UDISK/mitm_enabled"
RUN_COUNTER_FILE="/mnt/UDISK/mitm_run_counter"
LOG_DIR="/mnt/UDISK/log"
ARCHIVE_DIR="/mnt/UDISK/log/archive"
echo "========================================="
echo " MITM Logging - Disable"
echo "========================================="
echo ""
# Run cleanup first
if [ -f "/mnt/UDISK/mitm_cleanup.sh" ]; then
echo "Running cleanup..."
/mnt/UDISK/mitm_cleanup.sh
echo ""
fi
# Remove enabled flag
if [ -f "$ENABLED_FLAG" ]; then
echo "Disabling MITM mode..."
rm "$ENABLED_FLAG"
echo "✓ MITM mode disabled"
else
echo "MITM mode was already disabled"
fi
# Archive logs with timestamp
if [ -d "$LOG_DIR" ] && [ "$(ls -A $LOG_DIR/mitm_capture_*.log 2>/dev/null)" ]; then
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
ARCHIVE_SUBDIR="$ARCHIVE_DIR/capture_$TIMESTAMP"
echo ""
echo "Archiving logs..."
mkdir -p "$ARCHIVE_SUBDIR"
# Move all MITM capture logs to archive
mv "$LOG_DIR"/mitm_capture_*.log "$ARCHIVE_SUBDIR/" 2>/dev/null
# Copy run counter for reference
if [ -f "$RUN_COUNTER_FILE" ]; then
RUNS_COMPLETED=$(($(cat "$RUN_COUNTER_FILE") - 1))
echo "$RUNS_COMPLETED" > "$ARCHIVE_SUBDIR/runs_completed.txt"
fi
echo "✓ Logs archived to: $ARCHIVE_SUBDIR"
# Show summary
LOG_COUNT=$(ls -1 "$ARCHIVE_SUBDIR"/mitm_capture_*.log 2>/dev/null | wc -l)
TOTAL_SIZE=$(du -sh "$ARCHIVE_SUBDIR" | awk '{print $1}')
echo ""
echo "Archive Summary:"
echo " Location: $ARCHIVE_SUBDIR"
echo " Files: $LOG_COUNT log files"
echo " Total size: $TOTAL_SIZE"
echo " Runs completed: $RUNS_COMPLETED"
fi
# Clean up run counter
if [ -f "$RUN_COUNTER_FILE" ]; then
rm "$RUN_COUNTER_FILE"
fi
echo ""
echo "========================================="
echo " MITM Mode Disabled"
echo "========================================="
echo ""
echo "IMPORTANT: Reboot required to restore normal operation"
echo ""
echo "To reboot:"
echo " reboot"
echo ""
echo "To retrieve archived logs to dev machine:"
echo " ./tools/mitm_retrieve_logs.sh"
echo ""
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
#
# MITM Enable Script
# Enables MITM logging mode (takes effect on next boot)
#
# Usage: /mnt/UDISK/mitm_enable.sh [target_runs]
#
# This creates a flag file that tells mitm_boot_setup.sh to activate on next boot
ENABLED_FLAG="/mnt/UDISK/mitm_enabled"
RUN_COUNTER_FILE="/mnt/UDISK/mitm_run_counter"
LOG_DIR="/mnt/UDISK/log"
# Get target runs from argument or use default
TARGET_RUNS=${1:-5}
echo "========================================="
echo " MITM Logging - Enable"
echo "========================================="
echo ""
# Create log directory
mkdir -p "$LOG_DIR"
# Create enabled flag
echo "Enabling MITM logging mode..."
touch "$ENABLED_FLAG"
echo "✓ MITM mode enabled"
# Reset run counter
echo "1" > "$RUN_COUNTER_FILE"
echo "✓ Run counter reset to 1"
echo ""
echo "Configuration:"
echo " Target runs: $TARGET_RUNS"
echo " Log directory: $LOG_DIR"
echo ""
echo "========================================="
echo " MITM Mode Activated"
echo "========================================="
echo ""
echo "Next steps:"
echo " 1. Reboot the robot:"
echo " reboot"
echo ""
echo " 2. After boot, MITM will auto-start"
echo ""
echo " 3. Press physical button to start cleaning Run 1"
echo ""
echo " 4. When robot returns to dock, stop logging:"
echo " /mnt/UDISK/mitm_stop.sh"
echo ""
echo " 5. Repeat steps 1-4 for $TARGET_RUNS runs total"
echo ""
echo " 6. When complete, disable MITM:"
echo " /mnt/UDISK/mitm_disable.sh"
echo ""
+137
View File
@@ -0,0 +1,137 @@
#!/bin/sh /etc/rc.common
# MITM Logger Init Script
# Starts serial MITM proxy BEFORE Monitor/AuxCtrl
START=89
STOP=10
ENABLED_FLAG="/mnt/UDISK/mitm_enabled"
MITM_BINARY="/mnt/UDISK/binary/serial_mitm"
RUN_COUNTER_FILE="/mnt/UDISK/mitm_run_counter"
REAL_PORT="/dev/ttyS3"
HARDWARE_PORT="/dev/ttyS3_hardware"
VIRTUAL_PORT="/tmp/ttyS3_tap"
LOG_DIR="/mnt/UDISK/log"
start() {
echo "[MITM Init] Checking if MITM mode is enabled..."
# Only run if flag file exists
if [ ! -f "$ENABLED_FLAG" ]; then
echo "[MITM Init] MITM mode disabled. Skipping."
return 0
fi
echo "[MITM Init] MITM mode ENABLED. Starting setup..."
# Check if binary exists
if [ ! -f "$MITM_BINARY" ]; then
echo "[MITM Init] ERROR: MITM binary not found at $MITM_BINARY"
return 1
fi
# Create log directory
mkdir -p "$LOG_DIR"
# Read run counter
if [ -f "$RUN_COUNTER_FILE" ]; then
RUN_NUM=$(cat "$RUN_COUNTER_FILE")
else
RUN_NUM=1
echo 1 > "$RUN_COUNTER_FILE"
fi
echo "[MITM Init] Current run: $RUN_NUM"
# Check if real serial port exists
if [ ! -c "$REAL_PORT" ]; then
echo "[MITM Init] ERROR: Serial port $REAL_PORT not found!"
return 1
fi
# Check if already set up
if [ -c "$HARDWARE_PORT" ]; then
echo "[MITM Init] MITM already set up (found $HARDWARE_PORT)"
else
# Move real port to backup location
echo "[MITM Init] Moving $REAL_PORT -> $HARDWARE_PORT"
mv "$REAL_PORT" "$HARDWARE_PORT"
if [ $? -ne 0 ]; then
echo "[MITM Init] ERROR: Failed to move serial port"
return 1
fi
fi
# Start MITM proxy in background
echo "[MITM Init] Starting MITM proxy (run $RUN_NUM)..."
$MITM_BINARY $RUN_NUM >/tmp/serial_mitm.log 2>&1 &
MITM_PID=$!
# Save PID
echo $MITM_PID > /var/run/mitm.pid
# Wait for PTY to be created
sleep 2
# Check if virtual port was created
if [ ! -L "$VIRTUAL_PORT" ]; then
echo "[MITM Init] ERROR: Virtual port $VIRTUAL_PORT not created!"
kill $MITM_PID 2>/dev/null
mv "$HARDWARE_PORT" "$REAL_PORT" 2>/dev/null
return 1
fi
# Create symlink /dev/ttyS3 -> /tmp/ttyS3_tap
echo "[MITM Init] Creating symlink $REAL_PORT -> $VIRTUAL_PORT"
ln -sf "$VIRTUAL_PORT" "$REAL_PORT"
if [ $? -ne 0 ]; then
echo "[MITM Init] ERROR: Failed to create symlink"
kill $MITM_PID 2>/dev/null
mv "$HARDWARE_PORT" "$REAL_PORT" 2>/dev/null
return 1
fi
echo "[MITM Init] ✓ MITM setup complete!"
echo "[MITM Init] - MITM proxy running (PID: $MITM_PID)"
echo "[MITM Init] - Real port: $HARDWARE_PORT"
echo "[MITM Init] - Virtual port: $VIRTUAL_PORT (via PTY)"
echo "[MITM Init] - Symlink: $REAL_PORT -> $VIRTUAL_PORT"
echo "[MITM Init] - Run number: $RUN_NUM"
echo ""
return 0
}
stop() {
echo "[MITM Init] Stopping MITM proxy..."
# Kill MITM process
if [ -f /var/run/mitm.pid ]; then
PID=$(cat /var/run/mitm.pid)
kill -TERM $PID 2>/dev/null
sleep 1
kill -9 $PID 2>/dev/null
rm -f /var/run/mitm.pid
else
killall -9 serial_mitm 2>/dev/null
fi
# Remove symlink
if [ -L "$REAL_PORT" ]; then
rm -f "$REAL_PORT"
fi
# Restore original port
if [ -c "$HARDWARE_PORT" ]; then
mv "$HARDWARE_PORT" "$REAL_PORT" 2>/dev/null
fi
# Remove virtual port
rm -f "$VIRTUAL_PORT"
echo "[MITM Init] ✓ MITM stopped and cleaned up"
return 0
}
+122
View File
@@ -0,0 +1,122 @@
#!/bin/sh
#
# MITM Stop Script
# Run this when vacuum cleaning is complete and robot has returned to dock
#
# Usage: /mnt/UDISK/mitm_stop.sh
#
# This script:
# 1. Sends SIGTERM to serial_mitm for graceful shutdown
# 2. Increments run counter
# 3. Shows statistics about the capture
# 4. Prepares for next iteration
PID_FILE="/tmp/mitm.pid"
RUN_COUNTER_FILE="/mnt/UDISK/mitm_run_counter"
LOG_DIR="/mnt/UDISK/log"
TARGET_RUNS=5
echo "========================================="
echo " MITM Capture - Stop Logging"
echo "========================================="
echo ""
# Check if MITM is running
if [ ! -f "$PID_FILE" ]; then
echo "ERROR: MITM proxy PID file not found."
echo "Is MITM running? Check with: ps | grep serial_mitm"
exit 1
fi
MITM_PID=$(cat "$PID_FILE")
# Check if process is actually running
if ! kill -0 $MITM_PID 2>/dev/null; then
echo "WARNING: MITM proxy (PID $MITM_PID) is not running!"
echo "It may have crashed. Check logs."
rm "$PID_FILE"
exit 1
fi
# Get current run number
if [ -f "$RUN_COUNTER_FILE" ]; then
CURRENT_RUN=$(cat "$RUN_COUNTER_FILE")
else
CURRENT_RUN=1
fi
echo "Stopping capture for Run $CURRENT_RUN..."
echo ""
# Send SIGTERM for graceful shutdown
echo "Sending SIGTERM to MITM proxy (PID $MITM_PID)..."
kill -TERM $MITM_PID
# Wait for process to exit (with timeout)
WAIT_COUNT=0
while kill -0 $MITM_PID 2>/dev/null && [ $WAIT_COUNT -lt 10 ]; do
sleep 1
WAIT_COUNT=$((WAIT_COUNT + 1))
done
if kill -0 $MITM_PID 2>/dev/null; then
echo "WARNING: Process did not exit gracefully. Force killing..."
kill -9 $MITM_PID
sleep 1
fi
echo "✓ MITM proxy stopped"
rm "$PID_FILE"
# Find the most recent log file for this run
LATEST_LOG=$(ls -t "$LOG_DIR"/mitm_capture_run${CURRENT_RUN}_*.log 2>/dev/null | head -1)
if [ -n "$LATEST_LOG" ]; then
LOG_SIZE=$(du -h "$LATEST_LOG" | awk '{print $1}')
TX_PACKETS=$(grep -c "^\\[.*\\] TX " "$LATEST_LOG" 2>/dev/null || echo "?")
RX_PACKETS=$(grep -c "^\\[.*\\] RX " "$LATEST_LOG" 2>/dev/null || echo "?")
echo ""
echo "Capture Statistics:"
echo " Run: $CURRENT_RUN"
echo " Log file: $(basename "$LATEST_LOG")"
echo " Size: $LOG_SIZE"
echo " TX packets: $TX_PACKETS"
echo " RX packets: $RX_PACKETS"
else
echo ""
echo "WARNING: Could not find log file for run $CURRENT_RUN"
fi
# Increment run counter
NEXT_RUN=$((CURRENT_RUN + 1))
echo $NEXT_RUN > "$RUN_COUNTER_FILE"
echo ""
echo "========================================="
echo " Run $CURRENT_RUN Complete!"
echo "========================================="
echo ""
if [ $CURRENT_RUN -ge $TARGET_RUNS ]; then
echo "✅ TARGET ACHIEVED: $TARGET_RUNS runs completed!"
echo ""
echo "Next steps:"
echo " 1. Retrieve logs: (from dev machine)"
echo " ./tools/mitm_retrieve_logs.sh"
echo " 2. Disable MITM mode:"
echo " ./tools/mitm_disable.sh"
echo " 3. Reboot robot to restore normal operation"
else
echo "Progress: $CURRENT_RUN/$TARGET_RUNS runs completed"
echo ""
echo "Next steps for Run $NEXT_RUN:"
echo " 1. Reboot robot:"
echo " reboot"
echo " 2. Wait for boot (MITM will auto-start)"
echo " 3. Press robot button to start cleaning"
echo " 4. Run this script again when done:"
echo " /mnt/UDISK/mitm_stop.sh"
fi
echo ""
+433
View File
@@ -0,0 +1,433 @@
//! Serial Man-in-the-Middle Logger for GD32 Protocol Capture
//!
//! Creates a virtual serial port at /tmp/ttyS3_tap that AuxCtrl connects to.
//! This program forwards all data between the virtual port and real /dev/ttyS3_hardware,
//! while logging everything bidirectionally with microsecond timestamps.
//!
//! Features:
//! - Graceful shutdown on SIGTERM/SIGINT
//! - Run counter integration for multi-session captures
//! - GD32 protocol-aware packet logging
//! - Transparent PTY-based forwarding
//!
//! Setup (automated by mitm_boot_setup.sh):
//! 1. Move /dev/ttyS3 → /dev/ttyS3_hardware
//! 2. Run this program (creates /tmp/ttyS3_tap)
//! 3. Symlink /dev/ttyS3 → /tmp/ttyS3_tap
//! 4. Monitor/AuxCtrl starts normally, transparently uses MITM
//!
//! Manual usage:
//! ./serial_mitm [run_number]
//!
//! Exit:
//! Send SIGTERM (kill <pid>) or Ctrl+C for graceful shutdown
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::os::unix::io::{AsRawFd, RawFd};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
const REAL_PORT: &str = "/dev/ttyS3_hardware";
const VIRTUAL_PORT: &str = "/tmp/ttyS3_tap";
const LOG_DIR: &str = "/tmp"; // Use RAM instead of flash to avoid write latency
const RUN_COUNTER_FILE: &str = "/mnt/UDISK/mitm_run_counter";
const GPIO_233_VALUE: &str = "/sys/class/gpio/gpio233/value";
/// Global flag for graceful shutdown
static RUNNING: AtomicBool = AtomicBool::new(true);
/// Signal handler for SIGTERM and SIGINT
extern "C" fn signal_handler(_sig: libc::c_int) {
RUNNING.store(false, Ordering::Relaxed);
}
fn timestamp_micros() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros()
}
fn format_timestamp(micros: u128) -> String {
format!("{}.{:06}", micros / 1_000_000, micros % 1_000_000)
}
fn log_packet(log: &mut File, direction: &str, data: &[u8]) -> io::Result<()> {
let ts = timestamp_micros();
writeln!(
log,
"[{}] {} {} bytes",
format_timestamp(ts),
direction,
data.len()
)?;
// Hex dump
write!(log, " HEX: ")?;
for byte in data {
write!(log, "{:02X} ", byte)?;
}
writeln!(log)?;
// Try to decode as GD32 packet (FA FB header)
if data.len() >= 4 && data[0] == 0xFA && data[1] == 0xFB {
let len = data[2] as usize;
let cmd = data[3];
writeln!(log, " PKT: CMD=0x{:02X} LEN={}", cmd, len)?;
// Decode common commands
match cmd {
0x08 => writeln!(log, " (INIT - Initialization sequence)")?,
0x15 => writeln!(log, " (STATUS - Sensor/IMU/Battery data)")?,
0x66 => writeln!(log, " (HEARTBEAT - Keepalive packet)")?,
0x06 => writeln!(log, " (WAKE - Enable command)")?,
0x07 => writeln!(log, " (SETUP - System configuration)")?,
0x8D => writeln!(log, " (LED - Button LED state)")?,
0x97 => {
if data.len() >= 5 {
let power_state = data[4];
writeln!(log, " (LIDAR_POWER - GPIO 233 = {})", if power_state == 1 { "ON" } else { "OFF" })?;
} else {
writeln!(log, " (LIDAR_POWER - Incomplete packet)")?;
}
},
0x71 => {
if data.len() >= 8 {
let pwm = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
writeln!(log, " (LIDAR_PWM - Speed = {}%)", pwm)?;
} else {
writeln!(log, " (LIDAR_PWM - Incomplete packet)")?;
}
},
_ => {}
}
}
writeln!(log)?;
log.flush()?;
Ok(())
}
fn configure_serial(fd: RawFd) -> io::Result<()> {
unsafe {
let mut termios: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut termios) != 0 {
return Err(io::Error::last_os_error());
}
// Raw mode
libc::cfmakeraw(&mut termios);
// 115200 baud
libc::cfsetispeed(&mut termios, libc::B115200);
libc::cfsetospeed(&mut termios, libc::B115200);
// 8N1
termios.c_cflag &= !libc::PARENB; // No parity
termios.c_cflag &= !libc::CSTOPB; // 1 stop bit
termios.c_cflag &= !libc::CSIZE;
termios.c_cflag |= libc::CS8; // 8 data bits
termios.c_cflag |= libc::CREAD | libc::CLOCAL;
// Non-blocking
termios.c_cc[libc::VTIME] = 0;
termios.c_cc[libc::VMIN] = 0;
if libc::tcsetattr(fd, libc::TCSANOW, &termios) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn create_pty() -> io::Result<(RawFd, String)> {
unsafe {
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
if libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
) != 0
{
return Err(io::Error::last_os_error());
}
// Get slave name
let mut buf: [libc::c_char; 256] = [0; 256];
if libc::ttyname_r(slave, buf.as_mut_ptr(), buf.len()) != 0 {
libc::close(master);
libc::close(slave);
return Err(io::Error::last_os_error());
}
let slave_name = std::ffi::CStr::from_ptr(buf.as_ptr())
.to_string_lossy()
.into_owned();
libc::close(slave);
Ok((master, slave_name))
}
}
fn read_run_counter() -> u32 {
std::fs::read_to_string(RUN_COUNTER_FILE)
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(1)
}
/// Read current GPIO 233 value (0 or 1)
fn read_gpio_233() -> io::Result<u8> {
let mut content = String::new();
File::open(GPIO_233_VALUE)?.read_to_string(&mut content)?;
content
.trim()
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid GPIO value"))
}
/// Log GPIO state change
fn log_gpio_change(log: &mut File, old_state: u8, new_state: u8) -> io::Result<()> {
let ts = timestamp_micros();
writeln!(
log,
"[{}] GPIO_233: {} -> {} (Lidar Power {})",
format_timestamp(ts),
old_state,
new_state,
if new_state == 1 { "ON" } else { "OFF" }
)?;
writeln!(log)?;
log.flush()?;
Ok(())
}
/// Spawn GPIO monitoring thread
fn spawn_gpio_monitor(log_file: Arc<Mutex<File>>) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let mut last_state = read_gpio_233().unwrap_or(0);
// Log initial state
{
let ts = timestamp_micros();
if let Ok(mut log) = log_file.lock() {
let _ = writeln!(log, "[{}] GPIO_233: Initial state = {}", format_timestamp(ts), last_state);
let _ = writeln!(log);
let _ = log.flush();
}
}
while RUNNING.load(Ordering::Relaxed) {
if let Ok(current_state) = read_gpio_233()
&& current_state != last_state {
// State changed - log it!
if let Ok(mut log) = log_file.lock() {
let _ = log_gpio_change(&mut log, last_state, current_state);
}
last_state = current_state;
}
// Poll every 10ms (fast enough to catch all transitions)
std::thread::sleep(std::time::Duration::from_millis(10));
}
})
}
fn main() -> io::Result<()> {
// Install signal handlers
unsafe {
libc::signal(libc::SIGTERM, signal_handler as libc::sighandler_t);
libc::signal(libc::SIGINT, signal_handler as libc::sighandler_t);
}
// Determine run number
let run_number = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or_else(read_run_counter);
println!("========================================");
println!(" Serial MITM Logger - GD32 Protocol");
println!(" Run: {}", run_number);
println!(" Real port: {}", REAL_PORT);
println!("========================================\n");
// Create log directory if it doesn't exist
let _ = std::fs::create_dir_all(LOG_DIR);
// Generate log filename with timestamp and run number
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
let log_filename = format!(
"{}/mitm_capture_run{}_{}.log",
LOG_DIR, run_number, timestamp
);
println!("Log file: {}\n", log_filename);
// Open log file (wrapped in Arc<Mutex<>> for sharing with GPIO thread)
let log_file = Arc::new(Mutex::new(
OpenOptions::new()
.create(true)
.append(true)
.open(&log_filename)?
));
// Write session header
{
let mut log = log_file.lock().unwrap();
writeln!(log, "=")?;
writeln!(log, "=== MITM Session Started ===")?;
writeln!(log, "Run Number: {}", run_number)?;
writeln!(log, "Timestamp: {}", chrono::Local::now().to_rfc3339())?;
writeln!(log, "Real port: {}", REAL_PORT)?;
writeln!(log, "Virtual port: {}", VIRTUAL_PORT)?;
writeln!(log, "GPIO Monitoring: Enabled (GPIO 233)")?;
writeln!(log, "===\n")?;
log.flush()?;
}
// Start GPIO monitoring thread
println!("Starting GPIO 233 monitoring thread...");
let gpio_thread = spawn_gpio_monitor(Arc::clone(&log_file));
println!("✓ GPIO monitoring active");
// Open real serial port
println!("Opening real serial port {}...", REAL_PORT);
let real_serial = OpenOptions::new()
.read(true)
.write(true)
.open(REAL_PORT)?;
let real_fd = real_serial.as_raw_fd();
configure_serial(real_fd)?;
println!("✓ Real port opened and configured");
// Create pseudo-terminal
println!("Creating virtual serial port...");
let (pty_master, pty_slave_name) = create_pty()?;
// Configure PTY to raw mode to prevent data escaping
configure_serial(pty_master)?;
println!("✓ Virtual port created and configured at: {}", pty_slave_name);
// Create symlink
println!("Creating symlink {} -> {}", VIRTUAL_PORT, pty_slave_name);
let _ = std::fs::remove_file(VIRTUAL_PORT);
std::os::unix::fs::symlink(&pty_slave_name, VIRTUAL_PORT)?;
println!("✓ Symlink created");
println!("\nMITM proxy is running!");
println!("AuxCtrl will connect to {} (redirected)", VIRTUAL_PORT);
println!("Send SIGTERM (kill {}) or Ctrl+C to stop gracefully\n", unsafe {
libc::getpid()
});
// Statistics counters
let mut tx_packets = 0u64;
let mut rx_packets = 0u64;
let mut tx_bytes = 0u64;
let mut rx_bytes = 0u64;
// Proxy loop
let mut real_buf = [0u8; 1024];
let mut pty_buf = [0u8; 1024];
while RUNNING.load(Ordering::Relaxed) {
// Check for data from AuxCtrl (via PTY) -> Real Serial
unsafe {
let n = libc::read(
pty_master,
pty_buf.as_mut_ptr() as *mut libc::c_void,
pty_buf.len(),
);
if n > 0 {
let n = n as usize;
tx_packets += 1;
tx_bytes += n as u64;
// Log TX (AuxCtrl -> GD32)
if let Ok(mut log) = log_file.lock() {
let _ = log_packet(&mut log, "TX", &pty_buf[..n]);
}
// Forward to real serial port
let written = libc::write(real_fd, pty_buf.as_ptr() as *const libc::c_void, n);
if written < 0 {
eprintln!("Error writing to real port");
}
}
}
// Check for data from Real Serial -> AuxCtrl (via PTY)
unsafe {
let n = libc::read(
real_fd,
real_buf.as_mut_ptr() as *mut libc::c_void,
real_buf.len(),
);
if n > 0 {
let n = n as usize;
rx_packets += 1;
rx_bytes += n as u64;
// Log RX (GD32 -> AuxCtrl)
if let Ok(mut log) = log_file.lock() {
let _ = log_packet(&mut log, "RX", &real_buf[..n]);
}
// Forward to PTY (AuxCtrl)
let written = libc::write(pty_master, real_buf.as_ptr() as *const libc::c_void, n);
if written < 0 {
eprintln!("Error writing to PTY");
}
}
}
// Small sleep to prevent busy-waiting (100μs = 0.1ms)
std::thread::sleep(std::time::Duration::from_micros(100));
}
// Graceful shutdown
println!("\n\nShutting down gracefully...");
// Wait for GPIO monitoring thread to finish
println!("Stopping GPIO monitoring thread...");
let _ = gpio_thread.join();
println!("✓ GPIO thread stopped");
// Write session footer with statistics
{
let mut log = log_file.lock().unwrap();
writeln!(log, "\n=== MITM Session Ended ===")?;
writeln!(log, "End Timestamp: {}", chrono::Local::now().to_rfc3339())?;
writeln!(log, "TX Packets: {}", tx_packets)?;
writeln!(log, "RX Packets: {}", rx_packets)?;
writeln!(log, "TX Bytes: {}", tx_bytes)?;
writeln!(log, "RX Bytes: {}", rx_bytes)?;
writeln!(log, "===\n")?;
log.flush()?;
}
println!("Statistics:");
println!(" TX: {} packets, {} bytes", tx_packets, tx_bytes);
println!(" RX: {} packets, {} bytes", rx_packets, rx_bytes);
println!(" Log: {}", log_filename);
// Cleanup
unsafe {
libc::close(pty_master);
libc::close(real_fd);
}
let _ = std::fs::remove_file(VIRTUAL_PORT);
println!("✓ MITM proxy stopped cleanly\n");
Ok(())
}
+40
View File
@@ -0,0 +1,40 @@
# MITM Logging Tools
Helper scripts for automated GD32 protocol capture during vacuum cleaning operations.
## Quick Start
```bash
# 1. Deploy everything to robot
./mitm_deploy_all.sh
# 2. Enable MITM mode
./mitm_enable.sh
# 3. For each of 3-5 runs:
./mitm_reboot_robot.sh # Reboot & start MITM
# → Press button on robot to start cleaning
# → Wait for robot to dock
./mitm_finish_run.sh # Stop logging
# 4. Retrieve all logs
./mitm_retrieve_logs.sh
# 5. Disable MITM and restore
./mitm_disable.sh
```
## Available Scripts
| Script | Purpose |
|--------|---------|
| `mitm_deploy_all.sh` | Build serial_mitm binary and deploy all scripts to robot |
| `mitm_enable.sh` | Enable MITM mode (takes effect on next boot) |
| `mitm_disable.sh` | Disable MITM mode and restore normal operation |
| `mitm_reboot_robot.sh` | Reboot robot and wait for MITM to start |
| `mitm_finish_run.sh` | Stop current capture run and show progress |
| `mitm_retrieve_logs.sh` | Download all capture logs from robot |
## Documentation
See [../docs/MITM_LOGGING_GUIDE.md](../docs/MITM_LOGGING_GUIDE.md) for complete documentation.
+131
View File
@@ -0,0 +1,131 @@
#!/bin/bash
#
# MITM Deploy All Script
# Builds serial_mitm binary and deploys all scripts to robot
#
# Usage: ./tools/mitm_deploy_all.sh
#
# Requirements:
# - ARM cross-compilation target installed (armv7-unknown-linux-musleabihf)
# - sshpass installed
# - SSH access to robot configured
set -e # Exit on error
ROBOT_HOST="vacuum"
ROBOT_PASS="${ROBOT_PASSWORD:-}"
SCRIPT_DIR="scripts/robot"
TARGET_DIR="/mnt/UDISK"
BINARY_DIR="/mnt/UDISK/binary"
echo "========================================="
echo " MITM Deploy - Build and Deploy"
echo "========================================="
echo ""
# Check if we're in the right directory
if [ ! -f "Cargo.toml" ]; then
echo "ERROR: Must run from sangam-io root directory"
exit 1
fi
# Check if sshpass is available
if ! command -v sshpass &> /dev/null; then
echo "ERROR: sshpass not found. Install with: brew install sshpass"
exit 1
fi
# Check if ARM target is installed
echo "Checking ARM cross-compilation target..."
if ! rustup target list | grep -q "armv7-unknown-linux-musleabihf (installed)"; then
echo "Installing ARM target..."
rustup target add armv7-unknown-linux-musleabihf
fi
echo "✓ ARM target available"
echo ""
# Build serial_mitm for ARM
echo "Building serial_mitm binary for ARM..."
cargo build --release --bin serial_mitm --target armv7-unknown-linux-musleabihf
if [ ! -f "../target/armv7-unknown-linux-musleabihf/release/serial_mitm" ]; then
echo "ERROR: Build failed - binary not found"
exit 1
fi
BINARY_SIZE=$(du -h ../target/armv7-unknown-linux-musleabihf/release/serial_mitm | awk '{print $1}')
echo "✓ Binary built successfully ($BINARY_SIZE)"
echo ""
# Test robot connectivity
echo "Testing robot connectivity..."
if ! sshpass -p "$ROBOT_PASS" ssh -o ConnectTimeout=5 root@$ROBOT_HOST "echo OK" &>/dev/null; then
echo "ERROR: Cannot connect to robot at $ROBOT_HOST"
echo "Check network connection and SSH config"
exit 1
fi
echo "✓ Robot is reachable"
echo ""
# Create target directories on robot
echo "Creating directories on robot..."
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST "mkdir -p $BINARY_DIR $TARGET_DIR/log"
echo "✓ Directories created"
echo ""
# Deploy binary
echo "Deploying serial_mitm binary..."
cat ../target/armv7-unknown-linux-musleabihf/release/serial_mitm | \
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST \
"cat > $BINARY_DIR/serial_mitm && chmod +x $BINARY_DIR/serial_mitm"
echo "✓ Binary deployed to $BINARY_DIR/serial_mitm"
echo ""
# Deploy scripts
echo "Deploying robot-side scripts..."
for script in $SCRIPT_DIR/*.sh; do
SCRIPT_NAME=$(basename "$script")
echo " - $SCRIPT_NAME"
cat "$script" | \
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST \
"cat > $TARGET_DIR/$SCRIPT_NAME && chmod +x $TARGET_DIR/$SCRIPT_NAME"
done
echo "✓ All scripts deployed"
echo ""
# Deploy init script
echo "Installing MITM init script..."
cat "$SCRIPT_DIR/mitm_init.sh" | \
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST \
"cat > /etc/init.d/mitmLogger && chmod +x /etc/init.d/mitmLogger"
# Create rc.d symlink (S89 runs before Monitor at S91)
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST \
"ln -sf /etc/init.d/mitmLogger /etc/rc.d/S89mitmLogger 2>/dev/null || true"
echo "✓ Init script installed at /etc/init.d/mitmLogger"
echo "✓ Auto-start enabled (S89mitmLogger runs before Monitor)"
echo ""
echo "========================================="
echo " Deployment Complete!"
echo "========================================="
echo ""
echo "Deployed files:"
echo " Binary: $BINARY_DIR/serial_mitm ($BINARY_SIZE)"
echo " Scripts: $TARGET_DIR/*.sh"
echo ""
echo "Next steps:"
echo " 1. Enable MITM mode:"
echo " ./tools/mitm_enable.sh"
echo ""
echo " 2. Reboot robot to start first capture:"
echo " ./tools/mitm_reboot_robot.sh"
echo ""
echo " 3. Press physical button to start cleaning"
echo ""
echo " 4. When done, finish the run:"
echo " ./tools/mitm_finish_run.sh"
echo ""
echo " 5. Repeat steps 2-4 for 3-5 runs"
echo ""
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
#
# MITM Disable Script (Dev-side)
# Remotely disables MITM mode on robot
#
# Usage: ./tools/mitm_disable.sh
ROBOT_HOST="vacuum"
ROBOT_PASS="${ROBOT_PASSWORD:-}"
echo "========================================="
echo " MITM - Disable Mode"
echo "========================================="
echo ""
echo "Disabling MITM mode on robot..."
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST "/mnt/UDISK/mitm_disable.sh"
echo ""
echo "========================================="
echo " MITM Mode Disabled"
echo "========================================="
echo ""
echo "The robot requires a reboot to restore normal operation."
echo ""
echo "Reboot now? [y/N]: "
read -r RESPONSE
if [ "$RESPONSE" = "y" ] || [ "$RESPONSE" = "Y" ]; then
./tools/mitm_reboot_robot.sh
else
echo ""
echo "Skipping reboot. To reboot later:"
echo " ./tools/mitm_reboot_robot.sh"
echo ""
fi
echo "To retrieve archived logs:"
echo " ./tools/mitm_retrieve_logs.sh"
echo ""
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
#
# MITM Enable Script (Dev-side)
# Remotely enables MITM mode on robot
#
# Usage: ./tools/mitm_enable.sh [target_runs]
ROBOT_HOST="vacuum"
ROBOT_PASS="${ROBOT_PASSWORD:-}"
TARGET_RUNS="${1:-5}"
echo "========================================="
echo " MITM - Enable Mode"
echo "========================================="
echo ""
echo "Enabling MITM mode on robot..."
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST "/mnt/UDISK/mitm_enable.sh $TARGET_RUNS"
echo ""
echo "========================================="
echo " MITM Mode Enabled"
echo "========================================="
echo ""
echo "Configuration:"
echo " Target runs: $TARGET_RUNS"
echo ""
echo "Next steps:"
echo " 1. Reboot robot to start Run 1:"
echo " ./tools/mitm_reboot_robot.sh"
echo ""
echo " 2. Press physical button to start cleaning"
echo ""
echo " 3. When done, finish the run:"
echo " ./tools/mitm_finish_run.sh"
echo ""
echo " 4. Repeat steps 1-3 for $TARGET_RUNS total runs"
echo ""
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
#
# MITM Finish Run Script
# Stops MITM logging after a vacuum cleaning cycle completes
#
# Usage: ./tools/mitm_finish_run.sh
ROBOT_HOST="vacuum"
ROBOT_PASS="${ROBOT_PASSWORD:-}"
echo "========================================="
echo " MITM - Finish Current Run"
echo "========================================="
echo ""
echo "Stopping MITM logger on robot..."
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST "/mnt/UDISK/mitm_stop.sh"
echo ""
echo "========================================="
echo " Run Finished"
echo "========================================="
echo ""
# Get run status
CURRENT_RUN=$(sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST \
"cat /mnt/UDISK/mitm_run_counter 2>/dev/null" || echo "?")
if [ "$CURRENT_RUN" != "?" ]; then
COMPLETED_RUN=$((CURRENT_RUN - 1))
echo "Completed: Run $COMPLETED_RUN"
echo "Next: Run $CURRENT_RUN"
echo ""
if [ $COMPLETED_RUN -ge 5 ]; then
echo "🎉 All runs complete! (Target: 5)"
echo ""
echo "Next steps:"
echo " 1. Retrieve all logs:"
echo " ./tools/mitm_retrieve_logs.sh"
echo ""
echo " 2. Disable MITM mode:"
echo " ./tools/mitm_disable.sh"
echo ""
echo " 3. Reboot to restore normal operation:"
echo " ./tools/mitm_reboot_robot.sh"
else
echo "Progress: $COMPLETED_RUN/5 runs complete"
echo ""
echo "Next steps for Run $CURRENT_RUN:"
echo " 1. Reboot robot:"
echo " ./tools/mitm_reboot_robot.sh"
echo ""
echo " 2. Press button to start cleaning"
echo ""
echo " 3. Run this script again when done"
fi
fi
echo ""
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
#
# MITM Reboot Robot Script
# Reboots the robot and waits for it to come back online
#
# Usage: ./tools/mitm_reboot_robot.sh
ROBOT_HOST="vacuum"
ROBOT_PASS="${ROBOT_PASSWORD:-}"
echo "========================================="
echo " MITM - Reboot Robot"
echo "========================================="
echo ""
echo "Rebooting robot..."
sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST "sync && reboot" 2>/dev/null || true
echo "Waiting for robot to shut down..."
sleep 5
# Wait for robot to come back online
echo -n "Waiting for robot to boot up"
MAX_WAIT=60
WAIT_COUNT=0
while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
if sshpass -p "$ROBOT_PASS" ssh -o ConnectTimeout=2 root@$ROBOT_HOST "echo OK" &>/dev/null; then
echo " ✓"
break
fi
echo -n "."
sleep 2
WAIT_COUNT=$((WAIT_COUNT + 1))
done
if [ $WAIT_COUNT -ge $MAX_WAIT ]; then
echo " TIMEOUT"
echo ""
echo "Robot did not come back online within ${MAX_WAIT} seconds"
echo "Please check robot status manually"
exit 1
fi
# Give MITM proxy time to start
echo "Waiting for MITM proxy to initialize..."
sleep 3
# Check MITM status
echo ""
echo "Checking MITM status..."
MITM_RUNNING=$(sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST \
"ps | grep serial_mitm | grep -v grep" || echo "")
if [ -n "$MITM_RUNNING" ]; then
echo "✓ MITM proxy is running"
RUN_NUM=$(sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST \
"cat /mnt/UDISK/mitm_run_counter 2>/dev/null" || echo "?")
echo " Current run: $RUN_NUM"
else
echo "⚠ MITM proxy is NOT running"
echo " Check if MITM mode is enabled:"
echo " ssh root@$ROBOT_HOST 'ls -l /mnt/UDISK/mitm_enabled'"
fi
echo ""
echo "========================================="
echo " Robot Ready"
echo "========================================="
echo ""
echo "Next steps:"
echo " 1. Press physical button on robot to start cleaning"
echo " 2. Wait for robot to return to dock"
echo " 3. Stop logging: ./tools/mitm_finish_run.sh"
echo ""
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
#
# MITM Retrieve Logs Script
# Downloads all MITM capture logs from robot to local machine
#
# Usage: ./tools/mitm_retrieve_logs.sh [output_dir]
set -e
ROBOT_HOST="vacuum"
ROBOT_PASS="${ROBOT_PASSWORD:-}"
REMOTE_LOG_DIR="/mnt/UDISK/log"
DEFAULT_OUTPUT_DIR="./logs/mitm_captures_$(date +%Y%m%d_%H%M%S)"
OUTPUT_DIR="${1:-$DEFAULT_OUTPUT_DIR}"
echo "========================================="
echo " MITM Retrieve Logs"
echo "========================================="
echo ""
# Test robot connectivity
echo "Connecting to robot..."
if ! sshpass -p "$ROBOT_PASS" ssh -o ConnectTimeout=5 root@$ROBOT_HOST "echo OK" &>/dev/null; then
echo "ERROR: Cannot connect to robot at $ROBOT_HOST"
exit 1
fi
echo "✓ Connected"
echo ""
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Get list of log files
echo "Scanning for log files..."
LOG_FILES=$(sshpass -p "$ROBOT_PASS" ssh root@$ROBOT_HOST \
"ls -1 $REMOTE_LOG_DIR/mitm_capture_*.log 2>/dev/null" || echo "")
if [ -z "$LOG_FILES" ]; then
echo "No MITM capture logs found on robot."
echo ""
echo "Checked: $REMOTE_LOG_DIR/mitm_capture_*.log"
exit 0
fi
LOG_COUNT=$(echo "$LOG_FILES" | wc -l | tr -d ' ')
echo "Found $LOG_COUNT log file(s)"
echo ""
# Download each log file
echo "Downloading logs to: $OUTPUT_DIR"
echo ""
for REMOTE_FILE in $LOG_FILES; do
FILENAME=$(basename "$REMOTE_FILE")
LOCAL_FILE="$OUTPUT_DIR/$FILENAME"
echo -n " $FILENAME ... "
sshpass -p "$ROBOT_PASS" scp -q root@$ROBOT_HOST:$REMOTE_FILE "$LOCAL_FILE"
FILE_SIZE=$(du -h "$LOCAL_FILE" | awk '{print $1}')
echo "✓ ($FILE_SIZE)"
done
echo ""
# Get summary
TOTAL_SIZE=$(du -sh "$OUTPUT_DIR" | awk '{print $1}')
echo "========================================="
echo " Download Complete"
echo "========================================="
echo ""
echo "Summary:"
echo " Files downloaded: $LOG_COUNT"
echo " Total size: $TOTAL_SIZE"
echo " Location: $OUTPUT_DIR"
echo ""
echo "To analyze logs:"
echo " ls -lh $OUTPUT_DIR/"
echo " cat $OUTPUT_DIR/mitm_capture_run1_*.log | grep 'CMD='"
echo ""
+3 -3
View File
@@ -79,9 +79,9 @@ ssh root@vacuum "killall -9 AuxCtrl"
### Step 3: Deploy to Robot
```bash
# One-line deploy (replace <password> with your robot's root password)
# One-line deploy (replace $ROBOT_PASSWORD with your robot's root password)
cat target/armv7-unknown-linux-musleabihf/release/examples/test_lidar_scenario | \
sshpass -p "<password>" ssh root@vacuum \
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum \
"cat > /tmp/test_lidar && chmod +x /tmp/test_lidar"
```
@@ -145,7 +145,7 @@ vim src/devices/gd32/mod.rs
# Build, deploy, and run in one command
cargo build --example test_lidar_scenario --release --features="std,gd32,lidar" && \
cat target/armv7-unknown-linux-musleabihf/release/examples/test_lidar_scenario | \
sshpass -p "<password>" ssh root@vacuum \
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum \
"killall -9 AuxCtrl 2>/dev/null; cat > /tmp/test && chmod +x /tmp/test && /tmp/test"
```
+1 -1
View File
@@ -29,7 +29,7 @@ Binary location: `target/armv7-unknown-linux-musleabihf/release/examples/test_li
```bash
# Deploy and run
cat target/armv7-unknown-linux-musleabihf/release/examples/test_lidar_scenario | \
sshpass -p "<password>" ssh root@vacuum \
sshpass -p "$ROBOT_PASSWORD" ssh root@vacuum \
"killall -9 AuxCtrl 2>/dev/null; cat > /tmp/test && chmod +x /tmp/test && /tmp/test"
```
-106
View File
@@ -1,106 +0,0 @@
//! Serial port raw byte dumper
//!
//! Opens the GD32 serial port and dumps all received bytes in hex format
//! for protocol analysis and debugging.
use sangam_io::transport::SerialTransport;
use sangam_io::transport::Transport;
use std::time::{Duration, Instant};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info)
.init();
log::info!("=== GD32 Serial Port Dumper ===");
log::info!("");
// Open serial port
log::info!("Opening serial port /dev/ttyS3 at 115200 baud...");
let mut transport = SerialTransport::open("/dev/ttyS3", 115200)?;
log::info!("Serial port opened successfully");
log::info!("");
// Flush any old data
log::info!("Flushing old data...");
let mut discard = vec![0u8; 1024];
let mut total_flushed = 0;
while transport.available()? > 0 {
let read = transport.read(&mut discard)?;
total_flushed += read;
if read == 0 {
break;
}
}
log::info!("Flushed {} bytes", total_flushed);
log::info!("");
// Wait a bit for buffer to stabilize
std::thread::sleep(Duration::from_millis(200));
log::info!("Starting capture for 10 seconds...");
log::info!("Looking for sync bytes: FA FB");
log::info!("");
let start = Instant::now();
let duration = Duration::from_secs(10);
let mut buffer = vec![0u8; 256];
let mut total_bytes = 0;
let mut packet_count = 0;
while start.elapsed() < duration {
let available = transport.available()?;
if available > 0 {
let bytes_read = transport.read(&mut buffer)?;
if bytes_read > 0 {
total_bytes += bytes_read;
// Format as hex string
let hex_line: String = buffer[..bytes_read]
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<_>>()
.join(" ");
println!("[{:06}] {} bytes: {}", total_bytes, bytes_read, hex_line);
// Count sync bytes (FA FB)
for i in 0..bytes_read.saturating_sub(1) {
if buffer[i] == 0xFA && buffer[i + 1] == 0xFB {
packet_count += 1;
println!(" --> Sync found at offset {}", i);
// If we have enough bytes, show the packet header
if i + 6 <= bytes_read {
let length = buffer[i + 2];
let cmd_id = buffer[i + 3];
println!(
" LEN=0x{:02X} ({}) CMD=0x{:02X}",
length, length, cmd_id
);
}
}
}
}
} else {
std::thread::sleep(Duration::from_millis(10));
}
}
log::info!("");
log::info!("=== Capture Complete ===");
log::info!("Total bytes received: {}", total_bytes);
log::info!("Sync bytes (FA FB) found: {}", packet_count);
log::info!(
"Average bytes per packet: {}",
if packet_count > 0 {
total_bytes / packet_count
} else {
0
}
);
Ok(())
}
+42 -13
View File
@@ -26,26 +26,49 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut gd32 = Gd32Driver::new(gd32_transport)?;
log::info!("✓ GD32 initialized successfully");
log::info!(" - Heartbeat running at 20ms intervals");
log::info!(" - Initialization sequence completed");
log::info!("");
// Wait a bit for stabilization
thread::sleep(Duration::from_millis(500));
// CRITICAL: Wait 1.4s after init before powering lidar (matches AuxCtrl timing)
log::info!("Waiting 1.4 seconds for motor controller stabilization...");
thread::sleep(Duration::from_millis(1400));
// Step 2: Initialize Delta-2D Lidar
log::info!("Step 2: Initializing Delta-2D Lidar...");
// Step 2: Power on Lidar motor via GD32
log::info!("Step 2: Powering on Lidar motor...");
// CRITICAL: Complete AuxCtrl sequence discovered via MITM capture:
// 1. CMD=0x65 mode=0x02 - Switch to navigation mode (enables lidar control)
// 2. CMD=0xA2 (LidarPrep) - preparation command
// 3. CMD=0x97 (LidarPower) - enable GPIO 233
// 4. CMD=0x71 (LidarPWM) - set motor speed
// Switch to navigation mode (CRITICAL - without this, lidar commands don't work!)
gd32.set_motor_mode(0x02)?;
log::info!("✓ GD32 switched to navigation mode (CMD=0x65 mode=0x02)");
// Send preparation command (CMD=0xA2)
gd32.send_lidar_prep()?;
log::info!("✓ Lidar prep command sent (CMD=0xA2)");
// Enable power (CMD=0x97)
gd32.set_lidar_power(true)?;
log::info!("✓ Lidar power enabled (CMD=0x97)");
// Set PWM speed (CMD=0x71)
gd32.set_lidar_pwm(100)?; // Full speed (100%)
log::info!("✓ Lidar PWM set to 100% (CMD=0x71)");
log::info!(" - Waiting 2 seconds for motor spin-up...");
thread::sleep(Duration::from_secs(2));
log::info!("");
// Step 3: Initialize Delta-2D Lidar
log::info!("Step 3: Initializing Delta-2D Lidar...");
let lidar_transport = SerialTransport::open("/dev/ttyS1", 115200)?;
let mut lidar = Delta2DDriver::new(lidar_transport)?;
log::info!("✓ Lidar driver initialized");
log::info!("");
// Step 3: Power on Lidar motor via GD32
log::info!("Step 3: Powering on Lidar motor...");
gd32.set_lidar_power(true)?;
log::info!("✓ Lidar motor powered on");
log::info!(" - Waiting 2 seconds for motor spin-up...");
thread::sleep(Duration::from_secs(2));
log::info!("");
// Step 4: Start Lidar scanning
log::info!("Step 4: Starting Lidar scan...");
lidar.start()?;
@@ -114,8 +137,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Step 7: Power off Lidar motor
log::info!("Step 7: Powering off Lidar motor...");
// First stop PWM (CMD=0x71)
gd32.set_lidar_pwm(0)?;
log::info!("✓ Lidar PWM set to 0% (CMD=0x71)");
// Then disable power (CMD=0x97)
gd32.set_lidar_power(false)?;
log::info!("✓ Lidar motor powered off");
log::info!("✓ Lidar power disabled (CMD=0x97)");
log::info!("");
// Step 8: Clean exit (Drop implementations will handle cleanup)
+72 -3
View File
@@ -50,18 +50,85 @@ impl Delta2DDriver {
// Check if enough data available for header
let available = transport.available()?;
if available < 8 {
// Log availability periodically (every ~100 calls = ~1 second)
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
static CALL_COUNT: AtomicU32 = AtomicU32::new(0);
static MAX_BYTES_SEEN: AtomicUsize = AtomicUsize::new(0);
let count = CALL_COUNT.fetch_add(1, Ordering::Relaxed);
let max_seen = MAX_BYTES_SEEN.load(Ordering::Relaxed);
if available > max_seen {
MAX_BYTES_SEEN.store(available, Ordering::Relaxed);
log::info!("Delta2D: NEW MAX bytes available: {}", available);
}
if count % 200 == 0 {
log::info!("Delta2D: Bytes available on ttyS1: {} (max seen: {})", available, max_seen);
// Try to read and log small amounts to see what's being received
if available > 0 && available < 8 {
let mut peek_buf = vec![0u8; available.min(16)];
if let Ok(read) = transport.read(&mut peek_buf) {
log::warn!("Delta2D: Insufficient data - Read {} bytes (for inspection): {:02X?}", read, &peek_buf[..read]);
}
}
}
// Python uses "if ser.in_waiting > 8" - need MORE than 8 bytes
// This ensures we have header + some payload data ready
if available <= 8 {
return Ok(None);
}
// Read header
let mut header_buf = [0u8; 8];
let read = transport.read(&mut header_buf)?;
log::debug!("Delta2D: RX header {} bytes: {:02X?}", read, header_buf);
if read < 8 {
log::warn!("Delta2D: Header read incomplete: got {} bytes, expected 8", read);
return Ok(None);
}
let header = protocol::PacketHeader::parse(&header_buf)?;
let header = match protocol::PacketHeader::parse(&header_buf) {
Ok(h) => h,
Err(e) => {
log::warn!("Delta2D: Failed to parse header: {} (bytes: {:02X?})", e, header_buf);
return Err(e);
}
};
log::debug!("Delta2D: Parsed header - CMD=0x{:02X}, payload_len={}",
header.command_type as u8, header.payload_length);
// CRITICAL FIX: Wait for payload + CRC to arrive
// Python does: while ser.in_waiting < payload_length + 2: pass
let required_bytes = header.payload_length as usize + 2; // +2 for CRC
let wait_start = std::time::Instant::now();
let wait_timeout = std::time::Duration::from_millis(100);
loop {
let available = transport.available()?;
if available >= required_bytes {
log::debug!("Delta2D: Payload ready - {} bytes available, {} required",
available, required_bytes);
break;
}
if wait_start.elapsed() > wait_timeout {
log::warn!("Delta2D: Timeout waiting for payload - got {} bytes, needed {}",
available, required_bytes);
return Ok(None);
}
// Release lock briefly to let data arrive
drop(transport);
std::thread::sleep(std::time::Duration::from_millis(1));
transport = self.transport.lock();
}
// Release transport lock before calling read_exact
drop(transport);
@@ -77,12 +144,14 @@ impl Delta2DDriver {
match header.command_type {
CommandType::Measurement => {
log::debug!("Delta2D: Parsing measurement packet (payload: {} bytes)", payload.len());
let scan = protocol::parse_measurement(&payload)?;
log::info!("Delta2D: Received scan with {} points", scan.points.len());
Ok(Some(scan))
}
CommandType::Health => {
// Health packets don't contain scan data
log::debug!("Delta2D: Received health packet");
log::info!("Delta2D: Received health packet (payload: {} bytes)", payload.len());
Ok(None)
}
}
+45 -6
View File
@@ -19,9 +19,11 @@ const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(20);
///
/// The thread will:
/// 1. Send CMD=0x66 heartbeat every 20ms with motor speed commands
/// 2. Read and process CMD=0x15 status packets
/// 3. Update shared state with encoder and battery data
/// 4. Run until shutdown flag is set
/// 2. Send CMD=0x71 telemetry query every ~27ms (matches AuxCtrl frequency)
/// 3. Send CMD=0x06 wake command every ~250ms (matches AuxCtrl frequency)
/// 4. Read and process CMD=0x15 status packets
/// 5. Update shared state with encoder and battery data
/// 6. Run until shutdown flag is set
pub fn spawn_heartbeat_thread(
transport: Arc<Mutex<Box<dyn Transport>>>,
state: Arc<Mutex<Gd32State>>,
@@ -66,6 +68,12 @@ pub fn spawn_heartbeat_thread(
// Send packet
{
let mut transport = transport.lock();
// Log TX on first heartbeat and every 50th
if heartbeat_count == 1 || heartbeat_count % 50 == 0 {
log::debug!("GD32: TX {} bytes: {:02X?}", packet.len(), &packet[..packet.len().min(32)]);
}
if let Err(e) = transport.write(&packet) {
log::error!("GD32: Heartbeat send error: {}", e);
continue;
@@ -76,6 +84,13 @@ pub fn spawn_heartbeat_thread(
}
}
// Send CMD=0x06 wake command periodically (every ~250ms)
// AuxCtrl sends this 1,421x in 6min = ~4/sec = every ~250ms
// At 20ms intervals, every 12th heartbeat = 240ms
if heartbeat_count % 12 == 0 {
send_command_unlocked(&transport, &Gd32Command::Wake);
}
if heartbeat_count % 50 == 0 {
// Log every second
log::debug!(
@@ -96,6 +111,14 @@ pub fn spawn_heartbeat_thread(
})
}
/// Send a command without logging (helper for heartbeat loop)
fn send_command_unlocked(transport: &Arc<Mutex<Box<dyn Transport>>>, cmd: &Gd32Command) {
let packet = cmd.encode();
let mut transport = transport.lock();
let _ = transport.write(&packet);
let _ = transport.flush();
}
/// Read and process any available response packets
fn read_and_process_responses(
transport: &Arc<Mutex<Box<dyn Transport>>>,
@@ -119,9 +142,22 @@ fn read_and_process_responses(
return Ok(());
}
// Log raw bytes received
log::debug!(
"GD32: RX {} bytes: {:02X?}",
bytes_read,
&buffer[..bytes_read.min(32)]
);
// Try to decode response
match Gd32Response::decode(&buffer[..bytes_read]) {
Ok(response) => {
log::debug!(
"GD32: RX decoded - CMD=0x{:02X}, payload_len={}",
response.cmd_id,
response.payload.len()
);
if response.is_status_packet() {
// Update shared state
let mut state = state.lock();
@@ -133,16 +169,19 @@ fn read_and_process_responses(
state.error_code = response.error_code;
log::debug!(
"GD32: Status update - Battery: {:.2}V ({}%), Encoders: L={}, R={}",
"GD32: Status update - Battery: {:.2}V ({}%), Encoders: L={}, R={}, Error={}",
response.battery_voltage,
response.battery_level,
response.encoder_left,
response.encoder_right
response.encoder_right,
response.error_code
);
} else {
log::debug!("GD32: Non-status response: CMD=0x{:02X}", response.cmd_id);
}
}
Err(e) => {
log::debug!("GD32: Failed to decode packet: {}", e);
log::warn!("GD32: Failed to decode packet: {} (bytes: {:02X?})", e, &buffer[..bytes_read.min(16)]);
}
}
+128 -147
View File
@@ -34,38 +34,83 @@ pub struct Gd32Driver {
impl Gd32Driver {
/// Create new GD32 driver and initialize device
///
/// This will:
/// 1. Send CMD=0x08 initialization loop (up to 5 seconds)
/// 2. Send wake command (CMD=0x06)
/// 3. Set control mode (CMD=0x8D)
/// 4. Start heartbeat thread (CMD=0x66 every 20ms)
/// This matches AuxCtrl's exact initialization sequence from MITM capture:
/// 1. CMD=0x0C (Init sync), 0x8D (LED), 0x07 (Version), 0x65 (Motor type), 0x71 (Telemetry)
/// 2. CMD=0x8D burst (4x at T+245ms)
/// 3. CMD=0x06 (Wake at T+253ms)
/// 4. Brush init: 0x69, 0x6B (at T+280ms)
/// 5. CMD=0x66 heartbeat starts (at T+289ms)
pub fn new<T: Transport + 'static>(transport: T) -> Result<Self> {
let transport = Arc::new(Mutex::new(Box::new(transport) as Box<dyn Transport>));
let state = Arc::new(Mutex::new(Gd32State::default()));
let shutdown = Arc::new(Mutex::new(false));
// Phase 1: Initialization sequence
log::info!("GD32: Starting initialization sequence");
Self::initialize_device(&transport)?;
// Phase 1: Initial command burst (T+0ms) - matches AuxCtrl
log::info!("GD32: Sending initial command burst");
// Phase 2: Wake and setup
log::info!("GD32: Sending wake command");
Self::send_command(&transport, &Gd32Command::Wake)?;
thread::sleep(Duration::from_millis(50));
// CMD=0x0C - Init sync (first command AuxCtrl sends)
log::debug!(" -> CMD=0x0C (InitSync)");
Self::send_command(&transport, &Gd32Command::InitSync)?;
thread::sleep(Duration::from_millis(8));
log::info!("GD32: Setting control mode");
// CMD=0x8D - Button LED (early)
log::debug!(" -> CMD=0x8D (ButtonLED)");
Self::send_command(&transport, &Gd32Command::ButtonLedState(0x01))?;
thread::sleep(Duration::from_millis(50));
thread::sleep(Duration::from_millis(8));
// Phase 3: Start heartbeat thread
log::info!("GD32: Starting heartbeat thread");
// CMD=0x07 - System setup/version query
log::debug!(" -> CMD=0x07 (SystemSetup)");
Self::send_command(&transport, &Gd32Command::SystemSetup)?;
thread::sleep(Duration::from_millis(8));
// CMD=0x65 - Motor control type (mode=0)
log::debug!(" -> CMD=0x65 (MotorType=0)");
Self::send_command(&transport, &Gd32Command::MotorType(0))?;
thread::sleep(Duration::from_millis(8));
// CMD=0x71 - Lidar PWM (set to 0 initially, will be set properly when powering lidar)
log::debug!(" -> CMD=0x71 (LidarPWM=0)");
Self::send_command(&transport, &Gd32Command::LidarPWM(0))?;
// Phase 2: Wait ~245ms then send LED burst
log::info!("GD32: Waiting 245ms before LED burst");
thread::sleep(Duration::from_millis(200)); // Conservative timing
log::info!("GD32: Sending button LED burst (4x)");
for i in 0..4 {
Self::send_command(&transport, &Gd32Command::ButtonLedState(0x01))?;
if i < 3 {
thread::sleep(Duration::from_millis(8));
}
}
// Phase 3: Wake command (T+253ms)
thread::sleep(Duration::from_millis(8));
log::info!("GD32: Sending wake command (CMD=0x06)");
Self::send_command(&transport, &Gd32Command::Wake)?;
// Phase 4: Brush initialization (T+280ms)
thread::sleep(Duration::from_millis(27));
log::info!("GD32: Initializing brushes");
Self::send_command(&transport, &Gd32Command::SideBrushSpeed(0))?;
thread::sleep(Duration::from_millis(1));
Self::send_command(&transport, &Gd32Command::BrushControl(0x64))?; // Value from MITM log
// Phase 5: Start heartbeat thread (T+289ms)
thread::sleep(Duration::from_millis(9));
log::info!("GD32: Starting heartbeat thread (CMD=0x66)");
let heartbeat_thread = Some(heartbeat::spawn_heartbeat_thread(
Arc::clone(&transport),
Arc::clone(&state),
Arc::clone(&shutdown),
));
log::info!("GD32: Initialization complete");
// Wait for GD32 to respond with STATUS_DATA
log::info!("GD32: Waiting for STATUS_DATA (CMD=0x15) from GD32...");
Self::wait_for_status_packet(&transport, &state)?;
log::info!("GD32: Received STATUS_DATA - GD32 is ready");
log::info!("GD32: Initialization complete - ready for operations");
Ok(Gd32Driver {
transport,
@@ -75,160 +120,96 @@ impl Gd32Driver {
})
}
/// Initialize device with CMD=0x08 loop
/// Wait for GD32 to send CMD=0x15 STATUS_DATA packet
///
/// Sends initialization command every 200ms for up to 5 seconds
/// until GD32 responds with CMD=0x15 status packet.
fn initialize_device(transport: &Arc<Mutex<Box<dyn Transport>>>) -> Result<()> {
// STEP 1: Flush any leftover data from previous session
log::debug!("GD32: Flushing serial buffer...");
{
let mut transport = transport.lock();
let mut discard = vec![0u8; 1024];
let mut total_flushed = 0;
// Read until buffer is empty
while transport.available()? > 0 {
let read = transport.read(&mut discard)?;
total_flushed += read;
if read == 0 {
break;
}
}
if total_flushed > 0 {
log::debug!("GD32: Flushed {} bytes of old data", total_flushed);
}
}
// Small delay to ensure buffer is clear
thread::sleep(Duration::from_millis(100));
// STEP 2: Send initialization commands
let init_cmd = Gd32Command::default_initialize();
let init_packet = init_cmd.encode();
/// The heartbeat thread receives and processes status packets.
/// We check the shared state to see if battery voltage has been populated,
/// which indicates that at least one status packet has been received.
fn wait_for_status_packet(
_transport: &Arc<Mutex<Box<dyn Transport>>>,
state: &Arc<Mutex<Gd32State>>,
) -> Result<()> {
let start = Instant::now();
let timeout = Duration::from_secs(5);
let retry_interval = Duration::from_millis(200);
// Packet buffer to accumulate bytes across reads
let mut packet_buffer: Vec<u8> = Vec::new();
let timeout = Duration::from_secs(2);
while start.elapsed() < timeout {
// Send initialization command
{
let mut transport = transport.lock();
transport.write(&init_packet)?;
transport.flush()?;
// Check if we've received status data from heartbeat thread
let current_state = state.lock();
if current_state.battery_voltage > 0.0 {
// Battery voltage populated means we got CMD=0x15
drop(current_state);
return Ok(());
}
drop(current_state);
log::debug!("GD32: Sent initialization command");
// Try to read response with packet accumulation
let read_start = Instant::now();
while read_start.elapsed() < retry_interval {
if let Some(response) =
Self::try_read_response_buffered(transport, &mut packet_buffer)?
&& response.is_status_packet()
{
log::info!("GD32: Device initialized successfully");
return Ok(());
}
thread::sleep(Duration::from_millis(10));
}
thread::sleep(Duration::from_millis(20)); // Check every heartbeat interval
}
Err(Error::InitializationFailed(
"GD32 initialization timeout (no response after 5 seconds)".into(),
"GD32 did not send STATUS_DATA (CMD=0x15) after 2 seconds".into(),
))
}
/// Send a command to the GD32
fn send_command(transport: &Arc<Mutex<Box<dyn Transport>>>, cmd: &Gd32Command) -> Result<()> {
let packet = cmd.encode();
log::debug!("GD32: TX CMD=0x{:02X}, {} bytes: {:02X?}", cmd.cmd_id(), packet.len(), &packet);
let mut transport = transport.lock();
transport.write(&packet)?;
transport.flush()?;
Ok(())
}
/// Try to read a response packet with buffering (non-blocking)
fn try_read_response_buffered(
transport: &Arc<Mutex<Box<dyn Transport>>>,
packet_buffer: &mut Vec<u8>,
) -> Result<Option<Gd32Response>> {
let mut transport = transport.lock();
// Check if data is available
let available = transport.available()?;
if available > 0 {
// Read new data and append to buffer
let mut temp_buffer = vec![0u8; available.min(256)];
let bytes_read = transport.read(&mut temp_buffer)?;
if bytes_read > 0 {
packet_buffer.extend_from_slice(&temp_buffer[..bytes_read]);
log::debug!(
"GD32: Read {} bytes, buffer now {} bytes",
bytes_read,
packet_buffer.len()
);
}
}
// Release lock before processing
drop(transport);
// Try to decode a packet from accumulated buffer
if packet_buffer.len() >= 6 {
match Gd32Response::decode_with_sync(packet_buffer) {
Ok((bytes_consumed, response)) => {
// Remove consumed bytes from buffer
packet_buffer.drain(..bytes_consumed);
log::debug!(
"GD32: Decoded packet (CMD=0x{:02X}), {} bytes remaining in buffer",
response.cmd_id,
packet_buffer.len()
);
return Ok(Some(response));
}
Err(e) => {
// If buffer is getting too large without valid packets, discard some bytes to make progress
log::debug!(
"GD32: Decode error: {}, buffer size: {}",
e,
packet_buffer.len()
);
if packet_buffer.len() > 512 {
// Search for sync bytes manually, discard everything before first sync
let mut found_sync = false;
for i in 0..packet_buffer.len().saturating_sub(1) {
if packet_buffer[i] == 0xFA && packet_buffer[i + 1] == 0xFB {
// Found sync, keep from here
packet_buffer.drain(..i);
found_sync = true;
log::debug!("GD32: Discarded {} bytes of garbage, found sync", i);
break;
}
}
if !found_sync {
// No sync found, keep only last byte (might be first half of sync)
let to_discard = packet_buffer.len() - 1;
packet_buffer.drain(..to_discard);
log::debug!("GD32: No sync found, discarded {} bytes", to_discard);
}
}
}
}
}
Ok(None)
/// Set motor control mode via CMD=0x65
///
/// Mode values:
/// - 0x00: Initialization/idle mode
/// - 0x02: Navigation/cleaning mode (required for lidar control)
pub fn set_motor_mode(&mut self, mode: u8) -> Result<()> {
log::info!("GD32: Setting motor mode: 0x{:02X}", mode);
Self::send_command(&self.transport, &Gd32Command::MotorType(mode))?;
log::debug!("GD32: Sent CMD=0x65 (MotorType=0x{:02X})", mode);
Ok(())
}
/// Set lidar motor power
pub fn set_lidar_power(&mut self, on: bool) -> Result<()> {
log::info!("GD32: Setting lidar power: {}", on);
Self::send_command(&self.transport, &Gd32Command::LidarPower(on))
/// Send lidar preparation command via CMD=0xA2
///
/// Must be sent before enabling lidar power (CMD=0x97).
/// Discovered via MITM capture of AuxCtrl cleaning session.
pub fn send_lidar_prep(&mut self) -> Result<()> {
log::info!("GD32: Sending lidar preparation command");
Self::send_command(&self.transport, &Gd32Command::LidarPrep)?;
log::debug!("GD32: Sent CMD=0xA2 (LidarPrep)");
Ok(())
}
/// Set lidar motor power via CMD=0x97
///
/// Controls GPIO 233 through GD32 firmware using CMD=0x97.
/// Discovered via AuxCtrl decompilation at address 0x00060988.
/// NOTE: Must send CMD=0xA2 (LidarPrep) before this command!
pub fn set_lidar_power(&mut self, enable: bool) -> Result<()> {
log::info!("GD32: Setting lidar power: {}", enable);
Self::send_command(&self.transport, &Gd32Command::LidarPower(enable))?;
log::debug!("GD32: Sent CMD=0x97 (LidarPower={})", enable);
Ok(())
}
/// Set lidar motor PWM speed via CMD=0x71
///
/// Controls lidar motor speed (0-100%).
/// Discovered via AuxCtrl decompilation at address 0x0003f604.
///
/// # Arguments
/// * `pwm_percent` - PWM duty cycle (0-100%). Values outside range will be clamped.
pub fn set_lidar_pwm(&mut self, pwm_percent: i32) -> Result<()> {
let clamped = pwm_percent.clamp(0, 100);
log::info!("GD32: Setting lidar PWM: {}%", clamped);
Self::send_command(&self.transport, &Gd32Command::LidarPWM(pwm_percent))?;
log::debug!("GD32: Sent CMD=0x71 (LidarPWM={})", clamped);
Ok(())
}
/// Set vacuum blower speed (0-100%)
+84 -4
View File
@@ -59,18 +59,36 @@ fn calculate_checksum(cmd_id: u8, payload: &[u8]) -> Option<[u8; 2]> {
pub enum CommandId {
/// Initialize/wake-up sequence
Initialize = 0x08,
/// Initialization sync (AuxCtrl uses this as first command)
InitSync = 0x0C,
/// Wake/enable motors
Wake = 0x06,
/// System setup/configuration
SystemSetup = 0x07,
/// Calibration/sensor configuration
CalibrationConfig = 0x0D,
/// Motor control type configuration
MotorType = 0x65,
/// Heartbeat/keep-alive
Heartbeat = 0x66,
/// Motor speed control
MotorSpeed = 0x67,
/// Blower speed control
BlowerSpeed = 0x68,
/// Side brush speed
SideBrushSpeed = 0x69,
/// Rolling brush speed
RollingBrushSpeed = 0x6A,
/// Brush control
BrushControl = 0x6B,
/// Lidar PWM speed control (0-100%)
LidarPWM = 0x71,
/// Button LED state
ButtonLedState = 0x8D,
/// Lidar power control
LidarPower = 0x97,
/// Lidar preparation command (sent before power on)
LidarPrep = 0xA2,
/// Status data response
StatusData = 0x15,
}
@@ -83,8 +101,16 @@ pub enum Gd32Command {
/// Initialization payload (96 bytes)
payload: [u8; 96],
},
/// Initialization sync (first command sent by AuxCtrl)
InitSync,
/// Wake command
Wake,
/// System setup/configuration
SystemSetup,
/// Calibration/sensor configuration
CalibrationConfig,
/// Motor control type configuration
MotorType(u8),
/// Heartbeat with 8-byte data
Heartbeat {
/// Heartbeat data (8 bytes)
@@ -99,10 +125,20 @@ pub enum Gd32Command {
},
/// Blower speed
BlowerSpeed(u16),
/// Side brush speed
SideBrushSpeed(u8),
/// Rolling brush speed
RollingBrushSpeed(u8),
/// Brush control
BrushControl(u8),
/// Lidar PWM speed control (0-100%)
LidarPWM(i32),
/// Button LED state
ButtonLedState(u8),
/// Lidar power control
LidarPower(bool),
/// Lidar preparation command (sent before power on)
LidarPrep,
}
impl Gd32Command {
@@ -110,12 +146,21 @@ impl Gd32Command {
pub fn cmd_id(&self) -> u8 {
match self {
Gd32Command::Initialize { .. } => CommandId::Initialize as u8,
Gd32Command::InitSync => CommandId::InitSync as u8,
Gd32Command::Wake => CommandId::Wake as u8,
Gd32Command::SystemSetup => CommandId::SystemSetup as u8,
Gd32Command::CalibrationConfig => CommandId::CalibrationConfig as u8,
Gd32Command::MotorType(_) => CommandId::MotorType as u8,
Gd32Command::Heartbeat { .. } => CommandId::Heartbeat as u8,
Gd32Command::MotorSpeed { .. } => CommandId::MotorSpeed as u8,
Gd32Command::BlowerSpeed(_) => CommandId::BlowerSpeed as u8,
Gd32Command::SideBrushSpeed(_) => CommandId::SideBrushSpeed as u8,
Gd32Command::RollingBrushSpeed(_) => CommandId::RollingBrushSpeed as u8,
Gd32Command::BrushControl(_) => CommandId::BrushControl as u8,
Gd32Command::LidarPWM(_) => CommandId::LidarPWM as u8,
Gd32Command::ButtonLedState(_) => CommandId::ButtonLedState as u8,
Gd32Command::LidarPower(_) => CommandId::LidarPower as u8,
Gd32Command::LidarPrep => CommandId::LidarPrep as u8,
}
}
@@ -123,7 +168,11 @@ impl Gd32Command {
fn build_payload(&self) -> Vec<u8> {
match self {
Gd32Command::Initialize { payload } => payload.to_vec(),
Gd32Command::InitSync => vec![0x01],
Gd32Command::Wake => vec![0x00],
Gd32Command::SystemSetup => vec![0x00],
Gd32Command::CalibrationConfig => vec![0x00],
Gd32Command::MotorType(mode) => vec![*mode],
Gd32Command::Heartbeat { data } => data.to_vec(),
Gd32Command::MotorSpeed { left, right } => {
let mut payload = Vec::with_capacity(8);
@@ -132,8 +181,17 @@ impl Gd32Command {
payload
}
Gd32Command::BlowerSpeed(speed) => speed.to_le_bytes().to_vec(),
Gd32Command::SideBrushSpeed(speed) => vec![*speed],
Gd32Command::RollingBrushSpeed(speed) => vec![*speed],
Gd32Command::BrushControl(value) => vec![*value],
Gd32Command::LidarPWM(pwm_percent) => {
// Clamp to [0, 100] range and convert to 4-byte little-endian
let pwm = (*pwm_percent).clamp(0, 100);
(pwm as u32).to_le_bytes().to_vec()
}
Gd32Command::ButtonLedState(state) => vec![*state],
Gd32Command::LidarPower(on) => vec![if *on { 0x01 } else { 0x00 }],
Gd32Command::LidarPrep => vec![0x10, 0x0E, 0x00, 0x00],
}
}
@@ -454,13 +512,35 @@ mod tests {
#[test]
fn test_lidar_power_encoding() {
// Test power ON
let cmd = Gd32Command::LidarPower(true);
let packet = cmd.encode();
assert_eq!(packet[0], 0xFA);
assert_eq!(packet[1], 0xFB);
assert_eq!(packet[3], 0x97); // Lidar power CMD
assert_eq!(packet[4], 0x01); // ON
// Packet format: [0xFA 0xFB] [LEN] [CMD] [PAYLOAD] [CRC_HIGH] [CRC_LOW]
// For CMD=0x97, PAYLOAD=[0x01]:
// data = [0x97, 0x01] (even bytes)
// word = 0x9701
// checksum = 0x9701 → [0x97, 0x01]
assert_eq!(packet.len(), 7); // SYNC(2) + LEN(1) + CMD(1) + PAYLOAD(1) + CRC(2)
assert_eq!(packet[0], 0xFA); // SYNC1
assert_eq!(packet[1], 0xFB); // SYNC2
assert_eq!(packet[2], 0x04); // LEN = CMD(1) + PAYLOAD(1) + CRC(2)
assert_eq!(packet[3], 0x97); // CMD (Lidar power)
assert_eq!(packet[4], 0x01); // PAYLOAD (ON)
assert_eq!(packet[5], 0x97); // CRC high byte
assert_eq!(packet[6], 0x01); // CRC low byte
// Test power OFF
let cmd = Gd32Command::LidarPower(false);
let packet = cmd.encode();
// For CMD=0x97, PAYLOAD=[0x00]:
// data = [0x97, 0x00]
// word = 0x9700
// checksum = 0x9700 → [0x97, 0x00]
assert_eq!(packet[4], 0x00); // PAYLOAD (OFF)
assert_eq!(packet[5], 0x97); // CRC high byte
assert_eq!(packet[6], 0x00); // CRC low byte
}
#[test]