Pi Pico based Weather Station Project

I’ve been working on this for a while. It started out using a Raspberry Pi Zero but I have switched over to using a Pi Pico instead.
My Enviro (code) based Weather Station Build - Discussion / Projects - Pimoroni Buccaneers

Hardware wise I have the following:
Pico Lipo 16mb
BME680
BME280
LTR-559
VEML6075
RV3028
and two Pico Display pack V2’s.

The two Pico Display Packs are mounted on Perma Proto Half size boards. Then custom wired to another Perma Proto that has the Pico on it. Male headers on the Perma Proto for the Display Packs and female for the Pico. This lets me use different Chip Select and Backlight pins for each display. SPI0 CE0 and CE1 etc.
I’m using a custom st7789 Micro Python Library that is going to be released as a Unifying driver.
Unified display drivers · Issue #309 · pimoroni/pimoroni-pico (github.com)

The buttons and LED’s aren’t wired up, but will be at some point. The default GPIO will be used for Display 1, and free pins for Display 2.

The BME280 is used for the indoor readings and the BME680 for the outdoor. Right now its just a prototype proof of concept setup. It’s my coding setup. The QWICC header on the Pico Lipo goes to a hub, then to the BME280, the BME680 and a Perma Proto with the LTR-559, VEML6075 and RV3028 soldered to it. At some point I’ll split things up with a Pico Lipo, BME280 and displays indoors. And a Pico Lipo, LTR-559, VEML6075, RV3028, and BME680 outside. Plus wind and rain sensors.
?? means I haven’t coded for that sensor just yet. Only just recently got the use of the two Display Packs.

1 Like

Wall of code to follow. I had to get creative to use the two display packs with just the one frame buffer. There isn’t enough memory space for two frame buffers.
What I do is write to the buffer display 1 only, update the display, then clear the buffer. Then write to it for display 2 only, update the display, then clear the buffer. wash, rinse and repeat.

import time
import st7789

from machine import ADC, Pin
from pimoroni_i2c import PimoroniI2C
from breakout_bme280 import BreakoutBME280
from breakout_bme68x import BreakoutBME68X
from breakout_ltr559 import BreakoutLTR559
from breakout_rtc import BreakoutRTC

vsys = ADC(29)              
charging = Pin(24, Pin.IN)
conversion_factor = 3 * 3.3 / 65535

full_battery = 4.2
empty_battery = 2.8

frame_buffer = bytearray(240 * 320 * 2)
display1 = st7789.ST7789(width=240, height=320, buffer=frame_buffer, slot=0)
display2 = st7789.ST7789(width=240, height=320, buffer=frame_buffer, slot=1)
display1.set_backlight(1.0)
display2.set_backlight(1.0)

i2c = PimoroniI2C(sda=(4), scl=(5))

bme_out = BreakoutBME68X(i2c)
bme_in = BreakoutBME280(i2c,0x77)

ltr = BreakoutLTR559(i2c)

rtc = BreakoutRTC(i2c)
rtc.set_backup_switchover_mode(1)
rtc.set_24_hour()
rtc.update_time()

min_temp_in = None
max_temp_in = None
min_temp_out = None
max_temp_out = None
min_press_out = None
max_press_out = None

start_time = time.time()

rtc.enable_periodic_update_interrupt(True)

while True:
    
    if rtc.read_periodic_update_interrupt_flag():
        rtc.clear_periodic_update_interrupt_flag()

        if rtc.update_time():
            rtc_date = rtc.string_date()
            rtc_time = rtc.string_time()
    
    time_elapsed = time.time() - start_time
        
    # read the sensors
    temp_in, press_in, hum_in = bme_in.read()
    temp_out, press_out, hum_out, gas_resistance, status, gas_index, meas_index = bme_out.read() 
            
    # convert pressure to mb
    pressuremb = press_out / 100
    
    # header
    display1.set_pen(255, 255, 255)
    display1.text("indoor", 15, 0, 240, 2)
    display1.text("outdoor", 140, 0, 240, 2)
    display1.text("temperature", 25, 20, 240, 3)
    display1.text("min", 95, 78, 240, 3)
    display1.text("max", 92, 100, 240, 3)
    display1.text("humidity", 50, 130, 240, 3)
    display1.text("pressure", 50, 200, 240, 3)
        
    # indoor temperature
    temp_in = round(temp_in)
    
    if temp_in < 0:
        display1.set_pen(255, 255, 255)
    elif 0 <= temp_in < 12:
        display1.set_pen(0, 0, 255)
    elif 12 <= temp_in < 17:
        display1.set_pen(255, 255, 0)
    elif 17 <= temp_in < 25:
        display1.set_pen(0, 255, 0)
    elif 25 <= temp_in < 30:
        display1.set_pen(255, 140, 0)
    elif temp_in >= 30:
        display1.set_pen(255, 0, 0)
    else:
        display1.set_pen(0, 0, 0)
        
    display1.text('{:.0f}'.format(temp_in) + '`c', 15, 45, 240, 4)

    # outdoor temperature
    temp_out = round(temp_out)
    if temp_out < 0:
        display1.set_pen(255, 255, 255)
    elif 0 <= temp_out < 12:
        display1.set_pen(0, 0, 255)
    elif 12 <= temp_out < 17:
        display1.set_pen(255, 255, 0)
    elif 17 <= temp_out < 25:
        display1.set_pen(0, 255, 0)
    elif 25 <= temp_out < 30:
        display1.set_pen(255, 140, 0)
    elif temp_out >= 30:
        display1.set_pen(255, 0, 0)
    else:
        display1.set_pen(0, 0, 0)
        
    display1.text('{:.0f}'.format(temp_out) + '`c', 160, 45, 240, 4)

    # indoor min max temperature readings
    if time_elapsed > 5:
        if min_temp_in is not None and max_temp_in is not None:
            if temp_in < min_temp_in:
                min_temp_in = int(temp_in)
            elif temp_in > max_temp_in:
                max_temp_in = int(temp_in)
        else:
            min_temp_in = int(temp_in)
            max_temp_in = int(temp_in)
            
    if min_temp_in is not None and max_temp_in is not None:
        min_string_in = ('{:.0f}'.format(min_temp_in))
        max_string_in = ('{:.0f}'.format(max_temp_in))
    else:
        min_string_in = ""
        max_string_in = ""
        
    if min_temp_in is not None and max_temp_in is not None:
        if min_temp_in < 0:  # very cold
            display1.set_pen(255, 255, 255)
        elif 0 <= min_temp_in < 12:  # cold
            display1.set_pen(0, 0, 255)
        elif 12 <= min_temp_in < 17: # cool
            display1.set_pen(255, 255, 0)
        elif 17 <= min_temp_in < 25: # warm
            display1.set_pen(0, 255, 0)
        elif 25 <= min_temp_in < 30: # hot
            display1.set_pen(255, 140, 0)
        elif min_temp_in >= 30:      # very hot
            display1.set_pen(255, 0, 0)
        else:
            display.set_pen(0, 0, 0)
            
        display1.text(min_string_in, 25, 78, 240, 3)
        
        if max_temp_in < 0:  # very cold
            display1.set_pen(255, 255, 255)
        elif 0 <= max_temp_in < 12:  # cold
            display1.set_pen(0, 0, 255)
        elif 12 <= max_temp_in < 17: # cool
            display1.set_pen(255, 255, 0)
        elif 17 <= max_temp_in < 25: # warm
            display1.set_pen(0, 255, 0)
        elif 25 <= max_temp_in < 30: # hot
            display1.set_pen(255, 140, 0)
        elif max_temp_in >= 30:      # very hot
            display1.set_pen(255, 0, 0)
        else:
            display1.set_pen(0, 0, 0)
            
        display1.text(max_string_in, 25, 100, 240, 3)  

    # outdoor min max temperature readings
    if time_elapsed > 5:
        if min_temp_out is not None and max_temp_out is not None:
            if temp_out < min_temp_out:
                min_temp_out = int(temp_out)
            elif temp_out > max_temp_out:
                max_temp_out = int(temp_out)
        else:
            min_temp_out = int(temp_out)
            max_temp_out = int(temp_out)
            
    if min_temp_out is not None and max_temp_out is not None:
        min_string_out = ('{:.0f}'.format(min_temp_out))
        max_string_out = ('{:.0f}'.format(max_temp_out))
    else:
        min_string_out = ""
        max_string_out = ""
        
    if min_temp_out is not None and max_temp_out is not None:
        if min_temp_out < 0:  # very cold
            display1.set_pen(255, 255, 255)
        elif 0 <= min_temp_out < 12:  # cold
            display1.set_pen(0, 0, 255)
        elif 12 <= min_temp_out < 17: # cool
            display1.set_pen(255, 255, 0)
        elif 17 <= min_temp_out < 25: # warm
            display1.set_pen(0, 255, 0)
        elif 25 <= min_temp_out < 30: # hot
            display1.set_pen(255, 140, 0)
        elif min_temp_out >= 30:      # very hot
            display1.set_pen(255, 0, 0)
        else:
            display1.set_pen(0, 0, 0)
            
        display1.text(min_string_out, 180, 78, 240, 3)
        
        #max_temp_out = round(max_temp_out)
        
        if max_temp_out < 0:  # very cold
            display1.set_pen(255, 255, 255)
        elif 0 <= max_temp_out < 12:  # cold
            display1.set_pen(0, 0, 255)
        elif 12 <= max_temp_out < 17: # cool
            display1.set_pen(255, 255, 0)
        elif 17 <= max_temp_out < 25: # warm
            display1.set_pen(0, 255, 0)
        elif 25 <= max_temp_out < 30: # hot
            display1.set_pen(255, 140, 0)
        elif max_temp_out >= 30:      # very hot
            display1.set_pen(255, 0, 0)
        else:
            display1.set_pen(0, 0, 0)
            
        display1.text(max_string_out, 180, 100, 240, 3)

    # indoor humidity
    
    if hum_in < 30:
        display1.set_pen(255, 140, 0)
    elif 30 <= hum_in < 61:
        display1.set_pen(0, 255, 0)
    elif 61 <= hum_in < 81:
        display1.set_pen(255, 255, 0)
    elif hum_in >= 81:
        display1.set_pen(255, 0, 0)
    else:
        display1.set_pen(0, 0, 0)
        
    #display1.text((int(hum_in)), 20, 80, 240, 3)
    display1.text('{:.0f}'.format(hum_in) + '%', 10, 160, 240, 4)
  
    # outdoor humidity
    
    if hum_out < 30:
        display1.set_pen(255, 140, 0)
    elif 30 <= hum_out < 61:
        display1.set_pen(0, 255, 0)
    elif 61 <= hum_out < 81:
        display1.set_pen(255, 255, 0)
    elif hum_out >= 81:
        display1.set_pen(255, 0, 0)
    else:
        display1.set_pen(0, 0, 0)
        
    #display1.text((int(hum_in)), 20, 80, 240, 3)
    display1.text('{:.0f}'.format(hum_out) + '%', 160, 160, 240, 4)
    
    # outddor pressure reading on display 2    
    if pressuremb < 982:
        display1.set_pen(255, 0, 0)
        display1.text('{:.0f}'.format(pressuremb) + 'mb', 50, 230, 240, 4)
        display1.text("very low", 30, 265, 240, 4)
    elif 982 <= pressuremb < 1004:
        display1.set_pen(255, 255, 0)
        display1.text('{:.0f}'.format(pressuremb) + 'mb', 50, 230, 240, 4)
        display1.text("low", 80, 265, 240, 4)
    elif 1004 <= pressuremb < 1026:
        display1.set_pen(0, 255, 0)
        display1.text('{:.0f}'.format(pressuremb) + 'mb', 55, 230, 240, 4)
        display1.text("unsetled", 30, 265, 240, 4)
    elif 1026 <= pressuremb < 1048:
        display1.set_pen(0, 0, 255)
        display1.text('{:.0f}'.format(pressuremb) + 'mb', 50, 230, 240, 4)
        display1.text("high", 35, 265, 240, 4)
    elif pressuremb >= 1048:
        display1.set_pen(255, 140, 0)
        display1.text('{:.0f}'.format(pressuremb) + 'mb', 50, 230, 240, 4)
        display1.text("very high", 20, 265, 240, 4)
    else:
        display1.set_pen(0, 0, 0)
        display1.text('{:.0f}'.format(pressuremb) + 'mb', 50, 230, 240, 4)
        display1.text("", 25, 265, 240, 4)    

    # battery state
    voltage = vsys.read_u16() * conversion_factor
    percentage = 100 * ((voltage - empty_battery) / (full_battery - empty_battery))
    if percentage > 100:
        percentage = 100.00
    
    if charging.value() == 1:         # if it's plugged into USB power...
        display1.set_pen(0, 255, 0)
        display1.text("Battery", 5, 300, 240, 3)
        #display1.text("ON", 180, 280, 240, 3)
        display1.text('{:.0f}%'.format(percentage), 155, 300, 240, 3)
        #display1.text('{:.1f}v'.format(voltage), 155, 300, 240, 3)
        #display1.text("MAINS", 155, 300, 240, 3)
        display1.set_backlight(1.0)
        display2.set_backlight(1.0)
    else:                             # if not, display the battery stats
        display1.set_pen(255, 255, 0)
        display1.text("Battery", 5, 300, 240, 3)
        #display1.text("ON", 180, 280, 240, 3)        
        display1.text('{:.0f}%'.format(percentage), 155, 300, 240, 3)
        #display1.text("Batt", 155, 300, 240, 3)
        display1.set_backlight(0.5)
        display2.set_backlight(0.5)
        #display2.set_led(125,0,0)
        
    # time to update display 1
    display1.update()
    display1.set_pen(0, 0, 0)
    display1.clear()
    
    # header
    display2.set_pen(255, 255, 255)
    display2.text("rain fall", 50, 130, 240, 3)
    display2.text("wind", 80, 200, 240, 3)
    display2.text("@", 100, 230, 240, 4)

                                         
    display2.set_pen(255, 255, 255)
    
    hours = rtc.get_hours()
    minutes = rtc.get_minutes()
    display2.text(rtc.string_date(), 70, 30, 240, 2)
    #display2.text(f"{hours:02}:{minutes:02}", 190, 0, 320, 3)
    
    if hours <12:
        display2.text(f"{hours:2}:{minutes:02}:am", 50, 0, 240, 4)
    elif hours == 12:
        display2.text(f"{hours:2}:{minutes:02}:pm", 60, 0, 240, 4)
    elif hours >12:
        hours = hours - 12
        display2.text(f"{hours:2}:{minutes:02}:pm", 60, 0, 240, 4)
          
    reading = ltr.get_reading()
    light = reading[BreakoutLTR559.LUX]
    
    display2.set_pen(0, 255, 0)
    display2.text("sun", 10, 50, 240, 4)
    
    #Convert light level in lux to descriptive value.
    
    if light < 50:
        display2.text("dark", 120, 50, 240, 4)
    elif 50 <= light < 100:
        display2.text("dim", 110, 50, 240, 4)
    elif 100 <= light < 500:
        display2.text("light", 110, 50, 240, 4)
    elif light >= 500:
        display2.text("bright", 110, 50, 240, 4)
        
    # UV Index
    display2.set_pen(0, 255, 0)
    display2.text("UV:", 15, 80, 240, 4)
    display2.text("??", 140, 80, 240, 4)
            
        
    # percipitaion
     
    display2.set_pen(0, 255, 0)
    display2.text("??" + 'mm', 10, 160, 240, 3)
    display2.text("??" + 'mm/h', 120, 160, 240, 3)
    
    # wind

    display2.set_pen(0, 255, 0)
    display2.text("??", 50, 230, 240, 4)
    display2.text("??", 150, 230, 240, 4)
    display2.text("??", 100, 265, 240, 4)




#    display2.set_pen(255, 255, 255)
#    display2.text("wind:", 0, 190, 240, 3)
#    display2.text("NE", 80, 190, 240, 3)
    
    # wind direction
#    display2.set_pen(255, 255, 255)
#    display2.text("36 km/h", 130, 190, 240, 3)
        

    # battery state
    voltage = vsys.read_u16() * conversion_factor
    percentage = 100 * ((voltage - empty_battery) / (full_battery - empty_battery))
    if percentage > 100:
        percentage = 100.00
    
    if charging.value() == 1:         # if it's plugged into USB power...
        display2.set_pen(0, 255, 0)
        display2.text("Battery", 5, 300, 240, 3)
        #display2.text("ON", 180, 280, 240, 3)
        #display2.text('{:.1f}v'.format(voltage), 155, 300, 240, 3)
        display2.text('{:.0f}%'.format(percentage), 155, 300, 240, 3)
        #display2.text("MAINS", 155, 300, 240, 3)
        
    else:                             # if not, display the battery stats
        display2.set_pen(255, 255, 0)
        display2.text("Battery", 5, 300, 240, 3)
        #display2.text("ON", 180, 280, 240, 3)        
        display2.text('{:.0f}%'.format(percentage), 155, 300, 240, 3)
        #display2.text("Batt", 155, 300, 240, 3)
                    
    # update display 2
    display2.update()
    display2.set_pen(0, 0, 0)
    display2.clear()
    
    time.sleep(1.0)
    

Woot, Phil Howard aka @gadgetoid , pushed the two updates / fixes I needed into main.
My VEML6075 UV sensor is now working. Just wind and rain to sort out now.

Bill Gates is reported to have said,
"640K ought to be enough for anybody."
Wish I had 640kb! I’m hitting the 264kb limit of the SRAM. Getting memory allocation errors. Had to remove my min max temperature readings code to free up some space. It was a fair chunk of code so hopefully I’m good to go now for the really needed stuff.
The display buffer itself uses up about 154 kb. That leaves about 110 kb for my code to run in.

Nice project. I’m building my weatherstation for Home Assistant. I have already integrated wery good optical rain sensor RG-15. Light, UV (good hint from you for qarz glass) - LTR390, temperature and humidity - DHT22. Now I try to find good wind speed and direction sensor.
You pointed to Wind and Rain Sensors for Weather Station (Wind Vane / Anemometer / Ra - Pimoroni
Its’ not supported bu ESPHome yet, but looks good. Do you already have this sensor? I suppose for wind speed it uses pulses, but what signal you get from wind direction sensor?

The wind direction signal is a positive DC voltage that you read with one of the ADC inputs. I haven’t got that codded for my Pico just yet.

How is this going?
Interesting to see the switch to the Pico

I see you are saying you are running out of memory, I’m not sure how much this will help but you can optimise you if/ifelse/else checks which may save a little bit.

for example

    if hum_out < 30:
        display1.set_pen(255, 140, 0)
    elif 30 <= hum_out < 61:
        display1.set_pen(0, 255, 0)
    elif 61 <= hum_out < 81:
        display1.set_pen(255, 255, 0)
    elif hum_out >= 81:
        display1.set_pen(255, 0, 0)
    else:
        display1.set_pen(0, 0, 0)

can just be

    if hum_out < 30:
        display1.set_pen(255, 140, 0)
    elif hum_out < 61:
        display1.set_pen(0, 255, 0)
    elif hum_out < 81:
        display1.set_pen(255, 255, 0)
    elif hum_out >= 81:
        display1.set_pen(255, 0, 0)
    else:
        display1.set_pen(0, 0, 0)

you don’t need that initial boundary check as it’s implied by the if check above, it cannot get to the next elif is the above if is True.
So you’re saving one extra check each time. Assuming micropython doesn’t just scrub them out anyway. It certainly saves in file size.

In reality you don’t need the last elif/else, as it shouldn’t be able to reach the else ever. If it’s not a number it will probably fail before that with some sort of int error.

i.e.

    if hum_out < 30:
        display1.set_pen(255, 140, 0)
    elif hum_out < 61:
        display1.set_pen(0, 255, 0)
    elif hum_out < 81:
        display1.set_pen(255, 255, 0)
    else:
        display1.set_pen(255, 0, 0)

Do that through all the code and it should free up a bit of memory ? or at least drive space :-)

The new Pico Graphics have given me nice chunk back. I can use rgb332. And with some trickery I can use just one display buffer for both displays. The display buffer is what uses up your memory.

EDIT: Yeah, I do have some code cleanup to do.