feat(s5max): add build config and MCU handoff support

This commit is contained in:
Fabien Carrion
2026-09-12 21:44:48 -06:00
parent 16065e680a
commit 0c5d173bb8
4 changed files with 117 additions and 1 deletions
+5
View File
@@ -6,3 +6,8 @@
[target.armv7-unknown-linux-musleabihf]
linker = "armv7-unknown-linux-musleabihf-gcc"
rustflags = ["-C", "target-feature=+neon", "-C", "target-cpu=cortex-a7"]
# Roborock S5 Max stock firmware uses glibc/gnueabihf, not musl.
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
rustflags = ["-C", "target-feature=+neon", "-C", "target-cpu=cortex-a7"]
+17 -1
View File
@@ -1,6 +1,11 @@
# SangamIO
Hardware abstraction daemon for the CRL-200S robotic vacuum platform, providing real-time sensor streaming via UDP and command control via TCP.
Hardware abstraction daemon for robotic vacuum platforms, providing real-time sensor streaming via UDP and command control via TCP.
Supported hardware:
- CRL-200S
- Roborock S5 Max (rooted firmware `4.1.2_1668`)
## Overview
@@ -73,6 +78,13 @@ arm-linux-gnueabihf-strip \
target/armv7-unknown-linux-musleabihf/release/sangam-io
```
The Roborock S5 Max stock image uses glibc rather than musl:
```bash
rustup target add armv7-unknown-linux-gnueabihf
cargo build --release --target armv7-unknown-linux-gnueabihf
```
### Feature Flags
| Feature | Description |
@@ -103,6 +115,10 @@ ssh root@vacuum "RUST_LOG=info /usr/sbin/sangamio"
## Configuration
For the Roborock S5 Max, start with `roborock-s5max.toml`. Actuation is
disabled by default and live takeover must continue the stock MCU transmit
sequence using `roborock_s5max_sequence`.
Edit `sangamio.toml`:
```toml
+23
View File
@@ -0,0 +1,23 @@
# SangamIO configuration for a rooted Roborock S5 Max.
#
# Actuation remains disabled unless explicitly enabled below.
[device]
type = "roborock_s5max"
name = "Roborock S5 Max"
[device.roborock_s5max]
mcu_port = "/dev/ttyS2"
lds_port = "/dev/ttyS1"
lds_motor = "/dev/lds_motor"
watchdog = "/dev/watchdog"
wheel_mm_per_tick = 0.798
wheel_track_m = 0.229
lds_forward_angle_deg = 261.2
allow_actuation = false
# Required at runtime. Use 0 only when SangamIO owns the MCU from reset; a live
# handoff must set the final sequence captured from the frozen stock sender.
# last_stock_sequence = 0
[network]
bind_address = "0.0.0.0:5555"
@@ -0,0 +1,72 @@
//! Read the stock S5 Max AP transmit sequence from a frozen `rr_loader`.
//!
//! This is deliberately a separate handoff utility: the robot's 32-bit `dd`
//! cannot seek to `/proc/PID/mem` addresses above `i32::MAX`.
use std::env;
use std::fs;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::process::ExitCode;
const LIBRARY_NAME: &str = "libuart_api";
const SEQUENCE_OFFSET: u64 = 0x2ff4d;
fn run() -> Result<u8, String> {
let mut arguments = env::args().skip(1);
let pid = arguments
.next()
.ok_or_else(|| "usage: roborock_s5max_sequence <rr_loader-pid>".to_string())?;
if arguments.next().is_some() || pid.parse::<u32>().is_err() {
return Err("usage: roborock_s5max_sequence <rr_loader-pid>".to_string());
}
let maps_path = format!("/proc/{pid}/maps");
let maps =
fs::read_to_string(&maps_path).map_err(|error| format!("read {maps_path}: {error}"))?;
let load_bias = maps
.lines()
.find_map(|line| {
let mut fields = line.split_whitespace();
let range = fields.next()?;
fields.next()?;
let file_offset = fields.next()?;
if !line.contains(LIBRARY_NAME) {
return None;
}
let start = range.split_once('-')?.0;
let start = u64::from_str_radix(start, 16).ok()?;
let file_offset = u64::from_str_radix(file_offset, 16).ok()?;
start.checked_sub(file_offset)
})
.ok_or_else(|| format!("cannot find {LIBRARY_NAME} mapping in {maps_path}"))?;
let memory_path = format!("/proc/{pid}/mem");
let mut memory =
File::open(&memory_path).map_err(|error| format!("open {memory_path}: {error}"))?;
let address = load_bias
.checked_add(SEQUENCE_OFFSET)
.ok_or_else(|| "sequence address overflow".to_string())?;
memory
.seek(SeekFrom::Start(address))
.map_err(|error| format!("seek {memory_path} to 0x{address:x}: {error}"))?;
let mut sequence = [0_u8; 1];
memory
.read_exact(&mut sequence)
.map_err(|error| format!("read sequence at 0x{address:x}: {error}"))?;
Ok(sequence[0])
}
fn main() -> ExitCode {
match run() {
Ok(sequence) => {
println!("{sequence}");
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}