From c1cfb8d0f96a0bf69216dad35afd91c884c64364 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 19 Aug 2019 10:39:05 +0100 Subject: [PATCH 01/16] +x --- examples/all-in-one-no-pm.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/all-in-one-no-pm.py diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py old mode 100644 new mode 100755 -- 2.30.2 From 10d81356df22a2eff212caedac15c7f3dc16f026 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 19 Aug 2019 10:52:04 +0100 Subject: [PATCH 02/16] Switch temp comp method for #28 --- examples/compensated-temperature.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index 048eb80..7d0d0f1 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -2,7 +2,6 @@ import time from bme280 import BME280 -from subprocess import PIPE, Popen try: from smbus2 import SMBus @@ -24,10 +23,10 @@ bme280 = BME280(i2c_dev=bus) # Get the temperature of the CPU for compensation def get_cpu_temperature(): - process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE) - output, _error = process.communicate() - output = output.decode() - return float(output[output.index('=') + 1:output.rindex("'")]) + 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 -- 2.30.2 From 5f63416d347a4c95024fef4046dc2c0eb4642875 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 20 Aug 2019 16:15:24 +0100 Subject: [PATCH 03/16] Merged cipy-master --- examples/all-in-one-no-pm.py | 2 +- examples/all-in-one.py | 2 +- examples/luftdaten.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index bc19e12..4d6d463 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -78,7 +78,7 @@ def display_text(variable, data, unit): # Get the temperature of the CPU for compensation def get_cpu_temperature(): - process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE) + process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() output = output.decode() return float(output[output.index('=') + 1:output.rindex("'")]) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 695a007..8e82b6b 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -84,7 +84,7 @@ def display_text(variable, data, unit): # Get the temperature of the CPU for compensation def get_cpu_temperature(): - process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE) + process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() output = output.decode() return float(output[output.index('=') + 1:output.rindex("'")]) diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 77a6d82..9995914 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -72,7 +72,7 @@ def read_values(): # Get CPU temperature to use for compensation def get_cpu_temperature(): - process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE) + process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() output = output.decode() return float(output[output.index('=') + 1:output.rindex("'")]) -- 2.30.2 From 998bb2e90a2bab0ce43f92ee0988bfab3a29ea96 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 20 Aug 2019 16:17:12 +0100 Subject: [PATCH 04/16] Dropped obsolete .decode() --- examples/all-in-one-no-pm.py | 1 - examples/all-in-one.py | 1 - examples/luftdaten.py | 1 - 3 files changed, 3 deletions(-) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index 4d6d463..ab1a1a7 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -80,7 +80,6 @@ def display_text(variable, data, unit): def get_cpu_temperature(): process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() - output = output.decode() return float(output[output.index('=') + 1:output.rindex("'")]) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 8e82b6b..6314137 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -86,7 +86,6 @@ def display_text(variable, data, unit): def get_cpu_temperature(): process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() - output = output.decode() return float(output[output.index('=') + 1:output.rindex("'")]) diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 9995914..358d442 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -74,7 +74,6 @@ def read_values(): def get_cpu_temperature(): process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() - output = output.decode() return float(output[output.index('=') + 1:output.rindex("'")]) -- 2.30.2 From 2c6a2d7204a1618695a9c5df5da306daeca6e41b Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 20 Aug 2019 16:34:23 +0100 Subject: [PATCH 05/16] Replaced exception pass with log warning --- examples/all-in-one.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 85a9a15..eef99c3 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -102,7 +102,7 @@ factor = 0.8 cpu_temps = [get_cpu_temperature()] * 5 delay = 0.5 # Debounce the proximity tap -mode = 0 # The starting mode +mode = 0 # The starting mode last_page = 0 light = 1 @@ -194,7 +194,7 @@ try: try: data = pms5003.read() except pmsReadTimeoutError: - pass + logging.warn("Failed to read PMS5003") else: data = data.pm_ug_per_m3(1.0) display_text(variables[mode], data, unit) @@ -205,7 +205,7 @@ try: try: data = pms5003.read() except pmsReadTimeoutError: - pass + logging.warn("Failed to read PMS5003") else: data = data.pm_ug_per_m3(2.5) display_text(variables[mode], data, unit) @@ -216,7 +216,7 @@ try: try: data = pms5003.read() except pmsReadTimeoutError: - pass + logging.warn("Failed to read PMS5003") else: data = data.pm_ug_per_m3(10) display_text(variables[mode], data, unit) -- 2.30.2 From bca04496c248701cca287451021eed9f3c7389ed Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 12 Sep 2019 10:02:32 +0100 Subject: [PATCH 06/16] Transitional fix for new LTR559 library --- examples/all-in-one-no-pm.py | 7 ++++++- examples/all-in-one.py | 7 ++++++- examples/light.py | 8 +++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index 5486012..6bb6873 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -5,7 +5,12 @@ import colorsys import os import sys import ST7735 -import ltr559 +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 diff --git a/examples/all-in-one.py b/examples/all-in-one.py index eef99c3..87acc6f 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -5,7 +5,12 @@ import colorsys import os import sys import ST7735 -import ltr559 +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 diff --git a/examples/light.py b/examples/light.py index 3442fb4..b18a78b 100755 --- a/examples/light.py +++ b/examples/light.py @@ -1,8 +1,14 @@ #!/usr/bin/env python import time -import ltr559 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', -- 2.30.2 From aa747a416652b64a182d6e2594c08e2476d17d76 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 17 Sep 2019 11:45:48 +0100 Subject: [PATCH 07/16] Noise library and examples for basic FFT and frequency binning --- examples/noise-amps-at-freqs.py | 41 ++++++++++++++++++++ examples/noise-profile.py | 39 +++++++++++++++++++ library/enviroplus/noise.py | 66 +++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 examples/noise-amps-at-freqs.py create mode 100644 examples/noise-profile.py diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py new file mode 100644 index 0000000..0a8040b --- /dev/null +++ b/examples/noise-amps-at-freqs.py @@ -0,0 +1,41 @@ +import ST7735 +from PIL import Image, ImageDraw +from enviroplus.noise import Noise + +SAMPLERATE = 16000 + +FREQ_LOW = 100.0 +FREQ_HIGH = 2000.0 +WIDTH = 100 + +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) + diff --git a/examples/noise-profile.py b/examples/noise-profile.py new file mode 100644 index 0000000..70aa6ab --- /dev/null +++ b/examples/noise-profile.py @@ -0,0 +1,39 @@ +import ST7735 +from PIL import Image, ImageDraw +from enviroplus.noise import Noise + +SAMPLERATE = 16000 + +FREQ_LOW = 100.0 +FREQ_HIGH = 2000.0 +WIDTH = 100 + +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.measure() + 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) + diff --git a/library/enviroplus/noise.py b/library/enviroplus/noise.py index e69de29..57e869d 100644 --- a/library/enviroplus/noise.py +++ b/library/enviroplus/noise.py @@ -0,0 +1,66 @@ +import sounddevice +import numpy +import math + +class Noise(): + def __init__( + self, + sample_rate=16000, + duration=0.5): + + self.duration = duration + self.sample_rate = sample_rate + + def get_amplitudes_at_frequency_ranges(self, ranges): + 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): + 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): + + 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[self.noise_floor:mid_start]) + amp_mid = numpy.mean(magnitude[mid_start:high_start]) + amp_high = numpy.mean(magnitude[high_start:noise_ceiling]) + amp_total = (low + mid + 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' + ) + -- 2.30.2 From 18c9ae73f4dd170a69df03822193c3044f68f194 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 17 Sep 2019 11:52:42 +0100 Subject: [PATCH 08/16] Tidy up noise examples --- examples/noise-amps-at-freqs.py | 19 +++++++++++-------- examples/noise-profile.py | 11 ++++++----- 2 files changed, 17 insertions(+), 13 deletions(-) mode change 100644 => 100755 examples/noise-amps-at-freqs.py mode change 100644 => 100755 examples/noise-profile.py diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py old mode 100644 new mode 100755 index 0a8040b..8b1ddd5 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -2,11 +2,15 @@ import ST7735 from PIL import Image, ImageDraw from enviroplus.noise import Noise -SAMPLERATE = 16000 +print("""noise-amps-at-freqs.py - Measure amplitude from specific frequency bins -FREQ_LOW = 100.0 -FREQ_HIGH = 2000.0 -WIDTH = 100 +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() @@ -25,9 +29,9 @@ draw = ImageDraw.Draw(img) while True: amps = noise.get_amplitudes_at_frequency_ranges([ - (100,200), - (500,600), - (1000,1200) + (100, 200), + (500, 600), + (1000, 1200) ]) amps = [n * 32 for n in amps] img2 = img.copy() @@ -38,4 +42,3 @@ while True: draw.line((0, 0, 0, amps[2]), fill=(255, 0, 0)) disp.display(img) - diff --git a/examples/noise-profile.py b/examples/noise-profile.py old mode 100644 new mode 100755 index 70aa6ab..964dead --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -2,11 +2,13 @@ import ST7735 from PIL import Image, ImageDraw from enviroplus.noise import Noise -SAMPLERATE = 16000 +print("""noise-profile.py - Get a simple noise profile. -FREQ_LOW = 100.0 -FREQ_HIGH = 2000.0 -WIDTH = 100 +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() @@ -36,4 +38,3 @@ while True: draw.line((0, 0, 0, amp), fill=(int(low), int(mid), int(high))) disp.display(img) - -- 2.30.2 From c0ac7b9f141e492642600c9e9e8ee8533ba0d427 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 16 Oct 2019 10:41:10 +0100 Subject: [PATCH 09/16] Add dtoverlay for mic --- library/setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/library/setup.cfg b/library/setup.cfg index ed7cef7..b4c8a96 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -58,6 +58,7 @@ py3deps = python3-pil configtxt = dtoverlay=pi3-miniuart-bt + dtoverlay=adau7002-simple commands = printf "Setting up i2c and SPI..\n" raspi-config nonint do_spi 0 -- 2.30.2 From 4d6b7f96ada71d5f303218e84765d9e74583ef70 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 16 Oct 2019 10:41:23 +0100 Subject: [PATCH 10/16] Add DocStrings and linting --- library/enviroplus/noise.py | 48 +++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/library/enviroplus/noise.py b/library/enviroplus/noise.py index 57e869d..2e7472d 100644 --- a/library/enviroplus/noise.py +++ b/library/enviroplus/noise.py @@ -1,17 +1,27 @@ import sounddevice import numpy -import math + class Noise(): - def __init__( - self, - sample_rate=16000, - duration=0.5): + 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 = [] @@ -21,6 +31,12 @@ class Noise(): 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)) @@ -29,12 +45,21 @@ class Noise(): 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): + 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 @@ -63,4 +88,3 @@ class Noise(): channels=1, dtype='float64' ) - -- 2.30.2 From 504b0f7f30384243a55d2620dba68eadb93b4472 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 16 Oct 2019 10:42:21 +0100 Subject: [PATCH 11/16] Fix noise-profile example --- examples/noise-profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/noise-profile.py b/examples/noise-profile.py index 964dead..1afdff5 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -26,7 +26,7 @@ draw = ImageDraw.Draw(img) while True: - low, mid, high, amp = noise.measure() + low, mid, high, amp = noise.get_noise_profile() low *= 128 mid *= 128 high *= 128 -- 2.30.2 From c440294cc10ba86b4cdeae53b3b3592110493190 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 16 Oct 2019 10:44:48 +0100 Subject: [PATCH 12/16] Add deps for noise measurement --- library/setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/setup.cfg b/library/setup.cfg index b4c8a96..c48c7ba 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -33,6 +33,7 @@ install_requires = ltr559 st7735 ads1015 + sounddevice [flake8] exclude = @@ -51,11 +52,13 @@ py2deps = python-numpy python-smbus python-pil + libportaudio2 py3deps = python3-pip python3-numpy python3-smbus python3-pil + libportaudio2 configtxt = dtoverlay=pi3-miniuart-bt dtoverlay=adau7002-simple -- 2.30.2 From 426f1cbc68f7644f700066c5bc1068c518e8ca10 Mon Sep 17 00:00:00 2001 From: Kostadin Date: Sat, 26 Oct 2019 13:26:47 +0300 Subject: [PATCH 13/16] Fixed graphing problem for PMS5003 results The PMS5003 library returns integers. This causes the current formula for color scaling of the graph to not work properly because of integer division instead of float division. Converting the PMS5003 results from int to float solves the problem. --- examples/all-in-one.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 87acc6f..03e4d58 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -201,7 +201,7 @@ try: except pmsReadTimeoutError: logging.warn("Failed to read PMS5003") else: - data = data.pm_ug_per_m3(1.0) + data = float(data.pm_ug_per_m3(1.0)) display_text(variables[mode], data, unit) if mode == 8: @@ -212,7 +212,7 @@ try: except pmsReadTimeoutError: logging.warn("Failed to read PMS5003") else: - data = data.pm_ug_per_m3(2.5) + data = float(data.pm_ug_per_m3(2.5)) display_text(variables[mode], data, unit) if mode == 9: @@ -223,7 +223,7 @@ try: except pmsReadTimeoutError: logging.warn("Failed to read PMS5003") else: - data = data.pm_ug_per_m3(10) + data = float(data.pm_ug_per_m3(10)) display_text(variables[mode], data, unit) # Exit cleanly -- 2.30.2 From 71dc2962df1f5a90752c1dedc135bba82e1b3b7a Mon Sep 17 00:00:00 2001 From: Kostadin Date: Sat, 26 Oct 2019 14:45:18 +0300 Subject: [PATCH 14/16] Added a new combined mode example This is a modification of all-in-one.py. It adds another mode where all EnviroPlus and PMS5003 sensor readings are combined on one screen. Each variable that is displayed can have custom warning limits assigned which change the color of the text according to a predefined RGB palette. It allows for a quick glance of all sensor readings at once in order to jugde if everything is OK in the air or to quickly pinpoint a sensor reading that requires attention. In addition, the new combined mode saves each reading as soon as it is received for graphing later. As in all-in-one.py, moving your finger close to the proximity sensor switches the mode. --- examples/combined.py | 345 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 examples/combined.py diff --git a/examples/combined.py b/examples/combined.py new file mode 100644 index 0000000..e863de3 --- /dev/null +++ b/examples/combined.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python + +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 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 +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) +path = os.path.dirname(os.path.realpath(__file__)) +font = ImageFont.truetype(path + "/fonts/Asap/Asap-Bold.ttf", 20) +smallfont = ImageFont.truetype(path + "/fonts/Asap/Asap-Bold.ttf", 10) +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 + colours = [(v - min(values[variable]) + 1) / (max(values[variable]) + - min(values[variable]) + 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 xrange(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 xrange(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("'")]) + + +# Tuning factor for compensation. Decrease this number to adjust the +# temperature down, and increase to adjust up +factor = 1.95 + +cpu_temps = [get_cpu_temperature()] * 5 + +delay = 0.5 # Debounce the proximity tap +mode = 10 # The starting mode +last_page = 0 +light = 1 + +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) -- 2.30.2 From f9d69568aec8773d9541809f6cd22d44b0197431 Mon Sep 17 00:00:00 2001 From: Sam Birch Date: Mon, 9 Dec 2019 21:55:42 +1300 Subject: [PATCH 15/16] Remove inappropriate sleep between reads The PMS5003 seems to buffer unread samples. If you are reading at a lower rate than it takes readings, then a progressively larger delay will occur between changes in actual PM levels and indicated PM levels. To see the issue: 1) run this example with the `time.sleep(1)` *included* 2) wait a few minutes 3) burn a piece of paper near the sensor and wait for the reported PM levels to spike With the sleep included, there is a delay between introducing smoke and seeing reported levels rise (often a few minutes or longer). With the sleep removed you will see reported levels update almost immediately. The correct way to use the sensor is to read as fast as the sensor allows, and not make any assumptions about what rate samples will be published at --- examples/particulates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/particulates.py b/examples/particulates.py index 9aa9c37..c1b3c67 100755 --- a/examples/particulates.py +++ b/examples/particulates.py @@ -23,7 +23,6 @@ try: try: readings = pms5003.read() logging.info(readings) - time.sleep(1.0) except ReadTimeoutError: pms5003 = PMS5003() except KeyboardInterrupt: -- 2.30.2 From 7c3404f8cace2ebe8600e75d53f4b53c32c7002c Mon Sep 17 00:00:00 2001 From: Robert Bricheno Date: Mon, 17 Feb 2020 17:27:30 +0000 Subject: [PATCH 16/16] Sleep before first PMS5003 reading --- examples/combined.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/combined.py b/examples/combined.py index e863de3..3c0b58d 100644 --- a/examples/combined.py +++ b/examples/combined.py @@ -37,6 +37,7 @@ bme280 = BME280() # PMS5003 particulate sensor pms5003 = PMS5003() +time.sleep(1.0) # Create ST7735 LCD display class st7735 = ST7735.ST7735( -- 2.30.2