Do some more basic housekeeping to get to a building working version 0.0.1

This commit is contained in:
Paul Beech
2020-05-14 01:41:11 +01:00
parent bcf2c73bdf
commit 2c5c7a41b4
29 changed files with 288 additions and 1694 deletions

View File

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

View File

@@ -1,47 +1,41 @@
# Enviro+
# Grow HAT Mini
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
Designed as a tiny valet for your plants, Grow HAT mini will monitor the soil moiture for up to 3 plants, water them with tiny pumps, and show you their health on it's small but informative screen. Learn more - https://shop.pimoroni.com/products/grow
[![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)
[![Build Status](https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master)](https://travis-ci.com/pimoroni/grow-python)
[![Coverage Status](https://coveralls.io/repos/github/pimoroni/grow-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/grow-python?branch=master)
[![PyPi Package](https://img.shields.io/pypi/v/enviroplus.svg)](https://pypi.python.org/pypi/growhat)
[![Python Versions](https://img.shields.io/pypi/pyversions/enviroplus.svg)](https://pypi.python.org/pypi/growhat)
# 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.
You're best using the "One-line" install method.
## One-line (Installs from GitHub)
```
curl -sSL https://get.pimoroni.com/enviroplus | bash
curl -sSL https://get.pimoroni.com/grow | 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`
* `git clone https://github.com/pimoroni/grow-python`
* `cd grow-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`
* Run `sudo pip install growhat`
**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:
```

View File

@@ -1,27 +0,0 @@
#!/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

@@ -1,190 +0,0 @@
#!/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)

View File

@@ -4,16 +4,9 @@ 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 grow import moisture
from grow import pump
from subprocess import PIPE, Popen
from PIL import Image
from PIL import ImageDraw
@@ -26,18 +19,12 @@ logging.basicConfig(
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors
logging.info("""all-in-one.py - Displays readings from all of Grow HAT Mini's moisture 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,
@@ -111,16 +98,12 @@ last_page = 0
light = 1
# Create a values dict to store the data
variables = ["temperature",
"pressure",
"humidity",
"light",
"oxidised",
"reduced",
"nh3",
"pm1",
"pm25",
"pm10"]
variables = ["moisture1",
"moisture2",
"moisture3",
"pump1",
"pump2",
"pump3"]
values = {}
@@ -130,101 +113,12 @@ for v in variables:
# 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)
data = moisture.read_all()
display_text(variables[mode], data, unit)
time.sleep(delay)
# Exit cleanly
except KeyboardInterrupt:
sys.exit(0)

View File

@@ -4,16 +4,10 @@ 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 grow import moisture
from grow import pump
from subprocess import PIPE, Popen
from PIL import Image
from PIL import ImageDraw
@@ -32,13 +26,6 @@ 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,
@@ -71,27 +58,19 @@ message = ""
top_pos = 25
# Create a values dict to store the data
variables = ["temperature",
"pressure",
"humidity",
"light",
"oxidised",
"reduced",
"nh3",
"pm1",
"pm25",
"pm10"]
variables = ["moisture1",
"moisture2",
"moisture3",
"pump1",
"pump2",
"pump3"]
units = ["C",
"hPa",
"%",
"Lux",
"kO",
"kO",
"kO",
"ug/m3",
"ug/m3",
"ug/m3"]
units = ["Hz",
"Hz",
"Hz",
"",
"",
""]
# Define your own warning limits
# The limits definition follows the order of the variables array
@@ -111,18 +90,13 @@ limits = [[4, 18, 28, 35],
[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]]
[-1, -1, 450, 550]]
# 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
palette = [(0, 0, 255), # Dry
(0, 255, 255), # Damp
(0, 255, 0), # Moist
(255, 255, 0)] # Wet
values = {}

View File

@@ -1,53 +0,0 @@
#!/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)

View File

@@ -1,24 +0,0 @@
#!/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

View File

@@ -1,33 +0,0 @@
#!/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

View File

@@ -1,188 +0,0 @@
#!/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

@@ -1,44 +0,0 @@
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)

View File

@@ -1,40 +0,0 @@
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)

View File

@@ -1,29 +0,0 @@
#!/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

@@ -1,425 +0,0 @@
#!/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)

View File

@@ -1,35 +0,0 @@
#!/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)

View File

@@ -1,15 +1,3 @@
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
-----

View File

@@ -2,4 +2,4 @@ include CHANGELOG.txt
include LICENSE.txt
include README.rst
include setup.py
recursive-include enviroplus *.py
recursive-include grow *.py

View File

@@ -1,26 +1,22 @@
Enviro+
Grow
=======
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
Designed for looking after plants, Grow monitors moisture levels and runs simple pumps to water plants. Learn more -
https://shop.pimoroni.com/products/grow
|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.
The one-line installer enables the correct interfaces,
One-line (Installs from GitHub)
-------------------------------
::
curl -sSL https://get.pimoroni.com/enviroplus | bash
curl -sSL https://get.pimoroni.com/grow | bash
**Note** report issues with one-line installer here:
https://github.com/pimoroni/get
@@ -28,8 +24,8 @@ https://github.com/pimoroni/get
Or... Install and configure dependencies from GitHub:
-----------------------------------------------------
- ``git clone https://github.com/pimoroni/enviroplus-python``
- ``cd enviroplus-python``
- ``git clone https://github.com/pimoroni/grow-python``
- ``cd grow-python``
- ``sudo ./install.sh``
**Note** Raspbian Lite users may first need to install git:
@@ -38,7 +34,7 @@ Or... Install and configure dependencies from GitHub:
Or... Install from PyPi and configure manually:
-----------------------------------------------
- Run ``sudo pip install enviroplus``
- Run ``sudo pip install grow``
**Note** this wont perform any of the required configuration changes on
your Pi, you may additionally need to:
@@ -46,13 +42,6 @@ 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:
::
@@ -62,30 +51,18 @@ And install additional dependencies:
Help & Support
--------------
- GPIO Pinout - https://pinout.xyz/pinout/enviro\_plus
- GPIO Pinout - https://pinout.xyz/pinout/grow
- 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
.. |Build Status| image:: https://travis-ci.com/pimoroni/grow-python.svg?branch=master
:target: https://travis-ci.com/pimoroni/grow-python
.. |Coverage Status| image:: https://coveralls.io/repos/github/pimoroni/grow-python/badge.svg?branch=master
:target: https://coveralls.io/github/pimoroni/grow-python?branch=master
.. |PyPi Package| image:: https://img.shields.io/pypi/v/growhat.svg
:target: https://pypi.python.org/pypi/growhat
.. |Python Versions| image:: https://img.shields.io/pypi/pyversions/growhat.svg
:target: https://pypi.python.org/pypi/growhat
0.0.1
-----

View File

@@ -1 +1 @@
__version__ = '0.0.3'
__version__ = '0.0.1'

View File

@@ -1,140 +0,0 @@
"""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

93
library/grow/moisture.py Normal file
View File

@@ -0,0 +1,93 @@
import time
import atexit
import RPi.GPIO as GPIO
MOISTURE_1_PIN = 23
MOISTURE_2_PIN = 25
MOISTURE_3_PIN = 8
_is_setup = False
class Moisture(object):
__slots__ = 'in1', 'in2', 'in3'
def __init__(self, in1, in2, in3):
self.in1 = in1
self.in2 = in2
self.in3 = in3
def __repr__(self):
fmt = """Moisture 1: {in1} Hz
Moisture 1: {in2} Ohms
Moisture 1: {in3} Ohms"""
return fmt.format(
in1=self.in1,
in2=self.in2,
in3=self.in3)
__str__ = __repr__
def setup():
global _is_setup
global _moisture
if _is_setup:
return
_is_setup = True
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(MOISTURE_1_PIN, GPIO.IN)
GPIO.input(MOISTURE_1_PIN)
GPIO.setup(MOISTURE_2_PIN, GPIO.IN)
GPIO.input(MOISTURE_2_PIN)
GPIO.setup(MOISTURE_3_PIN, GPIO.IN)
GPIO.input(MOISTURE_3_PIN)
atexit.register(cleanup)
def set_moisture_wet(channel, value):
"""Set wet point for a moisture channel."""
_moisture[channel].wet = value
def set_moisture_wet(channel, value):
"""Set wet point for a moisture channel."""
_moisture[channel].dry = value
def read_moiture(channel):
"""Get current value for the additional ADC pin."""
setup()
return _moisture[channel].value
def cleanup():
GPIO.output(MOISTURE_1_PIN, 0)
GPIO.output(MOISTURE_2_PIN, 0)
GPIO.output(MOISTURE_3_PIN, 0)
def read_all():
"""Return gas resistence for oxidising, reducing and NH3"""
setup()
in1 = adc.get_voltage('in0/gnd')
in2 = adc.get_voltage('in1/gnd')
in3 = adc.get_voltage('in2/gnd')
try:
in1 = (ox * 56000) / (3.3 - ox)
except ZeroDivisionError:
in1 = 0
try:
in2 = (red * 56000) / (3.3 - red)
except ZeroDivisionError:
in2 = 0
try:
in3 = (nh3 * 56000) / (3.3 - nh3)
except ZeroDivisionError:
in3 = 0
return _moisture(in1, in2, in3)

View File

@@ -1,90 +0,0 @@
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'
)

78
library/grow/pump.py Normal file
View File

@@ -0,0 +1,78 @@
import time
import atexit
import RPi.GPIO as GPIO
PUMP_1_PIN = 17
PUMP_2_PIN = 27
PUMP_3_PIN = 22
_is_setup = False
class Pump(object):
__slots__ = 'out1', 'out2', 'out3'
def __init__(self, out1, out2, out3):
self.out1 = out1
self.out2 = out2
self.out3 = out3
def __repr__(self):
fmt = """Pump 1: {out1}
Pump 1: {out2} Ohms
Pump 1: {out3} Ohms"""
return fmt.format(
out1=self.out1,
out2=self.out2,
out3=self.out3)
__str__ = __repr__
def setup():
global _is_setup
global _pump
if _is_setup:
return
_is_setup = True
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(PUMP_1_PIN, GPIO.OUT)
GPIO.input(PUMP_1_PIN, 0)
GPIO.setup(PUMP_2_PIN, GPIO.OUT)
GPIO.input(PUMP_2_PIN, 0)
GPIO.setup(PUMP_3_PIN, GPIO.OUT)
GPIO.input(PUMP_3_PIN, 0)
atexit.register(cleanup)
def set_pump_on(channel):
"""Set wet point for a moisture channel."""
_pump[channel] = 1
def set_pump_off(channel, value):
"""Set wet point for a moisture channel."""
_pump[channel] = 0
def read_pump(channel):
"""Get current value for the additional ADC pin."""
setup()
return _pump[channel]
def cleanup():
GPIO.output(PUMP_1_PIN, 0)
GPIO.output(PUMP_2_PIN, 0)
GPIO.output(PUMP_3_PIN, 0)
def read_all():
"""Return pump state"""
setup()
in1 = _pump[1]
in2 = _pump[2]
in3 = _pump[3]
return _pump(in1, in2, in3)

View File

@@ -1,15 +1,15 @@
# -*- 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
name = grow
version = 0.0.1
author = Philip Howard, Paul Beech
author_email = paul@pimoroni.com
description = Grow HAT Mini. A plant valet add-on for the 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
GitHub=https://www.github.com/pimoroni/grow-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
@@ -26,13 +26,9 @@ classifiers =
Topic :: System :: Hardware
[options]
packages = enviroplus
packages = grow
install_requires =
pimoroni-bme280
pms5003
ltr559
st7735
ads1015
fonts
font-roboto
astral
@@ -58,7 +54,6 @@ py2deps =
python-pil
python-spidev
python-rpi.gpio
libportaudio2
py3deps =
python3-pip
python3-numpy
@@ -66,14 +61,8 @@ py3deps =
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

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
"""
Copyright (c) 2016 Pimoroni
Copyright (c) 2020 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

View File

@@ -18,15 +18,15 @@ class SMBusFakeDevice(MockSMBus):
def cleanup():
yield None
try:
del sys.modules['enviroplus']
del sys.modules['grow']
except KeyError:
pass
try:
del sys.modules['enviroplus.noise']
del sys.modules['grow.moisture']
except KeyError:
pass
try:
del sys.modules['enviroplus.gas']
del sys.modules['grow.pump']
except KeyError:
pass
@@ -72,15 +72,6 @@ def 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."""

View File

@@ -1,48 +0,0 @@
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

@@ -1,66 +1,56 @@
def test_gas_setup(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
gas.setup()
gas.setup()
def test_moisture_setup(GPIO, smbus):
from grow import moisture
moisture._is_setup = False
moisture.setup()
moisture.setup()
def test_gas_read_all(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
result = gas.read_all()
def test_moisture_read_all(GPIO, smbus):
from grow import moisture
moisture._is_setup = False
result = moisture.read_all()
assert type(result.oxidising) == float
assert int(result.oxidising) == 16641
assert type(result(1)) == float
assert int(result(1)) == 100
assert type(result.reducing) == float
assert int(result.reducing) == 16727
assert type(result(2)) == float
assert int(result(2)) == 500
assert type(result.nh3) == float
assert int(result.nh3) == 16813
assert type(result.(3)) == float
assert int(result.(3)) == 5000
assert "Oxidising" in str(result)
assert "Moisture" in str(result)
def test_gas_read_each(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
def test_moisture_read_each(GPIO, smbus):
from grow import moisture
moisture._is_setup = False
assert int(gas.read_oxidising()) == 16641
assert int(gas.read_reducing()) == 16727
assert int(gas.read_nh3()) == 16813
assert int(moisture.read(1)) == 100
assert int(moisture.read(2)) == 500
assert int(moisture.read(3)) == 5000
def test_gas_read_adc(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
def test_moisture_cleanup(GPIO, smbus):
from grow import moisture
moisture.cleanup()
gas.enable_adc(True)
gas.set_adc_gain(2.048)
assert gas.read_adc() == 0.255
GPIO.input.assert_called_with(moisture.MOISTURE_1_PIN, 0)
GPIO.input.assert_called_with(moisture.MOISTURE_2_PIN, 0)
GPIO.input.assert_called_with(moisture.MOISTURE_3_PIN, 0)
def test_pump_setup(GPIO, smbus):
from grow import pump
moisture._is_setup = False
moisture.setup()
moisture.setup()
def test_gas_read_adc_default_gain(GPIO, smbus):
from enviroplus import gas
gas._is_setup = False
def test_pump_cleanup(GPIO, smbus):
from grow import pump
pump.cleanup()
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)
GPIO.input.assert_called_with(moisture.PUMP_1_PIN, 0)
GPIO.input.assert_called_with(moisture.PUMP_2_PIN, 0)
GPIO.input.assert_called_with(moisture.PUMP_3_PIN, 0)

View File

@@ -22,12 +22,4 @@ 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"