Ports and devices
An NXT has three motor outputs (A, B, C) and four sensor inputs (1–4). In nxt-rs each one is a value and a type:
| Port | Value / type | Low-level enum |
|---|---|---|
| Output A, B, C | A, B, C | OutputPort::A, … |
| Input 1–4 | S1, S2, S3, S4 | InputPort::S1, … |
The values are zero-sized: they only carry their type. Because each port has
its own type, a device’s type records which socket it is on. A motor on
output B is a Motor<B, T>, and a light sensor on input 3 is a
Light<S3, T, Reflected>. (T is the transport.)
Taking devices
A device claims its port. Take devices one at a time:
nxt_rs::doc_example(|brick| async move {
use nxt_rs::prelude::*;
let left = brick.motor(B)?;
let bumper = brick.touch(S1).await?;
let sonar = brick.ultrasonic(S4).await?;
Ok(()) });
Or several at once with take and a tuple of specs:
nxt_rs::doc_example(|brick| async move {
use nxt_rs::prelude::*;
let (left, right, bumper, sonar) =
brick.take((B.motor(), C.motor(), S1.touch(), S4.ultrasonic())).await?;
Ok(()) });
| Device | One at a time | Spec for take |
|---|---|---|
| Motor | brick.motor(B)? | B.motor() |
| Touch sensor | brick.touch(S1).await? | S1.touch() |
| Light sensor | brick.light(S3).await? | S3.light() |
| Sound sensor | brick.sound(S2).await? | S2.sound() |
| Colour sensor | brick.color(S3).await? | S3.color() |
| Ultrasonic sensor | brick.ultrasonic(S4).await? | S4.ultrasonic() |
| Any analog sensor | brick.raw_sensor(S2, ty, mode).await? | S2.raw(ty, mode) |
I²C device D | brick.i2c::<D, _>(S4).await? | S4.i2c::<D>() |
| Raw I²C bus | brick.i2c_bus(S4, address, powered).await? | — |
| Bare port | brick.port(S2)? | S2.port() |
Sensor getters are async because they configure the port on the brick.
Motors need no configuration, so brick.motor is synchronous.
What the compiler checks
Wrong kind of port. Motors only go on outputs and sensors only on inputs:
async fn f(brick: nxt_rs::Brick<nxt_rs::transport::Mock>) -> nxt_rs::Result<()> {
use nxt_rs::prelude::*;
let motor = brick.motor(S1)?; // error: `S1` is not a motor output
Ok(()) }
async fn f(brick: nxt_rs::Brick<nxt_rs::transport::Mock>) -> nxt_rs::Result<()> {
use nxt_rs::prelude::*;
let (m,) = brick.take((S1.motor(),)).await?; // error: no method `motor` on `S1`
Ok(()) }
A port used twice in one group:
async fn f(brick: nxt_rs::Brick<nxt_rs::transport::Mock>) -> nxt_rs::Result<()> {
use nxt_rs::prelude::*;
let (a, b) = brick.take((B.motor(), B.motor())).await?; // error: port `B` is taken twice
Ok(()) }
Using a device after giving it away. Switching a sensor’s mode, or converting a port, consumes the old value:
async fn f(brick: nxt_rs::Brick<nxt_rs::transport::Mock>) -> nxt_rs::Result<()> {
use nxt_rs::prelude::*;
let light = brick.light(S3).await?;
let ambient = light.ambient().await?;
light.read().await?; // error: use of moved value `light`
Ok(()) }
What is checked at run time
Taking a port that is already claimed returns Error::PortTaken. That
happens when separate calls, or separate parts of your program, ask for the
same port:
nxt_rs::doc_example(|brick| async move {
use nxt_rs::{prelude::*, Error};
let motor = brick.motor(B)?;
assert!(matches!(brick.motor(B), Err(Error::PortTaken(_))));
Ok(()) });
A take group claims all its ports at once. If any of them is in use,
nothing is claimed. If configuring a device fails halfway, the ports claimed
so far are released again.
Releasing ports
A claim lasts as long as the device. Dropping a device releases its port:
nxt_rs::doc_example(|brick| async move {
use nxt_rs::prelude::*;
let motor = brick.motor(B)?;
drop(motor);
let motor_again = brick.motor(B)?; // fine
Ok(()) });
To reuse a port for something else without letting go of it, call
release(). It resets the hardware (coasts the motor, or unconfigures the
sensor) and gives back the still-claimed Port:
nxt_rs::doc_example(|brick| async move {
use nxt_rs::prelude::*;
let touch = brick.touch(S1).await?;
let port = touch.release().await?; // still claimed by us
let light = port.into_light().await?; // same socket, new device
Ok(()) });
Inspecting claims
brick.port_claims() shows which ports are in use. It is public so that
abstractions you build on the low-level API can take part in the same claim
system:
nxt_rs::doc_example(|brick| async move {
use nxt_rs::{prelude::*, port::AnyPort};
let _motor = brick.motor(B)?;
assert_eq!(brick.port_claims().claimed(), [AnyPort::Output(OutputPort::B)]);
Ok(()) });
Claims belong to a Brick and are shared by all its clones. They only apply
to this library within this process. They can’t stop another program, or a
program running on the brick, from using the same port.