rm-frame 0.1.1

A Rust implementation of DJI RoboMaster Protocol, including frame parsing and link management.
rm-frame-0.1.1 has been yanked.
Visit the last successful build: rm-frame-0.1.2

rm-proto

A no_std, allocation-free Rust implementation of the RoboMaster referee system and vision link communication protocols.

Crates

Crate Description
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

┌──────┬─────┬─────┬──────┬────────┬──────┬───────┐
│ SOF  │ LEN │ SEQ │ CRC8 │ CMD_ID │ DATA │ CRC16 │
├──────┼─────┼─────┼──────┼────────┼──────┼───────┤
│  1 B │ 2 B │ 1 B │  1 B │   2 B  │ N B  │  2 B  │
└──────┴─────┴─────┴──────┴────────┴──────┴───────┘
Field Details
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:

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:

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

Type When it occurs
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

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

Module Type CMD_ID Rate / Trigger Payload
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

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).

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

Feature Crates Effect
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.

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:

Category Members
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

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