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:
sendwrites one telegram andrecvreturns one. Over Bluetooth-style links, each telegram has a 2-byte little-endian length prefix (frameadds it). USB has no prefix.- The returned futures must be
Send. - After a timeout the
Brickdrops your future and carries on. A late reply is harmless, becauseBrickdiscards replies whose opcode doesn’t match. kind()decides the default timeout and how the transport is reported.- Only nxt-rs’s own
UsbandMocktransports can carry USB-only commands.