Raspberry Pi Pico Thermometer
Photo by Guillaume Coupy / Unsplash

Raspberry Pi Pico Thermometer

As I work on building my farm, one thing I'm aiming to do is collect and analyze data in order to better understand what's happening and how to improve my operation. I figure building custom devices using raspberry pis are a good way to go about this, because doing so will allow me to begin collecting data as well as automating various tasks.

One simple way to get started on a large scale project like that is to begin with a simple thermometer to track the temperature and humidity in my germination room. To build the thermometer, I purchased:

Building the Circuit

To get started, we need to build the circuit that allows the Pico to interact with the BME280. We'll be utilizing 4 pins on each device:

BME280 Pico
VIN 3v3
GND GND
SCK GP1
SD1 GP0

Install the Firmware

Next, we can install the CircuitPython firmware onto the Pico and utilize some pre-built Adafruit libraries to help us utilize the BME280.

Since we're using a Pico W, we can download the firmware here, and the version 8 libraries here.

To install the firmware, simply drag and drop the uf2 file onto the Pico after connecting it to the computer. Then copy the "adafruit_bme280" directory and "adafruit_requests.mpy" file into the pre-created "lib" directory on the Pico.

We'll also create a secrets.py and thp_sensor.py to hold our code for the project. At the end, the directory structure should look something like this:

💡
You can utilize an IDE like Thonny to connect to the Pico and start coding

Adding the Logic

To keep things separated, all of the actual sensor logic will live in thp_sensor.py while secrets such as the wireless SSID & password will live in secrets.py. Code.py is essentially the "main" file that runs when powering the system on.

Let's start with the sensor logic:

import board
import busio

from adafruit_bme280 import basic as adafruit_bme280

class ThpSensor():
    def __init__(self):
        # Create a sensor object using the board's default I2C bus
        self.i2c = busio.I2C(board.GP1, board.GP0)
        self.bme280 = adafruit_bme280.Adafruit_BME280_I2C(self.i2c)
        
        # Set the location's pressure (hPa) at sea level
        self.bme280.sea_level_pressure = 1013.25
    
    
    def get_temperature_f(self):
        return self.bme280.temperature * 1.8 + 32
    
    
    def get_temperature_c(self):
        return self.bme280.temperature
    
    
    def get_humidity(self):
        return self.bme280.relative_humidity
    
    
    def get_pressure(self):
        return self.bme280.pressure
    
    
    def get_altitude(self):
        return self.bme280.altitude

thp_sensor.py

Here, we're creating a sensor class that contains the core logic for interacting with the BME280 and allows us to read the temperature, humidity, pressure, and altitude. Later, we'll add the ability to generate telemetry that can be sent elsewhere.

Now lets update our secrets file:

SSID="XXX"
PASSWORD="YYY"
TOKEN="ZZZ"

secrets.py

SSID and PASSWORD will be used to connect to the wireless, while TOKEN is needed to post to the API endpoint that handles our device telemetry

Finally, let's update code.py to test everything so far:

import secrets
import wifi

from thp_sensor import ThpSensor

sensor = ThpSensor()

wifi.radio.connect(ssid=secrets.SSID, password=secrets.PASSWORD)

print("Connected")
print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])
print("My IP address is", wifi.radio.ipv4_address)
print("Temperature f is", sensor.get_temperature_f())
print("Temperature c is", sensor.get_temperature_c())
print("Humidity is", sensor.get_humidity())
print("Pressure is", sensor.get_pressure())

code.py

Making it Useful

Currently, our thermometer turns on, connects to the wireless, and captures the current temperature, humidity, and pressure. Now we'll make it more useful by continually capturing that data and sending it to a server where it can be processed and stored.

Let's start with generating the telemetry:

import time

...

class ThpSensor():
    def __init__(self, deviceId, offsetSeconds):
        self.deviceId = deviceId
        self.offset = offsetSeconds

        ...

    def format_time(self, datetime):
        return "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.000+00:00".format(
            datetime.tm_year,
            datetime.tm_mon,
            datetime.tm_mday,
            datetime.tm_hour,
            datetime.tm_min,
            datetime.tm_sec
        )
    
    
    def generate_telemetry(self):
        utc_timestamp = int(time.time() + self.offset)
        deviceId = self.deviceId
        formatted_timestamp = self.format_time(time.localtime(utc_timestamp))
        
        return [
            {
                "deviceId": deviceId,
                "timestamp": formatted_timestamp,
                "key": "temperature",
                "value": self.get_temperature_f()
            },
            {
                "deviceId": deviceId,
                "timestamp": formatted_timestamp,
                "key": "humidity",
                "value": self.get_humidity()
            }
        ]

thp_sensor.py

💡
You'll notice that for the timestamp, I'm adding the offset to create a UTC timestamp as opposed to using local time. This will store the UTC value in the database, and the web client adjusts for that when creating Date objects or using dayjs.

Now code.py can be updated to utilize the new ThpSensor functions and post the telemetry to an API that saves it to a database

import adafruit_requests
import socketpool
import ssl
import time

...

#################################
#          Definitions          #
#################################

DEVICE_ID = "some-device-id"
TIMEZONE_OFFSET_SECONDS = 21600
TELEMETRY_URL = "https://some-api.com"
TIME_DELAY = 60 * 60 # one hour

def connect_to_wireless():
    wifi.radio.connect(ssid=secrets.SSID, password=secrets.PASSWORD)
    print("Connected")
    print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])
    print("My IP address is", wifi.radio.ipv4_address)

def configure_requests():
    global pool
    global requests
    pool = socketpool.SocketPool(wifi.radio)
    requests = adafruit_requests.Session(pool, ssl.create_default_context())
    
def post_telemetry(telemetry_json):
    global requests
    try:
        header = {
            "Authorization": "Bearer " + secrets.TOKEN
        }
        response = requests.post(TELEMETRY_URL, json=telemetry_json, headers=header)
        json_response = response.json()
        print("Response:", json_response)
    except Exception as e:
        print("Error posting telemetry:", str(e))

#################################
#             Logic             #
#################################

sensor = ThpSensor(DEVICE_ID, TIMEZONE_OFFSET_SECONDS)
connect_to_wireless()
configure_requests()

while True:
    telemetry = sensor.generate_telemetry()
        
    for item in telemetry:
        post_telemetry(item)
        
    time.sleep(TIME_DELAY)

code.py

With these changes, the thermometer will continually send the temperature and humidity to our API once per hour.

Of course, building the API endpoint and actually handling the requests from our thermometer requires additional work. That is outside the scope of this post though, and I plan to go into more detail on that later.

Kevin Williams

Springfield, Missouri
A full stack software engineer since 2018, specializing in Azure and .Net.