paulsb
Published © GPL3+

Temp, Humidity and Pressure monitor/weather forecast/graphs

Rechargeable temp, humidity and pressure monitor, weekly history files and graph display. Weather forecast base on the Zambretti Forecaster.

BeginnerFull instructions provided3 hours1,021
Temp, Humidity and Pressure monitor/weather forecast/graphs

Things used in this project

Hardware components

Raspberry Pi Pico
Raspberry Pi Pico
×1
TP4056 5V 1A TYPE C Micro USB Board Module for 18650 Lithium Battery Charger
×1
3.7V 3000mAh Lithium Rechargeable Battery
×1
DHT11 Temperature Humidity Sensor Module
×1
AZDelivery 3 x GY-68 BMP180 Digital Barometric Pressure Temperature and Altitude Sensor Module Board
×1
AZDelivery I2C 0.96-inch OLED Display SSD1306 128x64 Pixels IIC 3.3V 5V White Character Display
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
×3
Hook Up Wire
×1
330k ohm resistor
×1
1M ohm resistor
×1
Mini Toggle Switch
×1
Heat Shrink Tubing
×1

Software apps and online services

Thonny IDE (link to setup instructions)

Story

Read more

Custom parts and enclosures

Project Box STL file

Project Box Lid STL file

Schematics

Breadboard

Schematic

Code

main.py

MicroPython
# (C) Paul Brace April 2022
# Temperature, Humidity and Pressure monitor with clock, weekly history V 1.0
# and weather forecast based on the Zambretti Forecaster
# Zamretti lookup table based on this article
#   https://web.archive.org/web/20110610213848/http:/www.meteormetrics.com/zambretti.htm
# This version uses the Pi Pico internal storage for the history
# storage files so you will have to use Thonny, or another program to
# access them from the board.

from machine import Pin, I2C, Timer, ADC
import utime as time
from ssd1306 import SSD1306_I2C
from bmp180 import BMP180
from dht11 import DHT11, InvalidChecksum, InvalidPulseCount
from myfont import myFont
import gc
from graph import oledGraph
from machine import RTC
from timeedit import timeAdjustment
from micropython import const
import micropython
import os

# Debug flag - set to true or false
DEBUG = False

# create clock object
rtc = RTC()                                        

# assign pin and create DHT11 object
pin = Pin(15, Pin.OUT, Pin.PULL_DOWN)
sensor = DHT11(pin)

# Set up mode, reset and adjust button pins
mode = Pin(17, Pin.IN, Pin.PULL_DOWN)      # Also up and day/hour
reset = Pin(16, Pin.IN, Pin.PULL_DOWN)     # Also down and month/minute
adjust = Pin(18, Pin.IN, Pin.PULL_DOWN)    # Also enter/move to next screen

# To capture button bounce
lastModePress = time.ticks_ms()
lastResetPress = time.ticks_ms()

# Set up ADC(0) to read voltage for battery monitor
analogValue = ADC(26)

# Simulate an enum for display modes
dispTime = const(0)             # Date and time display & summary
dispPress = const(1)            # Barometer display
dispPressGraph = const(2)       # Pressure recent history graph
dispTemp = const(3)             # Temperature display
dispTempGraph = const(4)        # Temperature recent history graph
dispHumid = const(5)            # Humidity display
dispHumidGraph = const(6)       # Humidity recent history graph
dispPressGraphScroll = const(7)  # Pressure history graph from start of file
dispTempGraphScroll = const(8)  # Temperature history graph from start of file
dispHumidGraphScroll = const(9) # Humidity history graph from start of file
dispPressGraphScrollPast = const(10)  # Temperature history graph from previous file displayed from start of file
dispTempGraphScrollPast = const(11)  # Temperature history graph from previous file displayed from start of file
dispHumidGraphScrollPast = const(12) # Humidity history graph from previous file displayed from start of file

currentMode = dispTime          # Current display setting

# Forecast table based on the Zambretti Forecaster
# the '#' is used to split the forecast for display
forecast = ["Settled Fine",
            "Fine Weather",
            "Becoming Fine",
            "Fine Becoming#Less Settled",
            "Fine, Possibly#showers",
            "Fairly Fine,#Improving",
            "Fairly Fine, Pos#showers, early",
            "Fairly Fine,#Showery Later",
            "Showery Early,#Improving",
            "Changeable#Mending",
            "Fairly Fine#Showers likely",
            "Rather Unsettled#Clearing Later",
            "Unsettled,#Probably Improve",
            "Showery, Bright# Intervals",
            "Showery, Becoming#more unsettled",
            "Changeable#some rain",
            "Unsettled, short#fine Intervals",
            "Unsettled,#Rain later",
            "Unsettled,#Rain at times",
            "Very Unsettled,#Finer at times",
            "Rain at times,#worse later",
            "Rain at times,#then very unsettled",
            "Rain at Frequent#Intervals",
            "Very Unsettled,#Rain",
            "Stormy, possibly#improving",
            "Stormy,#much rain"]    

# Set up the oled display
WIDTH  = const(128)            
HEIGHT = const(64)

i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=200000)       # Init I2C using pins GP8 & GP9 (default I2C0 pins)

oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)                  # Init oled display

myfont = myFont(oled)                                   # Create myFont object

graph = oledGraph(oled)                                 # Create graph object

bmp180 = BMP180(i2c)                                    # Init bmp180 also on ic2
bmp180.oversample_sett = 2
bmp180.baseline = 101325

# Debug Print out any addresses found
if DEBUG:
    devices = i2c.scan()
    if devices:
        for d in devices:
            print("IC2 device address: ",hex(d))

# File numbers will start at 1 and increment by 1 each week
# 0 used to indicate no files yet created
fileNumber = 0

# if first is True then monitor just switched on so will
# increment file number and create a new history file
first = True

# Flag set if there is an error on saving history
saveError = False
# Set to true if voltage too low
savingSuspended = False           

# Constants to control history file entries
MINUTES = const(60)            # number of minutes between calls to save data
FILE_PERIOD = const(10080)     # minutes - 1,440 in a day 10,080 in a week

temp = 0            # latest temperature reading
humid = 0           # latest humidity reading
press = 0           # latest pressure reading
count = 0           # number of readings in history period

pressSmoothing = []        # List of last 5 pressure readings
tempSmoothing = []         # List of last 5 temp readings 
humidSmoothing = []        # List of last 5 humid readings 

# Timer to trigger history store
history = Timer()

# Used to hold max and min since start-up
maxTemp = -99          # maximum temp
maxHumid = -99         # maximum humidity
minTemp = 99           # minimum temp
minHumid = 99          # minimum humidity

# used to stop constant refresh of graphs
currentDisplay = 0   

# Used for smoothing battery voltage readings
accVolts = 0
voltsCount = 0

# flags to allow interrupt of sleeping when button pressed
buttonPressed = False
adjusting = False

# Current elevation in feet used to calculate
# pressure at sea level
elevation = 180          

# Met office definitions - at sea level
HIGH_PRESS = 1022.69
LOW_PRESS = 1009.15

# Set a default time in case no config file with latest time
#now = (YEAR, MONTH, DAY, DAY number, HOUR, MINUTE, SECOND, micro_SECOND)
rtc.datetime((2022, 1, 1, 0, 12, 0, 0, 0))

# Display time out
DISP_TIME_OUT = const(10)       # Seconds after which display will revert to show time
displayShown = 0                # Time display was shown  

# List to store pressure readings in memory - saved every 5 minutes
# used to calculate movement
pressReadings = []

FIVE_MIN = 300000                    # Milliseconds in 5 minutes

# Time last updated set to give 1 minute settling down
# period before first pressure record saved
lastPressUpdate = time.ticks_ms() - FIVE_MIN + 60000

# Update log file used for main events and exceptions
def updateLog(event):
    global rtc, savingSuspended
    if not savingSuspended:
        try:
            log = open("monitorlog.txt", "a")
            now = rtc.datetime()
            formatstring = '{:02d}/{:02d}/{:04d}-{:02d}h{:02d}m{:02d}s    '
            log.write(formatstring.format(now[2], now[1], now[0], now[4], now[5], now[6]) + event + "\n")
            log.flush()
            log.close()
        except Exception as e:
            print("Error updating log file:" + str(e))

# One or 2 line message with pause
def message(text1, text2, pause):
    oled.fill(0)
    if text2 == '':
        pos = 30
    else:
        pos = 20
    oled.text(text1, 0, pos)
    if text2 != "":
       oled.text(text2, 0 ,pos + 15)
    oled.show()
    time.sleep(pause)

# Config file holds the current file number, elevation and last saved time
# time updated after each new record written to history file
def saveConfig(fileNumber):
    global rtc, savingSuspended, elevation
    if not savingSuspended:
        try:
            log = open("config.txt", "w")
            now = rtc.datetime()
            seconds = time.mktime(now)                # documentation indicated this returns seconds but appears to
                                                      # actually appears to return return minutes
            log.write(str(seconds) + "\n")
            log.write(str(fileNumber) + "\n")
            log.write(str(elevation) + "\n")
            log.flush()
            log.close()
        except Exception as e:
            print("Config file write error:" + str(e))

# Attempt to load config file and if fails create a new file
def loadConfig():
    global fileNumber, rtc, savingSuspended, elevation
    try:
        log = open("config.txt", "r")
        line = log.readline()
        seconds = int(line[0:len(line)-1])     # remove trailing \n
        now = time.gmtime(seconds)
        rtc.datetime(now)
        # remove the trailing \n nad convert
        line = log.readline()
        fileNumber = int(line[0:len(line)-1])
        line = log.readline()
        elevation = int(line[0:len(line)-1])
        log.close()
    except Exception as e:
        # Appears config file not yet created so start afresh
        print("Config file read error: ", e)
        saveConfig(fileNumber)

# Write file headers when new history files created
def writeFileHeader(fileName, fileNumber):
    updateLog("Creating new history file - " + fileName + "(" + str(fileNumber) + ")")
    now = rtc.datetime()
    log = open(fileName + "(" + str(fileNumber) + ").txt", "w")
    log.write("Time: {:02d}:{:02d} Date: {:02d}/{:02d}/{:04d}\n".format(now[4], now[5], now[2], now[1], now[0]))
    log.write("Time interval {:.2f} minutes\n".format(MINUTES))
    log.flush()
    log.close()

# Get the current battery voltage
# To calculate the actual voltage from the monitor pin reading.
# Using 1m and 330k ohm resistors divides the voltage by approx 4
# You may wany to substitute actual values of resistors in an equation (R1 + R2)/R2
# E.g. (1000 + 330)/330 = 4.03
# Alternatively take the voltage reading across the battery and from the joint between 
# the 2 resistors to ground and divide one by the other to get the value.
def readVoltage():
    return analogValue.read_u16() / 65536 * 3.3 * 4.03

# Set up with current reading
lastVolts = readVoltage()

# Routine to smooth the voltage reading
def getVoltage():
    global lastVolts, accVolts, voltsCount
    voltsCount += 1
    accVolts += readVoltage()
    # If 10 readings then calc new average otherwise return last average
    if voltsCount > 9:
        lastVolts = accVolts / voltsCount
        accVolts = 0
        voltsCount = 0
    return lastVolts

def testFreeSpace():
    global saveError, savingSuspended
    # Get status of flash memory and check enough free blocks
    # to store another set of files
    status = os.statvfs("")
    # status[3] = free blocks
    if status[3] <= 6:
        message("Insufficient", 'flash space ' + str(status[3]), 10)
        updateLog("Insufficient flash space to create new file set - free blocks " + str(status[3]))
        updateLog("History saving suspended")
        saveError = True
        savingSuspended = True
        return False
    updateLog("Flash memory free blocks " + str(status[3]))
    return True

# Store history data to file
def saveHistory(Timer):
    global temp, humid, press, count, first, fileNumber, saveError, savingSuspended
    if not count or savingSuspended:
        return
    try:
        message("Saving data...", '', 0)
        now = rtc.datetime()
        # Check if a file has been created
        if first:
            # Must be first run so write header for file
            if not testFreeSpace():
                return
            fileNumber += 1
            writeFileHeader("temperature", fileNumber)
            writeFileHeader("humidity", fileNumber)
            writeFileHeader("pressure", fileNumber)
            if DEBUG:
                writeFileHeader("voltage", fileNumber)
            saveConfig(fileNumber)
            first = False
        # Check size of file and if holding 1 week of readings start a new file
        # 56 byte header + 24 bytes per entry
        #log = open("temperature(" + str(fileNumber) + ").txt", "a")
        #size = log.tell()   This does not appear to work as returns 0 even though "a" should set to end of file
        #log.close()
        entriesInFilePeriod = FILE_PERIOD / MINUTES
        file_stats = os.stat("temperature(" + str(fileNumber) + ").txt")
        if file_stats[6] >= (56 + 24 * entriesInFilePeriod):
            if not testFreeSpace():
                return
            fileNumber += 1
            writeFileHeader("temperature", fileNumber)
            writeFileHeader("humidity", fileNumber)
            writeFileHeader("pressure", fileNumber)
            if DEBUG:
                writeFileHeader("voltage", fileNumber)
        log = open("temperature(" + str(fileNumber) + ").txt", "a")
        formatstring = '{:02d}/{:02d}/{:04d}-{:02d}h{:02d}m,{:2.2f}'
        log.write(formatstring.format(now[2], now[1], now[0], now[4], now[5], temp) + "\n")
        log.flush()
        log.close()
        log = open("humidity(" + str(fileNumber) + ").txt", "a")
        log.write(formatstring.format(now[2], now[1], now[0], now[4], now[5], humid) + "\n")
        log.flush()
        log.close()
        log = open("pressure(" + str(fileNumber) + ").txt", "a")
        log.write(formatstring.format(now[2], now[1], now[0], now[4], now[5], press) + "\n")
        log.flush()
        log.close()
        # Record battery voltage if in Debug mode
        if DEBUG:
            log = open("voltage(" + str(fileNumber) + ").txt", "a")
            log.write(formatstring.format(now[2], now[1], now[0], now[4], now[5], getVoltage()) + "\n")
            log.flush()
            log.close()
        # save config file to update time stamp and file number
        saveConfig(fileNumber)
        saveError = False
    except Exception as e:
        print(e)
        oled.fill(0)
        oled.text("Error:", 0, 10)
        oled.text("Unable to save", 0, 25)
        oled.text("readings.", 0, 40)
        oled.text("Restart monitor.", 0, 55)
        oled.show()
        saveError = True
        time.sleep(10)
    # Reset count so can check that new readings have been made since last save
    count = 0

# Set a timer to save the history data
def setTimer():
    global history
    history.init(freq=1/(MINUTES * 60), mode=Timer.PERIODIC, callback=saveHistory)

# Stop the timer calling the history data
def stopTimer():
    global history
    history.deinit()

# Call display for current mode
def updateDisplay():
    global currentMode, currentDisplay, fileNumber
    if currentMode == dispTime:
        showTime()
    elif currentMode == dispPress:
        showPress()
    elif currentMode == dispPressGraph and currentDisplay != dispPressGraph:
        graph.showGraph("pressure(" + str(fileNumber) + ").txt", False, False)
    elif currentMode == dispTemp:
        showTemp()
    elif currentMode == dispTempGraph and currentDisplay != dispTempGraph:
        graph.showGraph("temperature(" + str(fileNumber) + ").txt", False, False)
    elif currentMode == dispHumid:
        showHumid()
    elif currentMode == dispHumidGraph and currentDisplay != dispHumidGraph:
        graph.showGraph("humidity(" + str(fileNumber) + ").txt", False, False)
    elif currentMode == dispPressGraphScroll and\
             currentDisplay != dispPressGraphScroll:
        graph.showGraph("pressure(" + str(fileNumber) + ").txt", True, False)
    elif currentMode == dispTempGraphScroll and\
             currentDisplay != dispTempGraphScroll:
        graph.showGraph("temperature(" + str(fileNumber) + ").txt", True, False)
    elif currentMode == dispHumidGraphScroll and\
             currentDisplay != dispHumidGraphScroll:
        graph.showGraph("humidity(" + str(fileNumber) + ").txt", True, False)
    elif currentMode == dispPressGraphScrollPast and\
             currentDisplay != dispPressGraphScrollPast:
        graph.showGraph("pressure(" + str(fileNumber - 1) + ").txt", True, True)
    elif currentMode == dispTempGraphScrollPast and\
             currentDisplay != dispTempGraphScrollPast:
        graph.showGraph("temperature(" + str(fileNumber - 1) + ").txt", True, True)
    elif currentMode == dispHumidGraphScrollPast and\
             currentDisplay != dispHumidGraphScrollPast:
        graph.showGraph("humidity(" + str(fileNumber - 1) + ").txt", True, True)
    currentDisplay = currentMode
        
# Mode button pressed
def changeMode(pin):
    global lastModePress, currentMode, buttonPressed
    # allow 1 second after press to stop bounce and allow for release
    if time.ticks_ms() - lastModePress > 1000:
        lastModePress = time.ticks_ms()
        currentMode += 1;
        if currentMode > dispHumidGraphScrollPast:
            currentMode = dispTime
        buttonPressed = True

# Reset button pressed
def resetPressed(pin):
    global lastResetPress, buttonPressed, maxTemp, maxHumid, minTemp, minHumid
    # allow 1 second after press to stop bounce and rallow for elease
    if time.ticks_ms() - lastResetPress > 1000:
        lastResetPress = time.ticks_ms()
        maxTemp = -99          # maximum temp
        maxHumid = -99         # maximum humidity
        minTemp = 99           # minimum temp
        minHumid = 99          # minimum humidity
        buttonPressed = True

# Adjust pin pressed
def adjustPressed(pin):
    global adjusting, buttonPressed
    adjusting = True
    buttonPressed = True

# Set up interrupts for button pins
def setPinHandlers():
    mode.irq(trigger=Pin.IRQ_FALLING, handler = changeMode)
    reset.irq(trigger=Pin.IRQ_FALLING, handler = resetPressed)
    adjust.irq(trigger=Pin.IRQ_FALLING, handler = adjustPressed)

# Clear handlers so can use buttons in adjust mode
def clearPinHandlers():
    mode.irq(handler=None)
    reset.irq(handler=None)
    adjust.irq(handler=None)

# Display Temperature
def showTemp():
    global temp, maxTemp, minTemp
    oled.fill(0);                  
    myfont.text("Temp:",5,5)
    myfont.text(chr(33),25,25)  # Up arrow
    myfont.text(chr(34),25,45)  # Down arrow
    myfont.text("{:2.1f}".format(temp),70,5)
    myfont.text("{:2.1f}".format(maxTemp),70,25)
    myfont.text("{:2.1f}".format(minTemp),70,45)
    oled.text("c", 110, 5)
    oled.text("c", 110, 25)
    oled.text("c", 110, 45)
    oled.show()

# Display Humidity
def showHumid():
    global humid, maxHumid, minHumid
    oled.fill(0);              
    myfont.text("Humid:",10,5)
    myfont.text(chr(33),25,25)  # Up arrow
    myfont.text(chr(34),25,45)  # Down arrow
    myfont.text("{:2.1f}".format(humid),70,5)
    myfont.text("{:2.1f}".format(maxHumid),70,25)
    myfont.text("{:2.1f}".format(minHumid),70,45)
    oled.text("c", 110, 5)
    oled.text("c", 110, 25)
    oled.text("c", 110, 45)
    oled.show()

# Calculate pressure at sea level based on current reading, elevation and temperature
def seaPressure(press, temp):
    global evelvation
    return press * pow(1 - (0.0065 * elevation/3.281) / (20 + (0.0065 * elevation/3.281) + 273.15),-5.257 )

# Calculate the Z number. Z number is the position in the table for the weather forecast
def getZambretti():
    global press, temp, pressReadings
    now = rtc.datetime()
    if now[1] >=4 and now[1] <= 9:
        summer = True
    else:
        summer = False
    z = 0
    # Get pressure movement over at last 1 hour up to 3 hours
    count = len(pressReadings)
    if count > 11:
        seaPress = seaPressure(press, temp)
        if DEBUG:
            print("Sea level pressure: ", seaPress)
        pressChange = pressReadings[count - 1] - pressReadings[0]
        if pressChange <= -1.5:
            # drop of 1.5 or more considered falling for forecast
            if seaPress >= 1050:
                z = 1 #A
            elif seaPress >= 1040:
                z = 2 #B
            elif seaPress >= 1024:
                z = 4 #D
            elif seaPress >= 1018:
                z = 7 #H
            elif seaPress >= 1010:
                z = 15 #O
            elif seaPress >= 1004:
                if summer:
                    z = 18 #R
                else:
                    z = 15 #O
            elif seaPress >= 998:
                z = 21 #U
            elif seaPress >= 991:
                z = 22 #V
            else:
                if summer:
                    z = 24 #X
                else:
                    z = 22 #V
        elif pressChange >= 1.5:
            # Increase of 1.5 or more considered rising for forecast
            if seaPress >= 1030:
                z = 1 #A
            elif seaPress >= 1022:
                z = 2 #B
            elif seaPress >= 1012:
                if summer:
                    z = 3 #C
                else:
                    z = 6 #F
            elif seaPress >= 1007:
                z = 5 #F
            elif seaPress >= 1000:
                z = 7 #G
            elif seaPress >= 995:
                if summer:
                    z = 9 #I
                else:
                    z = 10 #J
            elif seaPress >= 990:
                z = 10 #J
            elif seaPress >= 984:
                if summer:
                    z = 12 #L
                else:
                    z = 13 #M
            elif seaPress >= 978:
                z = 13 #M
            elif seaPress >= 970:
                z = 17 #Q
            elif seaPress >= 965:
                z = 20 #T
            elif seaPress >= 959:
                z = 25 #Y
            else:
                z = 26 #Z
        else:
            # Otherwise considered steady for forecast
            if seaPress >= 1033:
                z = 1 #A
            elif seaPress >= 1023:
                z = 2 #B
            elif seaPress >= 1014:
                z = 5 #E
            elif seaPress >= 1009:
                z = 11 #K
            elif seaPress >= 1000:
                z = 14 #N
            elif seaPress >= 994:
                z = 16 #P
            elif seaPress >= 989:
                z = 19 #S
            elif seaPress >= 981:
                z = 23 #W
            elif seaPress >= 974:
                z = 24 #X
            else:
                z = 26 #Z
    return z - 1

# Show current pressure, trend and forecast
def showPress():
    global press, temp, pressReadings, HIGH_PRESS, LOW_PRESS
    # Get pressure at sea level
    seaPress = seaPressure(press, temp)
    
    if DEBUG:
        print("Sea pressure: ", seaPress)     
        z = getZambretti()                     
        print("Z = ", z)                      
    
    oled.fill(0);                  
    if seaPress > HIGH_PRESS:
        oled.text("High:",0,0)
    elif seaPress < LOW_PRESS:
        oled.text("Low:",0,0)
    else:
        oled.text("Normal:",0,0)
    oled.text("{:4.1f}".format(press),70,0)
    count = len(pressReadings)
    if count > 11:
        # Pressure change over latest 3 hour period
        # Using Met's definition
        
        # Rising (or falling) slowly
        # Pressure change of 0.1 to 1.5 hPa in the preceding three hours
        
        # Rising (or falling)
        # Pressure change of 1.6 to 3.5 hPa in the preceding three hours
        
        # Rising (or falling) quickly
        # Pressure change of 3.6 to 6.0 hPa in the preceding three hours
        
        # Rising (or falling) v. rapidly
        # Pressure change of more than 6.0 hPa in the preceding three hours
        
        pressChange = pressReadings[count - 1] - pressReadings[0]
        if abs(pressChange) > 6.0:
            oled.text("Rapid",0,20)
        elif abs(pressChange) >= 1.5:
            oled.text("Quick",0,20)
        elif abs(pressChange) >= 0.1:
            oled.text("Slow",0,20)
        else:
            oled.text("Steady :",0,20)
        if pressChange >= 0.1:
            myfont.text(chr(42), 56, 20)  # Up arrow small
        if pressChange <= -0.1:
            myfont.text(chr(43), 56, 20)  # Down arrow small 
        oled.text("{:4.1f}".format(pressChange),70,20)
        z = getZambretti()
        # Split forecast string and display
        if z >=0 and z <=25:
            mess = forecast[z].split('#')
            oled.text(mess[0], 0,35)
            if len(mess) > 1:
                oled.text(mess[1], 0,50)
    else:
        oled.text("Movement will be", 0, 20)
        oled.text("shown after 1", 0, 35)
        oled.text("hour.", 0, 50)
    oled.show()

# Display date and time
def showTime():
    global saveError, savingSuspended, temp, humid, press    
    oled.fill(0)          
    now = rtc.datetime()        # get current time
    formatstring = "Time : {:02d}:{:02d}"
    oled.text(formatstring.format(now[4], now[5]),0,0)
    formatstring = "Temp : {:0.1f}"
    oled.text(formatstring.format(temp), 0, 13)
    formatstring = "Humid: {:0.1f}"
    oled.text(formatstring.format(humid), 0, 26)
    formatstring = "Press: {:0.1f}"
    oled.text(formatstring.format(press), 0, 39)
    formatstring = "Date : {:02d}/{:02d}/{:02d}"
    oled.text(formatstring.format(now[2], now[1], now[0]-2000),0,52)
    # Display Battery Level
    volts = getVoltage()
    if volts > 3.84:
        batt = chr(37)     # Full icon
    elif volts > 3.7:
        batt = chr(38)     # 3/4 icon   
    elif volts > 3.54:
        batt = chr(39)     # 1/2 icon   
    elif volts > 3.4:
        batt = chr(40)     # 1/4 icon   
    else:
        batt = chr(41)     # Empty icon   
    myfont.text(batt, 110, 0)
    # if voltage < 3.0 v the suspend saving
    if not savingSuspended and volts < 3.0:
        updateLog("Voltage = {:.2f} history saving suspended.".format(volts))
        savingSuspended = True
        message("Battery low.", "History suspended", 5)
    if saveError or savingSuspended:
        myfont.text(chr(36), 110, 13) # Save error
    oled.show()

# Discard maximum and minimum readings and average remaining
def smoothReadings(readings):
    if len(readings) > 0:
        last = readings[len(readings) - 1]
    maxRead = 0
    minRead = 10000
    accRead = 0
    count = 0
    for i in range(len(readings)):
        if readings[i] > maxRead:
            maxRead = readings[i]
            maxR = i
        if readings[i] < minRead:
            minRead = readings[i]
            minR = i
    for i in range(len(readings)):
        if (i != maxR) and (i != minR):
            accRead += readings[i]
            count += 1
    if count != 0:
        # Debug print
        if DEBUG:
            print("last reading: ", last, "Readings used: ", count, "Acc: ", accRead, "Average: ", accRead/count)
        return accRead / count
    else:
        if DEBUG:
            print("last reading: ", last)
        return last
    

# Get current readings and smooth to return result
def getPressure():
    global pressSmoothing
    press = bmp180.pressure/100
    pressSmoothing.append(press)
    # If more than 5 readings remove oldest
    if len(pressSmoothing) > 5:
        pressSmoothing.pop(0)
    return smoothReadings(pressSmoothing)

# Get current readings and smooth to return result
def getTemp():
    global tempSmoothing
    temp = bmp180.temperature
    tempSmoothing.append(temp)
    # If more than 5 readings remove oldest
    if len(tempSmoothing) > 5:
        tempSmoothing.pop(0)
    return smoothReadings(tempSmoothing)

# Get current readings and smooth to return result
def getHumid():
    global humidSmoothing
    humid = sensor.humidity
    humidSmoothing.append(humid)
    # If more than 5 readings remove oldest
    if len(humidSmoothing) > 5:
        humidSmoothing.pop(0)
    return smoothReadings(humidSmoothing)

# Try to create data folder in case it does not exist
try:
    os.mkdir("data")
except:
    # Directory must exist do just continue
    time.sleep(0)

# Change to data folder to save files
os.chdir("data")

# load configuration file (or create)
loadConfig()

# Create timeAdjustment object
timeAdj = timeAdjustment(oled, myfont, rtc, adjust, mode, reset, elevation)

# Enter time set mode to enable time read from config to
# be corrected/confirmed
elevation = timeAdj.adjustTimeAndElevation()
showTime()                       # Display time
saveConfig(fileNumber)           # Write the new date/time to the config file
time.sleep_ms(1000)              # Wait 1 second to ensure button released/capture bounce
setPinHandlers()                 # Call setPinHandlers for initial set up
setTimer()                       # Timer to trigger history store
updateLog("Monitor started...")

# Main loop will run forever
while True:
    try:
        # Pause for 5 seconds before next sensor readings
        for i in range(0, 25):
            if not buttonPressed:
                time.sleep_ms(200)
        # If adjust button has been pressed then enter time set mode
        if adjusting:
            updateLog("Stopping interrupts")
            clearPinHandlers()
            stopTimer()
            elevation = timeAdj.adjustTimeAndElevation()
            showTime()                # display current time
            time.sleep_ms(1000)       # Wait 1 second to ensure button released/capture bounce
            setPinHandlers()
            setTimer()            
            updateLog("Possible time adjustment")
        # If mode changed then reset time display changed for time-out
        if buttonPressed:
            #update ticks display was last changed
            displayShown = time.ticks_ms()
        # if not showing time and more seconds than the time out has passed then revert to time
        if currentMode != dispTime and time.ticks_ms() > (displayShown + DISP_TIME_OUT * 1000):
            currentMode = dispTime
        # Get smoothed sensor readings
        temp = getTemp()
        humid = getHumid()
        press = getPressure()
        
        # Update maximum and minimum if required
        if len(tempSmoothing) >= 5:
            if maxTemp < temp:
                maxTemp = temp
            if maxHumid < humid:
                maxHumid = humid
            if minTemp > temp:
                minTemp = temp
            if minHumid > humid:
                minHumid = humid
            
        # If more than five minutes has passed append to pressure readings list
        # Could use a timer but this works well and allows shorter time to save first reading
        if time.ticks_ms() > (lastPressUpdate + FIVE_MIN):
            lastPressUpdate = time.ticks_ms()
            pressReadings.append(press)
            # If more than 3 hours in list remove oldest
            if len(pressReadings) > 36:
                pressReadings.pop(0)
            # Debug
            if DEBUG:
                print("Adding reading: ", press, "Readings = ", len(pressReadings))
        
        # Number of readings sync last history file update
        count += 1
        
        # Reset button status boolean
        buttonPressed = False
        adjusting = False
        
        # Call display for current mode
        updateDisplay()
        if DEBUG:
            gc.collect()
            micropython.mem_info()

    except InvalidPulseCount as e:
        # Do no log if just a pulse count error
        print("Pulse Exception: ", e)
    except Exception as e:
        # capture other exceptions, log and continue running
        print("Exception: ", e)
        updateLog("Exception: " + str(e))
    

myfont.py

MicroPython
# (C) Paul Brace March 2022
# Class to display text on an oled display using a custom font V 1.0
# Temperature, Humidity and pressure monitor with clock and weekly history

class myFont:
    # Define pixels for each letter, number and symbol we need
    # First tuple element is the width of the character followed by a tuple containing the bytes
    # "0" = pixel off "1" = pixel on
    H11 = (11, (0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01111111110,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100000110))

    u11 = (11, (0b00000000000,
                0b00000000000,
                0b00000000000,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100001110,
                0b00110011110,
                0b00011110110))

    m11 = (11, (0b00000000000,
                0b00000000000,
                0b00000000000,
                0b00111011100,
                0b01100100110,
                0b01100100110,
                0b01100100110,
                0b01100100110,
                0b01100100110,
                0b01100100110,
                0b01100100110))

    i11 = (6, (0b001100,
               0b001100,
               0b000000,
               0b001100,
               0b001100,
               0b001100,
               0b001100,
               0b001100,
               0b001100,
               0b001100,
               0b001100))

    d11 = (11, (0b00000000110,
                0b00000000110,
                0b00000000110,
                0b00111111110,
                0b01110001110,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100001110,
                0b01110011110,
                0b00111110110))


    T11 = (11, (0b01111111110,
                0b01111111110,
                0b00000110000,
                0b00000110000,
                0b00000110000,
                0b00000110000,
                0b00000110000,
                0b00000110000,
                0b00000110000,
                0b00000110000,
                0b00000110000))

    _11 = (11, (0b01111111110,
                0b01111111110,
                0b00111111100,
                0b00011111000,
                0b00001110000,
                0b00111111100,
                0b00001110000,
                0b00011110100,
                0b00111111100,
                0b01111111110,
                0b01111111110))

    e11 = (11, (0b00000000000,
                0b00000000000,
                0b00000000000,
                0b00111111100,
                0b01110000110,
                0b01100000010,
                0b01110000110,
                0b01101111000,
                0b01100000000,
                0b00110000010,
                0b00011111100))

    p11 = (11, (0b00000000000,
                0b00000000000,
                0b00000000000,
                0b00011111000,
                0b01110000100,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01100000110,
                0b01110000100,
                0b01101111000,
                0b01100000000,
                0b01100000000,
                0b01100000000))

    colon11 = (6, (0b000000,
                   0b000000,
                   0b000000,
                   0b001100,
                   0b001100,
                   0b001100,
                   0b000000,
                   0b000000,
                   0b001100,
                   0b001100,
                   0b001100))

    stop11 = (6, (0b000000,
                  0b000000,
                  0b000000,
                  0b000000,
                  0b000000,
                  0b000000,
                  0b000000,
                  0b000000,
                  0b001100,
                  0b001100,
                  0b001100))
    
    minus11 = (8, (0b00000000,
                   0b00000000,
                   0b00000000,
                   0b00000000,
                   0b00000000,
                   0b00111110,
                   0b00111110))
    
    slash = (8, (0b00000000,
                 0b00000110,
                 0b00000110,
                 0b00001100,
                 0b00001100,
                 0b00011000,
                 0b00011000,
                 0b00110000,
                 0b00110000,
                 0b01100000,
                 0b01100000))

    
    n011 =(11, (0b00001111000,
                0b00011111100,
                0b00110001110,
                0b00110000110,
                0b00110000110,
                0b00110000110,
                0b00110000110,
                0b00110000110,
                0b00110000110,
                0b00011111100,
                0b00001111000))
               
    n111 = (11, (0b00000110000,
                 0b00001110000,
                 0b00010110000,
                 0b00000110000,
                 0b00000110000,
                 0b00000110000,
                 0b00000110000,
                 0b00000110000,
                 0b00000110000,
                 0b00011111100,
                 0b00011111100))

    n211 = (11, (0b00011111000,
                 0b00111111100,
                 0b01100000110,
                 0b00000000110,
                 0b00000011000,
                 0b00000110000,
                 0b00001100000,
                 0b00011000000,
                 0b00110000000,
                 0b01111111110,
                 0b01111111110))
    
    n311 = (11, (0b00011111000,
                 0b00111111100,
                 0b01100000110,
                 0b00000000110,
                 0b00000011100,
                 0b00000111000,
                 0b00000011100,
                 0b00000000110,
                 0b01100000110,
                 0b00111111100,
                 0b00011111000))

    n411 =(11, (0b00000011000,
                0b00000111000,
                0b00001111000,
                0b00011011000,
                0b00110011000,
                0b01100011100,
                0b01111111100,
                0b01111111000,
                0b00000011000,
                0b00000111100,
                0b00000111100))

    n511 =(11, (0b00111111100,
                0b00111111100,
                0b00110000000,
                0b00110000000,
                0b00111111000,
                0b00000011100,
                0b00000000110,
                0b00000000110,
                0b00110000110,
                0b00111111100,
                0b00011111000))
    
    n611 =(11, (0b00000111100,
                0b00011111100,
                0b00011000000,
                0b00110000000,
                0b00110111100,
                0b00111111110,
                0b00110000110,
                0b00110000110,
                0b00110000110,
                0b00111111100,
                0b00001110000))
    
    n711 =(11, (0b00011111110,
                0b00011111110,
                0b00000000110,
                0b00000001100,
                0b00000001100,
                0b00000011000,
                0b00000011000,
                0b00000110000,
                0b00000110000,
                0b00001100000,
                0b00001100000))
    
    n811 =(11, (0b00001111000,
                0b00011111100,
                0b00110001110,
                0b00110000110,
                0b00011001100,
                0b00011111100,
                0b00111001110,
                0b00110000110,
                0b00110000110,
                0b00011111100,
                0b00001111000))
    
    n911 =(11, (0b00000111000,
                0b00011111110,
                0b00110000110,
                0b00110000110,
                0b00110000110,
                0b00111111110,
                0b00011110110,
                0b00000000110,
                0b00000001100,
                0b00011111100,
                0b00011110000))


    #Large Arrows used to indicate maximum and minimum
    #Up arrow
    upArrow =(11, (0b00000100000,
                   0b00001110000,
                   0b00011111000,
                   0b00111111100,
                   0b01111111110,
                   0b00001110000,
                   0b00001110000,
                   0b00001110000,
                   0b00001110000,
                   0b00001110000,
                   0b00001110000))

    # Down Arrow
    downArrow =(11, (0b00001110000,
                     0b00001110000,
                     0b00001110000,
                     0b00001110000,
                     0b00001110000,
                     0b00001110000,
                     0b01111111110,
                     0b00111111100,
                     0b00011111000,
                     0b00001110000,
                     0b00000100000))

    #Small Arrows used to indicate pressure direction
    #Up arrow
    upArrowS =(9, (0b000010000,
                   0b000111000,
                   0b001111100,
                   0b011111110,
                   0b111111111,
                   0b000111000,
                   0b000111000))

    # Down Arrow
    downArrowS =(9, (0b000111000,
                     0b000111000,
                     0b111111111,
                     0b011111110,
                     0b001111100,
                     0b000111000,
                     0b000010000))

    # SD card error icon
    sdError =  (15, (0b110000000000011,
                     0b011000000000110,
                     0b111111111111111,
                     0b100110000011001,
                     0b100011000110001,
                     0b100001111100001,
                     0b100011011111111,
                     0b100110100011000,
                     0b111111000001100,
                     0b011000000000110,
                     0b110000000000011))
    
    # Battery level icons
    BatteryFull =  (16, (0b1111111111111100,
                         0b1111111111111100,
                         0b1111111111111111,
                         0b1111111111111111,
                         0b1111111111111111,
                         0b1111111111111100,
                         0b1111111111111100))

    Battery3Q =  (16, (0b1111111111111100,
                       0b1111111111000100,
                       0b1111111111000111,
                       0b1111111111000111,
                       0b1111111111000111,
                       0b1111111111000100,
                       0b1111111111111100))
    
    Battery2Q =  (16, (0b1111111111111100,
                       0b1111111000000100,
                       0b1111111000000111,
                       0b1111111000000111,
                       0b1111111000000111,
                       0b1111111000000100,
                       0b1111111111111100))
    
    Battery1Q =  (16, (0b1111111111111100,
                       0b1111000000000100,
                       0b1111000000000111,
                       0b1111000000000111,
                       0b1111000000000111,
                       0b1111000000000100,
                       0b1111111111111100))
    
    BatteryEmpty =  (16, (0b1111111111111100,
                         0b1000000000000100,
                         0b1000000000000111,
                         0b1000000000000111,
                         0b1000000000000111,
                         0b1000000000000100,
                         0b1111111111111100))

    # Use dictionary to select character definition
    font = {
        "H": H11,
        "u": u11,
        "m": m11,
        "i": i11, 
        "d": d11,
        "T": T11,
        "e": e11,
        "p": p11,
        "0": n011,
        "1": n111,
        "2": n211,
        "3": n311,
        "4": n411,
        "5": n511,
        "6": n611,
        "7": n711,
        "8": n811,
        "9": n911,
        ".": stop11,
        ":": colon11,
        "-": minus11,
        "^": upArrow,
        "|": downArrow,
        "/": slash,
        "#": sdError,
        ">": BatteryFull,
        "!": Battery3Q,
        "$": Battery2Q,
        "%": Battery1Q,
        "<": BatteryEmpty,
        chr(33): upArrow,     # change to chr(33)
        chr(34): downArrow,   # change to chr(34)
        "/": slash,      
        chr(36): sdError,     # change to chr(36)
        chr(37): BatteryFull, # change to chr(37) 
        chr(38): Battery3Q,   # change to chr(38)
        chr(39): Battery2Q,   # change to chr(39)
        chr(40): Battery1Q,   # change to chr(40)
        chr(41): BatteryEmpty, # change to chr(41)
        chr(42): upArrowS,
        chr(43): downArrowS
    }


# initialiser parameter is an instantiated oled display
    def __init__(self, display):
        self._display = display
    
    # Display text
    def text(self, word, x, y):
        # Set pixels
        for letter in word:
            # get the definition - default to _11 if definition not found
            data = self.font.get(letter, self._11)
            width = data[0]
            pattern = "{:0" + str(width) + "b}"
            pos = x
            line = y
            for row in data[1]:
                pixels = pattern.format(row)
                for pix in pixels:
                    self._display.pixel(pos, line, int(pix))
                    pos += 1
                line += 1
                pos = x
            # Advance x position to position for next character     
            x += width

graph.py

MicroPython
# (C) Paul Brace March 2022
# Class to display temperature, humidity and pressure histoty graphs V 1.0

# Temperature, Humidity and Pressure monitor with clock and weekly history
# This version uses internal flash memory for the history
# storage files. So you will have to use Thonny, or another program to
# access them from the board.

class oledGraph:

    # initialiser parameter is an instantiated oled display
    def __init__(self, display):
        self._display = display
    
    # Read all lines from the file name passed
    def readLines(self, file):
        log = open(file, "r")
        lines = log.readlines()
        return lines

    # Display graph labels file name determines type of graph
    def showLabels(self, file, maximum, minimum, past, scroll):
        # Clear area where labels are to be shown in case grpah is scrolling
        for x in range(0, 32):
            for y in range(0, 64):
                self._display.pixel(x, y, 0)
        # determine labels to display and display them
        if file[0:4] == "temp":
            sign = "c"
            self._display.text("Temp", 0, 26)   # was 31
        elif file[0:4] == "pres":
            sign = "p"
            self._display.text("Pres", 0, 26)
        else:
            sign = "%"
            self._display.text("Hmid", 0, 26)
        if past:
            self._display.text("Past", 0, 15)   # was 20
        else:
            self._display.text("Curr", 0, 15)
        if scroll:
            self._display.text("Scrl", 0, 37)   # was 42
        self._display.text(str(maximum) + sign, 0, 0)
        self._display.text(str(minimum) + sign, 0, 54)


    # Routine to display graph file is name of history file to display
    # if scroll is true display full history scrolling
    # if scroll is false then last 96 points
    def showGraph(self, file, scroll, past):
        try:
            lines = self.readLines(file)
            # remove the 2 header lines
            lines.pop(0)
            lines.pop(0)
            # scroll = scroll graph for whole file
            # if false only keep last 96 points
            if not scroll and len(lines) > 96:
                for i in range(96, len(lines)):
                    lines.pop(0)
            # Calculate scale
            maximum = -99
            minimum = 9999
            for line in lines:
                v = float(line[18:len(line)-1])
                vu = int(v + 0.9)
                if vu > maximum:
                    maximum = vu
                vl = int(v)
                if vl < minimum:
                    minimum = vl
            if (maximum - minimum) != 0:
                yScale = 63.0 / (maximum - minimum);
            else:
                yScale = 1;
            # Display axis data
            self._display.fill(0)
            self.showLabels(file, maximum, minimum, past, scroll)
            # Plot graph
            first = True;
            i = 0
            for line in lines:
                point = float(line[18:len(line)-1])
                if first:
                    # Set the value of the first point and skip drawing
                    histValue = point
                    first = False
                    continue
                # Draw line from histValue to point and update histValue
                self._display.line(32 + i - 1, 63 - int((histValue - minimum) * yScale),
                         32 + i, 63 - int((point - minimum) * yScale), 1)
                histValue = point
                # if more than a screen of data and if there is scroll screen
                if i < 95:
                    i += 1
                else:
                    #show graph plotted so far
                    self._display.show();
                    # scroll left 1 pixel
                    self._display.scroll(-1, 0)
                    self.showLabels(file, maximum, minimum, past, scroll)
                    # clear far right vertical pixels
                    for y in range(0, 64):
                        self._display.pixel(127, y, 0)

            self._display.show();
        except Exception as e:
            print('Exception: ', e)
            self._display.fill(0)
            self._display.text("No data to", 0, 30)
            self._display.text("   display", 0, 45)
            self._display.show()
            

timeedit.py

MicroPython
# (C) Paul Brace March 2022
# Class to allow user to edit date and time (oled display) V 1.0
# Temperature and Humidity monitor with clock and weekly history


import utime as time
from machine import Pin
from micropython import const

# Define constants
# Time adjustment contants
SET_TIME = const(0)
SET_YEAR = const(1)
SET_DATE = const(2)
SET_ELEVATION = const(3)

#datetime tuple constants
YEAR = const(0)
MONTH = const(1)
DAY = const(2)
HOUR = const(4)
MINUTE = const(5)
SECOND = const(6)


# Class to allow time to be adjusted
class timeAdjustment:
    # Initialisation
    # display = instantiated oled display to use
    # large = instantiated object to display large text
    # rts = instantiated rtc object
    # enter, button2 and button 2 - the 3 pins used to do adjustments
    def __init__(self, display, myFont, rtc, enter, button1, button2, elevation):
        self._display = display
        self._myFont = myFont
        self._rtc = rtc
        self._enter = enter
        self._button1 = button1
        self._button2 = button2
        self._elevation = elevation
        self.currentMode = 0       # Initial mode of display

    # Set time
    def setTime(self,now):
        #now = (_YEAR, _MONTH, _DAY, _DAY number, _HOUR, _MINUTE, _SECOND, micro_SECOND)
        # now = new date/time
        # reset seconds to 0
        now[6] = 0
        now[7] = 0
        # save new time (now is a list so convert to a tuple)
        self._rtc.datetime(tuple(now))

    # Get current date and time
    def getTime(self):
        #returns (YEAR, MONTH, DAY, DAY number, HOUR, MINUTE, SECOND, micro_SECOND)
        t = self._rtc.datetime()
        # convert to a list so can be edited
        return list(t)
    
    def checkMonth(self, now):
        # check if month with 30 days
        if (now[MONTH] == 4 or now[MONTH] == 6 or now[MONTH] == 9 or now[MONTH] == 11) and (now[DAY] > 30):
            now[DAY] = 1
        # check if February and if a leap year
        elif now[MONTH] == 2 and now[DAY] > 29:
            now[DAY] = 1
        elif now[MONTH] == 2 and now[YEAR] % 4 != 0 and now[DAY] > 28:
            now[DAY] = 1;          

    def adj(self):
        # button1 used for up, DAY or HOUR
        # button2 used for down, MONTH or MINUTE
        # button.value() = true if pressed
        now = self.getTime()
        if self.currentMode == SET_TIME:
            if (self._button1.value()):
              if (now[HOUR] < 23):
                now[HOUR] += 1
              else:
                now[HOUR] = 0
              self.setTime(now);
            if (self._button2.value()):
              if (now[MINUTE] < 59):
                now[MINUTE] += 1
              else:
                now[MINUTE] = 0
              self.setTime(now)
        elif self.currentMode == SET_YEAR:
            if self._button1.value():
              now[YEAR] += 1
              self.setTime(now)
            if self._button2.value():
              now[YEAR] -= 1
              self.setTime(now)
        elif self.currentMode == SET_DATE:
            if self._button1.value():
              now[DAY] += 1
              if now[DAY] > 31:
                now[DAY] = 1
              self.checkMonth(now)
              self.setTime(now)
            if self._button2.value():
              now[MONTH] += 1
              if (now[MONTH] > 12):
                now[MONTH] = 1
              self.checkMonth(now)
              self.setTime(now)
        elif self.currentMode == SET_ELEVATION:
            if self._button1.value():
              self._elevation += 1
            if self._button2.value():
              self._elevation -= 1

    # display year for edit
    def showYear(self):
        now = self.getTime()
        self._display.fill(0)
        self._display.text("Adjust year:", 5, 10)
        self._myFont.text("{:04d}".format(now[YEAR]), 40, 30)
        self._display.show()

    # display day and month for edit
    def showDate(self):
        now = self.getTime()
        self._display.fill(0)
        self._display.text("Adjust DD/MM:", 5, 10)
        self._myFont.text("{:02d}/{:02d}".format(now[DAY], now[MONTH]), 40, 30)
        self._display.show()

    # display time for edit
    def showTime(self):
        now = self.getTime()
        self._display.fill(0)
        self._display.text("Adjust HH:MM:", 5, 10)
        self._myFont.text("{:02d}:{:02d}".format(now[HOUR], now[MINUTE]), 40, 30)
        self._display.show()

    # display time for edit
    def showElevation(self):
        self._display.fill(0)
        self._display.text("Set Elevation", 5, 10)
        self._display.text("in feet:", 5, 23)
        self._myFont.text("{:d}".format(self._elevation), 40, 36)
        self._display.show()
        
    # function to allow the user to adjust the time
    def adjustTimeAndElevation(self):
        while True:
            # Pause for button bounce
            time.sleep_ms(150)    
            # check if the enter button has been pressed if so move to
            # next element or return if finished
            if self._enter.value():
                self.currentMode += 1
                if self.currentMode > SET_ELEVATION:
                    self.currentMode = 0
                    return self._elevation
            if self.currentMode == SET_TIME:
                self.showTime()
            elif self.currentMode == SET_YEAR:
                self.showYear()
            elif self.currentMode == SET_DATE:
                self.showDate()
            elif self.currentMode == SET_ELEVATION:
                self.showElevation()
            self.adj()
            

bmp180.py

MicroPython
BMP180 library
'''
bmp180 is a micropython module for the Bosch BMP180 sensor. It measures
temperature as well as pressure, with a high enough resolution to calculate
altitude.
Breakoutboard: http://www.adafruit.com/products/1603  
data-sheet: http://ae-bst.resource.bosch.com/media/products/dokumente/
bmp180/BST-BMP180-DS000-09.pdf

The MIT License (MIT)
Copyright (c) 2014 Sebastian Plamauer, oeplse@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
'''

from ustruct import unpack as unp
from machine import I2C, Pin
import math
import time

# BMP180 class
class BMP180():
    '''
    Module for the BMP180 pressure sensor.
    '''

    _bmp_addr = 119             # adress of BMP180 is hardcoded on the sensor

    # init
    def __init__(self, i2c_bus):

        # create i2c obect
        _bmp_addr = self._bmp_addr
        self._bmp_i2c = i2c_bus
        #self._bmp_i2c.start() Only needed if use SoftIC"
        self.chip_id = self._bmp_i2c.readfrom_mem(_bmp_addr, 0xD0, 2)
        # read calibration data from EEPROM
        self._AC1 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xAA, 2))[0]
        self._AC2 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xAC, 2))[0]
        self._AC3 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xAE, 2))[0]
        self._AC4 = unp('>H', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB0, 2))[0]
        self._AC5 = unp('>H', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB2, 2))[0]
        self._AC6 = unp('>H', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB4, 2))[0]
        self._B1 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB6, 2))[0]
        self._B2 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB8, 2))[0]
        self._MB = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xBA, 2))[0]
        self._MC = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xBC, 2))[0]
        self._MD = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xBE, 2))[0]

        # settings to be adjusted by user
        self.oversample_setting = 3
        self.baseline = 101325.0

        # output raw
        self.UT_raw = None
        self.B5_raw = None
        self.MSB_raw = None
        self.LSB_raw = None
        self.XLSB_raw = None
        self.gauge = self.makegauge() # Generator instance
        for _ in range(128):
            next(self.gauge)
            time.sleep_ms(1)

    def compvaldump(self):
        '''
        Returns a list of all compensation values
        '''
        return [self._AC1, self._AC2, self._AC3, self._AC4, self._AC5, self._AC6, 
                self._B1, self._B2, self._MB, self._MC, self._MD, self.oversample_setting]

    # gauge raw
    def makegauge(self):
        '''
        Generator refreshing the raw measurments.
        '''
        delays = (5, 8, 14, 25)
        while True:
            self._bmp_i2c.writeto_mem(self._bmp_addr, 0xF4, bytearray([0x2E]))
            t_start = time.ticks_ms()
            while (time.ticks_ms() - t_start) <= 5: # 5mS delay
                yield None
            try:
                self.UT_raw = self._bmp_i2c.readfrom_mem(self._bmp_addr, 0xF6, 2)
            except:
                yield None
            self._bmp_i2c.writeto_mem(self._bmp_addr, 0xF4, bytearray([0x34+(self.oversample_setting << 6)]))
            t_pressure_ready = delays[self.oversample_setting]
            t_start = time.ticks_ms()
            while (time.ticks_ms() - t_start) <= t_pressure_ready:
                yield None
            try:
                self.MSB_raw = self._bmp_i2c.readfrom_mem(self._bmp_addr, 0xF6, 1)
                self.LSB_raw = self._bmp_i2c.readfrom_mem(self._bmp_addr, 0xF7, 1)
                self.XLSB_raw = self._bmp_i2c.readfrom_mem(self._bmp_addr, 0xF8, 1)
            except:
                yield None
            yield True

    def blocking_read(self):
        if next(self.gauge) is not None: # Discard old data
            pass
        while next(self.gauge) is None:
            pass

    @property
    def oversample_sett(self):
        return self.oversample_setting

    @oversample_sett.setter
    def oversample_sett(self, value):
        if value in range(4):
            self.oversample_setting = value
        else:
            print('oversample_sett can only be 0, 1, 2 or 3, using 3 instead')
            self.oversample_setting = 3

    @property
    def temperature(self):
        '''
        Temperature in degree C.
        '''
        next(self.gauge)
        try:
            UT = unp('>H', self.UT_raw)[0]
        except:
            return 0.0
        X1 = (UT-self._AC6)*self._AC5/2**15
        X2 = self._MC*2**11/(X1+self._MD)
        self.B5_raw = X1+X2
        return (((X1+X2)+8)/2**4)/10

    @property
    def pressure(self):
        '''
        Pressure in mbar.
        '''
        next(self.gauge)
        self.temperature  # Populate self.B5_raw
        try:
            MSB = unp('B', self.MSB_raw)[0]
            LSB = unp('B', self.LSB_raw)[0]
            XLSB = unp('B', self.XLSB_raw)[0]
        except:
            return 0.0
        UP = ((MSB << 16)+(LSB << 8)+XLSB) >> (8-self.oversample_setting)
        B6 = self.B5_raw-4000
        X1 = (self._B2*(B6**2/2**12))/2**11
        X2 = self._AC2*B6/2**11
        X3 = X1+X2
        B3 = ((int((self._AC1*4+X3)) << self.oversample_setting)+2)/4
        X1 = self._AC3*B6/2**13
        X2 = (self._B1*(B6**2/2**12))/2**16
        X3 = ((X1+X2)+2)/2**2
        B4 = abs(self._AC4)*(X3+32768)/2**15
        B7 = (abs(UP)-B3) * (50000 >> self.oversample_setting)
        if B7 < 0x80000000:
            pressure = (B7*2)/B4
        else:
            pressure = (B7/B4)*2
        X1 = (pressure/2**8)**2
        X1 = (X1*3038)/2**16
        X2 = (-7357*pressure)/2**16
        return pressure+(X1+X2+3791)/2**4

    @property
    def altitude(self):
        '''
        Altitude in m.
        '''
        try:
            p = -7990.0*math.log(self.pressure/self.baseline)
        except:
            p = 0.0
        return p

dht11.py

MicroPython
DHT11 library
# Library downloaded from
# https://github.com/ikornaselur/pico-libs

import array
import micropython
import utime
from machine import Pin
from micropython import const
 
class InvalidChecksum(Exception):
    pass
 
class InvalidPulseCount(Exception):
    pass
 
MAX_UNCHANGED = const(100)
MIN_INTERVAL_US = const(200000)
HIGH_LEVEL = const(50)
EXPECTED_PULSES = const(84)
 
class DHT11:
    _temperature: float
    _humidity: float
 
    def __init__(self, pin):
        self._pin = pin
        self._last_measure = utime.ticks_us()
        self._temperature = -1
        self._humidity = -1
 
    def measure(self):
        current_ticks = utime.ticks_us()
        if utime.ticks_diff(current_ticks, self._last_measure) < MIN_INTERVAL_US and (
            self._temperature > -1 or self._humidity > -1
        ):
            # Less than a second since last read, which is too soon according
            # to the datasheet
            return
 
        self._send_init_signal()
        pulses = self._capture_pulses()
        buffer = self._convert_pulses_to_buffer(pulses)
        self._verify_checksum(buffer)
 
        self._humidity = buffer[0] + buffer[1] / 10
        self._temperature = buffer[2] + buffer[3] / 10
        self._last_measure = utime.ticks_us()
 
    @property
    def humidity(self):
        self.measure()
        return self._humidity
 
    @property
    def temperature(self):
        self.measure()
        return self._temperature
 
    def _send_init_signal(self):
        self._pin.init(Pin.OUT, Pin.PULL_DOWN)
        self._pin.value(1)
        utime.sleep_ms(50)
        self._pin.value(0)
        utime.sleep_ms(18)
 
    @micropython.native
    def _capture_pulses(self):
        pin = self._pin
        pin.init(Pin.IN, Pin.PULL_UP)
 
        val = 1
        idx = 0
        transitions = bytearray(EXPECTED_PULSES)
        unchanged = 0
        timestamp = utime.ticks_us()
 
        while unchanged < MAX_UNCHANGED:
            if val != pin.value():
                if idx >= EXPECTED_PULSES:
                    raise InvalidPulseCount(
                        "Got more than {} pulses".format(EXPECTED_PULSES)
                    )
                now = utime.ticks_us()
                transitions[idx] = now - timestamp
                timestamp = now
                idx += 1
 
                val = 1 - val
                unchanged = 0
            else:
                unchanged += 1
        pin.init(Pin.OUT, Pin.PULL_DOWN)
        if idx != EXPECTED_PULSES:
            raise InvalidPulseCount(
                "Expected {} but got {} pulses".format(EXPECTED_PULSES, idx)
            )
        return transitions[4:]
 
    def _convert_pulses_to_buffer(self, pulses):
        """Convert a list of 80 pulses into a 5 byte buffer
        The resulting 5 bytes in the buffer will be:
            0: Integral relative humidity data
            1: Decimal relative humidity data
            2: Integral temperature data
            3: Decimal temperature data
            4: Checksum
        """
        # Convert the pulses to 40 bits
        binary = 0
        for idx in range(0, len(pulses), 2):
            binary = binary << 1 | int(pulses[idx] > HIGH_LEVEL)
 
        # Split into 5 bytes
        buffer = array.array("B")
        for shift in range(4, -1, -1):
            buffer.append(binary >> shift * 8 & 0xFF)
        return buffer
 
    def _verify_checksum(self, buffer):
        # Calculate checksum
        checksum = 0
        for buf in buffer[0:4]:
            checksum += buf
        if checksum & 0xFF != buffer[4]:
            raise InvalidChecksum()

ssd1306.py

MicroPython
OLED display library
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
# Downloaded from
# https://github.com/stlehmann/micropython-ssd1306
# Also available in Manage Packages in Thonny

from micropython import const
import framebuf


# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)

# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1306(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        self.buffer = bytearray(self.pages * self.width)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP | 0x00,  # off
            # address setting
            SET_MEM_ADDR,
            0x00,  # horizontal
            # resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
            SET_MUX_RATIO,
            self.height - 1,
            SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
            SET_DISP_OFFSET,
            0x00,
            SET_COM_PIN_CFG,
            0x02 if self.width > 2 * self.height else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV,
            0x80,
            SET_PRECHARGE,
            0x22 if self.external_vcc else 0xF1,
            SET_VCOM_DESEL,
            0x30,  # 0.83*Vcc
            # display
            SET_CONTRAST,
            0xFF,  # maximum
            SET_ENTIRE_ON,  # output follows RAM contents
            SET_NORM_INV,  # not inverted
            # charge pump
            SET_CHARGE_PUMP,
            0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,
        ):  # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)

    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        x0 = 0
        x1 = self.width - 1
        if self.width == 64:
            # displays with width of 64 pixels are shifted by 32
            x0 += 32
            x1 += 32
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_data(self.buffer)


class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        self.write_list = [b"\x40", None]  # Co=0, D/C#=1
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)


class SSD1306_SPI(SSD1306):
    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
        self.rate = 10 * 1024 * 1024
        dc.init(dc.OUT, value=0)
        res.init(res.OUT, value=0)
        cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs
        import time

        self.res(1)
        time.sleep_ms(1)
        self.res(0)
        time.sleep_ms(10)
        self.res(1)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([cmd]))
        self.cs(1)

    def write_data(self, buf):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(buf)
        self.cs(1)

Credits

paulsb

paulsb

4 projects • 28 followers

Comments