# rm-proto
A `no_std`, allocation-free Rust implementation of the RoboMaster referee system and vision link communication protocols.
## Crates
| `rm-frame` | Core framing layer: CRC, encode/decode, error types, optional remote control |
| `rm-link-serial` | Referee system serial protocol messages |
| `rm-link-vision` | Vision link messages (`Custom2Robot`) |
## Frame Format
```text
┌──────┬─────┬─────┬──────┬────────┬──────┬───────┐
│ SOF │ LEN │ SEQ │ CRC8 │ CMD_ID │ DATA │ CRC16 │
├──────┼─────┼─────┼──────┼────────┼──────┼───────┤
│ 1 B │ 2 B │ 1 B │ 1 B │ 2 B │ N B │ 2 B │
└──────┴─────┴─────┴──────┴────────┴──────┴───────┘
```
| SOF | Start of frame marker: `0xA5` |
| LEN | Payload length, little-endian `u16` |
| SEQ | Sequence number, wrapping `u8` |
| CRC8 | CRC8/DJI over `[SOF, LEN, SEQ]`, initial value `0xFF` |
| CMD_ID | Command ID, little-endian `u16` |
| DATA | Payload bytes |
| CRC16 | CRC16/DJI over the entire preceding frame, initial value `0xFFFF` |
Minimum frame size (empty payload): **9 bytes**.
---
## rm-frame
### The `Marshaler` Trait
Implement this trait to make a type encodable and decodable:
```rust
pub trait Marshaler: Sized {
const CMD_ID: u16;
const PAYLOAD_SIZE: u16;
fn marshal(&self, dst: &mut [u8]) -> Result<usize, MarshalerError>;
fn unmarshal(raw: &[u8]) -> Result<Self, MarshalerError>;
}
```
Implementing `Marshaler` automatically provides the `ImplMarshal` (encode-only) and `ImplUnMarshal` (decode-only) sub-traits. Types that only need one direction can implement these directly.
### `Messager`
The main entry point for sending and receiving frames:
```rust
let mut messager = Messager::new(0); // initial sequence number
// Encode a message into a buffer
let n = messager.pack(&msg, &mut buf)?;
// Decode a raw frame from a buffer
let (raw_frame, consumed) = messager.unpack(&buf)?;
// Decode directly into a typed message
let (msg, consumed) = messager.unmarshal::<MyMessage>(&buf)?;
```
### Error Types
| `PackError` | Buffer too small, invalid payload size |
| `UnPackError` | Missing header, bad checksum, incomplete data |
| `MarshalerError` | CMD_ID mismatch, wrong data length |
`UnPackError::skip()` returns how many bytes to advance for re-synchronization.
### CRC Utilities
```rust
use rm_frame::{calc_dji8, calc_dji16};
let crc8 = calc_dji8(&data);
let crc16 = calc_dji16(&data);
```
---
## rm-link-serial
Some referee system messages implement the full `Marshaler` trait (encode + decode).
### Messages
| `states` | `GameStatus` | `0x0001` | 1 Hz | 11 B |
| `result` | `GameResult` | `0x0002` | On match end | 1 B |
| `health` | `GameRobotHP` | `0x0003` | 3 Hz | 16 B |
| `event` | `GameEvent` | `0x0101` | 1 Hz | 4 B |
| `warning` | `RefereeWarning` | `0x0104` | On penalty / 1 Hz | 3 B |
| `dart` | `DartInfo` | `0x0105` | 1 Hz | 3 B |
| `status` | `RobotStatus` | `0x0201` | 10 Hz | 13 B |
| `heat` | `PowerHeat` | `0x0202` | 10 Hz | 14 B |
| `pos` | `RobotPos` | `0x0203` | 1 Hz | 12 B |
| `buff` | `RobotBuff` | `0x0204` | 3 Hz | 8 B |
| `hurt` | `HurtData` | `0x0206` | On damage | 1 B |
### Usage
```rust
use rm_frame::Messager;
use rm_link_serial::status::RobotStatus;
let messager = Messager::new(0);
let (status, _): (RobotStatus, usize) = messager.unmarshal(&buf)?;
println!("HP: {}/{}", status.current_hp, status.maximum_hp);
```
---
## rm-link-vision
### `Custom2Robot` — CMD_ID `0x0302`, 30 bytes
A custom controller-to-robot command carrying 6 joint angles (`f32`) and a gripper state (`bool`). Decode-only (`ImplUnMarshal`).
```rust
use rm_frame::Messager;
use rm_link_vision::Custom2Robot;
let messager = Messager::new(0);
let (cmd, _): (Custom2Robot, usize) = messager.unmarshal(&buf)?;
let joints = cmd.get_joints(); // &[f32; 6]
let gripper = cmd.get_gripper(); // bool
```
---
## Feature Flags
| `defmt` | all | Derives `defmt::Format` on error types and key enums for embedded structured logging |
| `remote` | `rm-frame` | Enables [`RemoteControl`] for decoding VT03/VT13 uplink control frames |
---
## `no_std` Support
All crates are `#![no_std]` with no heap allocation. They can be used directly in bare-metal and RTOS environments.
## Remote Control (Optional)
When the `remote` feature is enabled, `rm-frame` provides [`RemoteControl`] to decode DT7/DR16 RC receiver frames from VT03/VT13 vision links.
```rust
use rm_frame::RemoteControl;
let rc = RemoteControl::new();
// Call from your DMA/interrupt handler
rc.update(&raw_bytes)?;
// Read from any task
let ch = rc.right_horizontal(); // i16, roughly [-660, 660]
let sw = rc.switch(); // Switch::C / N / S
let w = rc.keyboard_w(); // bool
let vx = rc.mouse_vx(); // i16
```
**Available inputs:**
| Analog channels | `right_horizontal`, `right_vertical`, `left_horizontal`, `left_vertical` |
| Switch | `switch` (`C` / `N` / `S`) |
| Buttons | `pause`, `left_fn`, `right_fn`, `trigger`, `wheel` |
| Mouse | `mouse_vx/vy/vz`, `left/mid/right_button` |
| Keyboard | `W S A D Shift Ctrl Q E R F G Z X C V B` |
---
## Examples
```rust,ignore
static SIG_BUFFER: Signal<RawMutex, ([u8; 64], usize)> = Signal::new();
pub static RC: RemoteControl = unsafe { RemoteControl::const_new() };
#[embassy_executor::task]
pub async fn task(s: embassy_executor::SendSpawner, p: PictransSrc) -> ! {
let mut config = Config::default();
config.baudrate = 921600;
config.data_bits = DataBits::DataBits8;
config.parity = Parity::ParityNone;
config.stop_bits = StopBits::STOP1;
// Note: Config is valid, so Unwrap is safe.
let pt = UartRx::new(p.uart_p, Irqs, p.uart_rx, p.dma_rx, config).unwrap();
let mut dma_buf = [0u8; 256];
let mut pt = pt.into_ring_buffered(&mut dma_buf);
let mut buffer = [0u8; 64];
s.must_spawn(handler());
loop {
match pt.read(&mut buffer).await {
Ok(x) if x > 0 => {
SIG_BUFFER.signal((buffer, x));
}
Ok(_) => {
// No data received
}
Err(e) => {
defmt::error!("RC Read Error: {:?}", e);
}
};
}
}
#[embassy_executor::task]
async fn handler() -> ! {
let mut t = utils::init_ticker!(1, ms);
let msger = Messager::<DjiValidator>::new(0);
let mut data: _ = Vec::<u8, { 64 * 10 }>::new();
loop {
match select(t.next(), SIG_BUFFER.wait()).await {
Either::First(_) => t.reset(),
Either::Second((buffer, len)) => {
if let Err(_) = data.extend_from_slice(&buffer[..len]) {
defmt::warn!("Data Overflow, clearing buffer");
data.clear();
continue;
}
}
}
match msger.unpack(&data) {
Ok((frame, consumed)) => {
match frame.cmd_id() {
Custom2Robot::CMD_ID => match frame.unmarshal::<Custom2Robot>() {
Ok(x) => {
do_sth_external(&x);
}
Err(e) => {
defmt::warn!("Custom2Robot Error: {:?}", e);
}
},
x => {
defmt::warn!("Unknown CMD_ID: 0x{:04X}", x);
}
}
let _ = data.drain(..consumed);
}
Err(Error::MissingHeader { skip } | Error::ReSync { skip }) => {
match RC.update(&data) {
Ok(x) => {
Device::Controller.feed();
let _ = data.drain(..x);
}
Err(e) => {
let skip = skip.min(e.skip());
let _ = data.drain(..skip);
}
};
}
Err(e) => {
let _ = data.drain(..e.skip());
}
}
}
}
```
---
## Reference
This library implements the **RoboMaster University Series 2026 Communication Protocol V1.2.0** (2026-02-09).
See RoboMaster Resources Hub for official documentation and protocol details:
[RMU Communication Protocol](https://bbs.robomaster.com/wiki/20204847/811363)