feat(s5max): add documentation
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
# Roborock S5 Max reverse-engineering method
|
||||
|
||||
This document gives the method used to study the Roborock S5 Max hardware protocol.
|
||||
It applies to firmware `4.1.2_1668` and the MCU image named in the scope section.
|
||||
|
||||
The method starts with passive observation. Active operations occur only after the passive evidence defines the frame format and the safety behavior.
|
||||
|
||||
## Scope
|
||||
|
||||
The study used this robot software and firmware:
|
||||
|
||||
- Linux firmware `4.1.2_1668`.
|
||||
- Linux kernel `3.4.39` on ARMv7.
|
||||
- Ubuntu `14.04.3 LTS` with glibc `2.19`.
|
||||
- MCU image `RR_RUBYSLITE_MP_STM32.A3.G1_M4.0.1_RELEASE_20230207-210523_FULL.bin`.
|
||||
- Stock process host `rr_loader`.
|
||||
- Stock controller processes `RoboController` and `AppProxy`.
|
||||
- Stock UART library `libuart_api.so.4.0.1`.
|
||||
- Stock chassis plug-in `librr_plugin_chassis.so`.
|
||||
- Stock laser plug-in `librr_plugin_laser.so`.
|
||||
|
||||
Do not apply an address, offset, or command from this document to a different firmware without new evidence.
|
||||
Binary offsets and behavior can change between firmware versions.
|
||||
|
||||
## Evidence classes
|
||||
|
||||
Use one evidence class for each result:
|
||||
|
||||
- Confirmed: firmware code and a live capture give the same result.
|
||||
- Correlated: a controlled physical change gives a repeatable protocol change.
|
||||
- Observed: the frame and bytes are known, but their purpose is not known.
|
||||
|
||||
Do not give a semantic name to an observed field without more evidence.
|
||||
Keep unknown data available in the decoder.
|
||||
|
||||
## Safety rules
|
||||
|
||||
Do passive work before active work. A passive operation does not write to a robot device or change a shared FIFO cursor.
|
||||
|
||||
Use these rules for all live work:
|
||||
|
||||
1. Put the robot on a clear, level surface.
|
||||
2. Keep the robot in reach of the operator.
|
||||
3. Use a fixed time limit for each operation.
|
||||
4. Prepare the stop operation before the active operation.
|
||||
5. Keep wheel and cleaning-motor targets at zero during early work.
|
||||
6. Stop the stock owner before another process opens an exclusive UART.
|
||||
7. Do not let two processes own `/dev/ttyS2`, `/dev/ttyS1`, `/dev/lds_motor`, or `/dev/watchdog`.
|
||||
8. Restore the stock stack with a reboot after each ownership experiment.
|
||||
9. Make sure that Valetudo and the stock configuration remain available for recovery.
|
||||
|
||||
The hardware watchdog can reset the application processor. A replacement process must feed or disarm it during full takeover.
|
||||
|
||||
## Hardware inventory
|
||||
|
||||
Start with a read-only inventory of the robot. Record the kernel, firmware, mounts, partitions, processes, sockets, and device nodes.
|
||||
|
||||
The S5 Max uses these device paths:
|
||||
|
||||
| Path | Function |
|
||||
|------------------|--------------------------------------|
|
||||
| `/dev/ttyS2` | Main MCU UART at 1,152,000 baud |
|
||||
| `/dev/uart_mcu` | Kernel receive FIFO for the main MCU |
|
||||
| `/dev/ttyS1` | LDS UART at 115,200 baud |
|
||||
| `/dev/uart_lds` | Kernel receive FIFO for the LDS |
|
||||
| `/dev/lds_motor` | LDS motor controller |
|
||||
| `/dev/watchdog` | Hardware watchdog |
|
||||
|
||||
The stock process `rr_loader` owns the UART and LDS motor devices. `WatchDoge` owns the hardware watchdog during stock operation.
|
||||
|
||||
Use `/proc/<pid>/fd` and `/proc/<pid>/maps` to identify device owners and loaded libraries.
|
||||
Do not stop a process during this inventory.
|
||||
|
||||
## Copy the stock artifacts
|
||||
|
||||
Copy the binaries before static analysis. The robot uses a Dropbear server without an SFTP server.
|
||||
|
||||
Use legacy SCP mode:
|
||||
|
||||
```sh
|
||||
scp -O root@ROBOT:/opt/rockrobo/cleaner/bin/RoboController .
|
||||
scp -O root@ROBOT:/opt/rockrobo/cleaner/lib/libuart_api.so.4.0.1 .
|
||||
scp -O root@ROBOT:/opt/rockrobo/firmware/bin/MCU_FIRMWARE_MATCH .
|
||||
```
|
||||
|
||||
Record a hash for each copied file. An address is valid only for the matching binary.
|
||||
|
||||
## Find the transport boundary
|
||||
|
||||
Examine process file descriptors before disassembly. This step identifies named pipes, UARTs, shared memory, and kernel FIFOs.
|
||||
|
||||
The stock controller uses `libuart_api.so.4.0.1`. Useful exported names include `init_uart`, `read_data`, `sent_cmd_sigle_thread`, and `rua_keep_mcu_heartbeat`.
|
||||
|
||||
Search strings and symbols for these subjects:
|
||||
|
||||
- UART paths and baud rates.
|
||||
- Frame assembly and escaping.
|
||||
- ACK and retry operations.
|
||||
- Motor and sensor names.
|
||||
- LDS motor operations.
|
||||
- Watchdog operations.
|
||||
|
||||
Use Ghidra or radare2 for static analysis. Use ARM little-endian language settings for the Linux binaries.
|
||||
|
||||
The MCU image uses Thumb code. Load it at image base `0x08000000` when the vector table and references support that address.
|
||||
|
||||
## Observe receive traffic
|
||||
|
||||
The kernel receive FIFOs expose ring buffers through `mmap`. A ring buffer is a fixed memory area with wrapping indexes.
|
||||
|
||||
Map the FIFO with `PROT_READ`. Keep a private read position and do not change the stock tail value.
|
||||
|
||||
The observed FIFO metadata contains five `u32` values:
|
||||
|
||||
- Monotonic head.
|
||||
- Monotonic tail.
|
||||
- Ring mask.
|
||||
- Element size.
|
||||
- Initialization flag.
|
||||
|
||||
Use the difference between the current head and the private read position to copy new bytes.
|
||||
Handle ring wrap before frame decoding.
|
||||
|
||||
## Recover frame boundaries
|
||||
|
||||
Find repeated sync bytes and stable length fields. Preserve incomplete data between reads because one frame can span several reads.
|
||||
|
||||
For the MCU stream, distinguish binary frames from firmware text logs. They use different termination rules.
|
||||
|
||||
For the LDS stream, search for `FA` followed by an index from `A0` through `F9`.
|
||||
Use the XV11 checksum before you accept a packet.
|
||||
|
||||
Add captured byte sequences as unit-test fixtures. Include split frames, bad checksums, escape bytes, and unrelated leading bytes.
|
||||
|
||||
## Correlate data with controlled states
|
||||
|
||||
Change one physical state at a time. Keep all other conditions stable.
|
||||
|
||||
Use these passive states:
|
||||
|
||||
- Docked and idle.
|
||||
- Undocked and idle.
|
||||
- Straight cleaning.
|
||||
- Left turn.
|
||||
- Right turn.
|
||||
- Pause and resume.
|
||||
- Return to dock.
|
||||
- Final dock contact.
|
||||
- Dustbin removal and insertion.
|
||||
- Water-box removal and insertion.
|
||||
- Button press and release.
|
||||
- Front raised while stationary.
|
||||
- Left side raised while stationary.
|
||||
|
||||
Record the physical action and the capture time. Compare only reports that occur inside that interval.
|
||||
|
||||
Use wheel counters and quaternion yaw as independent measurements. A result is stronger when both measurements agree.
|
||||
|
||||
## Align MCU and LDS time
|
||||
|
||||
Capture both receive FIFOs in one process. Add one host monotonic timestamp to every MCU frame and LDS packet.
|
||||
|
||||
Fit MCU time to host time. Reject large scheduling outliers before geometric calibration.
|
||||
|
||||
Use full LDS revolutions for moving calibration. Partial revolutions are valid for a stationary fixed-wall test.
|
||||
|
||||
The S5 Max study used these calibrations:
|
||||
|
||||
- Ordinary cleaning supplied wheel-scale and track data.
|
||||
- A stationary flat wall supplied the LDS forward angle.
|
||||
- Controlled tilts supplied the IMU axis names.
|
||||
- Controlled dock positions supplied the dock-beacon classes.
|
||||
|
||||
Keep physical values in the device configuration. Motor wear and manufacturing variation can change them.
|
||||
|
||||
## Observe transmit traffic
|
||||
|
||||
Observe stock AP-to-MCU writes before you send a command. AP means application processor.
|
||||
|
||||
The stock image does not include `strace` or `gdb`. The study used `ptrace` at the `write` system-call boundary.
|
||||
|
||||
The observer used these limits:
|
||||
|
||||
1. Attach to the threads of the stock UART owner.
|
||||
2. Filter ARM system call 4 for the `/dev/ttyS2` file descriptor.
|
||||
3. Copy at most 4,096 bytes from each stopped write buffer.
|
||||
4. Do not change registers or process memory.
|
||||
5. Resume each thread immediately after the copy.
|
||||
6. Stop after a fixed capture interval.
|
||||
7. Detach every traced thread during cleanup.
|
||||
|
||||
Trace docked idle first. System-call observation can delay the stock process for a short time.
|
||||
|
||||
Use one host clock for transmit writes and receive FIFO frames. This permits ACK and response correlation.
|
||||
|
||||
## Capture the boot sequence
|
||||
|
||||
Cold boot contains queries and state changes that do not occur during normal idle operation.
|
||||
|
||||
The writable startup hook is `/mnt/reserve/_root.sh`. The read-only root file system does not require modification.
|
||||
|
||||
Use a one-shot marker and a detached helper. Make the helper fail open so stock boot continues after an observer error.
|
||||
|
||||
Make a byte-for-byte backup of `_root.sh` before a change. Use `bash -n` on the candidate script before installation.
|
||||
|
||||
After the capture, restore the backup and remove all markers. Make sure that no observer remains in the boot path.
|
||||
|
||||
## Do exclusive-ownership tests
|
||||
|
||||
Stop the stock hardware owner before an exclusive UART operation. Prevent `WatchDoge` from restarting that owner during the operation.
|
||||
|
||||
First, do a zero-I/O ownership test:
|
||||
|
||||
1. Open `/dev/ttyS2` with `O_RDWR`, `O_NOCTTY`, `O_NONBLOCK`, and `O_CLOEXEC`.
|
||||
2. Apply `TIOCEXCL`.
|
||||
3. Apply raw 1,152,000 8N1 termios.
|
||||
4. Make sure that the process is the only UART owner.
|
||||
5. Hold ownership for a fixed interval.
|
||||
6. Do not read or write the UART.
|
||||
7. Restore termios and close the descriptor.
|
||||
8. Reboot and examine the stock owners.
|
||||
|
||||
Do not continue if exclusive ownership or stock recovery fails.
|
||||
|
||||
## Add non-actuating queries
|
||||
|
||||
Continue the AP sequence from the last stock value. The sequence skips zero and wraps from `0xff` to `0x01`.
|
||||
|
||||
Add one query at a time. Use one write and a fixed response limit during the first operation.
|
||||
|
||||
The study used this order:
|
||||
|
||||
1. Query MCU version with `0x80`.
|
||||
2. Query device information with `0x88`.
|
||||
3. Query battery capacity with `0x8f`.
|
||||
4. Query current errors with `0x8c`.
|
||||
5. Synchronize sensors with `0x82`.
|
||||
|
||||
Implement acknowledgements in both directions before a continuous session. ACK means acknowledgement.
|
||||
|
||||
The stock retry rule uses three attempts and a 23 ms interval. A retry uses the same frame and sequence.
|
||||
|
||||
## Add a bounded replacement session
|
||||
|
||||
The continuous MCU session must own one transmit sequence and one UART writer.
|
||||
Other threads submit requests through channels.
|
||||
|
||||
The session must do these operations:
|
||||
|
||||
- Send the `d3` heartbeat once each second.
|
||||
- Send `d1` for each MCU frame that requests an ACK.
|
||||
- Send the same ACK for a duplicate frame.
|
||||
- Suppress duplicate delivery to the consumer.
|
||||
- Keep MCU footer order separate from AP transmit order.
|
||||
- Enter a fault state after lost reports or exhausted retries.
|
||||
|
||||
Do not reject a receive frame because its footer sequence is not consecutive. Concurrent MCU producers can change the observed order.
|
||||
|
||||
## Take ownership of the watchdog
|
||||
|
||||
The hardware watchdog uses a 16-second timeout. Feed it every two seconds during the bounded replacement session.
|
||||
|
||||
Use the watchdog magic-close operation on each normal and error exit. Restore the stock process after the experiment.
|
||||
|
||||
Do not stop `WatchDoge` without another watchdog owner. The application processor can reset after the timeout.
|
||||
|
||||
## Take ownership of the LDS motor
|
||||
|
||||
UART ownership does not keep the LDS rotor active. The stock laser plug-in drives `/dev/lds_motor`.
|
||||
|
||||
Use the recovered stock sequence:
|
||||
|
||||
1. Set the target to `30000`, which means 300.00 RPM.
|
||||
2. Set product ID `1` for the tested S5 Max.
|
||||
3. Start the motor with value `20000`.
|
||||
4. Send measured speed through `0x4004f812`.
|
||||
5. Send zero feedback every 50 ms until packets arrive.
|
||||
6. Stop the motor with `0x4004f814` on each exit path.
|
||||
|
||||
The docked power state prevents normal LDS rotation on the tested robot. Start the LDS only after the robot enters the operational discharge state.
|
||||
|
||||
## Add actuator operations
|
||||
|
||||
Do not send a nonzero actuator command before the stop set and deadman timer exist. A deadman timer stops motion when command updates stop.
|
||||
|
||||
Use this order for active acceptance work:
|
||||
|
||||
1. Send forced stationary `c0(v=0,w=0,forced=1)`.
|
||||
2. Make sure that wheel counters do not change.
|
||||
3. Send the full stop set.
|
||||
4. Operate one cleaning motor for a fixed interval.
|
||||
5. Make sure that feedback becomes nonzero.
|
||||
6. Stop that motor and make sure that feedback returns to zero.
|
||||
7. Operate the wheels with a short lease and `forced=0`.
|
||||
8. Stop lease updates and make sure that the deadman emits a forced stop.
|
||||
|
||||
The ordered stop set is:
|
||||
|
||||
1. Forced zero wheels.
|
||||
2. Hard fan stop.
|
||||
3. Side-brush stop.
|
||||
4. Main-brush stop.
|
||||
5. Pump stop.
|
||||
6. LDS motor stop.
|
||||
|
||||
Normal wheel movement must use `forced=0`. A nonzero forced value bypasses MCU bumper, drop, and dock interlocks.
|
||||
|
||||
## Preserve test evidence
|
||||
|
||||
Store the smallest useful byte fixtures in the repository. Do not store private addresses, passwords, or unrelated robot data.
|
||||
|
||||
Each protocol result must state:
|
||||
|
||||
- The firmware version.
|
||||
- The source binary hash.
|
||||
- The physical state.
|
||||
- The capture method.
|
||||
- The expected frame bytes.
|
||||
- The evidence class.
|
||||
- The known limits.
|
||||
|
||||
Keep large raw captures outside the source tree when the unit tests contain sufficient fixtures.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
Stop the work and restore the stock stack after one of these events:
|
||||
|
||||
- An unexpected wheel command.
|
||||
- A lost heartbeat.
|
||||
- An exhausted ACK retry.
|
||||
- A stale sensor report.
|
||||
- A UART read or write error.
|
||||
- A watchdog ownership error.
|
||||
- An LDS stop error.
|
||||
- A process that does not detach.
|
||||
- A stock process that does not return after reboot.
|
||||
|
||||
Do not continue an active sequence after a stop condition.
|
||||
|
||||
## Results from the tested unit
|
||||
|
||||
The method produced these working values for the tested robot:
|
||||
|
||||
| Quantity | Value |
|
||||
|----------------------------|----------------:|
|
||||
| Wheel distance | `0.798 mm/tick` |
|
||||
| Wheel track | `0.229 m` |
|
||||
| LDS forward raw angle | `261.2 deg` |
|
||||
| LDS target speed | `300 RPM` |
|
||||
| MCU UART | `1,152,000 8N1` |
|
||||
| LDS UART | `115,200 8N1` |
|
||||
| MCU state interval | `20 ms` |
|
||||
| MCU command retry interval | `23 ms` |
|
||||
| Drive lease | `250 ms` |
|
||||
| State-report limit | `500 ms` |
|
||||
| Hardware watchdog timeout | `16 s` |
|
||||
| Battery capacity wire unit | `100 mAh` |
|
||||
|
||||
These values are device defaults. Keep wheel and LDS geometry values adjustable for another unit.
|
||||
@@ -0,0 +1,564 @@
|
||||
# Roborock S5 Max hardware protocol
|
||||
|
||||
This document describes the protocol used by the native SangamIO driver for the Roborock S5 Max.
|
||||
The reference applies to firmware `4.1.2_1668` and the tested MCU image.
|
||||
|
||||
## Hardware interfaces
|
||||
|
||||
| Interface | Device | Parameters |
|
||||
|-----------------------|------------------|-----------------------|
|
||||
| Main MCU UART | `/dev/ttyS2` | 1,152,000 baud, 8N1 |
|
||||
| Main MCU receive FIFO | `/dev/uart_mcu` | 16 KiB ring |
|
||||
| LDS UART | `/dev/ttyS1` | 115,200 baud, 8N1 |
|
||||
| LDS receive FIFO | `/dev/uart_lds` | 16 KiB ring |
|
||||
| LDS motor | `/dev/lds_motor` | Linux ioctl interface |
|
||||
| Hardware watchdog | `/dev/watchdog` | 16-second timeout |
|
||||
|
||||
One worker owns the main MCU UART. It controls frame writes, sequence allocation, ACK processing, retries, and heartbeats.
|
||||
|
||||
Do not operate the stock hardware stack and SangamIO at the same time. They use the same device nodes.
|
||||
|
||||
## MCU binary frame
|
||||
|
||||
The MCU binary frame uses this wire form:
|
||||
|
||||
```text
|
||||
AA payload_length escape_overhead escaped(payload || crc8)
|
||||
```
|
||||
|
||||
`payload_length` is the unescaped payload length without the CRC byte.
|
||||
`escape_overhead` is one plus the number of escape expansions.
|
||||
|
||||
Use these escape pairs:
|
||||
|
||||
```text
|
||||
A9 00 -> A9
|
||||
A9 01 -> AA
|
||||
```
|
||||
|
||||
The CRC uses the Dallas/Maxim CRC-8 algorithm. It uses reflected polynomial `0x8c` and initial value zero.
|
||||
|
||||
Calculate the CRC over the unescaped payload. Append the CRC before escape encoding.
|
||||
|
||||
## MCU text frame
|
||||
|
||||
Firmware logs use a different wire form:
|
||||
|
||||
```text
|
||||
AA payload_length 00 F9 subtype text... 5E AA
|
||||
```
|
||||
|
||||
The final `AA` is a sentinel. It is not a binary-frame CRC.
|
||||
|
||||
Observed subtype `0x80` contains high-rate motor and IMU diagnostics. Observed subtype `0x81` contains state, battery, dock, and warning messages.
|
||||
|
||||
## Payload records
|
||||
|
||||
A normal payload contains type-length-value records:
|
||||
|
||||
```text
|
||||
record_id record_length record_data...
|
||||
```
|
||||
|
||||
The payload can contain more than one record. Periodic state record `0x07/0x40` starts with a fixed 64-byte body.
|
||||
|
||||
Most payloads end with footer record `0xd0/len2`:
|
||||
|
||||
```text
|
||||
d0 02 acknowledgement_request sequence
|
||||
```
|
||||
|
||||
The transmit sequence wraps from `0xff` to `0x01`. The sequence never uses zero.
|
||||
|
||||
If `acknowledgement_request` is one, the receiver sends `d1/len1` with the same sequence value.
|
||||
|
||||
Footer sequences from the MCU are ACK tokens. They are not one global receive order because MCU producers operate concurrently.
|
||||
|
||||
## Retry and duplicate rules
|
||||
|
||||
An ACK-required command uses a 23 ms response limit. The sender makes no more than three attempts.
|
||||
|
||||
A retry uses the same encoded frame and sequence. Do not allocate a new sequence for that retry.
|
||||
|
||||
Identify a duplicate MCU report with its footer sequence and exact unescaped payload. Send the ACK again, but deliver the report once.
|
||||
|
||||
## Heartbeat
|
||||
|
||||
The AP sends this normal heartbeat approximately once each second:
|
||||
|
||||
```text
|
||||
d3 04 00 00 00 00
|
||||
```
|
||||
|
||||
A nonzero `d3` value sets the MCU communication timeout. Value `0xffffffff` disables that timeout.
|
||||
|
||||
The MCU can also send `d3`. The stock AP ignores that reverse-direction record.
|
||||
|
||||
## Periodic state record
|
||||
|
||||
Record `0x07/len0x40` uses this layout. Offsets include the two-byte record header.
|
||||
|
||||
| Offset | Size | Type | Meaning |
|
||||
|-------:|---------:|----------|-------------------------------------------|
|
||||
| 0 | 1 | `u8` | Record ID `0x07` |
|
||||
| 1 | 1 | `u8` | Body length `0x40` |
|
||||
| 2 | 12 | `f32[3]` | Acceleration |
|
||||
| 14 | 12 | `f32[3]` | Angular rate |
|
||||
| 26 | 16 | `f32[4]` | Quaternion `(x, y, z, w)` |
|
||||
| 42 | 4 | `i32` | Left cumulative wheel ticks |
|
||||
| 46 | 4 | `i32` | Right cumulative wheel ticks |
|
||||
| 50 | 4 | `f32` | Filtered forward value in ticks per 20 ms |
|
||||
| 54 | 4 | `u32` | Reserved, observed as zero |
|
||||
| 58 | 8 | `u64` | MCU monotonic time in milliseconds |
|
||||
| 66 | variable | TLV | Additional reports |
|
||||
|
||||
All multibyte values are little-endian.
|
||||
|
||||
The IMU uses X forward, Y left, and Z up. Positive yaw is left or counterclockwise.
|
||||
|
||||
## MCU reports
|
||||
|
||||
| ID | Length | Meaning |
|
||||
|--------------:|---------:|-------------------------------------------------|
|
||||
| `0x01` | 64 | MCU version string |
|
||||
| `0x02` | 8 | System-mode tag and mode |
|
||||
| `0x04` | 4 | Startup and internal-error word |
|
||||
| `0x06` | 2 | Button report |
|
||||
| `0x08` | 10 | Battery summary |
|
||||
| `0x0a` | 8 | Factory and test data |
|
||||
| `0x0b` | 8 | Black-box event |
|
||||
| `0x0c` | 32 | Product or part number |
|
||||
| `0x0d` | 16 | Typed device-information block |
|
||||
| `0x0e` | 4 | RTC value |
|
||||
| `0x0f` | 2 | Dock voltage in millivolts |
|
||||
| `0x10` | 16 | Navigation-position cache |
|
||||
| `0x11` | variable | AP cache slice |
|
||||
| `0x12` | 2 | Charging-cycle value |
|
||||
| `0x13` | 4 | Dock IR masks |
|
||||
| `0x16` | 2 | Battery total capacity |
|
||||
| `0x18` | 3 | Water-pump hard fault |
|
||||
| `0x20` | 2 | Dock state |
|
||||
| `0x21` | 2 | Bumper state |
|
||||
| `0x22` | 2 | Drop or lift state |
|
||||
| `0x23` | 2 | Cliff state |
|
||||
| `0x24` | 2 | Dustbin state |
|
||||
| `0x25` | 2 | LDS cover bumper state |
|
||||
| `0x26` | 2 | Water-box state |
|
||||
| `0x42` | 2 | Wall-sensor ADC value |
|
||||
| `0x50` | 6 | Fan motor feedback |
|
||||
| `0x51` | 8 | Wheel motor feedback |
|
||||
| `0x52` | 6 | Main-brush feedback |
|
||||
| `0x53` | 6 | Side-brush feedback |
|
||||
| `0x54` | variable | Mop-motor record, logged only by stock software |
|
||||
| `0xd0` | 2 | Frame footer |
|
||||
| `0xd1` | 1 | ACK token |
|
||||
| `0xd2` | 8 | MCU time synchronization value |
|
||||
| `0xd3` | 4 | Heartbeat or heartbeat configuration |
|
||||
| `0xd5` | 28 | Persisted sensor calibration data |
|
||||
| `0xf1` | 16 | MCU identity tuple |
|
||||
| `0xf5` | 8 | MCU upgrade status |
|
||||
| `0xf6` | variable | NUL-terminated status string |
|
||||
| `0xf7`-`0xfa` | variable | MCU log records |
|
||||
|
||||
Unknown records remain available through the generic report iterator.
|
||||
|
||||
## Battery report
|
||||
|
||||
Record `0x08/len10` uses this layout:
|
||||
|
||||
| Bytes | Type | Meaning |
|
||||
|------:|-------|-----------------------------------|
|
||||
| 0..1 | `u16` | Voltage in millivolts |
|
||||
| 2..3 | `u16` | Current magnitude in milliamperes |
|
||||
| 4 | `u8` | State of charge in percent |
|
||||
| 5..9 | bytes | Reserved, observed as zero |
|
||||
|
||||
Record `0x16/len2` gives total battery capacity. The stock AP multiplies byte zero by 100 mAh and ignores byte one.
|
||||
|
||||
## Dock state
|
||||
|
||||
Record `0x20/len2` is a little-endian dock state:
|
||||
|
||||
| Value | Meaning |
|
||||
|------:|--------------------------------------|
|
||||
| `0` | Disconnected |
|
||||
| `1` | Dock supply and charging established |
|
||||
| `2` | Not observed |
|
||||
| `3` | Physical dock contact |
|
||||
|
||||
These values differ from internal MCU diagnostic names.
|
||||
|
||||
## Dock IR report
|
||||
|
||||
Record `0x13/len4` uses this layout:
|
||||
|
||||
| Byte | Meaning |
|
||||
|-----:|--------------------------------------|
|
||||
| 0 | Rolling mask from the left receiver |
|
||||
| 1 | Reserved |
|
||||
| 2 | Rolling mask from the right receiver |
|
||||
| 3 | Reserved |
|
||||
|
||||
Use these mask bits:
|
||||
|
||||
| Bit | Meaning |
|
||||
|-------:|----------------------------|
|
||||
| `0x01` | Far-left beacon class |
|
||||
| `0x02` | Far-right beacon class |
|
||||
| `0x04` | Close-guidance left class |
|
||||
| `0x08` | Close-guidance right class |
|
||||
| `0x10` | Common or center class |
|
||||
|
||||
## Attachment reports
|
||||
|
||||
Record `0x24` gives dustbin state. Value `00 00` means that the dustbin is present on the tested unit.
|
||||
|
||||
Record `0x26` gives water-box state. Value `00 00` means that the water box is present on the tested unit.
|
||||
|
||||
The tested hardware does not provide an independent electronic mop-cloth presence report.
|
||||
|
||||
## Motor feedback
|
||||
|
||||
Record `0x51/len8` uses this layout:
|
||||
|
||||
| Bytes | Meaning |
|
||||
|------:|-------------------------------------|
|
||||
| 0..1 | Left motor current in milliamperes |
|
||||
| 2..3 | Right motor current in milliamperes |
|
||||
| 4 | Signed left drive-demand level |
|
||||
| 5 | Signed right drive-demand level |
|
||||
| 6 | Left fault or result code |
|
||||
| 7 | Right fault or result code |
|
||||
|
||||
Records `0x50`, `0x52`, and `0x53` use one common six-byte layout:
|
||||
|
||||
| Bytes | Meaning |
|
||||
|------:|-------------------------------|
|
||||
| 0..1 | Sentinel `0xffff` |
|
||||
| 2..3 | Motor current in milliamperes |
|
||||
| 4 | Fault or result code |
|
||||
| 5 | Controller speed |
|
||||
|
||||
## Error word
|
||||
|
||||
Record `0x04/len4` is a little-endian `u32`:
|
||||
|
||||
| Field | Meaning on this MCU image | Stock result |
|
||||
|-------------:|----------------------------------------|----------------:|
|
||||
| Bit 0 | Invalid persistent test data | Diagnostic only |
|
||||
| Bit 1 | Gyro probe or identification failure | Error 101 |
|
||||
| Bit 2 | Repeated BMS communication failure | Error 111 |
|
||||
| Bit 3 | Compatibility field with no MCU setter | Error 112 |
|
||||
| Bit 4 | Compatibility field value 1 | Error 120 |
|
||||
| Bit 5 | Compatibility field value 2 | Error 121 |
|
||||
| Bits 4 and 5 | Compatibility field value 3 | Error 122 |
|
||||
|
||||
Treat bits 4 and 5 as one two-bit field. Do not report them as two independent errors.
|
||||
|
||||
## System modes
|
||||
|
||||
Record `0x02/len8` contains this value:
|
||||
|
||||
```text
|
||||
73 79 73 5f 6d 64 00 MM
|
||||
s y s _ m d \0 mode
|
||||
```
|
||||
|
||||
| Mode | Meaning |
|
||||
|-------:|-------------------------------------------|
|
||||
| `0x00` | Normal or awake |
|
||||
| `0x01` | Idle |
|
||||
| `0x02` | Factory mode |
|
||||
| `0x03` | Manual built-in test |
|
||||
| `0x04` | Automatic built-in test |
|
||||
| `0x05` | Mobility test |
|
||||
| `0x06` | Shutdown |
|
||||
| `0x07` | Reserved and rejected |
|
||||
| `0x08` | Energy-efficiency mode |
|
||||
| `0x09` | Retreading factory mode |
|
||||
| `0x20` | Start-key-only boot |
|
||||
| `0x21` | Watchdog reset without dock power |
|
||||
| `0x22` | Reset-pin boot without dock power |
|
||||
| `0x23` | Other reset without dock power |
|
||||
| `0x40` | Dock-only boot |
|
||||
| `0x41` | Watchdog reset with dock power |
|
||||
| `0x42` | Reset-pin boot with dock power |
|
||||
| `0x43` | Other reset with dock power |
|
||||
| `0x44` | AP power-on after a communication failure |
|
||||
|
||||
These modes describe MCU power and factory states. They do not describe cleaning or navigation states.
|
||||
|
||||
## AP commands
|
||||
|
||||
Each AP command follows the `d0` footer inside the payload. The command can request an ACK through footer byte zero.
|
||||
|
||||
| ID | Length | Meaning |
|
||||
|-------:|-------:|------------------------------------|
|
||||
| `0x80` | 0 | Query MCU version |
|
||||
| `0x82` | 0 | Synchronize sensor state |
|
||||
| `0x88` | 0 | Query device information |
|
||||
| `0x8c` | 0 | Query current errors |
|
||||
| `0x8f` | 0 | Query battery total capacity |
|
||||
| `0xb0` | 8 | Set MCU system mode |
|
||||
| `0xb1` | 4 | Enable or disable one subsystem |
|
||||
| `0xb3` | 16 | Set four LED channel tuples |
|
||||
| `0xb7` | 1 | Control charging |
|
||||
| `0xbb` | 1 | Set maximum LED brightness |
|
||||
| `0xbc` | 16 | Store navigation-position cache |
|
||||
| `0xbd` | 6 | Store robot-state cache |
|
||||
| `0xbe` | 2 | Set motor threshold |
|
||||
| `0xbf` | 2 | Set water-pump lease |
|
||||
| `0xc0` | 12 | Set wheel and yaw targets |
|
||||
| `0xc1` | 2 | Set fan target |
|
||||
| `0xc3` | 2 | Set main-brush target |
|
||||
| `0xc4` | 2 | Set side-brush target |
|
||||
| `0xd1` | 1 | Send an ACK |
|
||||
| `0xd3` | 4 | Send heartbeat or set its interval |
|
||||
|
||||
## Subsystem command
|
||||
|
||||
Command `0xb1/len4` is a little-endian selector. Bit zero is the enable value.
|
||||
|
||||
| Selector without bit zero | Subsystem |
|
||||
|--------------------------:|--------------------------|
|
||||
| `0x00002` | Bumper |
|
||||
| `0x00004` | Empty compatibility hook |
|
||||
| `0x00008` | Cliff sensors |
|
||||
| `0x00010` | Reserved |
|
||||
| `0x00020` | Drop or lift sensors |
|
||||
| `0x00040` | Dustbin sensor |
|
||||
| `0x00080` | Main brush |
|
||||
| `0x00100` | Side brush |
|
||||
| `0x00200` | Fan |
|
||||
| `0x00400` | Wheel odometry |
|
||||
| `0x00800` | Gyro |
|
||||
| `0x02000` | Dock IR receivers |
|
||||
| `0x04000` | Water-box sensor |
|
||||
| `0x08000` | Water pump |
|
||||
| `0x20000` | Wall sensor |
|
||||
|
||||
For example, value `0x00000002` disables the bumper. Value `0x00000003` enables the bumper.
|
||||
|
||||
## Wheel command
|
||||
|
||||
Command `0xc0/len12` uses this layout:
|
||||
|
||||
| Offset | Type | Meaning |
|
||||
|-------:|-------|----------------------------------------------|
|
||||
| 0 | `f32` | Mean wheel target in encoder ticks per 20 ms |
|
||||
| 4 | `f32` | Body yaw target in radians per second |
|
||||
| 8 | `u32` | Forced safety override |
|
||||
|
||||
Positive yaw turns left or counterclockwise. Negative yaw turns right or clockwise.
|
||||
|
||||
Use `forced=0` for normal movement. A nonzero value bypasses MCU interlocks for the bumper, drop sensors, and dock.
|
||||
|
||||
Enable wheel odometry with acknowledged `b1=0x00000401` before a nonzero wheel command.
|
||||
|
||||
Use a 250 ms drive lease. Send forced zero wheels when that lease expires.
|
||||
|
||||
## Cleaning-motor commands
|
||||
|
||||
Command `0xc1/len2` sets the fan target in byte zero. The MCU ignores byte one.
|
||||
|
||||
The user-visible fan presets use these values during active cleaning:
|
||||
|
||||
| User choice | Request |
|
||||
|-------------|--------:|
|
||||
| Off | 30 |
|
||||
| Minimum | 38 |
|
||||
| Medium-low | 55 |
|
||||
| Medium-high | 75 |
|
||||
| High | 100 |
|
||||
|
||||
User-visible Off is not a hard stop. Use `c1 = 00 00` for a physical fan stop.
|
||||
|
||||
Command `0xc3/len2` sets the main-brush target in byte zero. Byte one is a force-start flag.
|
||||
|
||||
Command `0xc4/len2` sets the side-brush target in byte zero. The tested MCU ignores byte one.
|
||||
|
||||
Normal cleaning uses main-brush request 71 and side-brush request 30. Return-home operation uses requests 45 and 20.
|
||||
|
||||
## Water-pump command
|
||||
|
||||
Command `0xbf/len2` is a timed lease. The MCU multiplies both bytes by ten for its internal counters.
|
||||
|
||||
Use `0b 00` for a 1.1-second ON lease. Use `00 ff` for the OFF lease.
|
||||
|
||||
The AP creates the water levels with ON-pulse scheduling:
|
||||
|
||||
| Level | Approximate ON-to-ON period |
|
||||
|--------|----------------------------:|
|
||||
| Low | 31.67 s |
|
||||
| Medium | 6.51 s |
|
||||
| High | 4.52 s |
|
||||
| Off | No periodic ON pulse |
|
||||
|
||||
The MCU has no separate Low, Medium, or High command value.
|
||||
|
||||
## LED command
|
||||
|
||||
Command `0xb3/len16` contains four channel tuples:
|
||||
|
||||
```text
|
||||
mode:u8 parameter:u8 argument:u16_le
|
||||
```
|
||||
|
||||
The binary structure is known. Some mode and argument values remain unknown.
|
||||
|
||||
## LDS packet
|
||||
|
||||
The LDS uses a 22-byte XV11 packet:
|
||||
|
||||
```text
|
||||
FA index(A0..F9) speed_le sample[4] checksum_le
|
||||
```
|
||||
|
||||
Index `A0` starts at zero degrees. Index `F9` starts at 356 degrees.
|
||||
|
||||
Each packet contains four one-degree samples. Ninety packets form one 360-sample revolution.
|
||||
|
||||
The speed value is `u16 / 64` RPM. The target speed is 300 RPM.
|
||||
|
||||
Each sample contains a distance and a signal value:
|
||||
|
||||
| Field | Meaning |
|
||||
|---------------------|-------------------------|
|
||||
| Distance bits 0..13 | Distance in millimeters |
|
||||
| Distance bit 14 | Signal-strength warning |
|
||||
| Distance bit 15 | Invalid or no return |
|
||||
| Signal `u16` | Return strength |
|
||||
|
||||
Use the XV11 15-bit rolling checksum over the first ten little-endian words.
|
||||
|
||||
Raw LDS angles increase clockwise. Convert them to the robot frame with this expression:
|
||||
|
||||
```text
|
||||
body_bearing = wrap(261.2 deg - raw_angle)
|
||||
```
|
||||
|
||||
Robot-frame bearings start at the front and increase counterclockwise.
|
||||
|
||||
## LDS motor interface
|
||||
|
||||
The LDS motor uses `/dev/lds_motor`:
|
||||
|
||||
| Operation | ioctl | Argument |
|
||||
|---------------------|-------------:|-----------------------:|
|
||||
| Set target speed | `0x4004f810` | `30000` |
|
||||
| Send measured speed | `0x4004f812` | RPM times 100 |
|
||||
| Start motor | `0x4004f813` | `20000` |
|
||||
| Stop motor | `0x4004f814` | `0` |
|
||||
| Set PID P | `0x4004f815` | Controller value |
|
||||
| Set PID I | `0x4004f816` | Controller value |
|
||||
| Set PID D | `0x4004f817` | Controller value |
|
||||
| Set duty | `0x4004f824` | Controller value |
|
||||
| Set product type | `0x4004f826` | `1` on the tested unit |
|
||||
|
||||
Send zero measured speed every 50 ms until a valid LDS packet arrives. Use a three-second cold-start grace period.
|
||||
|
||||
Stop the motor on disable, startup error, shutdown, and object destruction.
|
||||
|
||||
## Geometry and units
|
||||
|
||||
The tested unit uses these measured values:
|
||||
|
||||
| Quantity | Value |
|
||||
|-----------------------|----------------:|
|
||||
| Wheel distance | `0.798 mm/tick` |
|
||||
| Wheel track | `0.229 m` |
|
||||
| LDS forward raw angle | `261.2 deg` |
|
||||
| Battery capacity unit | `100 mAh` |
|
||||
|
||||
Use these odometry expressions:
|
||||
|
||||
```text
|
||||
delta_distance = 0.000798 * (delta_left + delta_right) / 2
|
||||
delta_yaw = 0.000798 * (delta_right - delta_left) / 0.229
|
||||
```
|
||||
|
||||
Keep the wheel and LDS geometry values in the device configuration. Another unit can require different values.
|
||||
|
||||
## Lifecycle and safety
|
||||
|
||||
The driver uses this lifecycle:
|
||||
|
||||
```text
|
||||
Created -> Synchronizing -> ReadOnlyReady -> Operational -> Stopping -> Stopped
|
||||
| |
|
||||
+--------------> Fault
|
||||
```
|
||||
|
||||
Use these safety limits:
|
||||
|
||||
| Limit | Value |
|
||||
|------------------------|---------:|
|
||||
| State-report freshness | `500 ms` |
|
||||
| Drive lease | `250 ms` |
|
||||
| ACK response | `23 ms` |
|
||||
| Command attempts | `3` |
|
||||
| MCU heartbeat interval | `1 s` |
|
||||
| Watchdog feed interval | `2 s` |
|
||||
| Watchdog timeout | `16 s` |
|
||||
|
||||
The driver rejects nonzero commands before synchronization and fresh safety reports.
|
||||
|
||||
The ordered stop set is forced zero wheels, hard fan stop, side-brush stop, main-brush stop, pump stop, and LDS stop.
|
||||
|
||||
Attempt all stop operations after one stop operation fails. Enter the Fault state after stale reports, UART errors, or exhausted ACK retries.
|
||||
|
||||
## Dock and LDS lifecycle
|
||||
|
||||
The physical dock state and the charging command are separate. Disabling charging does not remove physical dock contact.
|
||||
|
||||
The tested robot does not rotate the LDS normally while it remains docked. Start the LDS after the robot is physically undocked.
|
||||
|
||||
For an undocked start:
|
||||
|
||||
1. Synchronize the MCU.
|
||||
2. Hold forced zero wheels.
|
||||
3. Make sure that dock state is zero.
|
||||
4. Set LDS product ID `1`.
|
||||
5. Set the target speed to 300 RPM.
|
||||
6. Start the LDS motor.
|
||||
7. Send measured-speed feedback.
|
||||
8. Stop the LDS motor on each exit path.
|
||||
|
||||
## Build and deployment limits
|
||||
|
||||
Firmware `4.1.2_1668` uses glibc `2.19`. A binary linked to a new host glibc will not start on the robot.
|
||||
|
||||
Build the robot binary as static ARM musl:
|
||||
|
||||
```sh
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf
|
||||
```
|
||||
|
||||
Stop `rr_loader` and `WatchDoge` only through a process-handoff procedure. Preserve Valetudo for recovery.
|
||||
|
||||
Use the takeover and recovery procedure in `sangam-io/README.md`.
|
||||
|
||||
## Known limits
|
||||
|
||||
These fields remain incomplete:
|
||||
|
||||
- The names of the two `0x0b` black-box selector states.
|
||||
- Most bytes in device-information report `0x0d`.
|
||||
- Physical units for parts of calibration report `0xd5`.
|
||||
- The application grammar of status report `0xf6`.
|
||||
- Exact meanings for all LED tuple values.
|
||||
- The physical meaning of motor threshold 450.
|
||||
- Factory, maintenance, and firmware-upgrade operations.
|
||||
- LDS position relative to the wheel axle.
|
||||
|
||||
The driver keeps unknown records and raw fields available. It does not assign names without evidence.
|
||||
|
||||
## Related documents
|
||||
|
||||
- `protocol-mitm/docs/ROBOROCK_S5_MAX_REVERSE_ENGINEERING.md` gives the research method.
|
||||
- `sangam-io/README.md` gives the takeover and recovery procedure.
|
||||
- `docs/roborock-s5max-suite-deployment.md` gives the full suite deployment procedure.
|
||||
- `sangam-io/docs/roborock-s5max-driver-plan.md` gives the driver development plan.
|
||||
- `sangam-io/docs/roborock-s5max-reverse-engineering.md` gives the detailed research record.
|
||||
- `sangam-io/src/devices/crl200s/COMMANDS.md` gives the CRL-200S command reference.
|
||||
- `sangam-io/src/devices/crl200s/SENSORSTATUS.md` gives the CRL-200S status reference.
|
||||
Reference in New Issue
Block a user