Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom commands and raw telegrams

Every command is a type

Each protocol command is a struct implementing Command. The trait records the opcode, the reply type and which transports may carry it:

nxt_rs::doc_example(|brick| async move {
use nxt_rs::{port::OutputPort, protocol::direct::{GetBatteryLevel, GetOutputState}};

let millivolts = brick.send(GetBatteryLevel).await?;
let status = brick.send(GetOutputState { port: OutputPort::A }).await?;
Ok(()) });

The methods on direct() and system() are thin wrappers around send.

Defining your own

Enhanced firmware (NBC/NXC “enhanced”, leJOS-style experiments, your own builds) can add opcodes. Command isn’t sealed, so you can describe new commands and get the same typed send:

use nxt_rs::{
    error::{DecodeError, EncodeError},
    protocol::{
        Command, CommandKind, Reply,
        codec::{Decoder, Encoder},
        link::AnyLink,
    },
};

/// A hypothetical command returning the free RAM in bytes.
struct GetFreeMemory;

struct FreeMemory(u32);

impl Reply for FreeMemory {
    fn decode(dec: &mut Decoder<'_>) -> Result<Self, DecodeError> {
        Ok(FreeMemory(dec.u32()?))
    }
}

impl Command for GetFreeMemory {
    const KIND: CommandKind = CommandKind::Direct;
    const OPCODE: u8 = 0x42;
    type Link = AnyLink;           // or UsbLink for USB-only commands
    type Reply = FreeMemory;       // or () for status-only commands

    fn encode(&self, _enc: &mut Encoder<'_>) -> Result<(), EncodeError> {
        Ok(())                     // arguments go here, little-endian
    }
}

async fn f(brick: nxt_rs::Brick<nxt_rs::transport::Usb>) -> nxt_rs::Result<()> {
let free = brick.send(GetFreeMemory).await?.0;
Ok(()) }
  • Encoder writes u8, i8, u16, i32, … little-endian, plus bytes and padded for fixed-width fields.
  • Decoder reads them back and reports truncated replies as DecodeError.
  • A non-zero status in the reply becomes Error::Brick before your decoder runs.
  • If Reply = (), the command can also be sent with send_no_reply.

Encoding without a brick

The protocol layer does no I/O, so you can build and inspect telegrams directly, for example in tests or a packet logger:

use nxt_rs::protocol::{decode_reply, encode_request, direct::{GetBatteryLevel, PlayTone}};
use nxt_rs::units::{Hz, Millis16, Millivolts};

fn main() -> nxt_rs::Result<()> {
let tone = PlayTone { frequency: Hz::new(480)?, duration: Millis16::from_millis(500) };
let mut buf = [0; 64];
let len = encode_request(&tone, false, &mut buf)?;
assert_eq!(&buf[..len], &[0x80, 0x03, 0xE0, 0x01, 0xF4, 0x01]);

let battery = decode_reply::<GetBatteryLevel>(&[0x02, 0x0B, 0x00, 0x4C, 0x1D])?;
assert_eq!(battery, Millivolts(7500));
Ok(()) }

Raw telegrams

At the very bottom, raw_transact sends bytes and returns the raw reply. The status byte isn’t checked:

nxt_rs::doc_example(|brick| async move {
let reply = brick.raw_transact(&[0x00, 0x0B]).await?;   // GetBatteryLevel
assert_eq!(reply.opcode, 0x0B);
assert_eq!(reply.status, 0);
let millivolts = u16::from_le_bytes([reply.data[0], reply.data[1]]);

brick.raw_send(&[0x80, 0x0C]).await?;                   // StopSoundPlayback, no reply
Ok(()) });

The Protocol reference describes the telegram format.