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

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