The Quest for Local Weather Data

Anyone can check a weather app and see what temperature it is. Well, what you can’t do is figure out what temperature it is inside your own apartment. You might have a smart thermostat that can track and set a temperature, but can you trust how accurate that is? I certainly don’t. And a smart thermostat like Nest will only show you a limited amount of history at 1 hour granularity. What if you want more? Like minutely data for the past year? Guess I’ll just have to do it myself.

The Hardware

Actual embedded development was a little too intimidating, so I went with a Raspberry Pi 5. Actually 3, because I want the weather in multiple locations in my apartment and in my office.

Sensors

This project started with just a BME688 for basic temperature and humidity monitoring, but it quickly expanded to include all the sensors I could find on Adafruit:

SensorVoltageCurrent (mA)What it provides
PMSA003I3.3/5V100Particulate matter (air quality)
SCD-413.3/5V17CO2.
TSL25913.3/5V0.4Light levels
BME6883.3/5V1Gas, pressure, temp, humidity

Voltage and current is foreshadowing future issues.

Wiring

Circuit board v1

The software

The first version of the weather station was a simple python script. Adafruit provides fantastic python libraries for all the sensors I was using. As my first time working with I2C, this seemed like the Golden Path.

Debugging I2C issues, take 1

The weather station worked for a while. But occasionally weird things would show up. Some sensors would stop showing up, while others would get stuck on a particular value. Restarting the data collection process wouldn’t fix these issues, the only solution was rebooting the Pi.

hot_pi exited with code 1 (restarting)
hot_pi  | Traceback (most recent call last):
hot_pi  |   File "//app.py", line 13, in <module>
hot_pi  |     from hot_pi.seattle_weather import SeattleWeather
hot_pi  |   File "/hot_pi/seattle_weather.py", line 6, in <module>
hot_pi  |     from hot_pi.sensor import Sensor
hot_pi  |   File "/hot_pi/sensor.py", line 4, in <module>
hot_pi  |     import adafruit_bme680
hot_pi  |   File "/usr/local/lib/python3.10/dist-packages/adafruit_bme680.py", line 51, in <module>
hot_pi  |     from digitalio import DigitalInOut
hot_pi  |   File "/usr/local/lib/python3.10/dist-packages/digitalio.py", line 27, in <module>
hot_pi  |     from adafruit_blinka.microcontroller.bcm2712.pin import *
hot_pi  |   File "/usr/local/lib/python3.10/dist-packages/adafruit_blinka/microcontroller/bcm2712/pin.py", line 8, in <module>
hot_pi  |     D0 = Pin((4, 0))
hot_pi  |   File "/usr/local/lib/python3.10/dist-packages/adafruit_blinka/microcontroller/generic_linux/libgpiod/libgpiod_pin_2_x.py", line 34, in __init__
hot_pi  |     self._chip = gpiod.Chip(chip_id)
hot_pi  |   File "/usr/local/lib/python3.10/dist-packages/gpiod/chip.py", line 58, in __init__
hot_pi  |     self._chip = _ext.Chip(path)
hot_pi  | FileNotFoundError: [Errno 2] No such file or directory
hot_pi  | Exception ignored in: <function Pin.__del__ at 0x7fff84b5b130>
hot_pi  | Traceback (most recent call last):
hot_pi  |   File "/usr/local/lib/python3.10/dist-packages/adafruit_blinka/microcontroller/generic_linux/libgpiod/libgpiod_pin_2_x.py", line 38, in __del__
hot_pi  |     if self._line_request:
hot_pi  | AttributeError: 'Pin' object has no attribute '_line_request'

I considered elaborate monitoring solutions with hooks to reboot any misbehaving Pis. But I convinced myself it was Python and/or the Adafruit SDKs, thus the great Rust re-write began. In hindsight, I should have realized it was a hardware issue since the only way to fix the issue was to reboot the Pi.

The great Rust rewrite

I had dabbled with Rust for years, but nothing major. I knew Rust had the performance I needed to collect high precision weather events in my apartment every minute. Thanks to having already written the first version in Python I had a good sense of the architecture I wanted:

  • Generic sensor trait that I can use to create a list of sensors and iterate over
  • Store all data in a local file, and only delete once it’s been uploaded successfully
  • Initially I thought about implementing everything myself, but decided to be lazy and use existing crates for sensor communication: bosch-bme680, scd4x, pmsa003i, and tsl2591-eh-driver

After the quick rewrite everything was running smooth and efficiently (sipping CPU cycles and just 14 mb of memory). All I had left to do was wait and see if the problem was fixed.

Debugging I2C issues, take 2

At first the Rust re-write seemed much more stable. But then tragedy struck, the sensors started to fail again. Had I wasted hundreds of dollars (CO2 sensors are expensive, especially when you buy 3 of them) for nothing? Would I ever get to the bottom of what temperature and humidity my apartment is? Would I slowly die from CO2 poisoning and not realize it?

But there in the Rust logs was a new clue:

2026-01-28T06:32:51.815211Z  INFO Polling sensors...
2026-01-28T06:32:51.826657Z ERROR Error polling bme680: Failed to get sensor data: WriteReadError(I2CError { err: Errno(121) })
2026-01-28T06:32:51.836693Z ERROR Error polling tsl2591: Failed to read TSL2591 channels: I2cError(I2CError { err: Errno(121) })
2026-01-28T06:32:56.298361Z ERROR Error polling scd41: Failed to get measurement: I2c(I2CError { err: Errno(5) })
2026-01-28T06:32:56.298436Z ERROR Error polling pmsa003i: Failed to read PMSA003I sensor: I2C(I2CError { err: Errno(5) })

Errno(121) is EREMOTEIO and 5 is EIO which are both related to communication issues with the I2C devices which is exactly what’s happening, but why? The 4 boards were still showing up according to i2c-detect:

sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- 12 -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- 29 -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- 62 -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- 77

The smoking gun showed up in the kernel logs:

[336495.416190] i2c_designware 1f00074000.i2c: i2c_dw_handle_tx_abort: SDA stuck at low

Searching around for this issue quickly points to a hopefully easy fix:

  1. Pull-up resistors for SDA/SCL to compensate for the allegedly weak ones built into Pis
  2. Decoupling capacitors to smooth out power noise when the PMSA003I fan starts or SCD-41 heating element turns on

Up next, the circuit gets an upgrade.