Choosing the right editor for Raspberry Pi development has fundamentally changed since the launch of the Raspberry Pi 5 and the shift to Debian Bookworm. The days of coding directly on the Pi's desktop via a sluggish HDMI-connected monitor are largely behind us. Today, the most efficient workflow for serious embedded projects involves running the Pi headless and using a remote code editor, or leveraging lightweight native IDEs for quick scripting. But which setup actually saves you time when you are staring down an I2C bus fault at 2 AM?
In this guide, we will benchmark the two dominant workflows—VS Code Remote-SSH and Thonny—by building and debugging a concrete embedded project: an I2C environmental logger using a BME280 sensor and an SSD1306 OLED display. We will cover the exact hardware pitfalls of the Pi 5, the new Python environment restrictions in Bookworm, and the exact error strings you will encounter when things go wrong.
The Best Code Editor for Raspberry Pi: VS Code Remote vs. Thonny
Before we wire up the breadboard, let's look at the tools. The 'best' editor depends entirely on whether you are writing quick automation scripts or building complex, multi-file embedded applications. Below is a data-dense comparison of the top four editors used in the Pi ecosystem today.
| Editor / IDE | Best Use Case | RAM Overhead on Pi | Debugging & Intellisense | Setup Complexity |
|---|---|---|---|---|
| VS Code (Remote-SSH) | Complex projects, C++/Python, multi-file repos | ~150MB (Server component) | Excellent (Breakpoints, variable watch, full LSP) | Moderate (Requires SSH keys & VS Code extension) |
| Thonny | Beginner Python, quick sensor tests, education | ~120MB (Native Desktop) | Good (Visual step-through, variable explorer) | Low (Pre-installed on Pi OS Desktop) |
| Geany | Lightweight C/C++ scripting, bash scripts | ~40MB (Native Desktop) | Basic (Syntax highlighting, simple build commands) | Low (Available via apt) |
| Nano / Vim (Terminal) | Headless config edits, quick cron job fixes | < 5MB | None (Print-statement debugging only) | High (Steep learning curve for Vim) |
Project Build: I2C Environmental Logger (BME280 + SSD1306)
To properly test our editor's debugging capabilities, we need a project that interacts with hardware and is prone to specific, documentable errors. We are building an environmental logger that reads temperature, humidity, and pressure, then renders it on a local OLED.
Parts List
- Microcontroller: Raspberry Pi 5 (8GB variant) running Pi OS Bookworm (64-bit)
- Power Supply: Official Raspberry Pi 27W USB-C PD Power Supply (Critical for Pi 5 to avoid peripheral brownouts)
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652)
- Display: SSD1306 128x64 I2C OLED (Monochrome, 3.3V/5V tolerant variant)
- Wiring: 22 AWG solid core jumper wires, half-size breadboard
- Cooling: Raspberry Pi 5 Active Cooler (Required for sustained headless operation)
Pin Mapping Table
Both the BME280 and the SSD1306 will share the same hardware I2C bus (I2C1). The Raspberry Pi 5 defaults to GPIO 2 (SDA) and GPIO 3 (SCL) for this bus.
| Pi 5 GPIO (Physical Pin) | Function | BME280 Breakout | SSD1306 OLED |
|---|---|---|---|
| GPIO 2 (Pin 3) | SDA (I2C Data) | SDI / SDA | SDA |
| GPIO 3 (Pin 5) | SCL (I2C Clock) | SCK / SCL | SCL |
| 3V3 Power (Pin 1) | VCC (3.3V Logic) | VIN / VCC | VCC |
| Ground (Pin 6) | GND | GND | GND |
Writing and Debugging the Code in VS Code
For this build, we are targeting the Raspberry Pi 5 (8GB) running Bookworm. Bookworm introduces a massive change for Python developers: PEP 668. The OS now marks the system Python environment as 'externally managed', meaning you can no longer use sudo pip install globally without breaking OS dependencies. You must use a virtual environment (venv).
In VS Code Remote-SSH, open your integrated terminal and set up the environment:
mkdir ~/env-logger && cd ~/env-logger
python3 -m venv venv
source venv/bin/activate
pip install smbus2 luma.oled Pillow
Below is the complete, compilable Python script. It includes explicit I2C address definitions, hardware initialization, and robust error handling for the most common I2C faults.
#!/usr/bin/env python3
"""
Raspberry Pi 5 I2C Environmental Logger
Targets: BME280 (0x76) and SSD1306 (0x3C) on I2C Bus 1
"""
import time
import sys
from smbus2 import SMBus, i2c_msg
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import ImageFont
# --- PIN & BUS DEFINITIONS ---
I2C_BUS_NUMBER = 1 # Hardware I2C1 on Pi 5 (GPIO 2/3)
BME280_I2C_ADDR = 0x76 # Default Adafruit BME280 address
SSD1306_I2C_ADDR = 0x3C # Default SSD1306 address
# BME280 Registers (Simplified for demonstration)
BME280_REG_TEMP = 0xFA
BME280_REG_RESET = 0xE0
def init_bme280(bus):
"""Soft reset the BME280 to ensure clean state."""
try:
bus.write_byte_data(BME280_I2C_ADDR, BME280_REG_RESET, 0xB6)
time.sleep(0.1)
except OSError as e:
print(f"Failed to initialize BME280: {e}")
sys.exit(1)
def read_raw_temp(bus):
"""Read raw temperature bytes from BME280."""
# Read 3 bytes starting from temp register
msg = i2c_msg.read(BME280_I2C_ADDR, 3)
bus.i2c_rdwr(msg)
data = list(msg)
# Simplified raw conversion (real implementation requires calibration params)
raw_temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
return raw_temp
def main():
print("Initializing I2C Bus...")
try:
bus = SMBus(I2C_BUS_NUMBER)
except FileNotFoundError:
print("ERROR: I2C bus not found. Did you enable I2C in raspi-config?")
sys.exit(1)
init_bme280(bus)
# Initialize OLED Display
try:
serial_interface = i2c(port=I2C_BUS_NUMBER, address=SSD1306_I2C_ADDR)
oled = ssd1306(serial_interface)
except Exception as e:
print(f"ERROR: Failed to initialize SSD1306 OLED: {e}")
sys.exit(1)
print("System Online. Logging data...")
try:
while True:
raw_temp = read_raw_temp(bus)
# Mocking calibrated Celsius for display simplicity
approx_temp_c = 22.5
with canvas(oled) as draw:
draw.text((0, 0), "Env Logger v1.0", fill="white")
draw.text((0, 20), f"Raw ADC: {raw_temp}", fill="white")
draw.text((0, 40), f"Temp: {approx_temp_c}C", fill="white")
time.sleep(2.0)
except OSError as e:
# Catching the dreaded I2C Remote I/O Error
if e.errno == 121:
print(f"CRITICAL I2C FAULT: {e}")
print("Check wiring, pull-up resistors, and device power.")
else:
print(f"Unexpected OS Error: {e}")
except KeyboardInterrupt:
print("\nLogger stopped by user.")
finally:
bus.close()
oled.cleanup()
if __name__ == "__main__":
main()
Troubleshooting: When the I2C Bus and Pip Throw Errors
When working with embedded editors and hardware, the software will inevitably throw errors that map to physical reality. Here is how to debug the exact error strings you will encounter with this build.
Exact Error String 1: error: externally-managed-environment
If you try to run pip install smbus2 directly in the Pi 5 terminal without a virtual environment, you will hit this wall.
- Cause: Debian Bookworm enforces PEP 668 to prevent pip from overwriting system-level Python packages managed by
apt. - Fix: Always create a virtual environment (
python3 -m venv venv) or usepipxfor standalone CLI tools. Never use the--break-system-packagesflag unless you are willing to risk breaking your OS GUI.
Exact Error String 2: OSError: [Errno 121] Remote I/O error
This is the most common hardware-level error in Raspberry Pi I2C projects. It occurs when the Pi sends a clock signal but receives no ACK (acknowledge) bit back from the sensor.
The First Three Things to Check:
- Run
i2cdetect -y 1: If the BME280 (0x76) and OLED (0x3C) do not show up in the grid, the Pi physically cannot see them. If you seeUUinstead of a hex address, a kernel driver has already claimed the device (common with RTC modules). - Verify VCC Logic Levels: The Pi 5 GPIO is strictly 3.3V. If you are powering a 5V-only SSD1306 variant with 3.3V, the display might turn on, but the I2C logic high threshold won't be met, resulting in Errno 121.
- Measure the SDA/SCL Lines: Use a multimeter to check the voltage on the SDA and SCL lines while idle. They should read exactly 3.3V. If they read ~1.8V or float, your pull-up resistors are failing or missing.
Ranked Causes for I2C Failure
| Rank | Cause | Diagnostic Step |
|---|---|---|
| 1 | Loose breadboard connection or swapped SDA/SCL | Reseat wires; verify Pin 3 is SDA, Pin 5 is SCL. |
| 2 | I2C interface disabled in OS | Run sudo raspi-config -> Interface Options -> I2C -> Enable. |
| 3 | Missing or weak pull-up resistors (Pi 5 specific) | Add 4.7kΩ external pull-ups to 3.3V line. |
| 4 | Wrong I2C Bus Number in code | Pi 4 and 5 use Bus 1. Pi Zero W uses Bus 1. Older Pis used Bus 0. |
Extending and Simplifying the Build
Once you have the base logger running and rendering to the OLED via VS Code, you can easily adapt the project to fit different deployment scenarios.
How to Simplify the Build
If you are deploying this in a remote location (like a greenhouse or attic) where a screen is useless, drop the SSD1306 entirely. Remove the luma.oled dependencies, and modify the while loop to append the raw sensor data to a local CSV file using Python's built-in csv module. This reduces the RAM footprint by roughly 30MB and eliminates the most fragile physical component (the OLED ribbon cable).
How to Extend the Build
To turn this into a true IoT node, integrate the paho-mqtt library. Create a new Python module that publishes the calibrated temperature and humidity to an MQTT broker (like Mosquitto running on a Home Assistant server). You can set up a systemd service in VS Code to ensure the script runs on boot and automatically restarts if the I2C bus throws an unhandled fatal exception.
journalctl -u env-logger logs. Reserve Thonny for the days when you are sitting at a desk with a Pi 4 desktop setup and just need to quickly test a new sensor's I2C address before committing to a full VS Code workspace.






