I²C devices
Digital sensors, such as LEGO’s ultrasonic sensor and most HiTechnic and Mindsensors products, talk I²C (LEGO calls it “low-speed”) over the input port. nxt-rs gives you three levels of access.
A raw bus
I2cBus reads and writes the registers of one device:
nxt_rs::doc_example(|brick| async move {
use nxt_rs::{prelude::*, sensor::I2cAddress};
// LEGO's ultrasonic sensor: address 0x02, needs the 9 V supply.
let bus = brick.i2c_bus(S4, I2cAddress::new(0x02)?, true).await?;
let product = bus.read_string(0x08, 8).await?; // "LEGO"
let distance = bus.read(0x42, 1).await?[0];
bus.write(0x41, &[0x02]).await?; // continuous measurement mode
Ok(()) });
| Method | Does |
|---|---|
read(register, len) | Reads up to 16 bytes starting at register |
read_string(register, len) | The same, as a NUL-terminated string |
write(register, data) | Writes up to 14 bytes starting at register |
transfer(tx, rx_len) | Sends raw bytes (address included) and reads rx_len back |
Addresses use the 8-bit form common in NXT documentation (the 7-bit address
shifted left by one), so they’re always even. powered: true switches on
the 9 V supply that some sensors need. Transfers retry automatically a few
times when the brick reports a bus error.
Typed devices
Wrap a bus in your own type and implement I2cDevice to make a sensor take
part in brick.i2c and take, just like the built-in ones:
use nxt_rs::{
Result,
port::InputId,
sensor::{I2cAddress, I2cBus, I2cDevice},
transport::Transport,
};
/// A HiTechnic compass sensor.
pub struct Compass<P: InputId, T: Transport>(I2cBus<P, T>);
impl<P: InputId, T: Transport> I2cDevice<P, T> for Compass<P, T> {
const ADDRESS: I2cAddress = match I2cAddress::new(0x02) {
Ok(a) => a,
Err(_) => unreachable!(),
};
// const NEEDS_9V: bool = false; (the default)
fn from_bus(bus: I2cBus<P, T>) -> Self {
Self(bus)
}
fn into_bus(self) -> I2cBus<P, T> {
self.0
}
}
impl<P: InputId, T: Transport> Compass<P, T> {
/// Heading in degrees, 0–359.
pub async fn heading(&self) -> Result<u16> {
let b = self.0.read(0x44, 2).await?;
Ok(u16::from_le_bytes([b[0], b[1]]))
}
}
nxt_rs::doc_example_with(|brick, sim| async move {
use nxt_rs::prelude::*;
sim.set_i2c_registers(InputPort::S2, 0x02, 0x44, &[90, 0]);
let compass: Compass<_, _> = brick.i2c(S2).await?;
println!("heading {}°", compass.heading().await?);
// or in a group
drop(compass);
let (compass, motor) = brick.take((S2.i2c::<Compass<_, _>>(), A.motor())).await?;
Ok(()) });
I2cDevice::init runs once after the port is configured. Override it to put
the device into a known mode (the ultrasonic sensor uses it to select
continuous measurement).
Low level
At the protocol level, an I²C transaction is LSWrite, then polling
LSGetStatus until the data is ready, then LSRead.
brick.direct().ls_transfer(port, tx, rx_len) does all three, retrying while
the bus is busy. The individual commands are there too: see
Direct commands.