feat(s5max): add documentation

This commit is contained in:
Fabien Carrion
2026-09-19 12:19:42 -06:00
parent 928ffed9f9
commit 9c6a72cee9
2 changed files with 923 additions and 0 deletions
@@ -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.