When engineers and European makers search for a Raspberry Pi Datenlogger, they are usually looking for a low-power, always-on environmental monitoring node that won't corrupt its SD card after three weeks. This guide gives you the exact blueprint to build one. We target the Raspberry Pi 4 Model B (2GB variant)—not the newer Pi 5, because the Pi 4's 1.2W idle power draw makes it vastly superior for 24/7 solar or battery-backed logging compared to the Pi 5's 2.5W baseline.
We will interface a Bosch BME280 sensor via I2C, write a fault-tolerant Python script that batches CSV writes to prevent SD card wear, and debug the inevitable I2C bus errors that plague beginners.
Sensor Selection & Hardware Spec Sheet
Choosing the right sensor is where most data logger builds fail. The DHT22 is a prototyping toy; for continuous logging, you need an I2C sensor with low self-heating and factory calibration. Below is a data-dense comparison of the most common environmental sensors available in 2026.
| Sensor Model | Temp Accuracy | Humidity Accuracy | I2C Address | Avg Current Draw | Approx. Price (2026) |
|---|---|---|---|---|---|
| BME280 (Bosch) | ±1.0°C | ±3% RH | 0x76 or 0x77 | 3.6 µA @ 1Hz | $9.95 (Adafruit 2652) |
| BME680 (Bosch) | ±1.0°C | ±3% RH | 0x76 or 0x77 | 12 µA (gas heater off) | $14.50 |
| SHT41 (Sensirion) | ±0.2°C | ±1.8% RH | 0x44 | 0.4 µA @ 1Hz | $11.99 |
| DHT22 (Aosong) | ±0.5°C | ±2% RH | N/A (1-Wire) | 1.5 mA (active) | $5.00 |
Source: Component datasheets from Bosch Sensortec and Sensirion.
We are using the BME280 for this build. It offers the best balance of low self-heating (which prevents the Pi's ambient heat from skewing readings), low power consumption, and robust I2C communication.
Parts List
- Board: Raspberry Pi 4 Model B (2GB RAM)
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652)
- Storage: SanDisk High Endurance 32GB microSD (Critical: standard cards will suffer write-exhaustion within months of continuous CSV logging)
- Wiring: 4x Female-to-Female silicone jumper wires (26 AWG)
- Enclosure: DIN-rail or IP65 vented project box with a sintered PTFE vent plug to equalize pressure without letting moisture in.
Pin Mapping Table
The Raspberry Pi I2C bus is fixed to specific hardware pins. Do not attempt to bit-bang I2C on random GPIOs for a production logger; hardware I2C is required for reliable timing.
| BME280 Breakout Pin | Raspberry Pi 4 GPIO | Physical Pin # | Function |
|---|---|---|---|
| VIN / VCC | 3V3 | 1 | Power (Do NOT use 5V on 3.3V breakouts) |
| GND | GND | 6 | Common Ground |
| SCK / SCL | GPIO 3 (SCL) | 5 | I2C Clock |
| SDI / SDA | GPIO 2 (SDA) | 3 | I2C Data |
Assembly & I2C Configuration
Before writing code, the I2C interface must be enabled and verified at the OS level. The Raspberry Pi's internal I2C pull-up resistors are roughly 50kΩ. The BME280 datasheet recommends 4.7kΩ pull-ups for 400kHz operation. Fortunately, the Adafruit 2652 breakout includes onboard 10kΩ pull-ups, which are sufficient for our 100kHz default bus speed. If you use a bare sensor module without pull-ups, your bus will fail intermittently.
- Flash Raspberry Pi OS Lite (64-bit) to your High Endurance SD card using the official Raspberry Pi Imager. Enable SSH and set your WiFi credentials in the OS customization menu.
- Boot the Pi and SSH in. Run
sudo raspi-config, navigate to Interface Options > I2C, and enable it. - Reboot the Pi, then install the I2C tools:
sudo apt update && sudo apt install i2c-tools python3-pip - Verify the hardware connection by running:
i2cdetect -y 1 - You should see
77(or76) in the grid output. If the grid is empty, check your wiring before proceeding. - Install the required Python libraries:
pip3 install adafruit-circuitpython-bme280
Standard microSD cards use TLC NAND flash with limited program/erase (P/E) cycles. Writing a CSV row every 5 seconds will kill a standard 32GB card in under 6 months due to journaling overhead. Always use a "High Endurance" or "PRO Endurance" card designed for dashcams and security cameras, which use MLC NAND or heavily over-provisioned TLC.
The Python Logging Script
This script targets the Pi 4 and uses the CircuitPython BME280 library. It includes fault tolerance: if the I2C bus drops, it logs the error and retries rather than crashing the systemd service. To further protect the SD card, we write to a temporary RAM buffer and flush to the CSV file every 10 minutes.
#!/usr/bin/env python3
"""
Raspberry Pi Datenlogger - BME280 CSV Logger
Target Board: Raspberry Pi 4 Model B (2GB)
Sensor: BME280 via I2C (Address 0x77)
"""
import board
import busio
import adafruit_bme280
import csv
import time
import os
import logging
from datetime import datetime
# --- Configuration ---
LOG_DIR = '/home/pi/logger_data'
CSV_FILE = os.path.join(LOG_DIR, 'environmental_log.csv')
READ_INTERVAL_SEC = 60 # Read sensor every 60 seconds
FLUSH_INTERVAL_SEC = 600 # Write to SD card every 10 minutes (600s)
I2C_ADDRESS = 0x77 # Adafruit BME280 default is 0x77
# --- Setup Logging ---
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# --- Pin Definitions & I2C Setup ---
# Hardware I2C: SDA -> GPIO 2 (Pin 3), SCL -> GPIO 3 (Pin 5)
try:
i2c = busio.I2C(board.SCL, board.SDA)
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=I2C_ADDRESS)
sensor.sea_level_pressure = 1013.25 # Adjust for your local elevation
logging.info('BME280 sensor initialized successfully.')
except ValueError as e:
logging.critical(f'Failed to find BME280 at address {hex(I2C_ADDRESS)}. Check wiring. Error: {e}')
exit(1)
# --- Ensure Directory Exists ---
os.makedirs(LOG_DIR, exist_ok=True)
# --- Initialize CSV if missing ---
if not os.path.exists(CSV_FILE):
with open(CSV_FILE, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['timestamp', 'temp_c', 'humidity_pct', 'pressure_hpa', 'altitude_m'])
def get_sensor_readings():
"""Fetch readings with error handling for I2C bus drops."""
try:
t = round(sensor.temperature, 2)
h = round(sensor.humidity, 2)
p = round(sensor.pressure, 2)
a = round(sensor.altitude, 2)
return t, h, p, a
except OSError as e:
logging.error(f'I2C Read Failure: {e}')
return None, None, None, None
def main():
ram_buffer = []
last_flush_time = time.time()
logging.info('Starting logging loop...')
while True:
try:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
t, h, p, a = get_sensor_readings()
if t is not None:
ram_buffer.append([timestamp, t, h, p, a])
logging.debug(f'Buffered: {t}C, {h}%, {p}hPa')
# Flush buffer to SD card to minimize write cycles
current_time = time.time()
if (current_time - last_flush_time) >= FLUSH_INTERVAL_SEC and ram_buffer:
with open(CSV_FILE, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerows(ram_buffer)
logging.info(f'Flushed {len(ram_buffer)} rows to {CSV_FILE}')
ram_buffer.clear()
last_flush_time = current_time
time.sleep(READ_INTERVAL_SEC)
except KeyboardInterrupt:
logging.info('Shutdown requested. Flushing remaining buffer...')
if ram_buffer:
with open(CSV_FILE, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerows(ram_buffer)
break
except Exception as e:
logging.error(f'Unexpected main loop error: {e}')
time.sleep(10) # Prevent tight loop on catastrophic failure
if __name__ == '__main__':
main()
Save this as logger.py and run it via python3 logger.py. For a permanent installation, wrap this in a systemd service so it auto-starts on boot and restarts on failure.
Debugging "Remote I/O Error" & Common Failures
When running I2C sensors on the Pi, you will eventually encounter the most notorious error in embedded Linux:
OSError: [Errno 121] Remote I/O error
This error means the Pi's I2C controller attempted to clock data out, but the slave device (the BME280) did not acknowledge (ACK) the transaction. The bus timed out. If your script crashes with this, here are the first three things to check:
- Run
i2cdetect -y 1: If the address (77) shows up asUU, another process (or a previous crashed instance of your script) has locked the I2C bus. Reboot the Pi or kill the rogue Python process. - Verify VCC Voltage: Measure the voltage between the breakout's VCC and GND pins with a multimeter. It must be exactly 3.3V. If you accidentally wired it to the Pi's 5V pin, you have likely fried the sensor's internal voltage regulator, causing it to drop off the bus under load.
- Check for Pull-Up Resistors: If you are using a cheap, unbranded BME280 breakout from an online marketplace, it may lack onboard pull-up resistors. The Pi's internal 50kΩ pull-ups are too weak to pull the SDA line high fast enough at 100kHz. Solder 4.7kΩ resistors between SDA/SCL and 3.3V.
Ranked Causes of Intermittent Bus Drops
| Rank | Cause | Diagnostic Test | Fix |
|---|---|---|---|
| 1 | Loose Dupont jumper wires | Wiggle wires while running i2cdetect in a loop |
Solder header pins or use JST-SH connectors |
| 2 | Missing Pull-Up Resistors | Scope the SDA line; look for slow, rounded rise times | Add 4.7kΩ external pull-ups to 3.3V |
| 3 | Capacitive load from long wires | Fails only when wires exceed 30cm (12 inches) | Lower I2C bus speed to 50kHz in /boot/config.txt |
| 4 | Sensor brownout / thermal throttle | Sensor drops out when Pi CPU spikes | Ensure Pi power supply delivers full 3.0A / 5.1V |
For deeper debugging of the Linux I2C subsystem, refer to the Linux Kernel I2C Dev Interface documentation, which explains how user-space applications interact with the /dev/i2c-1 character device.
Extending vs. Simplifying the Build
Not every project requires a full Python environment. Depending on your deployment constraints, you should adjust the architecture.
How to Simplify (The Bash Route)
If you are deploying 50 loggers and want to eliminate Python dependencies entirely to save SD card space and boot time, you can read the BME280 using raw I2C bash commands and a cron job. While reading the raw compensation registers in bash is painful, you can swap the BME280 for an SHT41 and use i2cget to pull raw hex bytes, converting them via bc or awk. Alternatively, use a lightweight C binary compiled statically. This reduces the OS footprint to under 200MB and eliminates Python memory leaks.
How to Extend (The IoT Route)
If this Datenlogger needs to feed a dashboard, writing to a local CSV is a dead end. To extend this build:
- Add MQTT: Import
paho-mqttand publish the JSON payload to a local Mosquitto broker. This allows Home Assistant to ingest the data instantly. - Add InfluxDB: Replace the CSV writer with the
influxdb-client-pythonlibrary. Time-series databases handle missing data points and downsampling natively, which CSV files cannot do without external processing. - Add a Watchdog Timer (WDT): The Pi 4 has a hardware watchdog. Enable it in
/boot/config.txt(dtparam=watchdog=on) and use thepython-watchdoglibrary to pet the dog in your loop. If the I2C bus locks up and the Python script freezes, the hardware watchdog will hard-reset the Pi automatically, ensuring 99.9% uptime in remote locations.
By selecting the right endurance-rated storage, respecting I2C electrical limits, and batching your file writes, your Raspberry Pi data logger will run for years without maintenance.






