Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 3 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
|
Having created a few projects with the Arduino I wanted to experiment with the Raspberry Pi Pico as it can also run off a 3.7v battery and has a larger internal memory so offers the ability to create more complex projects.
This project is an evolution of my project “Temp and Humidity Monitor with Graphs and Battery Monitor”. I have added a BMP180 pressure and temperature module and only use the DHT11 for humidity as the BMP180 is more accurate for temperature. With the addition of monitoring pressure, I could also add a weather forecast based on the Zambretti Forecaster model. Finally, as there is over 1mb of available flash storage available, the history files have been enhanced to record a reading each hour, with date and time stamp. The system will store a week’s history in each file and then create a new file set with an incremented file number. There is sufficient flash storage for about 3 months of data and the system will stop storing data once the storage is nearly full. You will need to use Thonny, or some other program, to read/copy/delete the files which are contained in the data folder and restart the monitor to continue saving data. If you do not delete the config file, file numbering will continue in sequence, if you delete it then numbering will start again from 1.
The project uses the internal Pi Pico clock rather than a RTC module so the time/date has to be corrected each time the unit is turned off and on. However, the current time and date are stored in a config file each hour so the last recorded time and date are displayed for edit.
There are 13 screens of data, move from one to the next by pressing the mode button:
- Date, time and summary
- Barometer display with forecast
- Recent pressure history graph (last 4 days)
- Temperature with min and max temp since last switched on or reset
- Recent temperature history graph (last 4 days)
- Humidity with min and max temp since last switched on or reset
- Recent humidity history graph (last 4 days)
- Current pressure history graph (scrolling up to 1 week)
- Current temperature history graph (scrolling up to 1 week)
- Current humidity history graph (scrolling up to 1 week)
- Previous pressure history graph (scrolling up to 1 week)
- Previous temperature history graph (scrolling up to 1 week)
- Previous humidity history graph (scrolling up to 1 week)
There is a time out on selected screen and, if no button is pressed, the monitor will return to the time/summary display after 10 seconds
The monitor has 3 control buttons:
Button 1:
- monitor mode: change display
- setting mode: change hour, change day, year/altitude up
Button 2:
- monitor mode: reset min and max figures
- Setting mode: change minute, month, year/altitude down
Button 3:
- monitor mode: enter setting mode
- Setting move: accept setting and move to next item
The monitor can be powered directly via the UBS port of the Pico or via the rechargeable battery. A 3, 000mAh battery should last about 4 days and the monitor can continue to be used while it is charging.
Weather forecastThe weather forecast is based on the Zambretti Forecaster model. This article was used as the basis for the lookup tables:
https://web.archive.org/web/20110610213848/http:/www.meteormetrics.com/zambretti.htm
The codeThe code is written in MicroPython using Thonny as the IDE and uses the following downloaded libraries:
from ssd1306 import SSD1306_I2C
from bmp180 import BMP180
from dht11 import DHT11, InvalidChecksum, InvalidPulseCount
The libraries are included in the downloads and were obtained from:
- ssd1306.py – https://github.com/stlehmann/micropython-ssd1306
- bmp180.py – Adafruit
- dht11.py – https://github.com/ikornaselur/pico-libs
The display library only gives access to a single font and I wanted to use a larger font and icons for some of the displays, so I created a class called myFont
, in file myfont.py
. This allows characters to be easily defined visually in tuples which are then looked up using a dictionary. The first element of the tuple is the width of the character in pixels and the second another tuple containing the lines to be printed. Each line is a binary number with 0 indicating the pixel is off and 1 it is on. For example the large up arrow is defined:
upArrow =(11, (0b00000100000,
0b00001110000,
0b00011111000,
0b00111111100,
0b01111111110,
0b00001110000,
0b00001110000,
0b00001110000,
0b00001110000,
0b00001110000,
0b00001110000))
A character can have any number of lines.
A second class oledGraph
, in graph.py, provides the graph display via a method called showGraph
. This takes the following parameters:
- File – the name of the history file to be displayed
- Scroll – if true the full file is displayed, scrolling if there is more data than will fit on the screen. If false then the latest 96 hours are displayed
- Past – true if displaying the previous history file
A final class called timeAdjustment
is defined in file timeedit.py. This provides a method called adjustTimeAndElevation
that allows you to edit the time, date and elevation using the buttons.
The elevation is used to calculate pressure at sea level which is used for forecasting and to determine if the pressure is high, low or normal.
Smoothing
As readings from the sensors are not 100% consistent, the last 5 readings are held and the result smoothed by removing the highest and lowest reading and averaging the others.
Checking the battery voltage
Pin 26 (ADC0) is used as an Analog Input pin to read the voltage. This is done by dividing the voltage using 2 resistors, 1m and 330k ohms. This divides the voltage by approximately 4. The resulting voltage on the pin returns a value of 0=0 volts to 65, 536=3.3 volts, hence the following calculation will return the actual battery voltage:
def readVoltage():
return analogValue.read_u16() / 65536 * 3.3 * 4.03
Files
A timer is used to call the routines to save the history files every hour. They are stored as text files in csv format.
A log file is also maintained in which are recorded main events and any errors.
Debug mode
The DUBUG
flag, when set to True, will result in information being displayed in the Thonny shell when the monitor is running and connected to the computer.
In debug mode an addition file is also created recording the battery level each hour.
Comments are included in the code with further details.
3D Printed Project BoxI have designed and printed a box for the project and the STL files are included in the download. Print with supports.
Here is an image of the components in the box wired and secured with a hot glue gun:
The BMP180 and DHT11 are positioned on the right butted up together.
The battery is then laid across the top and the lid pushed on.
Wiring is soldered directly to the Pi Pico, battery and switches with female wire connectors connecting the sensors and display.
For the wiring I used the following hook up wire:
TUOFENG 22 Gauge Electric Wire, Tinned Copper Wire Kit 0.32mm² Flexible Silicone Wire
https://www.amazon.co.uk/gp/product/B07G72DRKC/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1
and :
ELEGOO 120pcs Multicolored Dupont Wire 40pin Male to Female, 40pin Male to Male, 40pin Female to Femal
I cut the female to female wires in half so that I could make a lead with 2 or 3 female connectors at one end soldered to a single wire that connected to the Pi Pico using heat shrink tubing to cover the joint, as shown in the picture.
# (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))
# (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
# (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()
# (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 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
# 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()
# 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)
Comments