I2C is a serial bus that allows connecting multiple peripherals using two shared lines: one carries data and the other carries the clock signal.
Sensors like the AHT20, the MPU6050, or many OLED displays exchange data using digital protocols. I2C (Inter-Integrated Circuit) is one of the most common.
In Arduino, this meant finding a specific library (sometimes there were 5 different ones for the same sensor), installing it, and struggling with the examples.
In DeviceScript, we can use a driver that translates the component’s protocol and exposes its measurements via services.
How the I2C Bus Works
The I2C protocol is wonderful because it only needs two wires (plus power) to connect dozens of sensors:
- SDA (Serial Data): Where data travels.
- SCL (Serial Clock): The clock that sets the pace.
Each sensor has a unique address (like an IP), so the microcontroller can ask “Sensor 0x38, what’s the temperature?” without the other sensors eavesdropping.
Board Pins
Before programming, we need to connect the wires. DeviceScript already knows which pins your board uses by default for I2C, but it’s good to review:
- ESP32 (Generic): Usually SDA: 21, SCL: 22.
- ESP32-C3: Varies by manufacturer, but often SDA: 4, SCL: 5 or similar.
- Raspberry Pi Pico W: SDA: GP4, SCL: GP5 (I2C0).
When using startService, DeviceScript will automatically configure these default pins. You don’t need to do Wire.begin(SDA, SCL) like in Arduino, although it can be configured if needed.
Using the Available Drivers
Here comes the new part. Instead of downloading a .zip, we use the JavaScript ecosystem.
DeviceScript groups most common drivers into a package called @devicescript/drivers. If you followed the installation steps, you probably already have it installed in your package.json.
If you want to check whether your sensor is supported, you don’t search Google for “Arduino library sensor X”; you search the DeviceScript catalog or directly on NPM.
To install the official drivers package (if you don’t have it):
- Open the terminal in VS Code (
Ctrl + ñ). - Type:
npm install @devicescript/driversThis will download definitions for dozens of sensors (SHT30, AHT20, MPU6050, SSD1306, etc.).
Example: Environmental Sensor
Let’s connect an AHT20 sensor (or an SHT30, they are very similar). These sensors measure temperature and humidity.
In C++, we would have to initialize the sensor, read the bytes, convert them… In DeviceScript, we use the startAHT20 function.
import { startAHT20 } from "@devicescript/drivers"
// 1. Start the driver
// Automatically looks for the sensor on the default I2C pins
const sensor = await startAHT20()
// 2. Use the services
// The AHT20 driver exposes TWO services: Temperature and Humidity
console.log("Sensor started. Waiting for data...")
sensor.temperature.reading.subscribe(temp => {
console.log(`Temperature: ${temp} °C`)
})
sensor.humidity.reading.subscribe(hum => {
console.log(`Humidity: ${hum} %`)
})What just happened?
The code does not directly import I2C or specify the address 0x38.
The startAHT20 driver:
- Configures the I2C bus.
- Detects the sensor.
- Returns an object containing the standard
TemperatureandHumidityservices.
The abstraction allows consuming a temperature service without coupling the rest of the logic to the sensor’s internal protocol.
Example: Accelerometer
Let’s raise the stakes. The MPU6050 is an IMU (Inertial Measurement Unit) sensor that provides acceleration and gyroscope data on 3 axes (X, Y, Z).
import { startMPU6050 } from "@devicescript/drivers"
// Start the driver
const imu = await startMPU6050()
// Subscribe to the accelerometer reading
imu.accelerometer.reading.subscribe(async (vector) => {
// vector is an array of 3 numbers [x, y, z]
const x = vector[0]
const y = vector[1]
const z = vector[2]
// Display data (using toFixed to avoid cluttering the console with decimals)
console.log(`Acceleration -> X: ${x.toFixed(2)}, Y: ${y.toFixed(2)}, Z: ${z.toFixed(2)}`)
})When you move the sensor, the values should change in real time.
If the Driver Doesn’t Exist
DeviceScript includes several drivers. If one doesn’t exist for your chosen sensor, we can talk to the I2C bus directly.
We can talk I2C “raw” using the @devicescript/i2c package. It’s a bit more advanced (similar to using Wire.h), but entirely possible.
import { i2c } from "@devicescript/i2c"
const ADDRESS = 0x23 // Example BH1750 (Light)
// Write command
await i2c.write(ADDRESS, Buffer.from([0x10])) // Start measurement
// Read 2 bytes
const data = await i2c.read(ADDRESS, 2)However, we will almost always prefer to look for or create a driver to encapsulate this logic and expose it as a clean service.