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

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(()) });
MethodDoes
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.