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(()) }
Encoderwritesu8,i8,u16,i32, … little-endian, plusbytesandpaddedfor fixed-width fields.Decoderreads them back and reports truncated replies asDecodeError.- A non-zero status in the reply becomes
Error::Brickbefore your decoder runs. - If
Reply = (), the command can also be sent withsend_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.