Running a Node.js environment (commonly referred to as a "node") on a Raspberry Pi for direct hardware control requires bridging the gap between JavaScript's asynchronous event loop and the Pi's memory-mapped GPIO and I2C registers. To successfully run a node on Raspberry Pi 5 for I2C sensor reading, you need the 64-bit Raspberry Pi OS (Bookworm), Node.js v20 LTS, and the i2c-bus npm package to interface with the /dev/i2c-1 character device.
This guide targets the Raspberry Pi 5 8GB variant running the official 64-bit Bookworm release. We will wire a BME280 environmental sensor, write a complete Node.js script with robust error handling, and debug the exact permission and I/O errors that frequently stall embedded JavaScript projects.
Hardware Spec Sheet & Pin Mapping
Before writing any code, verify your hardware. The Raspberry Pi 5 features a revised power delivery and I/O architecture compared to the Pi 4, but the standard 40-pin GPIO header maintains backward compatibility for I2C bus 1. Below is the exact bill of materials and pin mapping for this build.
| Component | Specific Model / Variant | Approx. Cost (2026) |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB RAM, 64-bit OS) | $80.00 |
| Sensor | BME280 I2C Temp/Humidity/Pressure Breakout | $6.00 - $10.00 |
| Wiring | Adafruit Pi Cobbler or standard 400-pt breadboard | $5.00 |
| Passives | 2x 4.7kΩ pull-up resistors (if breakout lacks them) | $0.10 |
I2C Pin Mapping Table
The BME280 communicates over I2C. Ensure you are connecting to the physical pins on the Pi 5 header, not the BCM GPIO numbers, to avoid wiring mistakes.
| Pi 5 Physical Pin | BCM GPIO / Function | BME280 Breakout Pin | Wire Color (Standard) |
|---|---|---|---|
| Pin 1 | 3V3 Power | VIN / VCC | Red |
| Pin 6 | Ground | GND | Black |
| Pin 3 | GPIO 2 (SDA1) | SDA | Blue |
| Pin 5 | GPIO 3 (SCL1) | SCL | Yellow |
Step-by-Step: OS Configuration and Wiring
- Enable the I2C Interface: Boot your Pi 5, open a terminal, and run
sudo raspi-config. Navigate to Interface Options > I2C and select Yes to enable the ARM I2C interface. Reboot the Pi. - Verify Hardware Detection: After rebooting, install the I2C tools via
sudo apt install i2c-tools. Runi2cdetect -y 1. You should see76or77in the grid, confirming the BME280 is responding on bus 1. - Install Node.js 20 LTS: The default apt repository often hosts outdated Node versions. Install the current LTS via NodeSource:
Verify withcurl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejsnode -v(should output v20.x.x). See the Node.js Release Schedule for LTS timelines. - Initialize the Project: Create a directory, initialize npm, and install the I2C library.
mkdir pi-i2c-node && cd pi-i2c-node npm init -y npm install i2c-bus
The Code: Reading I2C Sensor Data in Node.js
The following script targets the BME280's Chip ID register (0xD0). A correctly wired and powered BME280 will always return 0x60 from this register. This acts as a perfect "hello world" handshake before implementing complex temperature calibration math.
const i2c = require('i2c-bus');
// --- Pin & Address Definitions ---
const I2C_BUS_NUMBER = 1; // Raspberry Pi standard header I2C bus
const BME280_ADDRESS = 0x76; // Default BME280 I2C address (check i2cdetect if 0x77)
const CHIP_ID_REGISTER = 0xD0; // BME280 datasheet register for Chip ID
const EXPECTED_CHIP_ID = 0x60; // Hardcoded expected return value
async function verifySensorNode() {
let i2c1;
try {
console.log(`Opening I2C bus ${I2C_BUS_NUMBER}...`);
i2c1 = i2c.openSync(I2C_BUS_NUMBER);
// Read a single byte from the Chip ID register
const chipId = i2c1.readByteSync(BME280_ADDRESS, CHIP_ID_REGISTER);
console.log(`Read Chip ID: 0x${chipId.toString(16).toUpperCase()}`);
if (chipId !== EXPECTED_CHIP_ID) {
console.warn(`Warning: Expected 0x60, got 0x${chipId.toString(16)}. Check sensor variant.`);
} else {
console.log('Success: BME280 sensor verified and responding on the I2C node.');
}
} catch (err) {
// Granular error handling for embedded I/O failures
if (err.code === 'ENOENT') {
console.error(`CRITICAL: ${err.message}. Is I2C enabled in raspi-config?`);
} else if (err.code === 'EPERM') {
console.error(`CRITICAL: ${err.message}. Run script with sudo or add user to i2c group.`);
} else if (err.code === 'EREMOTEIO') {
console.error(`CRITICAL: ${err.message}. Check physical SDA/SCL wiring and pull-up resistors.`);
} else {
console.error(`Unexpected I2C Error: ${err.message}`);
}
} finally {
// Always release the file descriptor to prevent bus lockups
if (i2c1) {
i2c1.closeSync();
console.log('I2C bus closed.');
}
}
}
verifySensorNode();
Debugging: Exact Error Strings and Ranked Causes
When bridging JavaScript to Linux character devices, the V8 engine will throw system-level errors. If your script fails, here are the exact error strings and how to fix them.
1. "Error: ENOENT: no such file or directory, open '/dev/i2c-1'"
- Cause: The I2C kernel module is not loaded, or you are targeting the wrong bus number.
- Fix: Run
ls /dev/i2c*. If/dev/i2c-1is missing, runsudo raspi-configand re-enable I2C. On some custom Pi 5 device tree overlays, the bus might map to/dev/i2c-3; update theI2C_BUS_NUMBERconstant in the code accordingly.
2. "Error: EPERM: operation not permitted, open '/dev/i2c-1'"
- Cause: Your current Linux user lacks read/write permissions for the I2C device file.
- Fix: Add your user to the i2c group:
sudo usermod -aG i2c $USER. You must log out and log back in (or reboot) for group changes to take effect. Avoid running Node.js scripts withsudoin production, as it creates npm cache permission nightmares.
3. "Error: EREMOTEIO: Remote I/O error, read"
- Cause: The Pi sent a clock signal, but the sensor did not acknowledge (NACK). This is a physical layer failure.
- Fix: Verify SDA and SCL are not swapped. If using a raw BME280 chip on a breakout board without built-in resistors, you must add 4.7kΩ pull-up resistors between the SDA/SCL lines and the 3.3V rail. The Pi 5's internal pull-ups are often too weak for reliable I2C communication at 400kHz.
1. Run
i2cdetect -y 1 to confirm the hardware sees the address.2. Verify your user groups with the
groups command to ensure i2c is listed.3. Measure the voltage on the SDA and SCL pins with a multimeter; both should read close to 3.3V when idle.
Extending and Simplifying Your Node Build
Once the basic I2C handshake is working, you can extend this node on Raspberry Pi setup for production environments.
- Process Management: Do not rely on
node script.jsin a terminal. Installpm2globally (sudo npm install -g pm2) and runpm2 start sensor.js. This provides automatic restarts on crash and log aggregation. - MQTT Integration: To feed this data into Home Assistant, install the
mqttnpm package and publish the parsed sensor JSON to a local Mosquitto broker. - Simplifying with Node-RED: If writing raw JavaScript I2C code feels too low-level, you can install Node-RED, which is essentially a visual wrapper around Node.js. You can drag and drop a "Node-RED node on Raspberry Pi" to read I2C without writing the try/catch blocks manually. Consult the Raspberry Pi OS Documentation for the official Node-RED install script.
Frequently Asked Questions
Can I run a Node-RED node on Raspberry Pi alongside this custom script?
Yes, but they cannot access the I2C bus simultaneously if they are trying to control the same sensor. Linux file descriptors for /dev/i2c-1 can be opened by multiple processes, but concurrent read/write operations will corrupt the I2C transaction frames. If you run Node-RED, use its built-in I2C nodes rather than running a parallel custom Node.js script for the same hardware.
Why does my Node.js script crash with a segmentation fault on Pi 5?
Segmentation faults in Node.js on the Pi 5 are almost always caused by using an outdated, pre-compiled native C++ addon (like an old version of i2c-bus or pigpio) that was compiled for the ARMv7 (32-bit) architecture, while you are running a 64-bit ARMv8 OS. Delete your node_modules folder and run npm install again to force the C++ bindings to recompile against the Pi 5's 64-bit toolchain.
How do I auto-start my node script on Raspberry Pi boot?
The most robust method for the Bookworm OS release is using systemd. Create a service file at /etc/systemd/system/i2c-node.service. Define the ExecStart path pointing to your Node binary and script, set User=pi, and enable it via sudo systemctl enable i2c-node.service. This ensures the script waits for the networking and I2C kernel modules to fully initialize before executing.
Does the Raspberry Pi 5 require different I2C code than the Pi 4?
From a Node.js perspective, no. The i2c-bus library interacts with the Linux /dev/i2c-1 character device, which abstracts the underlying Broadcom (or RP1 on the Pi 5) silicon. However, the Pi 5's RP1 I/O controller handles pull-up resistors differently at the hardware level, making external 4.7kΩ physical pull-up resistors much more strictly required on the Pi 5 than on the Pi 4 for stable I2C communication.






