feat(s5max): add build config and MCU handoff support
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user