Initial commit

This commit is contained in:
Paul Beech
2020-05-12 04:24:11 +01:00
commit bcf2c73bdf
50 changed files with 2866 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
build/
_build/
*.o
*.so
*.a
*.py[cod]
*.egg-info
dist/
__pycache__
.DS_Store
*.deb
*.dsc
*.build
*.changes
*.orig.*
packaging/*tar.xz
library/debian/
.coverage
.pytest_cache
.tox
.vscode/

5
.stickler.yml Normal file
View File

@@ -0,0 +1,5 @@
---
linters:
flake8:
python: 3
max-line-length: 160

25
.travis.yml Normal file
View File

@@ -0,0 +1,25 @@
language: python
sudo: false
cache: pip
git:
submodules: true
matrix:
include:
- python: "2.7"
env: TOXENV=py27
- python: "3.5"
env: TOXENV=py35
install:
- pip install --ignore-installed --upgrade setuptools pip tox coveralls
script:
- cd library
- tox -vv
after_success: if [ "$TOXENV" == "py35" ]; then coveralls; fi
notifications:
email: false

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Paul Beech
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.

70
Makefile Normal file
View File

@@ -0,0 +1,70 @@
LIBRARY_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}')
LIBRARY_NAME=$(shell grep name library/setup.cfg | awk -F" = " '{print $$2}')
.PHONY: usage install uninstall
usage:
@echo "Library: ${LIBRARY_NAME}"
@echo "Version: ${LIBRARY_VERSION}\n"
@echo "Usage: make <target>, where target is one of:\n"
@echo "install: install the library locally from source"
@echo "uninstall: uninstall the local library"
@echo "check: peform basic integrity checks on the codebase"
@echo "python-readme: generate library/README.rst from README.md"
@echo "python-wheels: build python .whl files for distribution"
@echo "python-sdist: build python source distribution"
@echo "python-clean: clean python build and dist directories"
@echo "python-dist: build all python distribution files"
@echo "python-testdeploy: build all and deploy to test PyPi"
@echo "tag: tag the repository with the current version"
install:
./install.sh
uninstall:
./uninstall.sh
check:
@echo "Checking for trailing whitespace"
@! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO
@echo "Checking for DOS line-endings"
@! grep -IUrn --color "
" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile
@echo "Checking library/CHANGELOG.txt"
@cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION}
@echo "Checking library/${LIBRARY_NAME}/__init__.py"
@cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'"
tag:
git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}"
python-readme: library/README.rst
python-license: library/LICENSE.txt
library/README.rst: README.md library/CHANGELOG.txt
pandoc --from=markdown --to=rst -o library/README.rst README.md
echo "" >> library/README.rst
cat library/CHANGELOG.txt >> library/README.rst
library/LICENSE.txt: LICENSE
cp LICENSE library/LICENSE.txt
python-wheels: python-readme python-license
cd library; python3 setup.py bdist_wheel
cd library; python setup.py bdist_wheel
python-sdist: python-readme python-license
cd library; python setup.py sdist
python-clean:
-rm -r library/dist
-rm -r library/build
-rm -r library/*.egg-info
python-dist: python-clean python-wheels python-sdist
ls library/dist
python-testdeploy: python-dist
twine upload --repository-url https://test.pypi.org/legacy/ library/dist/*
python-deploy: check python-dist

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
# Enviro+
Designed for environmental monitoring, Enviro+ lets you measure air quality (pollutant gases and particulates), temperature, pressure, humidity, light, and noise level. Learn more - https://shop.pimoroni.com/products/enviro-plus
[![Build Status](https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master)](https://travis-ci.com/pimoroni/enviroplus-python)
[![Coverage Status](https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/enviroplus-python?branch=master)
[![PyPi Package](https://img.shields.io/pypi/v/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus)
[![Python Versions](https://img.shields.io/pypi/pyversions/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus)
# Installing
You're best using the "One-line" install method if you want all of the UART serial configuration for the PMS5003 particulate matter sensor to run automatically.
## One-line (Installs from GitHub)
```
curl -sSL https://get.pimoroni.com/enviroplus | bash
```
**Note** report issues with one-line installer here: https://github.com/pimoroni/get
## Or... Install and configure dependencies from GitHub:
* `git clone https://github.com/pimoroni/enviroplus-python`
* `cd enviroplus-python`
* `sudo ./install.sh`
**Note** Raspbian Lite users may first need to install git: `sudo apt install git`
## Or... Install from PyPi and configure manually:
* Run `sudo pip install enviroplus`
**Note** this wont perform any of the required configuration changes on your Pi, you may additionally need to:
* Enable i2c: `raspi-config nonint do_i2c 0`
* Enable SPI: `raspi-config nonint do_spi 0`
And if you're using a PMS5003 sensor you will need to:
* Enable serial: `raspi-config nonint set_config_var enable_uart 1 /boot/config.txt`
* Disable serial terminal: `sudo raspi-config nonint do_serial 1`
* Add `dtoverlay=pi3-miniuart-bt` to your `/boot/config.txt`
And install additional dependencies:
```
sudo apt install python-numpy python-smbus python-pil python-setuptools
```
## Help & Support
* GPIO Pinout - https://pinout.xyz/pinout/enviro_plus
* Support forums - http://forums.pimoroni.com/c/support
* Discord - https://discord.gg/hr93ByC

27
examples/adc.py Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import time
from enviroplus import gas
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""adc.py - Print readings from the MICS6814 Gas sensor.
Press Ctrl+C to exit!
""")
gas.enable_adc()
gas.set_adc_gain(4.096)
try:
while True:
readings = gas.read_all()
logging.info(readings)
time.sleep(1.0)
except KeyboardInterrupt:
pass

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
import time
import colorsys
import os
import sys
import ST7735
try:
# Transitional fix for breaking change in LTR559
from ltr559 import LTR559
ltr559 = LTR559()
except ImportError:
import ltr559
from bme280 import BME280
from enviroplus import gas
from subprocess import PIPE, Popen
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from fonts.ttf import RobotoMedium as UserFont
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors
Press Ctrl+C to exit!
""")
# BME280 temperature/pressure/humidity sensor
bme280 = BME280()
# Create ST7735 LCD display class
st7735 = ST7735.ST7735(
port=0,
cs=1,
dc=9,
backlight=12,
rotation=270,
spi_speed_hz=10000000
)
# Initialize display
st7735.begin()
WIDTH = st7735.width
HEIGHT = st7735.height
# Set up canvas and font
img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
path = os.path.dirname(os.path.realpath(__file__))
font_size = 20
font = ImageFont.truetype(UserFont, font_size)
message = ""
# The position of the top bar
top_pos = 25
# Displays data and text on the 0.96" LCD
def display_text(variable, data, unit):
# Maintain length of list
values[variable] = values[variable][1:] + [data]
# Scale the values for the variable between 0 and 1
vmin = min(values[variable])
vmax = max(values[variable])
colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values[variable]]
# Format the variable name and value
message = "{}: {:.1f} {}".format(variable[:4], data, unit)
logging.info(message)
draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255))
for i in range(len(colours)):
# Convert the values to colours from red to blue
colour = (1.0 - colours[i]) * 0.6
r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, 1.0, 1.0)]
# Draw a 1-pixel wide rectangle of colour
draw.rectangle((i, top_pos, i + 1, HEIGHT), (r, g, b))
# Draw a line graph in black
line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos))) + top_pos
draw.rectangle((i, line_y, i + 1, line_y + 1), (0, 0, 0))
# Write the text at the top in black
draw.text((0, 0), message, font=font, fill=(0, 0, 0))
st7735.display(img)
# Get the temperature of the CPU for compensation
def get_cpu_temperature():
process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True)
output, _error = process.communicate()
return float(output[output.index('=') + 1:output.rindex("'")])
# Tuning factor for compensation. Decrease this number to adjust the
# temperature down, and increase to adjust up
factor = 2.25
cpu_temps = [get_cpu_temperature()] * 5
delay = 0.5 # Debounce the proximity tap
mode = 0 # The starting mode
last_page = 0
light = 1
# Create a values dict to store the data
variables = ["temperature",
"pressure",
"humidity",
"light",
"oxidised",
"reduced",
"nh3"]
values = {}
for v in variables:
values[v] = [1] * WIDTH
# The main loop
try:
while True:
proximity = ltr559.get_proximity()
# If the proximity crosses the threshold, toggle the mode
if proximity > 1500 and time.time() - last_page > delay:
mode += 1
mode %= len(variables)
last_page = time.time()
# One mode for each variable
if mode == 0:
# variable = "temperature"
unit = "C"
cpu_temp = get_cpu_temperature()
# Smooth out with some averaging to decrease jitter
cpu_temps = cpu_temps[1:] + [cpu_temp]
avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
raw_temp = bme280.get_temperature()
data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
display_text(variables[mode], data, unit)
if mode == 1:
# variable = "pressure"
unit = "hPa"
data = bme280.get_pressure()
display_text(variables[mode], data, unit)
if mode == 2:
# variable = "humidity"
unit = "%"
data = bme280.get_humidity()
display_text(variables[mode], data, unit)
if mode == 3:
# variable = "light"
unit = "Lux"
if proximity < 10:
data = ltr559.get_lux()
else:
data = 1
display_text(variables[mode], data, unit)
if mode == 4:
# variable = "oxidised"
unit = "kO"
data = gas.read_all()
data = data.oxidising / 1000
display_text(variables[mode], data, unit)
if mode == 5:
# variable = "reduced"
unit = "kO"
data = gas.read_all()
data = data.reducing / 1000
display_text(variables[mode], data, unit)
if mode == 6:
# variable = "nh3"
unit = "kO"
data = gas.read_all()
data = data.nh3 / 1000
display_text(variables[mode], data, unit)
# Exit cleanly
except KeyboardInterrupt:
sys.exit(0)

230
examples/all-in-one.py Normal file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python3
import time
import colorsys
import sys
import ST7735
try:
# Transitional fix for breaking change in LTR559
from ltr559 import LTR559
ltr559 = LTR559()
except ImportError:
import ltr559
from bme280 import BME280
from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError
from enviroplus import gas
from subprocess import PIPE, Popen
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from fonts.ttf import RobotoMedium as UserFont
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors
Press Ctrl+C to exit!
""")
# BME280 temperature/pressure/humidity sensor
bme280 = BME280()
# PMS5003 particulate sensor
pms5003 = PMS5003()
# Create ST7735 LCD display class
st7735 = ST7735.ST7735(
port=0,
cs=1,
dc=9,
backlight=12,
rotation=270,
spi_speed_hz=10000000
)
# Initialize display
st7735.begin()
WIDTH = st7735.width
HEIGHT = st7735.height
# Set up canvas and font
img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
font_size = 20
font = ImageFont.truetype(UserFont, font_size)
message = ""
# The position of the top bar
top_pos = 25
# Displays data and text on the 0.96" LCD
def display_text(variable, data, unit):
# Maintain length of list
values[variable] = values[variable][1:] + [data]
# Scale the values for the variable between 0 and 1
vmin = min(values[variable])
vmax = max(values[variable])
colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values[variable]]
# Format the variable name and value
message = "{}: {:.1f} {}".format(variable[:4], data, unit)
logging.info(message)
draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255))
for i in range(len(colours)):
# Convert the values to colours from red to blue
colour = (1.0 - colours[i]) * 0.6
r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, 1.0, 1.0)]
# Draw a 1-pixel wide rectangle of colour
draw.rectangle((i, top_pos, i + 1, HEIGHT), (r, g, b))
# Draw a line graph in black
line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos))) + top_pos
draw.rectangle((i, line_y, i + 1, line_y + 1), (0, 0, 0))
# Write the text at the top in black
draw.text((0, 0), message, font=font, fill=(0, 0, 0))
st7735.display(img)
# Get the temperature of the CPU for compensation
def get_cpu_temperature():
process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True)
output, _error = process.communicate()
return float(output[output.index('=') + 1:output.rindex("'")])
# Tuning factor for compensation. Decrease this number to adjust the
# temperature down, and increase to adjust up
factor = 2.25
cpu_temps = [get_cpu_temperature()] * 5
delay = 0.5 # Debounce the proximity tap
mode = 0 # The starting mode
last_page = 0
light = 1
# Create a values dict to store the data
variables = ["temperature",
"pressure",
"humidity",
"light",
"oxidised",
"reduced",
"nh3",
"pm1",
"pm25",
"pm10"]
values = {}
for v in variables:
values[v] = [1] * WIDTH
# The main loop
try:
while True:
proximity = ltr559.get_proximity()
# If the proximity crosses the threshold, toggle the mode
if proximity > 1500 and time.time() - last_page > delay:
mode += 1
mode %= len(variables)
last_page = time.time()
# One mode for each variable
if mode == 0:
# variable = "temperature"
unit = "C"
cpu_temp = get_cpu_temperature()
# Smooth out with some averaging to decrease jitter
cpu_temps = cpu_temps[1:] + [cpu_temp]
avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
raw_temp = bme280.get_temperature()
data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
display_text(variables[mode], data, unit)
if mode == 1:
# variable = "pressure"
unit = "hPa"
data = bme280.get_pressure()
display_text(variables[mode], data, unit)
if mode == 2:
# variable = "humidity"
unit = "%"
data = bme280.get_humidity()
display_text(variables[mode], data, unit)
if mode == 3:
# variable = "light"
unit = "Lux"
if proximity < 10:
data = ltr559.get_lux()
else:
data = 1
display_text(variables[mode], data, unit)
if mode == 4:
# variable = "oxidised"
unit = "kO"
data = gas.read_all()
data = data.oxidising / 1000
display_text(variables[mode], data, unit)
if mode == 5:
# variable = "reduced"
unit = "kO"
data = gas.read_all()
data = data.reducing / 1000
display_text(variables[mode], data, unit)
if mode == 6:
# variable = "nh3"
unit = "kO"
data = gas.read_all()
data = data.nh3 / 1000
display_text(variables[mode], data, unit)
if mode == 7:
# variable = "pm1"
unit = "ug/m3"
try:
data = pms5003.read()
except pmsReadTimeoutError:
logging.warn("Failed to read PMS5003")
else:
data = float(data.pm_ug_per_m3(1.0))
display_text(variables[mode], data, unit)
if mode == 8:
# variable = "pm25"
unit = "ug/m3"
try:
data = pms5003.read()
except pmsReadTimeoutError:
logging.warn("Failed to read PMS5003")
else:
data = float(data.pm_ug_per_m3(2.5))
display_text(variables[mode], data, unit)
if mode == 9:
# variable = "pm10"
unit = "ug/m3"
try:
data = pms5003.read()
except pmsReadTimeoutError:
logging.warn("Failed to read PMS5003")
else:
data = float(data.pm_ug_per_m3(10))
display_text(variables[mode], data, unit)
# Exit cleanly
except KeyboardInterrupt:
sys.exit(0)

348
examples/combined.py Normal file
View File

@@ -0,0 +1,348 @@
#!/usr/bin/env python3
import time
import colorsys
import sys
import ST7735
try:
# Transitional fix for breaking change in LTR559
from ltr559 import LTR559
ltr559 = LTR559()
except ImportError:
import ltr559
from bme280 import BME280
from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError
from enviroplus import gas
from subprocess import PIPE, Popen
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from fonts.ttf import RobotoMedium as UserFont
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors
Press Ctrl+C to exit!
""")
# BME280 temperature/pressure/humidity sensor
bme280 = BME280()
# PMS5003 particulate sensor
pms5003 = PMS5003()
time.sleep(1.0)
# Create ST7735 LCD display class
st7735 = ST7735.ST7735(
port=0,
cs=1,
dc=9,
backlight=12,
rotation=270,
spi_speed_hz=10000000
)
# Initialize display
st7735.begin()
WIDTH = st7735.width
HEIGHT = st7735.height
# Set up canvas and font
img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
font_size_small = 10
font_size_large = 20
font = ImageFont.truetype(UserFont, font_size_large)
smallfont = ImageFont.truetype(UserFont, font_size_small)
x_offset = 2
y_offset = 2
message = ""
# The position of the top bar
top_pos = 25
# Create a values dict to store the data
variables = ["temperature",
"pressure",
"humidity",
"light",
"oxidised",
"reduced",
"nh3",
"pm1",
"pm25",
"pm10"]
units = ["C",
"hPa",
"%",
"Lux",
"kO",
"kO",
"kO",
"ug/m3",
"ug/m3",
"ug/m3"]
# Define your own warning limits
# The limits definition follows the order of the variables array
# Example limits explanation for temperature:
# [4,18,28,35] means
# [-273.15 .. 4] -> Dangerously Low
# (4 .. 18] -> Low
# (18 .. 28] -> Normal
# (28 .. 35] -> High
# (35 .. MAX] -> Dangerously High
# DISCLAIMER: The limits provided here are just examples and come
# with NO WARRANTY. The authors of this example code claim
# NO RESPONSIBILITY if reliance on the following values or this
# code in general leads to ANY DAMAGES or DEATH.
limits = [[4, 18, 28, 35],
[250, 650, 1013.25, 1015],
[20, 30, 60, 70],
[-1, -1, 30000, 100000],
[-1, -1, 40, 50],
[-1, -1, 450, 550],
[-1, -1, 200, 300],
[-1, -1, 50, 100],
[-1, -1, 50, 100],
[-1, -1, 50, 100]]
# RGB palette for values on the combined screen
palette = [(0, 0, 255), # Dangerously Low
(0, 255, 255), # Low
(0, 255, 0), # Normal
(255, 255, 0), # High
(255, 0, 0)] # Dangerously High
values = {}
# Displays data and text on the 0.96" LCD
def display_text(variable, data, unit):
# Maintain length of list
values[variable] = values[variable][1:] + [data]
# Scale the values for the variable between 0 and 1
vmin = min(values[variable])
vmax = max(values[variable])
colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values[variable]]
# Format the variable name and value
message = "{}: {:.1f} {}".format(variable[:4], data, unit)
logging.info(message)
draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255))
for i in range(len(colours)):
# Convert the values to colours from red to blue
colour = (1.0 - colours[i]) * 0.6
r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, 1.0, 1.0)]
# Draw a 1-pixel wide rectangle of colour
draw.rectangle((i, top_pos, i + 1, HEIGHT), (r, g, b))
# Draw a line graph in black
line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos))) + top_pos
draw.rectangle((i, line_y, i + 1, line_y + 1), (0, 0, 0))
# Write the text at the top in black
draw.text((0, 0), message, font=font, fill=(0, 0, 0))
st7735.display(img)
# Saves the data to be used in the graphs later and prints to the log
def save_data(idx, data):
variable = variables[idx]
# Maintain length of list
values[variable] = values[variable][1:] + [data]
unit = units[idx]
message = "{}: {:.1f} {}".format(variable[:4], data, unit)
logging.info(message)
# Displays all the text on the 0.96" LCD
def display_everything():
draw.rectangle((0, 0, WIDTH, HEIGHT), (0, 0, 0))
column_count = 2
row_count = (len(variables) / column_count)
for i in range(len(variables)):
variable = variables[i]
data_value = values[variable][-1]
unit = units[i]
x = x_offset + ((WIDTH / column_count) * (i / row_count))
y = y_offset + ((HEIGHT / row_count) * (i % row_count))
message = "{}: {:.1f} {}".format(variable[:4], data_value, unit)
lim = limits[i]
rgb = palette[0]
for j in range(len(lim)):
if data_value > lim[j]:
rgb = palette[j + 1]
draw.text((x, y), message, font=smallfont, fill=rgb)
st7735.display(img)
# Get the temperature of the CPU for compensation
def get_cpu_temperature():
process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True)
output, _error = process.communicate()
return float(output[output.index('=') + 1:output.rindex("'")])
def main():
# Tuning factor for compensation. Decrease this number to adjust the
# temperature down, and increase to adjust up
factor = 2.25
cpu_temps = [get_cpu_temperature()] * 5
delay = 0.5 # Debounce the proximity tap
mode = 10 # The starting mode
last_page = 0
for v in variables:
values[v] = [1] * WIDTH
# The main loop
try:
while True:
proximity = ltr559.get_proximity()
# If the proximity crosses the threshold, toggle the mode
if proximity > 1500 and time.time() - last_page > delay:
mode += 1
mode %= (len(variables) + 1)
last_page = time.time()
# One mode for each variable
if mode == 0:
# variable = "temperature"
unit = "C"
cpu_temp = get_cpu_temperature()
# Smooth out with some averaging to decrease jitter
cpu_temps = cpu_temps[1:] + [cpu_temp]
avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
raw_temp = bme280.get_temperature()
data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
display_text(variables[mode], data, unit)
if mode == 1:
# variable = "pressure"
unit = "hPa"
data = bme280.get_pressure()
display_text(variables[mode], data, unit)
if mode == 2:
# variable = "humidity"
unit = "%"
data = bme280.get_humidity()
display_text(variables[mode], data, unit)
if mode == 3:
# variable = "light"
unit = "Lux"
if proximity < 10:
data = ltr559.get_lux()
else:
data = 1
display_text(variables[mode], data, unit)
if mode == 4:
# variable = "oxidised"
unit = "kO"
data = gas.read_all()
data = data.oxidising / 1000
display_text(variables[mode], data, unit)
if mode == 5:
# variable = "reduced"
unit = "kO"
data = gas.read_all()
data = data.reducing / 1000
display_text(variables[mode], data, unit)
if mode == 6:
# variable = "nh3"
unit = "kO"
data = gas.read_all()
data = data.nh3 / 1000
display_text(variables[mode], data, unit)
if mode == 7:
# variable = "pm1"
unit = "ug/m3"
try:
data = pms5003.read()
except pmsReadTimeoutError:
logging.warn("Failed to read PMS5003")
else:
data = float(data.pm_ug_per_m3(1.0))
display_text(variables[mode], data, unit)
if mode == 8:
# variable = "pm25"
unit = "ug/m3"
try:
data = pms5003.read()
except pmsReadTimeoutError:
logging.warn("Failed to read PMS5003")
else:
data = float(data.pm_ug_per_m3(2.5))
display_text(variables[mode], data, unit)
if mode == 9:
# variable = "pm10"
unit = "ug/m3"
try:
data = pms5003.read()
except pmsReadTimeoutError:
logging.warn("Failed to read PMS5003")
else:
data = float(data.pm_ug_per_m3(10))
display_text(variables[mode], data, unit)
if mode == 10:
# Everything on one screen
cpu_temp = get_cpu_temperature()
# Smooth out with some averaging to decrease jitter
cpu_temps = cpu_temps[1:] + [cpu_temp]
avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
raw_temp = bme280.get_temperature()
raw_data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
save_data(0, raw_data)
display_everything()
raw_data = bme280.get_pressure()
save_data(1, raw_data)
display_everything()
raw_data = bme280.get_humidity()
save_data(2, raw_data)
if proximity < 10:
raw_data = ltr559.get_lux()
else:
raw_data = 1
save_data(3, raw_data)
display_everything()
gas_data = gas.read_all()
save_data(4, gas_data.oxidising / 1000)
save_data(5, gas_data.reducing / 1000)
save_data(6, gas_data.nh3 / 1000)
display_everything()
pms_data = None
try:
pms_data = pms5003.read()
except pmsReadTimeoutError:
logging.warn("Failed to read PMS5003")
else:
save_data(7, float(pms_data.pm_ug_per_m3(1.0)))
save_data(8, float(pms_data.pm_ug_per_m3(2.5)))
save_data(9, float(pms_data.pm_ug_per_m3(10)))
display_everything()
# Exit cleanly
except KeyboardInterrupt:
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
import time
from bme280 import BME280
try:
from smbus2 import SMBus
except ImportError:
from smbus import SMBus
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""compensated-temperature.py - Use the CPU temperature
to compensate temperature readings from the BME280 sensor.
Method adapted from Initial State's Enviro pHAT review:
https://medium.com/@InitialState/tutorial-review-enviro-phat-for-raspberry-pi-4cd6d8c63441
Press Ctrl+C to exit!
""")
bus = SMBus(1)
bme280 = BME280(i2c_dev=bus)
# Get the temperature of the CPU for compensation
def get_cpu_temperature():
with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
temp = f.read()
temp = int(temp) / 1000.0
return temp
# Tuning factor for compensation. Decrease this number to adjust the
# temperature down, and increase to adjust up
factor = 2.25
cpu_temps = [get_cpu_temperature()] * 5
while True:
cpu_temp = get_cpu_temperature()
# Smooth out with some averaging to decrease jitter
cpu_temps = cpu_temps[1:] + [cpu_temp]
avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
raw_temp = bme280.get_temperature()
comp_temp = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
logging.info("Compensated temperature: {:05.2f} *C".format(comp_temp))
time.sleep(1.0)

24
examples/gas.py Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
import time
from enviroplus import gas
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""gas.py - Print readings from the MICS6814 Gas sensor.
Press Ctrl+C to exit!
""")
try:
while True:
readings = gas.read_all()
logging.info(readings)
time.sleep(1.0)
except KeyboardInterrupt:
pass

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
examples/icons/bulb-dim.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
examples/icons/humidity.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

65
examples/lcd.py Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import ST7735
from PIL import Image, ImageDraw, ImageFont
from fonts.ttf import RobotoMedium as UserFont
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""lcd.py - Hello, World! example on the 0.96" LCD.
Press Ctrl+C to exit!
""")
# Create LCD class instance.
disp = ST7735.ST7735(
port=0,
cs=1,
dc=9,
backlight=12,
rotation=270,
spi_speed_hz=10000000
)
# Initialize display.
disp.begin()
# Width and height to calculate text position.
WIDTH = disp.width
HEIGHT = disp.height
# New canvas to draw on.
img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
# Text settings.
font_size = 25
font = ImageFont.truetype(UserFont, font_size)
text_colour = (255, 255, 255)
back_colour = (0, 170, 170)
message = "Hello, World!"
size_x, size_y = draw.textsize(message, font)
# Calculate text position
x = (WIDTH - size_x) / 2
y = (HEIGHT / 2) - (size_y / 2)
# Draw background rectangle and write text.
draw.rectangle((0, 0, 160, 80), back_colour)
draw.text((x, y), message, font=font, fill=text_colour)
disp.display(img)
# Keep running.
try:
while True:
pass
# Turn off backlight on control-c
except KeyboardInterrupt:
disp.set_backlight(0)

33
examples/light.py Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import time
import logging
try:
# Transitional fix for breaking change in LTR559
from ltr559 import LTR559
ltr559 = LTR559()
except ImportError:
import ltr559
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""light.py - Print readings from the LTR559 Light & Proximity sensor.
Press Ctrl+C to exit!
""")
try:
while True:
lux = ltr559.get_lux()
prox = ltr559.get_proximity()
logging.info("""Light: {:05.02f} Lux
Proximity: {:05.02f}
""".format(lux, prox))
time.sleep(1.0)
except KeyboardInterrupt:
pass

188
examples/luftdaten.py Normal file
View File

@@ -0,0 +1,188 @@
#!/usr/bin/env python3
import requests
import ST7735
import time
from bme280 import BME280
from pms5003 import PMS5003, ReadTimeoutError
from subprocess import PIPE, Popen, check_output
from PIL import Image, ImageDraw, ImageFont
from fonts.ttf import RobotoMedium as UserFont
try:
from smbus2 import SMBus
except ImportError:
from smbus import SMBus
print("""luftdaten.py - Reads temperature, pressure, humidity,
PM2.5, and PM10 from Enviro plus and sends data to Luftdaten,
the citizen science air quality project.
Note: you'll need to register with Luftdaten at:
https://meine.luftdaten.info/ and enter your Raspberry Pi
serial number that's displayed on the Enviro plus LCD along
with the other details before the data appears on the
Luftdaten map.
Press Ctrl+C to exit!
""")
bus = SMBus(1)
# Create BME280 instance
bme280 = BME280(i2c_dev=bus)
# Create LCD instance
disp = ST7735.ST7735(
port=0,
cs=1,
dc=9,
backlight=12,
rotation=270,
spi_speed_hz=10000000
)
# Initialize display
disp.begin()
# Create PMS5003 instance
pms5003 = PMS5003()
# Read values from BME280 and PMS5003 and return as dict
def read_values():
values = {}
cpu_temp = get_cpu_temperature()
raw_temp = bme280.get_temperature()
comp_temp = raw_temp - ((cpu_temp - raw_temp) / comp_factor)
values["temperature"] = "{:.2f}".format(comp_temp)
values["pressure"] = "{:.2f}".format(bme280.get_pressure() * 100)
values["humidity"] = "{:.2f}".format(bme280.get_humidity())
try:
pm_values = pms5003.read()
values["P2"] = str(pm_values.pm_ug_per_m3(2.5))
values["P1"] = str(pm_values.pm_ug_per_m3(10))
except ReadTimeoutError:
pms5003.reset()
pm_values = pms5003.read()
values["P2"] = str(pm_values.pm_ug_per_m3(2.5))
values["P1"] = str(pm_values.pm_ug_per_m3(10))
return values
# Get CPU temperature to use for compensation
def get_cpu_temperature():
process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True)
output, _error = process.communicate()
return float(output[output.index('=') + 1:output.rindex("'")])
# Get Raspberry Pi serial number to use as ID
def get_serial_number():
with open('/proc/cpuinfo', 'r') as f:
for line in f:
if line[0:6] == 'Serial':
return line.split(":")[1].strip()
# Check for Wi-Fi connection
def check_wifi():
if check_output(['hostname', '-I']):
return True
else:
return False
# Display Raspberry Pi serial and Wi-Fi status on LCD
def display_status():
wifi_status = "connected" if check_wifi() else "disconnected"
text_colour = (255, 255, 255)
back_colour = (0, 170, 170) if check_wifi() else (85, 15, 15)
id = get_serial_number()
message = "{}\nWi-Fi: {}".format(id, wifi_status)
img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
size_x, size_y = draw.textsize(message, font)
x = (WIDTH - size_x) / 2
y = (HEIGHT / 2) - (size_y / 2)
draw.rectangle((0, 0, 160, 80), back_colour)
draw.text((x, y), message, font=font, fill=text_colour)
disp.display(img)
def send_to_luftdaten(values, id):
pm_values = dict(i for i in values.items() if i[0].startswith("P"))
temp_values = dict(i for i in values.items() if not i[0].startswith("P"))
pm_values_json = [{"value_type": key, "value": val} for key, val in pm_values.items()]
temp_values_json = [{"value_type": key, "value": val} for key, val in temp_values.items()]
resp_1 = requests.post(
"https://api.luftdaten.info/v1/push-sensor-data/",
json={
"software_version": "enviro-plus 0.0.1",
"sensordatavalues": pm_values_json
},
headers={
"X-PIN": "1",
"X-Sensor": id,
"Content-Type": "application/json",
"cache-control": "no-cache"
}
)
resp_2 = requests.post(
"https://api.luftdaten.info/v1/push-sensor-data/",
json={
"software_version": "enviro-plus 0.0.1",
"sensordatavalues": temp_values_json
},
headers={
"X-PIN": "11",
"X-Sensor": id,
"Content-Type": "application/json",
"cache-control": "no-cache"
}
)
if resp_1.ok and resp_2.ok:
return True
else:
return False
# Compensation factor for temperature
comp_factor = 2.25
# Raspberry Pi ID to send to Luftdaten
id = "raspi-" + get_serial_number()
# Width and height to calculate text position
WIDTH = disp.width
HEIGHT = disp.height
# Text settings
font_size = 16
font = ImageFont.truetype(UserFont, font_size)
# Display Raspberry Pi serial and Wi-Fi status
print("Raspberry Pi serial: {}".format(get_serial_number()))
print("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected"))
time_since_update = 0
update_time = time.time()
# Main loop to read data, display, and send to Luftdaten
while True:
try:
time_since_update = time.time() - update_time
values = read_values()
print(values)
if time_since_update > 145:
resp = send_to_luftdaten(values, id)
update_time = time.time()
print("Response: {}\n".format("ok" if resp else "failed"))
display_status()
except Exception as e:
print(e)

View File

@@ -0,0 +1,44 @@
import ST7735
from PIL import Image, ImageDraw
from enviroplus.noise import Noise
print("""noise-amps-at-freqs.py - Measure amplitude from specific frequency bins
This example retrieves the median amplitude from 3 user-specified frequency ranges and plots them in Blue, Green and Red on the Enviro+ display.
As you play a continuous rising tone on your phone, you should notice peaks that correspond to the frequency entering each range.
Press Ctrl+C to exit!
""")
noise = Noise()
disp = ST7735.ST7735(
port=0,
cs=ST7735.BG_SPI_CS_FRONT,
dc=9,
backlight=12,
rotation=90)
disp.begin()
img = Image.new('RGB', (disp.width, disp.height), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
while True:
amps = noise.get_amplitudes_at_frequency_ranges([
(100, 200),
(500, 600),
(1000, 1200)
])
amps = [n * 32 for n in amps]
img2 = img.copy()
draw.rectangle((0, 0, disp.width, disp.height), (0, 0, 0))
img.paste(img2, (1, 0))
draw.line((0, 0, 0, amps[0]), fill=(0, 0, 255))
draw.line((0, 0, 0, amps[1]), fill=(0, 255, 0))
draw.line((0, 0, 0, amps[2]), fill=(255, 0, 0))
disp.display(img)

40
examples/noise-profile.py Normal file
View File

@@ -0,0 +1,40 @@
import ST7735
from PIL import Image, ImageDraw
from enviroplus.noise import Noise
print("""noise-profile.py - Get a simple noise profile.
This example grabs a basic 3-bin noise profile of low, medium and high frequency noise, plotting the noise characteristics as coloured bars.
Press Ctrl+C to exit!
""")
noise = Noise()
disp = ST7735.ST7735(
port=0,
cs=ST7735.BG_SPI_CS_FRONT,
dc=9,
backlight=12,
rotation=90)
disp.begin()
img = Image.new('RGB', (disp.width, disp.height), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
while True:
low, mid, high, amp = noise.get_noise_profile()
low *= 128
mid *= 128
high *= 128
amp *= 64
img2 = img.copy()
draw.rectangle((0, 0, disp.width, disp.height), (0, 0, 0))
img.paste(img2, (1, 0))
draw.line((0, 0, 0, amp), fill=(int(low), int(mid), int(high)))
disp.display(img)

29
examples/particulates.py Normal file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
import time
from pms5003 import PMS5003, ReadTimeoutError
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""particulates.py - Print readings from the PMS5003 particulate sensor.
Press Ctrl+C to exit!
""")
pms5003 = PMS5003()
time.sleep(1.0)
try:
while True:
try:
readings = pms5003.read()
logging.info(readings)
except ReadTimeoutError:
pms5003 = PMS5003()
except KeyboardInterrupt:
pass

View File

@@ -0,0 +1,425 @@
#!/usr/bin/env python3
import os
import time
import numpy
import colorsys
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from fonts.ttf import RobotoMedium as UserFont
import ST7735
from bme280 import BME280
from ltr559 import LTR559
import pytz
from pytz import timezone
from astral.geocoder import database, lookup
from astral.sun import sun
from datetime import datetime, timedelta
try:
from smbus2 import SMBus
except ImportError:
from smbus import SMBus
def calculate_y_pos(x, centre):
"""Calculates the y-coordinate on a parabolic curve, given x."""
centre = 80
y = 1 / centre * (x - centre) ** 2
return int(y)
def circle_coordinates(x, y, radius):
"""Calculates the bounds of a circle, given centre and radius."""
x1 = x - radius # Left
x2 = x + radius # Right
y1 = y - radius # Bottom
y2 = y + radius # Top
return (x1, y1, x2, y2)
def map_colour(x, centre, start_hue, end_hue, day):
"""Given an x coordinate and a centre point, a start and end hue (in degrees),
and a Boolean for day or night (day is True, night False), calculate a colour
hue representing the 'colour' of that time of day."""
start_hue = start_hue / 360 # Rescale to between 0 and 1
end_hue = end_hue / 360
sat = 1.0
# Dim the brightness as you move from the centre to the edges
val = 1 - (abs(centre - x) / (2 * centre))
# Ramp up towards centre, then back down
if x > centre:
x = (2 * centre) - x
# Calculate the hue
hue = start_hue + ((x / centre) * (end_hue - start_hue))
# At night, move towards purple/blue hues and reverse dimming
if not day:
hue = 1 - hue
val = 1 - val
r, g, b = [int(c * 255) for c in colorsys.hsv_to_rgb(hue, sat, val)]
return (r, g, b)
def x_from_sun_moon_time(progress, period, x_range):
"""Recalculate/rescale an amount of progress through a time period."""
x = int((progress / period) * x_range)
return x
def sun_moon_time(city_name, time_zone):
"""Calculate the progress through the current sun/moon period (i.e day or
night) from the last sunrise or sunset, given a datetime object 't'."""
city = lookup(city_name, database())
# Datetime objects for yesterday, today, tomorrow
utc = pytz.utc
utc_dt = datetime.now(tz=utc)
local_dt = utc_dt.astimezone(pytz.timezone(time_zone))
today = local_dt.date()
yesterday = today - timedelta(1)
tomorrow = today + timedelta(1)
# Sun objects for yesterday, today, tomorrow
sun_yesterday = sun(city.observer, date=yesterday)
sun_today = sun(city.observer, date=today)
sun_tomorrow = sun(city.observer, date=tomorrow)
# Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow
sunset_yesterday = sun_yesterday["sunset"]
sunrise_today = sun_today["sunrise"]
sunset_today = sun_today["sunset"]
sunrise_tomorrow = sun_tomorrow["sunrise"]
# Work out lengths of day or night period and progress through period
if sunrise_today < local_dt < sunset_today:
day = True
period = sunset_today - sunrise_today
# mid = sunrise_today + (period / 2)
progress = local_dt - sunrise_today
elif local_dt > sunset_today:
day = False
period = sunrise_tomorrow - sunset_today
# mid = sunset_today + (period / 2)
progress = local_dt - sunset_today
else:
day = False
period = sunrise_today - sunset_yesterday
# mid = sunset_yesterday + (period / 2)
progress = local_dt - sunset_yesterday
# Convert time deltas to seconds
progress = progress.total_seconds()
period = period.total_seconds()
return (progress, period, day, local_dt)
def draw_background(progress, period, day):
"""Given an amount of progress through the day or night, draw the
background colour and overlay a blurred sun/moon."""
# x-coordinate for sun/moon
x = x_from_sun_moon_time(progress, period, WIDTH)
# If it's day, then move right to left
if day:
x = WIDTH - x
# Calculate position on sun/moon's curve
centre = WIDTH / 2
y = calculate_y_pos(x, centre)
# Background colour
background = map_colour(x, 80, mid_hue, day_hue, day)
# New image for background colour
img = Image.new('RGBA', (WIDTH, HEIGHT), color=background)
# draw = ImageDraw.Draw(img)
# New image for sun/moon overlay
overlay = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0))
overlay_draw = ImageDraw.Draw(overlay)
# Draw the sun/moon
circle = circle_coordinates(x, y, sun_radius)
overlay_draw.ellipse(circle, fill=(200, 200, 50, opacity))
# Overlay the sun/moon on the background as an alpha matte
composite = Image.alpha_composite(img, overlay).filter(ImageFilter.GaussianBlur(radius=blur))
return composite
def overlay_text(img, position, text, font, align_right=False, rectangle=False):
draw = ImageDraw.Draw(img)
w, h = font.getsize(text)
if align_right:
x, y = position
x -= w
position = (x, y)
if rectangle:
x += 1
y += 1
position = (x, y)
border = 1
rect = (x - border, y, x + w, y + h + border)
rect_img = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0))
rect_draw = ImageDraw.Draw(rect_img)
rect_draw.rectangle(rect, (255, 255, 255))
rect_draw.text(position, text, font=font, fill=(0, 0, 0, 0))
img = Image.alpha_composite(img, rect_img)
else:
draw.text(position, text, font=font, fill=(255, 255, 255))
return img
def get_cpu_temperature():
with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
temp = f.read()
temp = int(temp) / 1000.0
return temp
def correct_humidity(humidity, temperature, corr_temperature):
dewpoint = temperature - ((100 - humidity) / 5)
corr_humidity = 100 - (5 * (corr_temperature - dewpoint))
return min(100, corr_humidity)
def analyse_pressure(pressure, t):
global time_vals, pressure_vals, trend
if len(pressure_vals) > num_vals:
pressure_vals = pressure_vals[1:] + [pressure]
time_vals = time_vals[1:] + [t]
# Calculate line of best fit
line = numpy.polyfit(time_vals, pressure_vals, 1, full=True)
# Calculate slope, variance, and confidence
slope = line[0][0]
intercept = line[0][1]
variance = numpy.var(pressure_vals)
residuals = numpy.var([(slope * x + intercept - y) for x, y in zip(time_vals, pressure_vals)])
r_squared = 1 - residuals / variance
# Calculate change in pressure per hour
change_per_hour = slope * 60 * 60
# variance_per_hour = variance * 60 * 60
mean_pressure = numpy.mean(pressure_vals)
# Calculate trend
if r_squared > 0.5:
if change_per_hour > 0.5:
trend = ">"
elif change_per_hour < -0.5:
trend = "<"
elif -0.5 <= change_per_hour <= 0.5:
trend = "-"
if trend != "-":
if abs(change_per_hour) > 3:
trend *= 2
else:
pressure_vals.append(pressure)
time_vals.append(t)
mean_pressure = numpy.mean(pressure_vals)
change_per_hour = 0
trend = "-"
# time.sleep(interval)
return (mean_pressure, change_per_hour, trend)
def describe_pressure(pressure):
"""Convert pressure into barometer-type description."""
if pressure < 970:
description = "storm"
elif 970 <= pressure < 990:
description = "rain"
elif 990 <= pressure < 1010:
description = "change"
elif 1010 <= pressure < 1030:
description = "fair"
elif pressure >= 1030:
description = "dry"
else:
description = ""
return description
def describe_humidity(humidity):
"""Convert relative humidity into good/bad description."""
if 40 < humidity < 60:
description = "good"
else:
description = "bad"
return description
def describe_light(light):
"""Convert light level in lux to descriptive value."""
if light < 50:
description = "dark"
elif 50 <= light < 100:
description = "dim"
elif 100 <= light < 500:
description = "light"
elif light >= 500:
description = "bright"
return description
# Initialise the LCD
disp = ST7735.ST7735(
port=0,
cs=1,
dc=9,
backlight=12,
rotation=270,
spi_speed_hz=10000000
)
disp.begin()
WIDTH = disp.width
HEIGHT = disp.height
# The city and timezone that you want to display.
city_name = "Sheffield"
time_zone = "Europe/London"
# Values that alter the look of the background
blur = 50
opacity = 125
mid_hue = 0
day_hue = 25
sun_radius = 50
# Fonts
font_sm = ImageFont.truetype(UserFont, 12)
font_lg = ImageFont.truetype(UserFont, 14)
# Margins
margin = 3
# Set up BME280 weather sensor
bus = SMBus(1)
bme280 = BME280(i2c_dev=bus)
min_temp = None
max_temp = None
factor = 2.25
cpu_temps = [get_cpu_temperature()] * 5
# Set up light sensor
ltr559 = LTR559()
# Pressure variables
pressure_vals = []
time_vals = []
num_vals = 1000
interval = 1
trend = "-"
# Keep track of time elapsed
start_time = time.time()
while True:
path = os.path.dirname(os.path.realpath(__file__))
progress, period, day, local_dt = sun_moon_time(city_name, time_zone)
background = draw_background(progress, period, day)
# Time.
time_elapsed = time.time() - start_time
date_string = local_dt.strftime("%d %b %y").lstrip('0')
time_string = local_dt.strftime("%H:%M")
img = overlay_text(background, (0 + margin, 0 + margin), time_string, font_lg)
img = overlay_text(img, (WIDTH - margin, 0 + margin), date_string, font_lg, align_right=True)
# Temperature
temperature = bme280.get_temperature()
# Corrected temperature
cpu_temp = get_cpu_temperature()
cpu_temps = cpu_temps[1:] + [cpu_temp]
avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
corr_temperature = temperature - ((avg_cpu_temp - temperature) / factor)
if time_elapsed > 30:
if min_temp is not None and max_temp is not None:
if corr_temperature < min_temp:
min_temp = corr_temperature
elif corr_temperature > max_temp:
max_temp = corr_temperature
else:
min_temp = corr_temperature
max_temp = corr_temperature
temp_string = f"{corr_temperature:.0f}°C"
img = overlay_text(img, (68, 18), temp_string, font_lg, align_right=True)
spacing = font_lg.getsize(temp_string)[1] + 1
if min_temp is not None and max_temp is not None:
range_string = f"{min_temp:.0f}-{max_temp:.0f}"
else:
range_string = "------"
img = overlay_text(img, (68, 18 + spacing), range_string, font_sm, align_right=True, rectangle=True)
temp_icon = Image.open(f"{path}/icons/temperature.png")
img.paste(temp_icon, (margin, 18), mask=temp_icon)
# Humidity
humidity = bme280.get_humidity()
corr_humidity = correct_humidity(humidity, temperature, corr_temperature)
humidity_string = f"{corr_humidity:.0f}%"
img = overlay_text(img, (68, 48), humidity_string, font_lg, align_right=True)
spacing = font_lg.getsize(humidity_string)[1] + 1
humidity_desc = describe_humidity(corr_humidity).upper()
img = overlay_text(img, (68, 48 + spacing), humidity_desc, font_sm, align_right=True, rectangle=True)
humidity_icon = Image.open(f"{path}/icons/humidity-{humidity_desc.lower()}.png")
img.paste(humidity_icon, (margin, 48), mask=humidity_icon)
# Light
light = ltr559.get_lux()
light_string = f"{int(light):,}"
img = overlay_text(img, (WIDTH - margin, 18), light_string, font_lg, align_right=True)
spacing = font_lg.getsize(light_string.replace(",", ""))[1] + 1
light_desc = describe_light(light).upper()
img = overlay_text(img, (WIDTH - margin - 1, 18 + spacing), light_desc, font_sm, align_right=True, rectangle=True)
light_icon = Image.open(f"{path}/icons/bulb-{light_desc.lower()}.png")
img.paste(humidity_icon, (80, 18), mask=light_icon)
# Pressure
pressure = bme280.get_pressure()
t = time.time()
mean_pressure, change_per_hour, trend = analyse_pressure(pressure, t)
pressure_string = f"{int(mean_pressure):,} {trend}"
img = overlay_text(img, (WIDTH - margin, 48), pressure_string, font_lg, align_right=True)
pressure_desc = describe_pressure(mean_pressure).upper()
spacing = font_lg.getsize(pressure_string.replace(",", ""))[1] + 1
img = overlay_text(img, (WIDTH - margin - 1, 48 + spacing), pressure_desc, font_sm, align_right=True, rectangle=True)
pressure_icon = Image.open(f"{path}/icons/weather-{pressure_desc.lower()}.png")
img.paste(pressure_icon, (80, 48), mask=pressure_icon)
# Display image
disp.display(img)

35
examples/weather.py Normal file
View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import time
from bme280 import BME280
try:
from smbus2 import SMBus
except ImportError:
from smbus import SMBus
import logging
logging.basicConfig(
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""weather.py - Print readings from the BME280 weather sensor.
Press Ctrl+C to exit!
""")
bus = SMBus(1)
bme280 = BME280(i2c_dev=bus)
while True:
temperature = bme280.get_temperature()
pressure = bme280.get_pressure()
humidity = bme280.get_humidity()
logging.info("""Temperature: {:05.2f} *C
Pressure: {:05.2f} hPa
Relative humidity: {:05.2f} %
""".format(temperature, pressure, humidity))
time.sleep(1)

193
install.sh Normal file
View File

@@ -0,0 +1,193 @@
#!/bin/bash
CONFIG=/boot/config.txt
DATESTAMP=`date "+%Y-%M-%d-%H-%M-%S"`
CONFIG_BACKUP=false
APT_HAS_UPDATED=false
USER_HOME=/home/$SUDO_USER
RESOURCES_TOP_DIR=$USER_HOME/Pimoroni
WD=`pwd`
user_check() {
if [ $(id -u) -ne 0 ]; then
printf "Script must be run as root. Try 'sudo ./install.sh'\n"
exit 1
fi
}
confirm() {
if [ "$FORCE" == '-y' ]; then
true
else
read -r -p "$1 [y/N] " response < /dev/tty
if [[ $response =~ ^(yes|y|Y)$ ]]; then
true
else
false
fi
fi
}
prompt() {
read -r -p "$1 [y/N] " response < /dev/tty
if [[ $response =~ ^(yes|y|Y)$ ]]; then
true
else
false
fi
}
success() {
echo -e "$(tput setaf 2)$1$(tput sgr0)"
}
inform() {
echo -e "$(tput setaf 6)$1$(tput sgr0)"
}
warning() {
echo -e "$(tput setaf 1)$1$(tput sgr0)"
}
function do_config_backup {
if [ ! $CONFIG_BACKUP == true ]; then
CONFIG_BACKUP=true
FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt"
inform "Backing up $CONFIG to /boot/$FILENAME\n"
cp $CONFIG /boot/$FILENAME
mkdir -p $RESOURCES_TOP_DIR/config-backups/
cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME
if [ -f "$UNINSTALLER" ]; then
echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER
fi
fi
}
function apt_pkg_install {
PACKAGES=()
PACKAGES_IN=("$@")
for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do
PACKAGE="${PACKAGES_IN[$i]}"
printf "Checking for $PACKAGE\n"
dpkg -L $PACKAGE > /dev/null 2>&1
if [ "$?" == "1" ]; then
PACKAGES+=("$PACKAGE")
fi
done
PACKAGES="${PACKAGES[@]}"
if ! [ "$PACKAGES" == "" ]; then
echo "Installing missing packages: $PACKAGES"
if [ ! $APT_HAS_UPDATED ]; then
apt update
APT_HAS_UPDATED=true
fi
apt install -y $PACKAGES
if [ -f "$UNINSTALLER" ]; then
echo "apt uninstall -y $PACKAGES"
fi
fi
}
user_check
apt_pkg_install python-configparser
CONFIG_VARS=`python - <<EOF
from configparser import ConfigParser
c = ConfigParser()
c.read('library/setup.cfg')
p = dict(c['pimoroni'])
# Convert multi-line config entries into bash arrays
for k in p.keys():
fmt = '"{}"'
if '\n' in p[k]:
p[k] = "'\n\t'".join(p[k].split('\n')[1:])
fmt = "('{}')"
p[k] = fmt.format(p[k])
print("""
LIBRARY_NAME="{name}"
LIBRARY_VERSION="{version}"
""".format(**c['metadata']))
print("""
PY3_DEPS={py3deps}
PY2_DEPS={py2deps}
SETUP_CMDS={commands}
CONFIG_TXT={configtxt}
""".format(**p))
EOF`
if [ $? -ne 0 ]; then
warning "Error parsing configuration...\n"
exit 1
fi
eval $CONFIG_VARS
RESOURCES_DIR=$RESOURCES_TOP_DIR/$LIBRARY_NAME
UNINSTALLER=$RESOURCES_DIR/uninstall.sh
mkdir -p $RESOURCES_DIR
cat << EOF > $UNINSTALLER
printf "It's recommended you run these steps manually.\n"
printf "If you want to run the full script, open it in\n"
printf "an editor and remove 'exit 1' from below.\n"
exit 1
EOF
printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n"
cd library
printf "Installing for Python 2..\n"
apt_pkg_install "${PY2_DEPS[@]}"
python setup.py install > /dev/null
if [ $? -eq 0 ]; then
success "Done!\n"
echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER
fi
if [ -f "/usr/bin/python3" ]; then
printf "Installing for Python 3..\n"
apt_pkg_install "${PY3_DEPS[@]}"
python3 setup.py install > /dev/null
if [ $? -eq 0 ]; then
success "Done!\n"
echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER
fi
fi
cd $WD
for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do
CMD="${SETUP_CMDS[$i]}"
# Attempt to catch anything that touches /boot/config.txt and trigger a backup
if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then
do_config_backup
fi
eval $CMD
done
for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do
CONFIG_LINE="${CONFIG_TXT[$i]}"
if ! [ "$CONFIG_LINE" == "" ]; then
do_config_backup
inform "Adding $CONFIG_LINE to $CONFIG\n"
sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG
if ! grep -q "^$CONFIG_LINE" $CONFIG; then
printf "$CONFIG_LINE\n" >> $CONFIG
fi
fi
done
if [ -d "examples" ]; then
if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then
inform "Copying examples to $RESOURCES_DIR"
cp -r examples/ $RESOURCES_DIR
echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER
fi
fi
success "\nAll done!"
inform "If this is your first time installing you should reboot for hardware changes to take effect.\n"
inform "Find uninstall steps in $UNINSTALLER\n"

4
library/.coveragerc Normal file
View File

@@ -0,0 +1,4 @@
[run]
source = enviroplus
omit =
.tox/*

16
library/CHANGELOG.txt Normal file
View File

@@ -0,0 +1,16 @@
0.0.3
-----
* Fix "self.noise_floor" bug in get_noise_profile
0.0.2
-----
* Add support for extra ADC channel in Gas
* Handle breaking change in new ltr559 library
* Add Noise functionality
0.0.1
-----
* Initial Release

21
library/LICENSE.txt Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Pimoroni Ltd.
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.

5
library/MANIFEST.in Normal file
View File

@@ -0,0 +1,5 @@
include CHANGELOG.txt
include LICENSE.txt
include README.rst
include setup.py
recursive-include enviroplus *.py

93
library/README.rst Normal file
View File

@@ -0,0 +1,93 @@
Enviro+
=======
Designed for environmental monitoring, Enviro+ lets you measure air
quality (pollutant gases and particulates), temperature, pressure,
humidity, light, and noise level. Learn more -
https://shop.pimoroni.com/products/enviro-plus
|Build Status| |Coverage Status| |PyPi Package| |Python Versions|
Installing
==========
You're best using the "One-line" install method if you want all of the
UART serial configuration for the PMS5003 particulate matter sensor to
run automatically.
One-line (Installs from GitHub)
-------------------------------
::
curl -sSL https://get.pimoroni.com/enviroplus | bash
**Note** report issues with one-line installer here:
https://github.com/pimoroni/get
Or... Install and configure dependencies from GitHub:
-----------------------------------------------------
- ``git clone https://github.com/pimoroni/enviroplus-python``
- ``cd enviroplus-python``
- ``sudo ./install.sh``
**Note** Raspbian Lite users may first need to install git:
``sudo apt install git``
Or... Install from PyPi and configure manually:
-----------------------------------------------
- Run ``sudo pip install enviroplus``
**Note** this wont perform any of the required configuration changes on
your Pi, you may additionally need to:
- Enable i2c: ``raspi-config nonint do_i2c 0``
- Enable SPI: ``raspi-config nonint do_spi 0``
And if you're using a PMS5003 sensor you will need to:
- Enable serial:
``raspi-config nonint set_config_var enable_uart 1 /boot/config.txt``
- Disable serial terminal: ``sudo raspi-config nonint do_serial 1``
- Add ``dtoverlay=pi3-miniuart-bt`` to your ``/boot/config.txt``
And install additional dependencies:
::
sudo apt install python-numpy python-smbus python-pil python-setuptools
Help & Support
--------------
- GPIO Pinout - https://pinout.xyz/pinout/enviro\_plus
- Support forums - http://forums.pimoroni.com/c/support
- Discord - https://discord.gg/hr93ByC
.. |Build Status| image:: https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master
:target: https://travis-ci.com/pimoroni/enviroplus-python
.. |Coverage Status| image:: https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master
:target: https://coveralls.io/github/pimoroni/enviroplus-python?branch=master
.. |PyPi Package| image:: https://img.shields.io/pypi/v/enviroplus.svg
:target: https://pypi.python.org/pypi/enviroplus
.. |Python Versions| image:: https://img.shields.io/pypi/pyversions/enviroplus.svg
:target: https://pypi.python.org/pypi/enviroplus
0.0.3
-----
* Fix "self.noise_floor" bug in get_noise_profile
0.0.2
-----
* Add support for extra ADC channel in Gas
* Handle breaking change in new ltr559 library
* Add Noise functionality
0.0.1
-----
* Initial Release

1
library/grow/__init__.py Normal file
View File

@@ -0,0 +1 @@
__version__ = '0.0.3'

140
library/grow/gas.py Normal file
View File

@@ -0,0 +1,140 @@
"""Read the MICS6814 via an ads1015 ADC"""
import time
import atexit
import ads1015
import RPi.GPIO as GPIO
MICS6814_HEATER_PIN = 24
MICS6814_GAIN = 6.144
ads1015.I2C_ADDRESS_DEFAULT = ads1015.I2C_ADDRESS_ALTERNATE
_is_setup = False
_adc_enabled = False
_adc_gain = 6.148
class Mics6814Reading(object):
__slots__ = 'oxidising', 'reducing', 'nh3', 'adc'
def __init__(self, ox, red, nh3, adc=None):
self.oxidising = ox
self.reducing = red
self.nh3 = nh3
self.adc = adc
def __repr__(self):
fmt = """Oxidising: {ox:05.02f} Ohms
Reducing: {red:05.02f} Ohms
NH3: {nh3:05.02f} Ohms"""
if self.adc is not None:
fmt += """
ADC: {adc:05.02f} Volts
"""
return fmt.format(
ox=self.oxidising,
red=self.reducing,
nh3=self.nh3,
adc=self.adc)
__str__ = __repr__
def setup():
global adc, _is_setup
if _is_setup:
return
_is_setup = True
adc = ads1015.ADS1015(i2c_addr=0x49)
adc.set_mode('single')
adc.set_programmable_gain(MICS6814_GAIN)
adc.set_sample_rate(1600)
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(MICS6814_HEATER_PIN, GPIO.OUT)
GPIO.output(MICS6814_HEATER_PIN, 1)
atexit.register(cleanup)
def enable_adc(value=True):
"""Enable reading from the additional ADC pin."""
global _adc_enabled
_adc_enabled = value
def set_adc_gain(value):
"""Set gain value for the additional ADC pin."""
global _adc_gain
_adc_gain = value
def cleanup():
GPIO.output(MICS6814_HEATER_PIN, 0)
def read_all():
"""Return gas resistence for oxidising, reducing and NH3"""
setup()
ox = adc.get_voltage('in0/gnd')
red = adc.get_voltage('in1/gnd')
nh3 = adc.get_voltage('in2/gnd')
try:
ox = (ox * 56000) / (3.3 - ox)
except ZeroDivisionError:
ox = 0
try:
red = (red * 56000) / (3.3 - red)
except ZeroDivisionError:
red = 0
try:
nh3 = (nh3 * 56000) / (3.3 - nh3)
except ZeroDivisionError:
nh3 = 0
analog = None
if _adc_enabled:
if _adc_gain == MICS6814_GAIN:
analog = adc.get_voltage('ref/gnd')
else:
adc.set_programmable_gain(_adc_gain)
time.sleep(0.05)
analog = adc.get_voltage('ref/gnd')
adc.set_programmable_gain(MICS6814_GAIN)
return Mics6814Reading(ox, red, nh3, analog)
def read_oxidising():
"""Return gas resistance for oxidising gases.
Eg chlorine, nitrous oxide
"""
setup()
return read_all().oxidising
def read_reducing():
"""Return gas resistance for reducing gases.
Eg hydrogen, carbon monoxide
"""
setup()
return read_all().reducing
def read_nh3():
"""Return gas resistance for nh3/ammonia"""
setup()
return read_all().nh3
def read_adc():
"""Return spare ADC channel value"""
setup()
return read_all().adc

90
library/grow/noise.py Normal file
View File

@@ -0,0 +1,90 @@
import sounddevice
import numpy
class Noise():
def __init__(self,
sample_rate=16000,
duration=0.5):
"""Noise measurement.
:param sample_rate: Sample rate in Hz
:param duraton: Duration, in seconds, of noise sample capture
"""
self.duration = duration
self.sample_rate = sample_rate
def get_amplitudes_at_frequency_ranges(self, ranges):
"""Return the mean amplitude of frequencies in the given ranges.
:param ranges: List of ranges including a start and end range
"""
recording = self._record()
magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate))
result = []
for r in ranges:
start, end = r
result.append(numpy.mean(magnitude[start:end]))
return result
def get_amplitude_at_frequency_range(self, start, end):
"""Return the mean amplitude of frequencies in the specified range.
:param start: Start frequency (in Hz)
:param end: End frequency (in Hz)
"""
n = self.sample_rate // 2
if start > n or end > n:
raise ValueError("Maxmimum frequency is {}".format(n))
recording = self._record()
magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate))
return numpy.mean(magnitude[start:end])
def get_noise_profile(self,
noise_floor=100,
low=0.12,
mid=0.36,
high=None):
"""Returns a noise charateristic profile.
Bins all frequencies into 3 weighted groups expressed as a percentage of the total frequency range.
:param noise_floor: "High-pass" frequency, exclude frequencies below this value
:param low: Percentage of frequency ranges to count in the low bin (as a float, 0.5 = 50%)
:param mid: Percentage of frequency ranges to count in the mid bin (as a float, 0.5 = 50%)
:param high: Optional percentage for high bin, effectively creates a "Low-pass" if total percentage is less than 100%
"""
if high is None:
high = 1.0 - low - mid
recording = self._record()
magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate))
sample_count = (self.sample_rate // 2) - noise_floor
mid_start = noise_floor + int(sample_count * low)
high_start = mid_start + int(sample_count * mid)
noise_ceiling = high_start + int(sample_count * high)
amp_low = numpy.mean(magnitude[noise_floor:mid_start])
amp_mid = numpy.mean(magnitude[mid_start:high_start])
amp_high = numpy.mean(magnitude[high_start:noise_ceiling])
amp_total = (amp_low + amp_mid + amp_high) / 3.0
return amp_low, amp_mid, amp_high, amp_total
def _record(self):
return sounddevice.rec(
int(self.duration * self.sample_rate),
samplerate=self.sample_rate,
blocking=True,
channels=1,
dtype='float64'
)

79
library/setup.cfg Normal file
View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
[metadata]
name = enviroplus
version = 0.0.3
author = Philip Howard
author_email = phil@pimoroni.com
description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi
long_description = file: README.rst
keywords = Raspberry Pi
url = https://www.pimoroni.com
project_urls =
GitHub=https://www.github.com/pimoroni/enviroplus-python
license = MIT
# This includes the license file(s) in the wheel.
# https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file
license_files = LICENSE.txt
classifiers =
Development Status :: 4 - Beta
Operating System :: POSIX :: Linux
License :: OSI Approved :: MIT License
Intended Audience :: Developers
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Topic :: Software Development
Topic :: Software Development :: Libraries
Topic :: System :: Hardware
[options]
packages = enviroplus
install_requires =
pimoroni-bme280
pms5003
ltr559
st7735
ads1015
fonts
font-roboto
astral
pytz
sounddevice
[flake8]
exclude =
.tox,
.eggs,
.git,
__pycache__,
build,
dist
ignore =
E501
[pimoroni]
py2deps =
python-pip
python-numpy
python-smbus
python-pil
python-spidev
python-rpi.gpio
libportaudio2
py3deps =
python3-pip
python3-numpy
python3-smbus
python3-pil
python3-spidev
python3-rpi.gpio
libportaudio2
configtxt =
dtoverlay=pi3-miniuart-bt
dtoverlay=adau7002-simple
commands =
printf "Setting up i2c and SPI..\n"
raspi-config nonint do_spi 0
raspi-config nonint do_i2c 0
printf "Setting up serial for PMS5003..\n"
raspi-config nonint do_serial 1 # Disable serial terminal over /dev/ttyAMA0
raspi-config nonint set_config_var enable_uart 1 $CONFIG # Enable serial port

33
library/setup.py Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python
"""
Copyright (c) 2016 Pimoroni
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 setuptools import setup, __version__
from pkg_resources import parse_version
minimum_version = parse_version('30.4.0')
if parse_version(__version__) < minimum_version:
raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version))
setup()

90
library/tests/conftest.py Normal file
View File

@@ -0,0 +1,90 @@
"""Test configuration.
These allow the mocking of various Python modules
that might otherwise have runtime side-effects.
"""
import sys
import mock
import pytest
from i2cdevice import MockSMBus
class SMBusFakeDevice(MockSMBus):
def __init__(self, i2c_bus):
MockSMBus.__init__(self, i2c_bus)
self.regs[0x00:0x01] = 0x0f, 0x00
@pytest.fixture(scope='function', autouse=True)
def cleanup():
yield None
try:
del sys.modules['enviroplus']
except KeyError:
pass
try:
del sys.modules['enviroplus.noise']
except KeyError:
pass
try:
del sys.modules['enviroplus.gas']
except KeyError:
pass
@pytest.fixture(scope='function', autouse=False)
def GPIO():
"""Mock RPi.GPIO module."""
GPIO = mock.MagicMock()
# Fudge for Python < 37 (possibly earlier)
sys.modules['RPi'] = mock.Mock()
sys.modules['RPi'].GPIO = GPIO
sys.modules['RPi.GPIO'] = GPIO
yield GPIO
del sys.modules['RPi']
del sys.modules['RPi.GPIO']
@pytest.fixture(scope='function', autouse=False)
def spidev():
"""Mock spidev module."""
spidev = mock.MagicMock()
sys.modules['spidev'] = spidev
yield spidev
del sys.modules['spidev']
@pytest.fixture(scope='function', autouse=False)
def smbus():
"""Mock smbus module."""
smbus = mock.MagicMock()
smbus.SMBus = SMBusFakeDevice
sys.modules['smbus'] = smbus
yield smbus
del sys.modules['smbus']
@pytest.fixture(scope='function', autouse=False)
def atexit():
"""Mock atexit module."""
atexit = mock.MagicMock()
sys.modules['atexit'] = atexit
yield atexit
del sys.modules['atexit']
@pytest.fixture(scope='function', autouse=False)
def sounddevice():
"""Mock sounddevice module."""
sounddevice = mock.MagicMock()
sys.modules['sounddevice'] = sounddevice
yield sounddevice
del sys.modules['sounddevice']
@pytest.fixture(scope='function', autouse=False)
def numpy():
"""Mock numpy module."""
numpy = mock.MagicMock()
sys.modules['numpy'] = numpy
yield numpy
del sys.modules['numpy']

View File

@@ -0,0 +1,48 @@
import pytest
def test_noise_setup(sounddevice, numpy):
from enviroplus.noise import Noise
noise = Noise(sample_rate=16000, duration=0.1)
del noise
def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy):
from enviroplus.noise import Noise
noise = Noise(sample_rate=16000, duration=0.1)
noise.get_amplitudes_at_frequency_ranges([
(100, 500),
(501, 1000)
])
sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64')
def test_noise_get_noise_profile(sounddevice, numpy):
from enviroplus.noise import Noise
numpy.mean.return_value = 10.0
noise = Noise(sample_rate=16000, duration=0.1)
amp_low, amp_mid, amp_high, amp_total = noise.get_noise_profile(
noise_floor=100,
low=0.12,
mid=0.36,
high=None)
sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64')
assert amp_total == 10.0
def test_get_amplitude_at_frequency_range(sounddevice, numpy):
from enviroplus.noise import Noise
noise = Noise(sample_rate=16000, duration=0.1)
noise.get_amplitude_at_frequency_range(0, 8000)
with pytest.raises(ValueError):
noise.get_amplitude_at_frequency_range(0, 16000)

View File

@@ -0,0 +1,66 @@
def test_gas_setup(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
gas.setup()
gas.setup()
def test_gas_read_all(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
result = gas.read_all()
assert type(result.oxidising) == float
assert int(result.oxidising) == 16641
assert type(result.reducing) == float
assert int(result.reducing) == 16727
assert type(result.nh3) == float
assert int(result.nh3) == 16813
assert "Oxidising" in str(result)
def test_gas_read_each(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
assert int(gas.read_oxidising()) == 16641
assert int(gas.read_reducing()) == 16727
assert int(gas.read_nh3()) == 16813
def test_gas_read_adc(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
gas.enable_adc(True)
gas.set_adc_gain(2.048)
assert gas.read_adc() == 0.255
def test_gas_read_adc_default_gain(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
gas.enable_adc(True)
gas.set_adc_gain(gas.MICS6814_GAIN)
assert gas.read_adc() == 0.765
def test_gas_read_adc_str(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
gas.enable_adc(True)
gas.set_adc_gain(2.048)
assert 'ADC' in str(gas.read_all())
def test_gas_cleanup(GPIO, smbus):
from enviroplus import gas
gas.cleanup()
GPIO.output.assert_called_with(gas.MICS6814_HEATER_PIN, 0)

24
library/tox.ini Normal file
View File

@@ -0,0 +1,24 @@
[tox]
envlist = py{27,35},qa
skip_missing_interpreters = True
[testenv]
commands =
python setup.py install
coverage run -m py.test -v -r wsx
coverage report
deps =
mock
pytest>=3.1
pytest-cov
[testenv:qa]
commands =
check-manifest --ignore tox.ini,tests*,.coveragerc
python setup.py check -m -r -s
flake8 --ignore E501
rstcheck README.rst
deps =
check-manifest
flake8
rstcheck

33
uninstall.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
LIBRARY_VERSION=`cat library/setup.cfg | grep version | awk -F" = " '{print $2}'`
LIBRARY_NAME=`cat library/setup.cfg | grep name | awk -F" = " '{print $2}'`
printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n"
if [ $(id -u) -ne 0 ]; then
printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n"
exit 1
fi
cd library
printf "Unnstalling for Python 2..\n"
pip uninstall $LIBRARY_NAME
if [ -f "/usr/bin/pip3" ]; then
printf "Uninstalling for Python 3..\n"
pip3 uninstall $LIBRARY_NAME
fi
cd ..
printf "Disabling serial..\n"
# Enable serial terminal over /dev/ttyAMA0
raspi-config nonint do_serial 0
# Disable serial port
raspi-config nonint set_config_var enable_uart 0 /boot/config.txt
# Switch serial port back to miniUART
sed -i 's/^dtoverlay=pi3-miniuart-bt # for Enviro+/#dtoverlay=pi3-miniuart-bt # for Enviro+/' /boot/config.txt
printf "Done!\n"