para:i2c
import i2c from "para:i2c";A small module wrapping the Linux i2c-dev character device (/dev/i2c-N). Combined-message transactions go through I2C_RDWR; SMBus shortcuts (Read/Write Byte/Word/Block) go through I2C_SMBUS. No vendored libi2c; pure ioctl on the kernel character device.
para:i2c is currently Linux-only.
buses()
Section titled “buses()”Synchronously enumerates /dev/i2c-N entries with the driver-supplied bus name and decoded capability flags.
i2c.buses();// [// { path: "/dev/i2c-1", name: "bcm2835 (i2c@7e804000)",// capabilities: ["i2c", "smbus_quick", "smbus_read_byte_data", ...] },// ...// ]The name is read from /sys/class/i2c-dev/<dev>/name. Capabilities are decoded from the kernel’s I2C_FUNCS bitmap — useful for verifying that the controller exposes the SMBus subset your sensor needs before you try to talk to it.
open(path)
Section titled “open(path)”Opens a bus. Returns a Bus.
await using bus = i2c.open("/dev/i2c-1");bus.path; // "/dev/i2c-1"bus.name; // driver name from sysfsbus.capabilities; // decoded I2C_FUNCS flagsBus is AsyncDisposable — await using releases the fd at scope exit.
bus.scan()
Section titled “bus.scan()”Probe the bus for device addresses that ack. Returns the 7-bit addresses present, matching i2cdetect -y N’s “Quick” probe mode.
const present = await bus.scan();// [0x40, 0x76]Skips the reserved 0x00–0x02 + 0x78–0x7F ranges. Some sensors latch on data byte writes — scan() uses Quick (no data byte) everywhere to avoid corrupting them.
bus.device(addr)
Section titled “bus.device(addr)”Bind to a 7-bit address. Cheap — no syscall. Returns a Device you can read/write from.
const dev = bus.device(0x76);
await dev.write(Uint8Array.of(0xF7)); // raw writeconst buf = await dev.read(6); // raw readdev.transact(segments)
Section titled “dev.transact(segments)”Combined-message transaction — the right shape for most chip protocols. Each segment is { write: Uint8Array } or { read: number }. The kernel issues all segments back-to-back with a repeated start (no STOP between segments), so register-access patterns work correctly without the device closing the bus state mid-transaction.
const [, payload] = await dev.transact([ { write: Uint8Array.of(0xF7) }, // register address { read: 6 }, // 6-byte payload]);// payload: Uint8Array(6) — the read resultReturns one slot per segment: read segments yield a Uint8Array, write segments yield undefined (so indices stay aligned with the input array).
dev.smbus
Section titled “dev.smbus”SMBus shortcuts. Most chips speak a strict SMBus subset; these helpers are an ergonomic wrapper over ioctl(I2C_SMBUS).
const id = await dev.smbus.readByte(0xD0); // read byte at registerconst t = await dev.smbus.readWord(0xFA); // read word at registerawait dev.smbus.writeByte(0xF4, 0x27); // write byte to registerawait dev.smbus.writeWord(0xF4, 0x0327); // write word to registerconst ack = await dev.smbus.quick(true); // SMBus Quick — true = write-direction probe
// Variable-length block read / write:const block = await dev.smbus.readBlock(0xC2);await dev.smbus.writeBlock(0xC2, Uint8Array.of(0x01, 0x02, 0x03));Pi 5 note
Section titled “Pi 5 note”On stock Pi 5, the user header’s i2c-1 isn’t enabled by default. Add dtparam=i2c_arm=on to /boot/firmware/config.txt and reboot. The internal buses (/dev/i2c-11, /dev/i2c-12) are always present but route to non-header peripherals (HDMI, camera CSI, etc.).
Reactive sensor reads
Section titled “Reactive sensor reads”Pair with para:signals’ fromInterval to turn any periodic register read into a reactive Signal. The shape is identical for any chip — replace the register address and decoding with whatever the sensor’s datasheet says.
import i2c from "para:i2c";import sigs from "para:signals";
await using bus = i2c.open("/dev/i2c-1");const sensor = bus.device(0x76);
// Poll a temperature register every 500 ms.const temp = sigs.fromInterval( () => sensor.smbus.readWord(0xFA), 500,);
// Threshold-derived state — auto-recomputes when temp updates.const isHot = sigs.derived(() => (temp.signal.get() ?? 0) > 30);
sigs.effect(() => { if (isHot.get()) console.log("HOT:", temp.signal.get());});import i2c from "para:i2c";import sigs from "para:signals";
await using bus = i2c.open("/dev/i2c-1");const sensor = bus.device(0x76);
const temp = sigs.fromInterval( () => sensor.smbus.readWord(0xFA), 500,);
const isHot = sigs.derived(() => (temp.signal.get() ?? 0) > 30);
effect { if (isHot.get()) console.log("HOT:", temp.signal.get());}fromInterval returns { signal, dispose }. Disposing stops the polling — useful when switching between sensors at runtime.