An I2C driver is the layer that translates a device’s protocol into an interface the program can use. To write one, we need the datasheet and access to the bus.
If a sensor speaks I2C (or SPI), we can talk to it. We only need two things:
- Its Datasheet.
- The low-level functions
i2c.writeandi2c.read.
Today we will learn how to create a “Mini-Driver”. It won’t be a complete Jacdac specification (that’s for extra credit), but it will be a class or function that allows us to cleanly use the sensor in our project.
Reading the Datasheet
Before writing code, we need to know what to tell the sensor. For this example, we will implement a driver for the BH1750, a very common light sensor.
If we open its datasheet, we will find a command table like this:
| Instruction | Opcode (Hex) | Description |
|---|---|---|
| Power Down | 0x00 | Turns off the sensor |
| Power On | 0x01 | Turns on the sensor |
| Continuous H-Res | 0x10 | Starts measuring in high resolution |
And it tells us that after sending the measurement command, we must wait 120ms and then read 2 bytes (high byte and low byte) that form the lux value.
We have our plan!
- Send
0x10(Start Measurement). - Wait.
- Read 2 bytes.
- Combine them mathematically.
Using I2CSensorDriver
The @devicescript/i2c package provides direct bus access and register operations. For a complete driver, it is more convenient to inherit from I2CSensorDriver, which keeps the address and provides writeBuf and readBuf.
Let’s implement the reading in a class. The literal hex`10` creates the buffer with the measurement command without converting it to text.
import { sleep } from "@devicescript/core"
import { I2CSensorDriver } from "@devicescript/drivers"
export class BH1750Driver extends I2CSensorDriver<{ lux: number }> {
constructor(address = 0x23) {
super(address, { readCacheTime: 200 })
}
override async initDriver() {
await this.writeBuf(hex`01`) // Power On
}
override async readData() {
await this.writeBuf(hex`10`) // Continuous H-Resolution Mode
await sleep(180)
const data = await this.readBuf(2)
const raw = (data[0] << 8) | data[1]
return { lux: raw / 1.2 }
}
}Now we initialize the driver and read the value. read() executes readData() and returns the object with the measurement.
const mySensor = new BH1750Driver(0x23)
await mySensor.init()
setInterval(async () => {
const { lux } = await mySensor.read()
console.log(`Light: ${lux.toFixed(1)} lx`)
}, 1000)This example focuses on communication. A reusable driver should validate responses, document the measurement mode, and expose a standard service, for example LightLevel.