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

Choosing at run time and custom transports

Brick<T> and the transport type

Brick is generic over its transport: Brick<Usb>, Brick<Bluetooth>, Brick<Serial>, Brick<Mock>. The type decides which commands are available. USB-only commands exist only on Brick<Usb> (and the mock).

To write code that works with any brick, be generic:

use nxt_rs::{Brick, transport::Transport};

async fn report<T: Transport>(brick: &Brick<T>) -> nxt_rs::Result<()> {
    println!("{} ({:?}): {}", brick.name().await?, brick.transport_kind(), brick.battery().await?);
    Ok(())
}
nxt_rs::doc_example(|brick| async move { report(&brick).await?; Ok(()) });

Deciding at run time: AnyTransport

When the link is chosen at run time, for example from a command-line flag, erase the type with AnyTransport:

use nxt_rs::{Brick, blocking::Block, transport::{AnyTransport, Mock, Usb}};

fn main() -> nxt_rs::Result<()> {
let simulate = std::env::args().any(|a| a == "--simulate");
let brick: Brick<AnyTransport> = if simulate {
    Brick::new(AnyTransport::new(Mock::simulator()))
} else {
    Brick::new(AnyTransport::new(Usb::first().block()?))
};
Ok(()) }

nxt_rs::connect_any() does this for you. It tries USB first, then the paired Bluetooth bricks (with the bluetooth feature).

A Brick<AnyTransport> can’t send USB-only commands, because the compiler can’t know the link is USB.

Writing your own transport

A transport moves complete telegrams: a type byte, an opcode and arguments. Implement nxt_rs::transport::Transport to bridge the brick over anything, for example a TCP relay:

use nxt_rs::{
    Brick,
    error::TransportError,
    protocol::MAX_TELEGRAM_LEN,
    transport::{Transport, TransportKind, frame},
};
use std::{io::{Read, Write}, net::TcpStream};

/// Forwards Bluetooth-framed telegrams over TCP.
/// (Blocking I/O for brevity; use your runtime's async socket in real code.)
struct Tcp(TcpStream);

impl Transport for Tcp {
    fn kind(&self) -> TransportKind {
        TransportKind::Serial
    }

    async fn send(&mut self, telegram: &[u8]) -> Result<(), TransportError> {
        let (buf, len) = frame(telegram)?;
        self.0.write_all(&buf[..len])?;
        Ok(())
    }

    async fn recv(&mut self, buf: &mut [u8; MAX_TELEGRAM_LEN]) -> Result<usize, TransportError> {
        let mut len = [0; 2];
        self.0.read_exact(&mut len)?;
        let len = usize::from(u16::from_le_bytes(len));
        if len > MAX_TELEGRAM_LEN {
            return Err(TransportError::InvalidFrame("too long"));
        }
        self.0.read_exact(&mut buf[..len])?;
        Ok(len)
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
let brick = Brick::new(Tcp(TcpStream::connect("robot.local:5000")?));
Ok(()) }

Things to get right:

  • send writes one telegram and recv returns one. Over Bluetooth-style links, each telegram has a 2-byte little-endian length prefix (frame adds it). USB has no prefix.
  • The returned futures must be Send.
  • After a timeout the Brick drops your future and carries on. A late reply is harmless, because Brick discards replies whose opcode doesn’t match.
  • kind() decides the default timeout and how the transport is reported.
  • Only nxt-rs’s own Usb and Mock transports can carry USB-only commands.