Target Board and Component Selection
For a dedicated, always-on environmental monitor, the Raspberry Pi Zero 2 W is the definitive target board. It draws roughly 1.2W at idle and peaks around 2.5W under load, making it vastly superior to the Pi 4 or Pi 5 for off-grid or solar-powered weather stations where every milliamp-hour counts. This build targets the Pi Zero 2 W running Raspberry Pi OS Lite (64-bit, Bookworm).
A common mistake in DIY meteorology is relying on the cheap DHT11 or DHT22 sensors. These suffer from severe hysteresis and drift. Instead, we use the Bosch BME280 for ambient air (temperature, humidity, barometric pressure) and a waterproof DS18B20 for ground or outdoor probe temperature. We will also integrate a BH1750 digital ambient light sensor for lux tracking.
Sensor Specifications and Bus Requirements
Before wiring, you must understand the electrical characteristics of your sensors. The I2C bus is particularly unforgiving of long wire runs and missing pull-up resistors. Below is the exact specification matrix for the components used in this build.
| Sensor Module | Measured Variables | Interface | Default Hex Address | Operating Range | Accuracy / Resolution |
|---|---|---|---|---|---|
| BME280 (Bosch) | Temp, Humidity, Pressure | I2C / SPI | 0x76 (SDO=GND) or 0x77 | -40°C to +85°C | ±1.0°C / ±3% RH / ±1 hPa |
| DS18B20 (Maxim) | Outdoor/Ground Temp | 1-Wire | N/A (ROM Serial) | -55°C to +125°C | ±0.5°C (from -10°C to +85°C) |
| BH1750 (ROHM) | Ambient Light (Lux) | I2C | 0x23 (ADDR=L) | 1 to 65,535 lx | ±20% (Resolution down to 1 lx) |
| Pi Zero 2 W | Compute / Logging | GPIO Header | N/A | 0°C to +70°C (SoC) | Quad-core 1.0GHz / 512MB RAM |
Wiring and Pin Mapping
Bus capacitance on the I2C lines (SDA/SCL) is strictly limited to 400pF by the NXP I2C specification. Standard 22 AWG jumper wire adds roughly 15-20pF per foot. If you route your BME280 outside the enclosure using 15 feet of cable, you will exceed the capacitance limit, resulting in corrupted data or total bus lockups. Keep I2C wire runs under 1 meter (3 feet). If you need longer runs, use an I2C bus extender IC like the P82B96.
| Pi Zero 2 W Pin (BCM) | Physical Pin # | Target Sensor Pin | Notes & Required Passives |
|---|---|---|---|
| 3.3V Power | 1 | BME280 VCC, BH1750 VCC | Do NOT use 5V; BME280 is strictly 3.3V logic. |
| 5V Power | 2 | DS18B20 VDD (Red) | Required for external power mode (not parasitic). |
| GPIO 2 (SDA1) | 3 | BME280 SDA, BH1750 SDA | Pi has onboard 1.8k pull-ups; sufficient for <1m runs. |
| GPIO 3 (SCL1) | 5 | BME280 SCL, BH1750 SCL | Shared I2C clock line. |
| GPIO 4 (1-Wire) | 7 | DS18B20 DATA (Yellow) | Requires 4.7kΩ pull-up resistor to 3.3V. |
| Ground | 6, 9, 14 | All Sensor GND pins | Ensure common ground reference across all modules. |
Software Setup and Python Data Logger
First, enable the I2C and 1-Wire interfaces via the Raspberry Pi configuration tool. Open your terminal and run sudo raspi-config. Navigate to Interface Options -> I2C (Enable), and Interface Options -> 1-Wire (Enable). Reboot the Pi.
Next, install the required Python libraries in a virtual environment to comply with PEP 668 (externally managed environments in Bookworm):
python3 -m venv ~/weather_env
source ~/weather_env/bin/activate
pip install adafruit-circuitpython-bme280 w1thermsensor smbus2
Below is the complete, compilable Python script. It initializes the sensors, handles bus exceptions gracefully, and logs the output to both the console and a CSV file.
import time
import csv
import os
import board
import busio
import adafruit_bme280
from w1thermsensor import W1ThermSensor, Sensor
from smbus2 import SMBus
from datetime import datetime
# --- Pin & Address Definitions ---
I2C_BUS_ID = 1
BH1750_ADDR = 0x23
BME280_ADDR = 0x76 # Assumes SDO is tied to GND
CSV_FILE = "weather_log.csv"
# --- Hardware Initialization ---
try:
# Initialize I2C bus for BME280 (using Adafruit Blinka)
i2c = busio.I2C(board.SCL, board.SDA)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=BME280_ADDR)
bme280.sea_level_pressure = 1013.25 # Adjust to your local altitude
# Initialize 1-Wire for DS18B20
ds18b20 = W1ThermSensor(Sensor.DS18B20)
# Initialize SMBus for BH1750 (Light Sensor)
smbus = SMBus(I2C_BUS_ID)
print("[INFO] All sensors initialized successfully.")
except FileNotFoundError as e:
print(f"[FATAL] I2C bus not found. Did you enable I2C in raspi-config? Error: {e}")
exit(1)
except Exception as e:
print(f"[FATAL] Hardware initialization failed: {e}")
exit(1)
# --- CSV Header Setup ---
if not os.path.exists(CSV_FILE):
with open(CSV_FILE, mode='w', newline='') as file:
writer = csv.writer(file)
writer.writerow(["Timestamp", "Temp_C", "Humidity_%", "Pressure_hPa", "Ground_Temp_C", "Lux"])
# --- Main Logging Loop ---
try:
while True:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Read BME280
try:
amb_temp = round(bme280.temperature, 2)
humidity = round(bme280.relative_humidity, 2)
pressure = round(bme280.pressure, 2)
except OSError as e:
print(f"[WARN] BME280 I2C Read Error: {e}")
amb_temp, humidity, pressure = None, None, None
# Read DS18B20
try:
ground_temp = round(ds18b20.get_temperature(), 2)
except Exception as e:
print(f"[WARN] DS18B20 Read Error: {e}")
ground_temp = None
# Read BH1750
try:
raw_lux = smbus.read_i2c_block_data(BH1750_ADDR, 0x10, 2)
lux = round((raw_lux[0] << 8 | raw_lux[1]) / 1.2, 1)
except OSError as e:
print(f"[WARN] BH1750 I2C Read Error: {e}")
lux = None
# Log to Console and CSV
print(f"{timestamp} | {amb_temp}°C | {humidity}% | {pressure}hPa | Gnd:{ground_temp}°C | {lux}lx")
with open(CSV_FILE, mode='a', newline='') as file:
writer = csv.writer(file)
writer.writerow([timestamp, amb_temp, humidity, pressure, ground_temp, lux])
# BME280 requires a pause between high-resolution reads to avoid self-heating
time.sleep(60)
except KeyboardInterrupt:
print("\n[INFO] Logging stopped by user.")
finally:
smbus.close()
Debugging: The First Three Things to Check
When deploying embedded hardware outdoors, things will fail. If your script crashes or returns null data, do not rewrite the code. Check the physical and kernel layers first using this ranked decision path.
1. The I2C NACK: OSError: [Errno 121] Remote I/O error
This exact string means the Pi sent an address over the SDA line, but no device acknowledged it (NACK).
Causes & Fixes:
- Wrong Address: Many cheap BME280 breakouts have the SDO pin pulled HIGH by default, making the address
0x77instead of0x76. Runi2cdetect -y 1in the terminal. If you see77, update theBME280_ADDRvariable in the code. - Missing Pull-ups: If your breakout board lacks onboard pull-up resistors and you are using long wires, the signal edges are too slow. Add 4.7kΩ resistors to SDA and SCL.
- Bus Lockup: A brownout can leave the I2C bus in a locked state. Reboot the Pi or physically disconnect the VCC line to the sensors for 5 seconds to reset the bus logic.
2. The 1-Wire Ghost: w1thermsensor.errors.NoSensorFoundError
The Python library cannot find the kernel module mapping for the DS18B20.
Causes & Fixes:
- Missing Device Tree Overlay: In newer Raspberry Pi OS versions, the 1-Wire overlay must be explicitly defined. Open
/boot/firmware/config.txtand ensure the linedtoverlay=w1-gpio,gpiopin=4is present at the bottom. Reboot. - Parasitic Power Failure: If you wired the DS18B20 in parasitic power mode (VDD tied to GND), the Pi's GPIO cannot source enough current for the temperature conversion phase. Wire VDD to 3.3V or 5V explicitly.
3. The Environment Block: error: externally-managed-environment
This occurs when you try to run pip install globally on Raspberry Pi OS Bookworm.
Fix: You must use a virtual environment (as shown in the setup steps) or the pipx utility. Never use the --break-system-packages flag, as it can corrupt the OS-level Python dependencies required by raspi-config.
Extending or Simplifying the Build
This architecture is modular. Depending on your deployment constraints, you can scale the hardware up or down without rewriting the core logic.
How to Simplify (Low Cost / Low Power)
If you are deploying a dozen nodes on a tight budget, drop the DS18B20 and BH1750. The BME280 alone provides highly accurate ambient temperature, humidity, and pressure. By removing the 1-Wire bus, you eliminate the need for the 4.7kΩ pull-up resistor and reduce the Pi's idle current draw by roughly 1mA. You can power this simplified node for weeks on a standard 10,000mAh LiPo battery pack.
How to Extend (Professional Telemetry)
To turn this into a professional telemetry node, add a LoRaWAN HAT (like the Dragino LoRa/GPS HAT) and an anemometer (wind speed). Wind speed sensors typically use a reed switch that closes once per revolution. You can wire this to a spare GPIO (e.g., GPIO 17) and use the Pi's hardware interrupt capabilities via the gpiozero library's Button class to count pulses without blocking the main Python loop. Push the aggregated CSV data to an InfluxDB instance using the influxdb-client Python library, and visualize it on a Grafana dashboard.






