From 876dfbe7c3ec660f7ba93c4f7777333b7cf1e1cc Mon Sep 17 00:00:00 2001 From: makeupsomething Date: Thu, 1 Aug 2019 15:09:58 +0900 Subject: [PATCH 001/122] Fix typo --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 1fec5c84..3e5b8984 100755 --- a/install.sh +++ b/install.sh @@ -189,5 +189,5 @@ if [ -d "examples" ]; then fi success "\nAll done!" -inform "If this is your first time installing you should reboot for hardware changes to tkae effect.\n" +inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" inform "Find uninstall steps in $UNINSTALLER\n" From bd92c5fb0a72d6d08bcf0f6973f39637f68af1db Mon Sep 17 00:00:00 2001 From: Richard Hayler Date: Sat, 3 Aug 2019 20:43:47 +0100 Subject: [PATCH 002/122] Update all-in-one-no-pm.py added decoding of bytestring to provide Python3 compatibility. --- examples/all-in-one-no-pm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index 367ebeb0..bc19e123 100644 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -80,6 +80,7 @@ def display_text(variable, data, unit): 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("'")]) From 847937ac3d550e805764a741344a5994aa3148da Mon Sep 17 00:00:00 2001 From: Richard Hayler Date: Sat, 3 Aug 2019 20:47:08 +0100 Subject: [PATCH 003/122] Update all-in-one.py added decoding of bytestring to provide Python3 compatibility. --- examples/all-in-one.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 8ba76815..695a007e 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -86,6 +86,7 @@ def display_text(variable, data, unit): 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("'")]) From dc633dfb1034e4158aaa3b5bf9e579711728c1d5 Mon Sep 17 00:00:00 2001 From: Richard Hayler Date: Sat, 3 Aug 2019 20:48:26 +0100 Subject: [PATCH 004/122] Update luftdaten.py added decoding of bytestring to provide Python3 compatibility. --- examples/luftdaten.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 4d3d49ac..99a40c0c 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -73,6 +73,7 @@ def read_values(): 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("'")]) From e2d010e2ae9761418d5a59f79cc4b60a251665d5 Mon Sep 17 00:00:00 2001 From: Ciprian Manea Date: Sun, 4 Aug 2019 01:27:29 +0300 Subject: [PATCH 005/122] testing all examples, with corrections for Python3 support --- examples/all-in-one-no-pm.py | 2 +- examples/all-in-one.py | 2 +- examples/compensated-temperature.py | 3 +-- examples/luftdaten.py | 2 +- 4 files changed, 4 insertions(+), 5 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 index 367ebeb0..ab1a1a76 --- 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() return float(output[output.index('=') + 1:output.rindex("'")]) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 8ba76815..6314137e 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() return float(output[output.index('=') + 1:output.rindex("'")]) diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index 048eb804..36796100 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -24,9 +24,8 @@ # 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 4d3d49ac..c5b03873 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -71,7 +71,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() return float(output[output.index('=') + 1:output.rindex("'")]) From 27156d25c1b1c310a64f31ba9f22aaf27569c4fd Mon Sep 17 00:00:00 2001 From: Ciprian Manea Date: Sun, 4 Aug 2019 01:49:01 +0300 Subject: [PATCH 006/122] allow temperature reads to converge a bit faster --- examples/all-in-one-no-pm.py | 2 +- examples/all-in-one.py | 2 +- examples/compensated-temperature.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 ab1a1a76..f7ebcdcb 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -87,7 +87,7 @@ def get_cpu_temperature(): # temperature down, and increase to adjust up factor = 0.8 -cpu_temps = [0] * 5 +cpu_temps = [get_cpu_temperature()] * 5 delay = 0.5 # Debounce the proximity tap mode = 0 # The starting mode diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 6314137e..ca0760b6 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -93,7 +93,7 @@ def get_cpu_temperature(): # temperature down, and increase to adjust up factor = 0.8 -cpu_temps = [0] * 5 +cpu_temps = [get_cpu_temperature()] * 5 delay = 0.5 # Debounce the proximity tap mode = 0 # The starting mode diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index 36796100..2733f56c 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -33,7 +33,7 @@ def get_cpu_temperature(): # temperature down, and increase to adjust up factor = 0.8 -cpu_temps = [0] * 5 +cpu_temps = [get_cpu_temperature()] * 5 while True: cpu_temp = get_cpu_temperature() From e8fa1c067e150a5ef58ea053fc49e5afa46e81cd Mon Sep 17 00:00:00 2001 From: Ciprian Manea Date: Sun, 4 Aug 2019 11:08:54 +0300 Subject: [PATCH 007/122] use generic variables; handle pms5003.ReadTimeoutError --- examples/all-in-one.py | 48 +++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index ca0760b6..bfefe7ad 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -130,7 +130,7 @@ def get_cpu_temperature(): # One mode for each variable if mode == 0: - variable = "temperature" + # variable = "temperature" unit = "C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter @@ -138,70 +138,74 @@ def get_cpu_temperature(): 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(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 1: - variable = "pressure" + # variable = "pressure" unit = "hPa" data = bme280.get_pressure() - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 2: - variable = "humidity" + # variable = "humidity" unit = "%" data = bme280.get_humidity() - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 3: - variable = "light" + # variable = "light" unit = "Lux" if proximity < 10: data = ltr559.get_lux() else: data = 1 - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 4: - variable = "oxidised" + # variable = "oxidised" unit = "kO" data = gas.read_all() data = data.oxidising / 1000 - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 5: - variable = "reduced" + # variable = "reduced" unit = "kO" data = gas.read_all() data = data.reducing / 1000 - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 6: - variable = "nh3" + # variable = "nh3" unit = "kO" data = gas.read_all() data = data.nh3 / 1000 - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 7: - variable = "pm1" + #variable = "pm1" unit = "ug/m3" - data = pms5003.read() - data = data.pm_ug_per_m3(1.0) - display_text(variable, data, unit) + try: + data = pms5003.read() + except pms5003.ReadTimeoutError: + pass + else: + data = data.pm_ug_per_m3(1.0) + display_text(variables[mode], data, unit) if mode == 8: - variable = "pm25" + # variable = "pm25" unit = "ug/m3" data = pms5003.read() data = data.pm_ug_per_m3(2.5) - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 9: - variable = "pm10" + # variable = "pm10" unit = "ug/m3" data = pms5003.read() data = data.pm_ug_per_m3(10) - display_text(variable, data, unit) + display_text(variables[mode], data, unit) # Exit cleanly except KeyboardInterrupt: From 69294f3c5533bbc58457624ee3e1da1a7b3201b0 Mon Sep 17 00:00:00 2001 From: Ciprian Manea Date: Sun, 4 Aug 2019 11:17:53 +0300 Subject: [PATCH 008/122] use generic variables in -no-pm code too --- examples/all-in-one-no-pm.py | 28 ++++++++++++++-------------- examples/all-in-one.py | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index f7ebcdcb..962f6275 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -121,7 +121,7 @@ def get_cpu_temperature(): # One mode for each variable if mode == 0: - variable = "temperature" + # variable = "temperature" unit = "C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter @@ -129,49 +129,49 @@ def get_cpu_temperature(): 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(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 1: - variable = "pressure" + # variable = "pressure" unit = "hPa" data = bme280.get_pressure() - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 2: - variable = "humidity" + # variable = "humidity" unit = "%" data = bme280.get_humidity() - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 3: - variable = "light" + # variable = "light" unit = "Lux" if proximity < 10: data = ltr559.get_lux() else: data = 1 - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 4: - variable = "oxidised" + # variable = "oxidised" unit = "kO" data = gas.read_all() data = data.oxidising / 1000 - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 5: - variable = "reduced" + # variable = "reduced" unit = "kO" data = gas.read_all() data = data.reducing / 1000 - display_text(variable, data, unit) + display_text(variables[mode], data, unit) if mode == 6: - variable = "nh3" + # variable = "nh3" unit = "kO" data = gas.read_all() data = data.nh3 / 1000 - display_text(variable, data, unit) + display_text(variables[mode], data, unit) # Exit cleanly except KeyboardInterrupt: diff --git a/examples/all-in-one.py b/examples/all-in-one.py index bfefe7ad..63439519 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -183,7 +183,7 @@ def get_cpu_temperature(): display_text(variables[mode], data, unit) if mode == 7: - #variable = "pm1" + # variable = "pm1" unit = "ug/m3" try: data = pms5003.read() From 10b73e181c132294ea7ab9ee8b0ddc1fb2670781 Mon Sep 17 00:00:00 2001 From: Ciprian Manea Date: Mon, 5 Aug 2019 21:17:29 +0300 Subject: [PATCH 009/122] improved logging with timestamps (instead of print) --- examples/adc.py | 10 ++++++++-- examples/all-in-one-no-pm.py | 10 ++++++++-- examples/all-in-one.py | 10 ++++++++-- examples/compensated-temperature.py | 11 +++++++++-- examples/gas.py | 10 ++++++++-- examples/lcd.py | 8 +++++++- examples/light.py | 10 ++++++++-- examples/luftdaten.py | 19 +++++++++++++------ examples/particulates.py | 10 ++++++++-- examples/weather.py | 11 +++++++++-- 10 files changed, 86 insertions(+), 23 deletions(-) diff --git a/examples/adc.py b/examples/adc.py index c4cf45cf..82bda415 100755 --- a/examples/adc.py +++ b/examples/adc.py @@ -2,8 +2,14 @@ import time from enviroplus import gas +import logging -print("""adc.py - Print readings from the MICS6814 Gas sensor. +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! @@ -15,7 +21,7 @@ try: while True: readings = gas.read_all() - print(readings) + logging.info(readings) time.sleep(1.0) except KeyboardInterrupt: pass diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index 962f6275..54860126 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -13,8 +13,14 @@ from PIL import Image from PIL import ImageDraw from PIL import ImageFont +import logging -print("""all-in-one.py - Displays readings from all of Enviro plus' sensors +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! """) @@ -58,7 +64,7 @@ def display_text(variable, data, unit): - min(values[variable]) + 1) for v in values[variable]] # Format the variable name and value message = "{}: {:.1f} {}".format(variable[:4], data, unit) - print(message) + 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 diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 63439519..87840901 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -14,8 +14,14 @@ from PIL import Image from PIL import ImageDraw from PIL import ImageFont +import logging -print("""all-in-one.py - Displays readings from all of Enviro plus' sensors +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! @@ -64,7 +70,7 @@ def display_text(variable, data, unit): - min(values[variable]) + 1) for v in values[variable]] # Format the variable name and value message = "{}: {:.1f} {}".format(variable[:4], data, unit) - print(message) + 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 diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index 2733f56c..6a9c115e 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -9,7 +9,14 @@ except ImportError: from smbus import SMBus -print("""compensated-temperature.py - Use the CPU temperature +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 @@ -42,5 +49,5 @@ def get_cpu_temperature(): 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) - print("Compensated temperature: {:05.2f} *C".format(comp_temp)) + logging.info("Compensated temperature: {:05.2f} *C".format(comp_temp)) time.sleep(1.0) diff --git a/examples/gas.py b/examples/gas.py index faf6eac8..2f849442 100755 --- a/examples/gas.py +++ b/examples/gas.py @@ -2,8 +2,14 @@ import time from enviroplus import gas +import logging -print("""gas.py - Print readings from the MICS6814 Gas sensor. +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! @@ -12,7 +18,7 @@ try: while True: readings = gas.read_all() - print(readings) + logging.info(readings) time.sleep(1.0) except KeyboardInterrupt: pass diff --git a/examples/lcd.py b/examples/lcd.py index aa0e1936..7e50c94e 100755 --- a/examples/lcd.py +++ b/examples/lcd.py @@ -2,8 +2,14 @@ import ST7735 from PIL import Image, ImageDraw, ImageFont +import logging -print("""lcd.py - Hello, World! example on the 0.96" LCD. +logging.basicConfig( + format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + level=logging.INFO, + datefmt='%Y-%m-%d %H:%M:%S') + +logging.info("""lcd.py - Hello, World! example on the 0.96" LCD. Press Ctrl+C to exit! diff --git a/examples/light.py b/examples/light.py index 216477a7..3442fb48 100755 --- a/examples/light.py +++ b/examples/light.py @@ -2,8 +2,14 @@ import time import ltr559 +import logging -print("""light.py - Print readings from the LTR559 Light & Proximity sensor. +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! @@ -13,7 +19,7 @@ while True: lux = ltr559.get_lux() prox = ltr559.get_proximity() - print("""Light: {:05.02f} Lux + logging.info("""Light: {:05.02f} Lux Proximity: {:05.02f} """.format(lux, prox)) time.sleep(1.0) diff --git a/examples/luftdaten.py b/examples/luftdaten.py index c5b03873..dd6e9677 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -12,7 +12,14 @@ except ImportError: from smbus import SMBus -print("""luftdaten.py - Reads temperature, pressure, humidity, +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("""luftdaten.py - Reads temperature, pressure, humidity, PM2.5, and PM10 from Enviro plus and sends data to Luftdaten, the citizen science air quality project. @@ -162,16 +169,16 @@ def send_to_luftdaten(values, id): font = ImageFont.truetype("fonts/Asap/Asap-Bold.ttf", 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")) +logging.info("Raspberry Pi serial: {}".format(get_serial_number())) +logging.info("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) # Main loop to read data, display, and send to Luftdaten while True: try: values = read_values() - print(values) + logging.info(values) resp = send_to_luftdaten(values, id) - print("Response: {}\n".format("ok" if resp else "failed")) + logging.info("Response: {}\n".format("ok" if resp else "failed")) display_status() except Exception as e: - print(e) + logging.info(e) diff --git a/examples/particulates.py b/examples/particulates.py index e71ca5d9..9aa9c378 100755 --- a/examples/particulates.py +++ b/examples/particulates.py @@ -2,8 +2,14 @@ import time from pms5003 import PMS5003, ReadTimeoutError +import logging -print("""particulates.py - Print readings from the PMS5003 particulate sensor. +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! @@ -16,7 +22,7 @@ while True: try: readings = pms5003.read() - print(readings) + logging.info(readings) time.sleep(1.0) except ReadTimeoutError: pms5003 = PMS5003() diff --git a/examples/weather.py b/examples/weather.py index 17aba204..50360210 100755 --- a/examples/weather.py +++ b/examples/weather.py @@ -8,7 +8,14 @@ except ImportError: from smbus import SMBus -print("""weather.py - Print readings from the BME280 weather sensor. +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! @@ -21,7 +28,7 @@ temperature = bme280.get_temperature() pressure = bme280.get_pressure() humidity = bme280.get_humidity() - print("""Temperature: {:05.2f} *C + logging.info("""Temperature: {:05.2f} *C Pressure: {:05.2f} hPa Relative humidity: {:05.2f} % """.format(temperature, pressure, humidity)) From 8a7d81b6e515c44a2aec4172db1fb40ef2930183 Mon Sep 17 00:00:00 2001 From: Ciprian Manea Date: Sun, 18 Aug 2019 22:18:58 +0300 Subject: [PATCH 010/122] catching the correct ReadTimeoutError for pms5003 --- examples/all-in-one.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 63439519..74b49581 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -8,7 +8,7 @@ import ltr559 from bme280 import BME280 -from pms5003 import PMS5003 +from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError from enviroplus import gas from subprocess import PIPE, Popen from PIL import Image @@ -187,7 +187,7 @@ def get_cpu_temperature(): unit = "ug/m3" try: data = pms5003.read() - except pms5003.ReadTimeoutError: + except pmsReadTimeoutError: pass else: data = data.pm_ug_per_m3(1.0) @@ -196,16 +196,24 @@ def get_cpu_temperature(): if mode == 8: # variable = "pm25" unit = "ug/m3" - data = pms5003.read() - data = data.pm_ug_per_m3(2.5) - display_text(variables[mode], data, unit) + try: + data = pms5003.read() + except pmsReadTimeoutError: + pass + else: + data = data.pm_ug_per_m3(2.5) + display_text(variables[mode], data, unit) if mode == 9: # variable = "pm10" unit = "ug/m3" - data = pms5003.read() - data = data.pm_ug_per_m3(10) - display_text(variables[mode], data, unit) + try: + data = pms5003.read() + except pmsReadTimeoutError: + pass + else: + data = data.pm_ug_per_m3(10) + display_text(variables[mode], data, unit) # Exit cleanly except KeyboardInterrupt: From c1cfb8d0f96a0bf69216dad35afd91c884c64364 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 19 Aug 2019 10:39:05 +0100 Subject: [PATCH 011/122] +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 From 10d81356df22a2eff212caedac15c7f3dc16f026 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 19 Aug 2019 10:52:04 +0100 Subject: [PATCH 012/122] 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 048eb804..7d0d0f1b 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 @@ # 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 From 5f63416d347a4c95024fef4046dc2c0eb4642875 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 20 Aug 2019 16:15:24 +0100 Subject: [PATCH 013/122] 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 bc19e123..4d6d4635 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 695a007e..8e82b6b9 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 77a6d820..9995914e 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("'")]) From 998bb2e90a2bab0ce43f92ee0988bfab3a29ea96 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 20 Aug 2019 16:17:12 +0100 Subject: [PATCH 014/122] 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 4d6d4635..ab1a1a76 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 8e82b6b9..6314137e 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 9995914e..358d442f 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("'")]) From 2c6a2d7204a1618695a9c5df5da306daeca6e41b Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 20 Aug 2019 16:34:23 +0100 Subject: [PATCH 015/122] 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 85a9a155..eef99c36 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -102,7 +102,7 @@ def get_cpu_temperature(): 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 @@ def get_cpu_temperature(): 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 @@ def get_cpu_temperature(): 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 @@ def get_cpu_temperature(): 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) From bca04496c248701cca287451021eed9f3c7389ed Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 12 Sep 2019 10:02:32 +0100 Subject: [PATCH 016/122] 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 54860126..6bb6873d 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -5,7 +5,12 @@ 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 eef99c36..87acc6fd 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -5,7 +5,12 @@ 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 3442fb48..b18a78b5 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', From aa747a416652b64a182d6e2594c08e2476d17d76 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 17 Sep 2019 11:45:48 +0100 Subject: [PATCH 017/122] 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 00000000..0a8040b0 --- /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 00000000..70aa6ab6 --- /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 e69de29b..57e869d8 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' + ) + From 18c9ae73f4dd170a69df03822193c3044f68f194 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 17 Sep 2019 11:52:42 +0100 Subject: [PATCH 018/122] 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 0a8040b0..8b1ddd57 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -2,11 +2,15 @@ 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 @@ 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 @@ 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 70aa6ab6..964deadb --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -2,11 +2,13 @@ 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 @@ draw.line((0, 0, 0, amp), fill=(int(low), int(mid), int(high))) disp.display(img) - From c0ac7b9f141e492642600c9e9e8ee8533ba0d427 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 16 Oct 2019 10:41:10 +0100 Subject: [PATCH 019/122] Add dtoverlay for mic --- library/setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/library/setup.cfg b/library/setup.cfg index ed7cef7f..b4c8a96b 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 From 4d6b7f96ada71d5f303218e84765d9e74583ef70 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 16 Oct 2019 10:41:23 +0100 Subject: [PATCH 020/122] 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 57e869d8..2e7472d1 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 @@ def get_amplitudes_at_frequency_ranges(self, ranges): 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 @@ def get_amplitude_at_frequency_range(self, start, end): 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 @@ def _record(self): channels=1, dtype='float64' ) - From 504b0f7f30384243a55d2620dba68eadb93b4472 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 16 Oct 2019 10:42:21 +0100 Subject: [PATCH 021/122] 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 964deadb..1afdff5b 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -26,7 +26,7 @@ while True: - low, mid, high, amp = noise.measure() + low, mid, high, amp = noise.get_noise_profile() low *= 128 mid *= 128 high *= 128 From c440294cc10ba86b4cdeae53b3b3592110493190 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 16 Oct 2019 10:44:48 +0100 Subject: [PATCH 022/122] 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 b4c8a96b..c48c7bad 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 From 426f1cbc68f7644f700066c5bc1068c518e8ca10 Mon Sep 17 00:00:00 2001 From: Kostadin Date: Sat, 26 Oct 2019 13:26:47 +0300 Subject: [PATCH 023/122] 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 87acc6fd..03e4d588 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -201,7 +201,7 @@ def get_cpu_temperature(): 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 @@ def get_cpu_temperature(): 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 @@ def get_cpu_temperature(): 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 From 71dc2962df1f5a90752c1dedc135bba82e1b3b7a Mon Sep 17 00:00:00 2001 From: Kostadin Date: Sat, 26 Oct 2019 14:45:18 +0300 Subject: [PATCH 024/122] 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 00000000..e863de31 --- /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) From 2fe226f8a1755e1cf487c972b56b9166b4f2ff4b Mon Sep 17 00:00:00 2001 From: sandyjmacdonald Date: Fri, 8 Nov 2019 20:26:45 +0000 Subject: [PATCH 025/122] Adding all in one weather and light display example --- examples/icons/bulb-bright.png | Bin 0 -> 2533 bytes examples/icons/bulb-dark.png | Bin 0 -> 2817 bytes examples/icons/bulb-dim.png | Bin 0 -> 2817 bytes examples/icons/bulb-light.png | Bin 0 -> 2820 bytes examples/icons/humidity-bad.png | Bin 0 -> 2983 bytes examples/icons/humidity-good.png | Bin 0 -> 2530 bytes examples/icons/humidity.png | Bin 0 -> 2983 bytes examples/icons/temperature.png | Bin 0 -> 3188 bytes examples/icons/weather-change.png | Bin 0 -> 2770 bytes examples/icons/weather-dry.png | Bin 0 -> 3155 bytes examples/icons/weather-fair.png | Bin 0 -> 3155 bytes examples/icons/weather-rain.png | Bin 0 -> 2850 bytes examples/icons/weather-storm.png | Bin 0 -> 2732 bytes examples/weather-and-light.py | 409 ++++++++++++++++++++++++++++++ 14 files changed, 409 insertions(+) create mode 100644 examples/icons/bulb-bright.png create mode 100644 examples/icons/bulb-dark.png create mode 100644 examples/icons/bulb-dim.png create mode 100644 examples/icons/bulb-light.png create mode 100644 examples/icons/humidity-bad.png create mode 100644 examples/icons/humidity-good.png create mode 100644 examples/icons/humidity.png create mode 100644 examples/icons/temperature.png create mode 100644 examples/icons/weather-change.png create mode 100644 examples/icons/weather-dry.png create mode 100644 examples/icons/weather-fair.png create mode 100644 examples/icons/weather-rain.png create mode 100644 examples/icons/weather-storm.png create mode 100644 examples/weather-and-light.py diff --git a/examples/icons/bulb-bright.png b/examples/icons/bulb-bright.png new file mode 100644 index 0000000000000000000000000000000000000000..5697a81bf2fe2f2a7f45f7e448650ba321cb92e5 GIT binary patch literal 2533 zcmbVN2~-nz8XrYak<ww?E8@;`px@4LU8^zv{c z=x@~r0D!=E=lS5jCfav{9{&APbRY--E7~Mnu9ue*^$JHZ6!dX(0RZc)v)O_OU+2x5 z^L(pF@4Z=T$g3T880*#xHve_%fpyNmG(F_(zVBb(XH<61)QjZhA37&!=5&k>RgRfY zCT%`AV|fSZC#DzdG5X;~OxXQs)-yz}d8Fo#-DH_dHneTLTS+x-DyI}%Ry4<5JN`Ap zzb8WtIMw#($L3F-N(}DZ2;BVw-}c8AyH!SlMRkURuR;)nGiZzseBXJ-dqe z)$d}|u08dNRrz~&M^-=RNP0f(;*eP6;X}wLfsal;_JUU4-p((`o7KC_8f#mDmCZi* z>gmbT<(J9k$9;2Bu8h;)-<5OLwyANv)MA90+oCa;n8;7RYv}Oj$NSRR)amz5*#ZX# zdx|+t2X}mO_?StokXe|q{Zea+ft$wag4@i#)@Tz0H)Y;jwYTNYp?ZGciPo#^^TYKI zGYaDzzG+BJEgT{HC+eH_9&QR4ot6J1=)J?aEu$|7+ZtwSW4#Z2|HRlC?sb|owMxDlS@wF>t@6##76u6}krph&okQ$#AI z^U6g+3L-^hh#XVlSlTickCH&J41XuBOty?xVXjAUixz!*CtBqftw1O~hzgBV3K7?% zxJJt*!xY+%R)wSg#gFCdWp5M^SBbzFs`Nuq=~ANR=Yp06BoRmdFO*0KiBY8t#$zLq z!C?qRArG6^ArL@(xe9~jLWIv_6LH(f5{U?MuxBvs5T=ksV}y!Hj8LYS6v`6YljwB1 zJ&j7EGMG%^k~|L;Mr!l6Brp1v@}5cw-Z!xHxAQFL1>&MoR4G73cv$U-;7{j#d}1q6 zu|$er{Csk2D|^1eAa{uhPh<3AulOQI7FSXUIA2r{EY#LLn<&&K91#&0k0rl3dJDAb za0DlPcMU8cRHztJ!%DevxiO;T0#v^qAu(E*9bM^~3{-`6^-PhZ6wW8$9qkF6qk#T>7{Tsjdz zo71owQu-(o;YgYL)$4mU(db)+o2a4 z%bd^Iy)`Ctdv=#)H;Pvs_|}lTgE8>-&NJlW1VX$i=rh_$SQppz;^1J)ps{I1>6aHL z@06(&^?AXim8q>94!NVky5MSFghOzc^f2e`Yr>fij9FvTVA{iN5d+(7PIV5mA`B`g zcg4J}SLa+v(A3HuH)IKO+nMA)+&CnjX)eFj6l+nT`no%=Y@-=1m+$%cMX)$1(W867 zhZVcWvxE)nJ_%fdet@+%5V#LGI`%R8_ooNi?@ZtDJaTBALA=)&(&Cgenm7KgCfG;N zj=nt<>{eqw)6pZ$sxQCf_f^l;CFWKH6{j(1jOtv=>5g;scds=Ro5!}VJzwZ|Q(`*0 zW;_aHSvKFS=nA%N{F^Ub7MO9Y(aQ#VYMN%2 zsw;?)!+Y03<_!ZnZ%B{M^pA~@GOOg@>I2D)7wyb|O zb+5^3@LXj@MYnN@niE^;`u4WH4$N$O`nb0!f&H-NhgU6zE`R?PRz-fAS5Ntjr#&1aPZ@Kx;INv9?_Fg@kRc)Pi_g)Ln@k-GgBs=P9Bb_3= t?>sl%Zk<-`v;W9hy;l?5QL`!a=;}7%Yn_)i8e8pOHQ&{Pcg-av`QNaA<`Vz_ literal 0 HcmV?d00001 diff --git a/examples/icons/bulb-dark.png b/examples/icons/bulb-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a91e24b580505621046235f3ded1a7058bb0281a GIT binary patch literal 2817 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 literal 0 HcmV?d00001 diff --git a/examples/icons/weather-change.png b/examples/icons/weather-change.png new file mode 100644 index 0000000000000000000000000000000000000000..21215b78d5a1946229fb900aac9fe2dccc7b10d0 GIT binary patch literal 2770 zcmcIm3se(V8jjim0_sv+krmtNAd1RlW|GNkG*VuMC}APc1(aGQlL?uHWa1>4K&_zV zQQBHdu!|L+tvs|;UAvyGg;nuU0Y#84^+C5HJ<3wqMyl2atnLht)YIy&r{`?WnatdK zzx(~K@BTBT3Gu2)-oNt}2n3VVvC&%Y9>`z6@!p?WfF{xjRCZ( zNP!@LmgH`ZO=|Y_6i0@@R4W?cGa4Q=}80aqAL` z1T`Oo>w-UPd3|ruIV`1XgI%!jNSF8Os=IF&yn5MBaO$tzuxE9h>QD}_=cvD_+V9QG zqt^q3c3_R*<$8bHOGD2{znGtAkLhZ(JX5vwrJRQL^J~BRK2lb&Ctf?L3IaMyze>Qz z9}ZDh?;P@|Rov_tXFD*|{>8VYy^m;$!~`V()+sV!kg1)zp>)Az}2L*W<$tFZH2z7;#oUXgK=%Sfxaqs z=YxjC;CcOx>a;i8K2~o3zVYuHYF?LpbF8GK#wAQC@D94XqB(V7$b5SFL2(NaeRklt<`icG1wV>~F z+sCWo*G7Ga7pY%_^kDcRV4(x&2uzfX z034=lvlVwJ!C_vUd*-KM5EzEovXo#1ZxBe;BmhwiO92w06e3Yf0>~6XR4T?4iZDQg zpmG?I!>AZSr8tJ*A`vk9137D~-hgYP7mwQFMoQ3VvsrK$w%hGOyI9DunJ}tQC}2bc zi$oBIfULP@8{vS=*5EOQXv#{mw8cg(mLhBno6IoTV}%;-3p@)D6~z}u z0fcG@k~Z_yb9qyvyQpZwMkzra0ih^_$dgeNmq>B3eAx&}!|6az*oa@^L^vwPMT#*T zqo)nI{|%)laRbAe2rf6YiO8g2i#c=HhDL*{%~l&>CMk8a66Aan(zG7ei^T*fBQQv& z*JBWdU?PYxNOcfGpaz{tE+=$^Y;1otL+0>xe&qEX|!FBKuKR-YY%FD3f4PF(oA-6w?_XOeZx!I)y<3 zi9{j^X9B^bQW6AyvKz%@GpvTub4eCII`dW6<)q&U*YD`Es;Weheu!BP>KT*{wL zaqyFzw-T%VhwD;QB*icVB$kLN2$P7#kPeaSAQ?&O400XCX+%C2`Y&DoNuY(qxFrmpTd#r2}ji3wSS_s3s2=i$HM;@MLN3aV$~{l}Izw7oz1(!TID zt}VsYrdrXx%5tjgWZFuP-pW>w>78ETZO!bbDoN>Kg`%p%OA-(_e`xB?^!;_qca=J> z5nlM2xcLolHHFH*o{|!Vkjee$J`4HU`}9`C(D~Z4bDc}J-R*w9Khf#)Llb^grfCxS z&8zh(s;_y^`;8X%K-%IzU7=q-`s2P(MPK3lIf09UUimOBaaqRpzoZ^&4GI5bV+if( zx49S5o_KL!U>*i9)9OpMEZHmXEB13<$kU#%c=h(~nz1-Gtb9fO?RyH#>1pl9OY8=4 z$Fhic$W?sK$AZn!<{%$z>G~lkHo)mq2Zr~|0)kyI{6}MbNtVAQW5Vawz_%RE&dFJy zXVCMLzdV+rMU1aDeYr?GeqHnBwe?e;f2Z-bw{gLp^zND_S95-T=EK~3;+Ss)OC6hv Ug65=L*Z*`SQRmW$##i#Tb+bk}8 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 literal 0 HcmV?d00001 diff --git a/examples/icons/weather-storm.png b/examples/icons/weather-storm.png new file mode 100644 index 0000000000000000000000000000000000000000..20172459e6d0f2f58b0e4bf61310592ba25b4ead GIT binary patch literal 2732 zcmcIm3se->86LsHf`}p(d=-b~5o(y7$G!(PxC>-ib;l*u5~_#~Vp4_Es8}FA5CYL+s}xd=&wx>hk*F1nompVxNi@gPp3XV5JNMr2 ze*f#c|4fN4Q9adrjyC`RQ#A>3$?X4Z?w;=fTDan}AJG06c6fD7gZfAgxWCokyvJ`#X6sjh zf?RMN@a8dp$HLLclGCrQ&W-PHvrevCyfCNv+{N{`?y97P`xBF=) zTGk}Jb@N|7T|56AzV+SVDV2eV3wB-Omv_qMs+(Gu?ibwXNw4nqLT(cu%}VC`%&2iw zC$5kE6JpeRS8UsMVupWGNUOSbZ%BDy?-!8)1=HR?7=6fm|-7?sQAFoMAt z2C)dpo^Npwd62~(GQki>+ew?r>M$`DkYgnDOpZgrXFVMY!EANWTI}O#ViN}E5mp!# zAY4dJAVs=x)*M^5(>O)KbT)0KEe<=2MO|1nOIoc9{#;tK*@d<{;#aXAjmP#}v^_Q7 zO2f&toyoD0bo?sTV#tJJR_;XGjm!(;a4ox{vEs2dns6|-REEi(D3r4=a4cYKELRvg z7@;LdlZB)HC1+}U7ad19Xa%1`Kqv|!vQ!ks#S&a7`_&jq%j$q49K?@t7>=Phf=%EU z%4Ep@DHKKG2F7M4*xZ=RL?#VeEtyUmS}m@z*d2t0q&0C0KI@ynWTJ3dE+(XyR1P7C z5P?JzIRWVrS`3K{C?zKh61`Y1o7f-6kU3o4xcy@irWlgtaAgaZi42qolZhY{qY(&^ zi%>`=)Jq|R#-vg?D#1tun_zpxW@1kfk^L+y=M}{=O2s0Hkd}}TD$*MukzQhe^m2n3 z!Z1vXA}At~NJu{Tz1>(Qo3UvbicPZc*?A4yzio`cl+7+Y+s8SbJN1SqnCxs(^T$t4 z3cYgtDcb}(C596ucM24Ik}E$=@yDl4&&lP>c|As(YouAy=PP0iVP_1ET*5{xGuf2C zoZ{dYId3Oc{j}?}P)bu0LJ!IHh=i4aSi%*K%Rh#S2$Z7rdROQ_cKv&i{@1RLr^r6^BfXUUS~;bOQ>LaBk)uzY*X-ixjM^o~+Nbaom;F?S?<)tM&X9>dRu0ace(U5b zygQy#J6lT6+0K;3HhIy1cG9iZyLla0FbJ5>XV4+js^%OfD)x@oF`9c?G)bn!~(^enu{ z@ZMfT+|x#8P@%gwbj>RNG~|u>`!c?i=-;du^s8I&&2YJQ#a;gc6AP^$P5iJ&$a~ULBD2TDb_ZW_w=TT%&Ah1Nug)kq?&h9! z;7rrz(?J=JJ!@OwlSAvX4sGvKU%PrfX|l@ReCk8Nt*AOlQm-*|a@WHvl`A0kTDZCP z@8Q^DNZ%$<^6otLJ=Q#3xM|(fvgB&x_Qu}_DUJ7QdT(U?Ay{-HsL;KSvYim!u6i6F zWxl;DJ!qw-Rj~7q33Eqg8GjzKxi^WtN-g+6@Amrnh_cAa;&Y!zfsTQ9`g%74B0~R9@%4I5hEzaR*fy8~y`E!S9~{ literal 0 HcmV?d00001 diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py new file mode 100644 index 00000000..a0d755d3 --- /dev/null +++ b/examples/weather-and-light.py @@ -0,0 +1,409 @@ +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 astral import Astral +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(dt, 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'.""" + + a = Astral() + city = a[city_name] + + # Datetime objects for yesterday, today, tomorrow + today = dt.date() + dt = pytz.timezone(time_zone).localize(dt) + yesterday = today - timedelta(1) + tomorrow = today + timedelta(1) + + # Sun objects for yesterfay, today, tomorrow + sun_yesterday = city.sun(date=yesterday, local=True) + sun = city.sun(date=today, local=True) + sun_tomorrow = city.sun(date=tomorrow, local=True) + + # Work out sunset yesterday, sunrise/sunset today, and sunrise tomorrow + sunset_yesterday = sun_yesterday["sunset"] + sunrise_today = sun["sunrise"] + sunset_today = sun["sunset"] + sunrise_tomorrow = sun_tomorrow["sunrise"] + + # Work out lengths of day or night period and progress through period + if sunrise_today < dt < sunset_today: + day = True + period = sunset_today - sunrise_today + mid = sunrise_today + (period / 2) + progress = dt - sunrise_today + + elif dt > sunset_today: + day = False + period = sunrise_tomorrow - sunset_today + mid = sunset_today + (period / 2) + progress = dt - sunset_today + + else: + day = False + period = sunrise_today - sunset_yesterday + mid = sunset_yesterday + (period / 2) + progress = dt - sunset_yesterday + + # Convert time deltas to seconds + progress = progress.total_seconds() + period = period.total_seconds() + + return (progress, period, day) + + +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 + +dt = datetime.now() + +# Set up BME280 weather sensor +bus = SMBus(1) +bme280 = BME280(i2c_dev=bus) + +min_temp = bme280.get_temperature() +max_temp = bme280.get_temperature() + +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 = "-" + +while True: + dt = datetime.now() +# dt += timedelta(minutes=5) + progress, period, day = sun_moon_time(dt, city_name, time_zone) + background = draw_background(progress, period, day) + + # Time. + date_string = dt.strftime("%d %b %y").lstrip('0') + time_string = 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 corr_temperature < min_temp: + min_temp = corr_temperature + elif corr_temperature > max_temp: + 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 + range_string = f"{min_temp:.0f}-{max_temp:.0f}" + img = overlay_text(img, (68, 18 + spacing), range_string, font_sm, align_right=True, rectangle=True) + temp_icon = Image.open("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("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("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("icons/weather-" + pressure_desc.lower() + ".png") + img.paste(pressure_icon, (80, 48), mask=pressure_icon) + + # Display image + disp.display(img) From f9d69568aec8773d9541809f6cd22d44b0197431 Mon Sep 17 00:00:00 2001 From: Sam Birch Date: Mon, 9 Dec 2019 21:55:42 +1300 Subject: [PATCH 026/122] 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 9aa9c378..c1b3c677 100755 --- a/examples/particulates.py +++ b/examples/particulates.py @@ -23,7 +23,6 @@ try: readings = pms5003.read() logging.info(readings) - time.sleep(1.0) except ReadTimeoutError: pms5003 = PMS5003() except KeyboardInterrupt: From 7c3404f8cace2ebe8600e75d53f4b53c32c7002c Mon Sep 17 00:00:00 2001 From: Robert Bricheno Date: Mon, 17 Feb 2020 17:27:30 +0000 Subject: [PATCH 027/122] 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 e863de31..3c0b58d1 100644 --- a/examples/combined.py +++ b/examples/combined.py @@ -37,6 +37,7 @@ # PMS5003 particulate sensor pms5003 = PMS5003() +time.sleep(1.0) # Create ST7735 LCD display class st7735 = ST7735.ST7735( From df20089d0d749ef7cccc25f4567203a010e31e8d Mon Sep 17 00:00:00 2001 From: Sandy Macdonald Date: Wed, 19 Feb 2020 10:10:53 +0000 Subject: [PATCH 028/122] Adding Python 3 shebang --- examples/weather-and-light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index a0d755d3..c7417641 100644 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import time import numpy import colorsys From 20442c9a53edb05b925faa07eb8360bd46442978 Mon Sep 17 00:00:00 2001 From: sandyjmacdonald Date: Mon, 24 Feb 2020 13:46:54 +0000 Subject: [PATCH 029/122] Python 3-related fixes, moving to pip fonts, fixing Astral errors. --- examples/adc.py | 2 +- examples/all-in-one-no-pm.py | 8 +- examples/all-in-one.py | 9 +- examples/combined.py | 16 ++-- examples/compensated-temperature.py | 4 +- examples/fonts/Asap/Asap-Bold.ttf | Bin 63356 -> 0 bytes examples/fonts/Asap/Asap-BoldItalic.ttf | Bin 66080 -> 0 bytes examples/fonts/Asap/Asap-Italic.ttf | Bin 66192 -> 0 bytes examples/fonts/Asap/Asap-Medium.ttf | Bin 62776 -> 0 bytes examples/fonts/Asap/Asap-MediumItalic.ttf | Bin 66228 -> 0 bytes examples/fonts/Asap/Asap-Regular.ttf | Bin 62780 -> 0 bytes examples/fonts/Asap/Asap-SemiBold.ttf | Bin 63140 -> 0 bytes examples/fonts/Asap/Asap-SemiBoldItalic.ttf | Bin 66240 -> 0 bytes examples/fonts/Asap/OFL.txt | 93 -------------------- examples/gas.py | 2 +- examples/lcd.py | 5 +- examples/light.py | 2 +- examples/luftdaten.py | 8 +- examples/particulates.py | 2 +- examples/weather-and-light.py | 53 ++++++----- examples/weather.py | 2 +- library/setup.cfg | 4 + library/setup.py | 2 +- 23 files changed, 71 insertions(+), 141 deletions(-) mode change 100644 => 100755 examples/combined.py delete mode 100644 examples/fonts/Asap/Asap-Bold.ttf delete mode 100644 examples/fonts/Asap/Asap-BoldItalic.ttf delete mode 100644 examples/fonts/Asap/Asap-Italic.ttf delete mode 100644 examples/fonts/Asap/Asap-Medium.ttf delete mode 100644 examples/fonts/Asap/Asap-MediumItalic.ttf delete mode 100644 examples/fonts/Asap/Asap-Regular.ttf delete mode 100644 examples/fonts/Asap/Asap-SemiBold.ttf delete mode 100644 examples/fonts/Asap/Asap-SemiBoldItalic.ttf delete mode 100644 examples/fonts/Asap/OFL.txt mode change 100644 => 100755 examples/weather-and-light.py diff --git a/examples/adc.py b/examples/adc.py index 82bda415..a345d232 100755 --- a/examples/adc.py +++ b/examples/adc.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import time from enviroplus import gas diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index 6bb6873d..db1ca8c9 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import time import colorsys @@ -18,6 +18,7 @@ from PIL import Image from PIL import ImageDraw from PIL import ImageFont +from fonts.ttf import RobotoMedium as UserFont import logging logging.basicConfig( @@ -52,7 +53,8 @@ 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) +font_size = 20 +font = ImageFont.truetype(UserFont, FontSize) message = "" @@ -96,7 +98,7 @@ def get_cpu_temperature(): # Tuning factor for compensation. Decrease this number to adjust the # temperature down, and increase to adjust up -factor = 0.8 +factor = 2.25 cpu_temps = [get_cpu_temperature()] * 5 diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 03e4d588..c0423e6d 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import time import colorsys @@ -19,6 +19,7 @@ from PIL import Image from PIL import ImageDraw from PIL import ImageFont +from fonts.ttf import RobotoMedium as UserFont import logging logging.basicConfig( @@ -57,8 +58,8 @@ # 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) +font_size = 20 +font = ImageFont.truetype(UserFont, font_size) message = "" @@ -102,7 +103,7 @@ def get_cpu_temperature(): # Tuning factor for compensation. Decrease this number to adjust the # temperature down, and increase to adjust up -factor = 0.8 +factor = 2.25 cpu_temps = [get_cpu_temperature()] * 5 diff --git a/examples/combined.py b/examples/combined.py old mode 100644 new mode 100755 index e863de31..43668a5f --- a/examples/combined.py +++ b/examples/combined.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import time import colorsys @@ -19,6 +19,7 @@ from PIL import Image from PIL import ImageDraw from PIL import ImageFont +from fonts.ttf import RobotoMedium as UserFont import logging logging.basicConfig( @@ -57,9 +58,10 @@ # 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) +font_size_small = 10 +font_size_large = 20 +font = ImageFont.truetype(UserFont, font_size_large) +smallfont = ImageFont.truetype(UserFont, font_size_small) x_offset = 2 y_offset = 2 @@ -166,7 +168,7 @@ 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)): + for i in range(len(variables)): variable = variables[i] data_value = values[variable][-1] unit = units[i] @@ -175,7 +177,7 @@ def display_everything(): message = "{}: {:.1f} {}".format(variable[:4], data_value, unit) lim = limits[i] rgb = palette[0] - for j in xrange(len(lim)): + for j in range(len(lim)): if data_value > lim[j]: rgb = palette[j+1] draw.text((x, y), message, font=smallfont, fill=rgb) @@ -192,7 +194,7 @@ def get_cpu_temperature(): # Tuning factor for compensation. Decrease this number to adjust the # temperature down, and increase to adjust up -factor = 1.95 +factor = 2.25 cpu_temps = [get_cpu_temperature()] * 5 diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index 74b6babf..b648f576 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import time from bme280 import BME280 @@ -38,7 +38,7 @@ def get_cpu_temperature(): # Tuning factor for compensation. Decrease this number to adjust the # temperature down, and increase to adjust up -factor = 0.8 +factor = 2.25 cpu_temps = [get_cpu_temperature()] * 5 diff --git a/examples/fonts/Asap/Asap-Bold.ttf b/examples/fonts/Asap/Asap-Bold.ttf deleted file mode 100644 index 52a14e546d1cbb6b955f72de15028e0e06145854..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63356 zcmcG12Y_5v)&IM1dhcatcKYnh>};9cnQhtKWRp!#HoXvP5<&_Dk`O`>2rUR0P>^0! znt)2PfD{2yK)z29L>)Rnms4EFuGfkem4uypUz#nWW~FGp7Dw#{oW`^?(ffCv8umjpYb+Hdix(zyG!*{m<3t{+b;-H|`jkd3J{+ zJ+Txx_#~ZV!0RXSLy|_)OD4%GIV8!Ga3xqGV92_d0nY{ld#`L|H<$0YJ$gI4NWcc&Tr4GlwImtjdRCIYSAm~ifj1Ex#qzKorsgL^~MrxpYa}+uA!jW%n|s=4 zj!=7d%9S(reu?910h6VAX{2P6k7?V5d3ozw@)*I`zJSRvCG z0S@+E^; zZ;aI2nTMNhFJTSIYbi|nz9nrvApd+A1xK4RfDoArB=teh~Mcg8F5Ew^U zB(uakuGNTxDXCh;C5QDzR@HXj(a$i3LzYn3=p3age7Z)x*Wc!>tu-ftesboS&5SBeJfc!Ru7S|Z9itx(QF*he!US1Q_Jdqq2BSexQmVAjC15gvy|Sk=n)!=6ZvwN`l|H&ohbmDg6mH&);+tQp#e zJg52cO=!OfmP*o+Ru;yoGoyG-RVsx~E4|+5g_i;~Uh${oo6PQxIW1;WNGrUliu%wL zEzFz_*_>8m`TtZ^a&)cJ>hDiGn!Oq){HyATqKgOO9<7$0Izn%d&-IN8=FO})>|1G` zhrVc)roy`#-?tj89*0e@M-sGftL#zrC956nQnAiz&G;+RlzhJ1@O=5lKA*$k_d5nu z!ZaOYyr_L|4r|0MoH@@RHTKC$46@+WD)14nDsGoUEIGH?qy(2I1Do zahryqO6lKVpf{=y7{FwLSI8aVPWV6*qA%A2zwpW?L#+Aa!peZj7)%9&HoMjCx$=^6 z>A9I>?eJ(K+Pc(HwSK?fXmgp&dY##*pKAQ>-D9XED?OTy26kK(eWafReOb1h%O;KX zUW~RKvDe74PNh0&kTJL}8r&DIyXBVaUU*^Y^5si6h#|fA+uy!-&yKBIca#@U&uB?Y z@nDjoR`^Aa$oD}zDRAyL5IiN|h$|X3uW<~l3I;)`0pHWW!#T>dxAP~GJc_MKZh?wV zogyh;iM>G>`8-LQ1xaAYaqFN@9UJ6_2tAMQlVB(s3jWjQ8GMpZy_|o%LBUJjTfqzX zu!WpcDn2qvKjA;r#yOs-Pu2;sT{Tmf?{wXeRk+Z@#hDGwEyxl7~ z_r@|(8FlbVrJcN8=NyDjI{VhR_TdR;bbp!|*I?67ZomuG_@*@0sASG}dR@Ro9zk$#^G<`UGC*D=~>>Krzq@_^^c z72kr})O^$|@|qg^$ZN`F)=-a6u`haj<-av0$#HVWy{g}2I5QZEhk`^Mc@=B9<>*7N zloLFHkCT9tmK5*?cBle(%7mkUYvhuw<8mObC%F;VXW8K@m*h(j#e@Zy;BESRiS{;n zCFUgCT?x*l-c&EM;|YJ|cY9M{(OL2M2Oq@agLq}S@+aVsEJcx#`3YJpkiQKcahIaH z9kf^FT~znxD;W~Lg!}`r#iMl6>m(*Q)vi5vmEUD{xyR#k_&nu)rQ0}rgabCj;~VI) zww>T&3kp+T15<0s%v0xoF*4{!|U z4RPxxUC6Kh5b(5y#z|bCkuK(#a}2BCS?S9Pyp3b7;3F5My@b!uzlowMd~(u7YCFuN zp?NRbZ)Fc~xWI?zy@0n!U*hsHeP}*w0lo2&^B&`<$OyuMN?6;%r?<(}4ZhWln#=Y^ zufoTWUXutavv=q@(A%Y<*(>@jNLTQFrJ)bmQuOP?h>-Q`EW+FGsPw8&tz7BzYGb~p zEc)7VtkGQ?m?axqJK2vCiE`tH?KCFT##cCxSMf1vU@~ueHl$+-qyRLLX5g|O;OicU zay07FhsHo6FdNNl@3OQOx{N=0$=KChv^=C$#lQR>VJwB90Ogmt z3?0n*S1)~?%L<3{SwwJ+JWC>8)2wqcqAn>3c!^yu;MC4AoIN2CH{j(U!h1x11#4Kf zQW21L=pCpZZwmg-Xi7NA;M7PVkn;-`EExQI72nwICcjy(M7o~N8SVw24K*>jGczkUzxJv99 z=%?}zVJnOVi`5Zz9kZ6fhIPke-2}XmiCi4bE5ci82iDD8)edP*aeY?FA7yyEa z@zo50qyrDlx4EPjq}d}RT?l%0$BN;t=KlD?ukk_n_q{ps$Q-JoKXNL3e>8$59n~ZD ztI@NAk06Z{>1n~?f|A7`zv3?V8y3o9a7|6>44m%ZX#>h3`AfgP_15bsi9u{h z>p};-Dx~Gui)BWU`Vcg{QHcaTB{Ft`(Ca|3O6;T};(C=MgZHT{p?T8;f8{Q2!H7nR zg^%UD5wvP6w9+bhx5(rkXfzDxee8g-wRH6E*xx{HmTypx$MalP3c$>%$!jM7kcYue^zHl5Yr*LByV3dY7Oj+#hkoxawk{({m}~H#w=1#X8L-Iq}@5N{kV6e1E)>Kito9 z-k^r&`hjp#jB%b_SvB?)mk4+ZJ6ak0`P}X#q4}h=zA6eSRxQO*tSRP+mV1u8hQue| zwfiJBI`Q+61!X#K`us?Tt@WU33rM*4=8eIFh~JUhL%61*)(iQ(+?k;~+znJa&#+?r==(oo0M(e`W=im82N4GU$Gg+pVU#w@f z@e{VczH9q}Qu()?G>bS++>V|-N+xL*kp&l=EU**RS)_Rqv$0X?s`h-d6=x{`BF4?{ zXsb-T<^ghrzv}g{`5Uji@y07R&PQ5h8$}xX{zT7z8Z7%1>o0s`x2K8t+3a;!CqRuA z>xcPGUSJb4IBqpNp)Q7Doq}pNDobFqJ-%NZammUpxnp{h_oYjv^uZ#9bLd%X)+gVJ zvx_pVt=YRPEk1&%GFtI`eLVM1i(aD|g*U%$OsmLtEaJ`Xj7<*o z+D@ItT&icLla426>G+c<(?YyC7`#bP{2=mM2ur9lKdN7pwH$;MSVS7ls>eR+>+s}k zYjn|QeXD7SD~lL9`EabcDZ56n?V+v}Uag@${<2MPviKfLgx`y^Yl`Vq{Y}D`N6%co zE>Pr4te;3VzRJOA;rMYk3%wwmORs{X-kVO|T zrn7DJwYlt+dF?Yc^%!gY9*-j*w>sUiY`iYujUK#w)}akP7qUW=fIZ*2c(ZVr2W$BHL7|B&rj#gPPnz4Q~Oz z*3p}FN^kN*y#3on=JlW8l#9Q;or5tWIOcrrU&&`gBQtZ!HzNKfidu(paVdkPn~ z_?7&yf>GWydez<+^pC`A@QtuGHFl-GUv}DM!5)%PBlHmAl9UKXlEr(uEE*AGRl|$S ziSLCho`>8)4Ig(UA4LT`)mg~jUkxiU(A&GHH_?0bUgMc3uzTY1^6K_kv)b8r@NVFQ zGpTQq>+`tAfc*13$5DKK2sp*I0^Y=`;WVEFyn(H+x}H300nf0F3cO7wTm_u`&GnqN z(f#JwkyY(b7Fe{?Dqq6Ki>`l7GfS!mmU`qrNygPwTovi?UP$y&ZAr1C{904t>b-j_ zoaio8oND>$f}yZPq}uXB8^$>(iZS6Sdv-ANw6 z8`_5q=1FLiEOk|z{s662cc5kNukplURX6B|Qj1yih_^TCzadWx^%@p>HAbCFBkSbl zns7~~CahWFa(V5G4ZW6WFbOgLe1A+{?sTof+j+p~9K9(ND?f6mD;=9@F}aOwz-mGw z{+{`Z*YvUeSjeOwJi~0UnAst-W|eO>Tfj4cFPF3LDsslxJ`_03HUV#7KO|l%aGGrb zo?#DG!6{A^@Ep5O(RO9|Q1AoQD}Kxl53!k85I_V#uhpvy2ogZO(frJP=6Jlm)w1&x zV{J{g&;0QJnEJAqyr#G}(PCd=in(i|_Nei5;$Y!{VUe60Jc)Vsu`JwW2 zapDkfjdSRS3WtK6VgV-(33vncp%8}zoH!)l8CIjf+c;JNPBRxat#UnaPF$a3{;GCJ z9|?FX))a=@CutV&0*i4@W3^JA1znDcDy*ZRtdG{AiP>ZkFZx6OI*h{6L=Pb%QV0|9SjsL6qL5pd#xfM=DR zEn(Od!coA92Uxkm^|XTc2ycHL+RuuWLUaq2dg; zw?lwNYG=Me!OFY8v~Z_ZV=?OO@t(|AY6hAQT7m>{qQ0Z9w^n{QUfy-d-drpMr`KqA zZ*Pw#SFUC$n5vKR$!=B?2Zp?7j^X192dtb@0VfU!cmp$4!HEL`o?$ix-o_~qaN>Z8 zb6|A)Ir0`N{gM=jc3S1GDmY1jfESpD!zp99isLCN>t;*uaX3XH)vF4#rMD~B-^H&N z5%6s3T@D}Z&(D_5;PBCsFk8AG(kDxKL@0liJ+P9n1i~kzYP}>M5SsJcEF$qO;xM?0 zS-XsccxQW(vf;lad9pS_)`^l zQ#G7(fv!)Xq5#)(pTxzf&ciqVtE~Pmg;BHfc0cX+Ip$D)5AJX)JhJ^(d1}@7G@2v7 zFEFla6ndzNkG|KS?u+`@5*%^o628k}jRgN@+#O;2t%ibvIFNy2myfPzF27b;1*onE zYM9K3%HJJR@p}n1L(h<^TAwMw8fA54&(buj+PMiPA<<(K8|o~M8*kzT~@d?GIx9qM08I5)GW02 zb4b!(_miOh&S&OaMfXzNC3IDWxfR_@bJJ6Sa~)c_eppwN&Lp2e=<0t~t|whN48NxW zZ(&x*Em@!~@?mJ7@7@^Rd(vr8Yz_Q^{pvbHrDjLNO%k@w%7=+eI4buQJ2akBGaJR8 zltGxN+M;!Vh*5%JSy-hBl$YKRtl_&;nqach;bF@DQK77vqHdKFRo02aM_4Day$@D? zM^*z?f;gzLfg|l$^~DOis2S>@WKngZCcrH-!dlAI(0*aWuVTh0vzYdbthhmspJh3N z&RMhTTUv{6c0+eivDL@6_pI2lC2JmH(_3qsYn+tenYKE9=B(h_sqD^dV!-~(4!cQn z9t`;M=}V4gsYp0z6c#+4zF>!~d1{BvY((lOkZ{flqVT<;<u#VfI|p1$hTzAs#Hwdd6y#EB${&x4O?)w2F2_*&mQ1D(UuNt(b=B!#jlF_u zoL5x8F`-Z7ztHol=jtnzJ{|D#a!RG9Nc9lo0uU~44Ldt2-stQ^@;!j^JIb;SuV9qe z^WCSNeDdk1pX`ZSy`6eT(AQuOdZq>Kj+$Avps#stQ&XYPe7GEaVC|j@FWj?duQr@A zhMYEy-W@Vnn89L+o5Bu!EISqr(7$U0=j02)i6k=TB>l74yA=-A175`J28YzV=4jT4 zge*v?q&4C15f6POjo?|aQx3S@9$PiXIs_j#d+^0(aMN}paj+#R-zxH*M^`gY@O1_M zrZFCGEWZJM_6W_@#z#@by~yIKJZsr7P1u#vJ`S9As{js~8#M`)X7)bhy%yY1q6{I_ z2|rE3D&6JbYU}p1&)&KH=z6FoU3XiRZt6&<7dVYy-u>lsFWG4=Olhg;A2y@8bEB$r z-{&*&Lykc`dEAN|zt1B90dE=sC;1leMkZ>ENq4-@XRd%ZFbmofp8IOWb6+Se1B}em z=(8Lq_PN~Ln(dc(_SH$c>hAqjY z%T3Ok)=%h7Pl5qwf2#YeAJ=`}KQ}(ukK26zIBxTVT&d_1vVB4~WU4&h_qk0J@GNpi ziZdWN;#a1|D7q^TMl(xRl<&lf{nTlxK>5d*X-o=WR#Ur0&gs!@A*Lj* zgW;u$_sl0A;!PjAXLk?x(yJ$@S&v?~(VK0+LH3O7FfP|uoMryzlxPN8OamM~(p-;u z0R21h9B}WSZq2xYogc#ujcu&l{%ax*VMO|o11An8d2XCmF8tt?EcFbPrDu5^KXkDh zJfhgbo03&5F%&&0%3lBkFa+w}lkdmbvlxrkICw8wu%TXlcueV6;un<*fdj5&Y4XZ> z-^c_=UnGk*^e*5*^2!x>lLG$)a65V5%#ZCj@HnM&`1St;JWLi=xjrME$1S74WEh^6 z&Q;)ToW>-#H=tcQi?{PPg?6=_oU})6hcOwJS82bMeGk0S0)MJ{qTM|n)JGz=8Rl29 z!a1pmbL#Gakrfui&!6K_zKOHw`f>L(D4TpLj2>qP19svOf8Yryc(xE-d`#!F6!>GJ zO1fbj-K2tq`l;F$!?hAw)sE8q-rnAChz)P%5^&3?wqEX!_r~!zxJsCNN26C05fV_N zS87TndFTz`V?`#b4i-Mtd07#%W1J%3>uS%05$-e zPK(*5F)A`OS9ozYS)9eC_tD8*%BR5xD-s8c{y5Yq{hp6lteO)dn>%{z)f|k;o^c0b z-ycWsAHh1cXxmoV&j(6Jj|U$EVYL3NTqfVL zAC*JYLy}=-A2X}OZma6qMm?+g*;n0J#hMd3q@suWGpcC7DS~deZMZ|)M@X1}vOaiv zt?1V~+&(fMU>lf6ZTF!`mq+K)ne4`O#+&3WWP5%V@M@!JeJkP3e~+^-lGww|SNKqY z`Gf*r!RZn3S<-`28;3i2f8u(g6Z`m->rbg%ufhJkTNHSs$awJfH7vznnPgG$M(h0K z7NjZLX)a;bkKRRxM4mzt3xkMDLuS1>6armdzN5~|$vd71y6Fxl-#q7Jm|U-0#jZMQ z`6JLvQcy%KiXGl#CnyzL)+jd$_mta5<-IVL{DZQTj+#+1>+mc;ITsy=D|Htyarkpm zQf_}3I*-1sTul~3T-~VTcZ%4@Nf@i}67~oWUr!c8T#piVJ`akRnRGpN*^S((Rv~FT zmh+khR;NyTg~lp|6Y(_w2dwL?vZOTtS?Mc8KSQI62Qz zjZlG;ofhyWRt+bv3wQ(VAXl;_8d&9Qnt;4ZK4Z>mG_41kr5ZzXWv-HD6LT^{I)cMV zl1UQ^cvVh+#GKDD)CyJFAzxTr-zp=Mq3D`k`AjKJ)m)0M3BZz%ETlz6Da<-nc$At1 zMFABX^TFYg+V+lVBlXe9y;ZqZ?VKtFrQp=d^%Hi(j?hnSGU1eD0$xp9_4U=X^>RO7 zTrX(jE3asW{Mb2^+n-#vQ^{#giU((Xs-V_`)`yzP9q5>~t*>=LqRS?o-6%W-vQ8smaB@zyjg_Wo1v9Z0Pa zF6_?@r=5IgcdePWqa|{upS*fX++wk&&E@;$%Y3Oq^8=bI1DQe-yLZO6Jr~x;ZBe_~ zl9{!6&)&K^Tij~0z7@K|H!u+P-5pvqKr=(&LsoL>C@CcCN!D3#u)xyvJy})FY|Okg zyb8pQ+>fM$qp~&$7dSv}Fin?Al6H7MT1lj5H733~f6}b}O~lL%{k_{qu8@@%3B!(K z9g&{BNUY2E_7YordMe9hF~!=mR!YsvQuk0%_5e3puN~K?Q_)`r7WWpD&)3sxWbO#BH^Q;<(IpvYiwBgjB)!4+34zVHt8qC`w%d|{r;9_t5t zO4)dg+oq3N#_@ww^$DM^&lvMib_o+KPV&qA^Q@p$6mFH9_KTr4ON~zTdhbXrN()B8WBAoq4T$7 z97ek@6{wB+LzzEr%I1QSTI^$>R6A{Z_kodjFz>qP3o0 zU&J12=&K&85Q>3dLy}0#1IHfHz7%!}o-9BgYx_#*?X>0KP&U?T;Y# zr6u#n6j6A(R(}eOYY(=>v}4cI)TC=ag>V|H(eyRYIWipXRkFd;|+p zi^R!WHeGA1$m``>tMUx{I0b9t@wNO-SUte)=Ab1*@lIfRC8! z8-q$Sft#x`1Bddwa{oxQ`Z@4$Ag4&qQS$#`W^BZaFvzo{r%(kdW(MKg51qHalA@|J zQTM|)*0*ZRR!rYvWAIm5sngVsoeDEUXOGWcMa~IZK?UV!wpQf48eUZ9eGf+C6WIpO zj8_$@`7l1v%4tTUD`@sqr5mb3@g>d1B`eqpq{bejD4flpcWPNo61fgwCwe2T1gwU0 z4+@;(U;%FuISB<$+DgD181h_|cE}eO@C^A>9NxyMQQ<{e7p+`B+;5KZ))hGU;-Z~a zc_#3p4E%@kQu#9SD)=5Z=x3S$R5{64G{<4nNH<+x+R|Dv=D{V+RVkY4t#2W0eS5l^ zN254cy$Mdi7kCjJCye4>8^>3LSJ6G<`YO5sC%Q#Df^J&7Ai5vKP7`GHDqi~7bTdM6 zabF27rK1j*I~RClb17}_$hmMw2`la>;nkSh_-D6;ZYaSXnH2XFpWRV?0}1gV{~7pd z<9rozkY{~W@y~TdhIr#BK~blg$*z&bDeo2OM*XaUeKY0aDsb|r1yv&3#Uul~ zqL)EcEo@6=uO0?JP{O*Zx>*cn;QPkwu?4Wz(4(`Pv~F{cp|z%0bG@N8F8kQ8unW-l zfOA^In#2P>Yy&Je`!Ju}u%^F3%QGr1!)5d!v5L)w9b(^~`We|Qg9kPSODlZyAL_+1 z>u_r!u+4x+Ds{E~40&2Et(nd=OsCZu*mQks5Ig^-BhjqG0=$t;*L6&jPr@$1)>i#g zT&2}&u>2%XAqa4M4*LSlmhy+%fFV{>r)N5!#aw>h=ru(Q*iXldKI{xE7t9vlJGdh5 zw-`LefAHOb3LP~6&Q#{#Nt~mCKGIVH-azvToEC7>Qv#l0Cs)CT+sUzAT<>59Ci^4! z)Iio9iEzpT!vupk41h8M2GT|3eiUTE51r+zuUPuxxCO~nhie-41V;KSmtHO)vu(CH zfJ9}t(e=j{T_(4|x*-@^Z#Cfai+|*wxA}bAuuTx7WQITc2lP(4PtJbyLReIG4Ej|` zV1LlOVQ6*rYWr@{PK`qnOdhS*2tf$SHro4nq|awEeT)5>3q$SU>uMhGfg7TIqWX)1 zdsM+$ffM%xyg_=W3QpV;@C^214PS4oz>92C6@0k;ob=BsIB7f4POB`ktO6gJ2LfK; zRWyut>#?V0OJ(PvM|Drc&Owdpi6jbM%^MGrr@@Xi`ZRz31-&ir?ev+ote1NlT|Tp} z{4@R*bc%KfKU$Td)!O$tegaPP3wR^;q7j_}PV@_S7JJt?94pU+p9(K3{K9Ig+{LvR z<~eq4@zr1{#nQZhT$4w+wcuegON|))Q8qsMXlC zuGsYY(v;}p=6+hT3ddc|ZoE#tGF@Bi$r-H9{%O%v-NMD}aQFZ}4A!{ZYz^M+08P~Q zKRJ$<2_94|F~Nxk0^UHHwsJl3K)^Gq4l93%+ANX*f*X)OerM?M%Jsx!aeWT^0EgQl z84&PR8M>%)J;{K87nC!Rsj{lN@_e>*6~CS$$LiIS+0u0!K03ocTe^|MN5)~YJbMKn zK`vpcBmnvair=Yvo#N{Zs+T1xAhy<54Gtpi>X3VyTq;ReW^Qk%FcP&0r|s=K!f0^n z?f!iD8~&-;!#5-LZk+AWS`d2=u_LLjf3<9@DIamw#pzHmoH1G)r&_&XUfrLKheEdU zI?uz&o-4o?feU$LH}G*9-q}Yu(l`mY*hxmY7=lxMP8WH~JaKWD%^VA+RZY5C2dh%0w-_Qj`(O1q0Uf~CYj;tqXQjs*N@@-6&fGEjoYQE4 zi;|0EYxHw$R(5|jk!yE4Y<3ni*?cRGNXN8Blij5;hr3<&aN2A|-v#yni=%G~RY6o& z^(qN};>eQZ~&lhZNW)IAsF=N)O88c>gXEK?(g#$zZ?3?^e`Dd7oXoV_i zgpWnHYy|fbb()kWV*0lwIyGwfw<$IFWA@ao=37&x8V_!R5+3}mr>2xThn-nIo8&5l zwtfnXkb5nPlm}*YRTj%Y98nSkV(8Qx@rl%yNFC%L?7pdmW*lX8Yja^L58FR*+I6N~ zI81FtZ0o0zRM?@h8?jns7rukg#uT_Cr6J!W~>ca9lmF?5<26ihdJ+2!W&QhQ7(Qqb} zMO~$VU)EW<=U{@|R*@*-2@~+cMK{F+=&+<5%4zExbbkBlx$KIhR+B84qOou;bhs`T zvUa1|#w;5b{^}5GkGIyJw5JpMW_8JS=e$-HTlo>;YcXGH`0-A>gbwUOFes9 zI_q@^L($QoEj`D1I1%<55c6zqC{z;@=$sYxz>5^; zJITxs4k@{7ESje7RBLzl%;S7ctXTM1miqm@=?enRe%ro4ymM&ZH$D9eWkzLI_sJ{B2K?mXB;>&*xb=@ z$n;sqOs#8eX-GG)Le|)R;-R&|RercD zRaXc*yq)z}NFrT6^a*=Ueyp0mq;qH{(Nt1#16oyjw{*@Z&Q9h0zMgoS29tH8IVksZodAP<{1UUtajC(Xa#`-FphG5I`8>GWOac(~ zq(3D+KsF4)t6Y5O?#NLqzhu!^D=1FHd0;;Dk^HcN`HFGmVl?JzvDX0%&xE90 zFo|&+C5$7unzWVcm1YBKUmmFd-k^%+Q>UGlU zk^}bZd}XYACncvaa(u$6*(xj@@hMKzJPS5u4HFLT7q2on^Zrh(-o`PC?cx`C^gQ}a;k!?3ex(YLTU`y?h zRfV~%CgJw`Jiegm#|d9NV)HlG#>vVg*mZ0^Mj391urib@!6*ml&d%JoL~IKm7Bujr zDXdR63m0@vz|^(SZ?~Iti*V~8`$C=3;x=0crWgZetEH#MVrAEPO=k0wpuuSi$~DW$ ze^_24dvSY5ZE%UnV)8mxtbhtI|d#eJ`88f#m!;fH@~TA2o%kK(KH18QTb|| zTo)bOVlcp%GHEVLNWW%x(hd*RegVtWj{5pY1P_)FuaSCOo0fjcn%Gm+N5?3Of9}*& zx;LGkN`F61@fZ5gnI!{YcWe_6c3 zzI`yXN4iV87I7r6WPsnnZL5elyb%*_!{muZTW9)r;~2d_cF}a~cWOUwJ}ex1mzUy~ z)H}Rie^sA+XNQ7I^?QNK6&xM$EDJ=cpNX;s@cS6JZl~C^LpglX@8in_`l^n0`Fk58 zv7+paSmyFpUvaqD4RO1@e3;AeM}M^B)~_IUKI?I!LI@&pfrscOi$gh;tB&9i$(5NK*BVyA1{`g`gi_ zB`OkrI>|+f0!aLheok*ac$x3l$$AufsMqa zo^~0ON<45jQ1?ixR2`PUEf}qMiIHaq^ZtDMM@qH{dHUo2(E@io?3WDpG=lHPz2hwG zA`QF3ha%RxMgV%8vcK2dD_f1S)9F5c#3%MkyYqHkJRaCoqH(Ug#aKnzFQprJFVHZf znw&FnBhYAk&QCNOMrr^t^3$sHfI1n#VKooTRur6@M^_R#Ti!_dp50rg_?sG)2^53*$q5kEM1aWG^E1^E+6YmDs% z2Og4aT*qelIvOUEJ~cg}rLCQ{89Rg88idLkI0H#uTBSUR=`~1M@VQDx4*Hf$Z-^N}4PUuPpi=#cERr4or$klBGHSXU5|3wb*LTv)HLn@nZw6%a3g-q03JHvQN#J6 z-e;P3GMv3wi5pa&K?6|YGI$0$&~($C8*u}^!8ZQFM`P$D>Geum9R1-fq+Gc|2{a8i zLoHBSXhnXSzj%E6eg^gcuhLKpi)b7iFB&4!FI=7QsSJh?iqBi;FB({2bvWyFy=|qb zx_aj?okzcS&bj~DtbjRdaI+- zl0#|xm%OE?cAoh3anzFY==aargZ8OKN&1cEEmRS3tIdb2BC{k9nmdSP`&RyDq|D-1 z?7fM9#qOtHmFJE6RV{v%9`P&U1|O$9u78?-^z^9TMDd%*_`hky<&V*C9vkr+85xLX z_9S~A7QFzyuAH(DJt=svxLCyr;P>0lG#L!0Gi`QfgK66~Q{MI97)Z0@8nf4K_nNP< zC!A*MMHg8uj^ubSvNCyK^Fjq18a2LLP9vra8OE$Q0eoeiaA?3TNP(5r@xw8Y=A^?y zIGCLYyNVak84IKFCf-OKn+Z8HmILF`OReS5noVhK0t%K-4qvQr4ShJYhpmU*&a$_p zs|Ih84D79;HwJIPHy^N9*jo90tP%eO_w55$*3R-->mP)dg}oyE z6mYlVw<<$#mnp!jCr1z~cCz2z>tU~OPtanqIP@J$THvBXui@7}$FJc{a({hka3gapw8vFy4kW=?IQ0^JvPyMCm*oRCd8UE3324n+lAw(meKO8C6ttPcfPwB$MRaGJkrwD)*4jcfk@bAJ!ML+ zp8po!yJzTb_D5*zZfQELC+aw3g{mdo{r*yWGvdaMPT{J6&s^z}ZwFrc_$@YQ2}UE5 zq^OPaF}=gSd8RLbdv550k)BlFlJ1?W!*Xro9A6FYxS{nK6WM9=JC9u+j<$tsj%rwD zcQ$HL){w=K$3b-7TLKL$`VZS=ibta{*<*LPaNiA+#>%b^H!PXD`B+m^RKC!b^TqAu z7aDUlXF}R=XHU5kUMTmxvs^Zy&Py~Nl@A?I%B0=7Q;Hh=irxg?fyxJewRfmby?vR# zj?`m>u?W4U8=0p5;2yejZ>l}LJ^FtH{Ft$i>GdD+Et$@^UFQh9X0EL_o^+biZNQ>s z)Mw#uK2q=Kdm{g6@ked^%{I5)m~1fDX58?9{yo&nHuCjS9)-dC$9NDeuv3Jjz@MCJ z5Wv+w9n@|P6q0V6)fsY|EeFlbcGx^7y+`NqYAnT|BUD_b>#2R#;WD$i?>U^oX$zMx z^+ZjnS~_th7U&!NQJiLM)6m_xvoOg4TmZfqGnpNpXHFV^nr4KBibjTK#;Gi!!exGc zHiB|OA`3=@s$l`5`sZ82&{GZj^9>ZG$f{$T* zktBzf=S~3Jv`VLh8u4QE@iUlf>cA%IAV_|kWrJ}_ytyrG)1+&fn~tTA$JW0}FXbyd zK9ALDcgJcj;0p!|o@olT8XKCI@K3#m*Yj755kKV|-tAOyN?q7O!L-^HU*v(?HjG~7 z9VrJmcmYxiKv`}#UA+PVa?Uw~z&W^e#24w!7y0v=YpYIamv3-hdeRy)(Z;8im z;cuq%=MUAB+cAoh&NjdzJP0qH?g1dPU%yBaP!FSEV85dnp154Wn#&)JeHDcey8i^P zB5V;x+CxEp6WMF<*6zHT`#IOrch~Z_S3B)!y;izI_M+Bz950DH7xobSpgil2kzP_g z?9hko_q5toH>^8_2Bqm;t#7MzE&Hb_pbI$6#>+1=nH+d)y6iFzF_|vA+-Me%wG)3O zF2>JkwWt>P4KH@NTVVOKBeMp2E{&DtwnTOA-z#*-9H=dix4yi1Vkp`ou@#bu{=-$p%tlDEccYCK+Jo~I|y?0$gbVXGBjL!^d( zwk+USbT}+}o!*2k55P`<(i~*0;~+>$Ml#D<_A1@KfC913x;o-8ri>0+2Xow@)2A{~ zR#$#G>M-fq%?_K*Q9jIQFz21E)?K@-ta(LP-AG$ zpwm&SDfW)Bd>A#wZq}O|(elfzF6uCA?@=21fu>$QXlS>@HV>;b8`vi zjPS+CS_b@(TJ8!84~$HI7B(wG5uSdM*KIMH0~*Wp=J3ope%bFa*sMma*)LZhUcC)x zt4wrFXcqm7u3;V-cM#WOH=o1Kj3$??(Ytg;_0uE!`;w%%U3vz7`XZ{};BF@O22ig} zC(kGjCiiUMl8I{lB_Y|;oA=GrPYqy=#c9&p!g^Cr&N(GA-4k#+T{`pc=^&U`gOfR! z6X+PcMth{uW^~gh^ca=q$!>Ny+1|9MXBgH+xbe$f<*#soxy$FtdFRFV@)7a)egR)X zgP3u<{3fG10VOX;J`51`2l%tiIG(9*KFsja!Aac3zS zi`LY{6TzBj7zNX?+y5F`#r9}+@m-pP5b%Mwr)=h+Ps2WhSA>-5!XA8;lDMUVpLw?&W9fQ9fVULaIHd@2$e_@GQ;?2w z@{X#z{y>5}mWx^q9+SZs9GIr}85dievW}T_R~w3vM)s1S82wt5o#_sG{RXco8VI}; zn`f~t_sS>5yaw&QT`}UYS`#K?eFer(<;#K>t-dZM)Ph5Vu%U2)DwP)kzwj+9GZSApJkK14}8C%`1 zY-q#1U)XZ(s?)D`*{3z;(5a5!h4y9ms#H^+g=U18ND;6qUotU;FCmjbrRo7Jw}}dq z5Jnx>ebVy;}Nsy*hXhUx(LOg|N>bVl$Pm3*zIh1xFmQfF9+& z2EEQKUr=k*Yt4hF)Uy9sy}Xc07t2NUisb?z8K|a6L)B&~&V5+LpcVhmfH##dsI;al zl*KFWPFK)v9YxJ*fqqQ+ni-Tg<+sEaCmk?(qsUZi2Kt5tN$UYsZOcWOl*$rRop`9+Ar4X8x*iEJ%D!wlPdF#lfQ?I|l))G8n^XARQ8dkF778YzuO-=bp=oU-)7d7mt=bwKOEw3E|71Op( zZ|pU@dehV97glz}+g*BZ$XYP@Q_0@c%u|;h^S#aeCogK~3i*uo<2%<~zWn58c3X_5 znn=DdB^i?+Uv}C0gReL!6ON>hIB3afGizM|S7_NU?CJg;)0dq#bK4_354~uy)oe(F zf`{I+^_&+@FV9_?3#?qSriHXt0DLCUM>bIh2~^&W+Uk&u z$SkGtOyRiRCvQYi_q@7>rQI8McXu}ww;$0m&2Q62oVohWmLo>jbeETPH1$l|Jhh7# zcjx@>ok#W^;n%tsm6on`hEfGXVF}%-!cVjw+{Qh_Z<#o z&n@)IrRyWCDLoEPsE^w<;ZV?-DH&v|c*3fxl7R$6Y`Eu^e7LZI7c&HI+-UcjG^JQa zJgu{6OH=DI8&(xmSS%4P_7J2!mow#ZhH%PFv2s z`>a6L90;Oz1}C%0zf~~oBkB7z9F;0-#9@>N6iR{&eFpgt=!;HKdz{H+tCew3T7^B)~+wrX`@*J%c=#d@6Eae@2K5sx=; zD9s?8Q}-9#8!gSVR{}}CkB;Vu*lW;E$yU&eIEtbnQe|5WA zq1H{2q`S8K99(BaYV1d>zHZuck|sLuYLS5@Iro6FVKX0x^IN7bbs(@-js&!cRF zX5B2TvfYP#9r7vg^3)YOKG_a~Pv{`W(6JX2Xfe&RecwkC2H4CR+J#@CUYROstIv;= zD+Tna)U2YTsQ$6j<7^Jh$Aj^~57l?DN^hZpcDMX*`1e>JLe>M!MQla>9ukG*^FfP2 zNT8{Bgu;~?70_~(+#JLW*3CMnJKc9^&$QmnD_tI|-%ZCBo>A;e*2KKVo03kQ+i3Bc zt)c7oezj@g)Rp&6-y856^*H*khuwQ#dm(Y^nZ$+WpXipzQ)w_FpCBX@)G14FKb)9+LrOLX$~aj+oI9( z(?)|M`s}k&$i~rfzjOu0gid;uWbU$%Pr>VdxOZ_Cj!HcR4_P@G6^EnZ4-wiE_~lgo z%c|e?R9%T{Hp_j|6!`dFv<QBf`CCQ<#_4Y?0&M6Da<6m| zcIjc|p33_u;KJ-1*3Cpq>ilsJMjq$*ZsN?DXz@~|Mbwi3d(mRWN2C@5VI$r0E|@Ua z);ime;+TQG#}>KQVBKWWo6U}FE-21erWTI_k1oR*#e^f*fP|sqmnlrwr}8s1ZkNSo zvd5CCgw0^HxZIhUdAToMir5^;**aZLs0~}B6bIM8E@z~p^12JkXND(Oj zkKJb18Rfa+TwV2yF~qm}NWJoj`aDfuC4B{_4?}wcd#+tnf9Ygsso#>S`mMZ5sfIvh zgv82UmAi-NBnRXbC{n~o!-0T86+W<_U!N+!H1?Q4xqF;@!~u){9cn}6$9FMPC4FU{ zc4EFgtxQQ=qZx7zeQoF$w34e=d*#j@Ek*sY>hSdcQ#WJsotgOMcAOu4v}7iHx&DV5 zrO+_oSDiM{P%`v1pIlfOFd2iXV9;i_+H3l)xzQ&A85T7|Lq%n;bf5g(_-o2NRhYX7@e?~iK{9_5anvoj|#8>t-&+G!P%Eo}RElD%v4 zl8u#fxtC?c?4xR~@n_%e1DJi-RnmxcmromqzfO7_sxh2U<9H^oao~-}{j5{97`!Tz z+lgU|ft8eTErzrSbN3ZtF@}G)X@tcXIo9J?jQ^dK#gO4E_p!58O+T*GjH~H&qeba6 zYWj){Lua7(5jrC^fzFURhUQCU+>zWoTJy-w)c%DFQz2NUPA9iLm@qDFE^x&|>S z3bqJ)+N7i^F+)#8mW!wo3}c2#Ac$(=f9>Ys3l3BBb|V3+X=`L%J?5e z1SUyGY2LyszJ)JM{|}Ki;hvjsNguLp>}~lJ+UKn?YR=H?)1I%rOnbfdcI|!I$8=s@ zT=ym26}ms_-qzRY57lqhpP+wA|6Bd*2BX1iNE_A}jy3EuoMpJk@HN9P48JqHVfdF} z(74@rrtx2vPsWSU1PJZpZCV>>KUJ z+jrZ)WWU1xJ4d%;j$^CiWXB%I#g6x#I_FmB?_4Wg>s-gWPH~;kqEKy54sm z<37oKj{6CZ!xQ#oJ-a+#_x!I{_V#%fdJpn$@NV}$>V4Y#qHn%$m2bW8INuL_Z~Gm% z0sJceb^hD@Kk)y~|Azlx{=tAL;0q)IO@S?eU4gR#7X_{kd^7Ofz(YYfXb*;hvw~j> zek*uS@X_GYHTs&SnkhB2YnIghHwc+jI(`q%fp4#goxyTcd=OTZI{5A4^ zltqt>Zi}7~-5Y%;`f;o|wjs7Xc3SM0v6o|S#gp;l;$MhA75{Dg_4vONxkOuHdSW24 zCh<(-mE_9gv#CQ;n^HSdXQcL~u1wvO`gdw5-Jaf>{#E*wx-E5Y)xDRAXO7H#FY|Eb zmCV00Ls?5UkWFO^*`Dm&?27Dl+1s+O=NfZ|o{9RL^ zDb-YH>S>zW^gz=SP0uyEn-6K;)V#C#(dLf}`NH9aiwoBkZY_Mj@JNf)Vr>bwoZfOl z%jGRMw7lIKXkFBLP3tYK`-;Y*x45u)P;o?rVFj?b)`M+C%LN+E=$9*}kp))b{h*FKz!u`_J27Xn(E!o%WAA z>>UFgYdVhV*wJx%#|0gicihl%N5}mgk9R!V@%zqrXJcnq=M|kdc7D{gxa-ocZ*;xf zH8iDe$_Z2MobtDBUw2>kuI`_9f6&w3v$N-#o;Of#(bv1Z_XoYtPHmj}^=ZjzXZC6O zcJ}?C@7caLrnBj(>C>lgn||f=$7bYbOq=n=8NcY4`Xl}G`#1HU-G6QWD>IWb=gr(U z^QxJT&U|U+e`Yyn;74<&eqTN%#O`&n7v^3mf6Qw{eN&yY|hzp-kh77d;Hw{ z=jrFw&P&f*F>l+v@6LO3zJLCv`4`W>fBwe{dKa9v;N67{3+FGqeBsjzf3xuA0pq~T zf!zaN9C&h3V$s!$9$WOz;+n;?79YR(k4vU6Ibq4uOCw9qUi$3PPnP+YbuT+(*|(Sd zZFy?>yyY90pS=9K;^S zwQBvUGge))>Yi2at+uZ&u3oeH($#mb{x$cJptIzQ&p7*u;#x=V`;rEF3jcodNcWZW z`uukW-Y$O?2P=J|t%LS5K%en{sEIR3|A)7D$Q%FvJv5g|j2`kbX+Yj8HI4hYPa4q0 z#6!McvdZ|ZzG`of2IwIt|2q%%0ou6%c#MAh0q{S-cNMP#I2n4ne1|lhh3J9(z9C7= z!pNoo&SBx9e~VYZ#Wl~dOEDC9$f(ex2m3SHd|n!$*XjrA#s=7%Qb@B9*F7uE)~*+? z^uGF`d0qMZ67c>1^YEZudNgT1Kp!~Ooj<6rBk_LJgS{vDCwjnU_k*VyxlS^U`}iMx z|KEBXE$K8nhxUzr$Q#giVd&%iKDJ0<`Ty1S-GNb6+5hLfH}j?@1PDchFhrz8z)6K* zOJ*{W4$?tI(ntbnriieviw%)=5tU`F$f}EFT{oZ$h;=R4P|;--JD_Vp*1oI-neXSE z_a+(8-}mtpQO=AFM8UIIM=2Dh` zQAiQ~7w$j#1;Cd_#XrlK1`s>Im0^Q%Jlw|y2H_5t{^)y#4b303(Y=OBwS_ zg+7j%aQ8>vPB~V9cH}WWqi#n7=x*Li{{Iy|rQ`W1kh=W;@g{9o>j!0}Wq>rKoPGu? z(avrL;FP_yC-wqlF zIN9h)UZL}&6ykfR8+hwZTd+1jIBt&bGkV4kzzxM%_;rI`{RvbI=+DC`7my;eGR6%8 zFMR_10&oki)!LOM?C02&;|Pc+<>in!Qsf%zP@ap&{sHYJECZQajl(WfrE#MhIHQ*&c46p=%gM@fJ;8p-` zDvtk(=&|eeaee%IxHG5#WsDV` zxdA(30-ZBBhC$4h)W9x-1-1(bcq!EDGWcx|4Cg z&c*y2zJ{;kKXNvqdwz`jO#PjWqcoUhtflRLK#@~3<`xqqeCU3`$C%r;OdqEg9eNkFnz$;1Fp|B zGOf%mncXsbWcJC-$UG@?Oy>N|suN%M(ZHS^ftwKS#~V%Sm7DeaSALrJ^H;dLrMJV) zc5t)XB+~*ngF3hgW(>?I$(W0kSY5`ljO)P7`j8Ro9_k(H8wx2mg-3BS4cx4DxakUR zj_cs265KGfrKOLdrJy}nR3>KU-{F5B-l#1Jtw{djUE(Q+dmT1^`sydN;3NO`9<<$i zUlDWf9eelgy=U)*drS95_NMOnanA>P4(?gB2Q73@${yGIhlt+)0PyMi@4o-Y`|F6_ znP6?udRvLe8+e4IVSHljHx3wwjKij328DtuhKU;LY2uZ)&ObB544IkcK$O%FGt10F zm^||%^8m~PrpWBS_-DRjlf_KfVSC*PZ14+s%XJ5CnM^Z!^Q-(8zs*na_xv366<^|i z;4QM(picM`$N6RSM`%-H{5%2rS*PP1zXWH_GiVlWATFbFT7;YVucQ_9J6cP(&|j&Y z{zjYWG2V?@f73XTcNwSCtF)g!!(F1^ae#x|m3!bTV}o#;TbQ$XG*9Q5Jd4laDqh6W zhA-zU_;>ue(VyRdZrQ*2MgD^S!(Z}2d=9e@zEW~L--A0?E!-;A*C;YF43oFn>w z27C#zm4Bp5=ytqmbsMe6_`RGfXfM5nnd4)6pZDg0W=Bs zZI0#sG!t^rY#z)*=xolSb1{pY!za-^%r^7+WUAyGjCT1{#d(}dH9V4j!;`3-N(P8O?(M$kMwT&*RfIN4lU_-`5pcje}s3} zGK~{>JHC6+4fi} z#uERf?LncmcZ-(QMxv$dY3fQ>S9{TvB^#}x$VRs(s&-FNZFP8Cuy%O+9B-s7u{U;uMWT6?4(pU9 zo2(){Eb>4~m!Llib~iEmqG{>fM@PX#yFZ!+OM0{l84#NXwl1&@Z|7lKP)Yh1ZHl#1 zk1^d(mZHc7>VER@c4OFsey`W%Mv;sMvuVh(%N0dA&5soMyLwHxn~@h+6s3byOF)*w z!vm*+B@1~pqwTI}*~0mez8m3;w1Z_US~9%d99GmF+Tw)zz%|!?zZXYzZZ;%#lU@An zEYFdsl7y4AFyYa}O|U`W(n%c7NV_4=D;Z9-*}xg?5I096k&_&|>DYsyyB#~h?n$!y zlI#Jo|E(1KaZzLi9(%$xNx$fw3P@fC&Ho5$+==|Eq z*7!>g$h4R!iM|_$p>&F-F4;H`w7`@lTjQ^9kpOPD+fIU`6}2dwaxm`J!t1M*YSJh!J?Mok|h7 zsK+c_i}@`PT6kFuN-Ova(NsLE1<;H+unPAug$N@VI1oD$&@lu!p{rt;H*X9*{vq?8h2 zJ7Y^3x8nJuF=w6(UMjTgn?P&9^Cb<*c_XOR@Slv_O-Sr{#<#=u3x9@C4bFhleGvarUjj}L%pTxs(v(N|{ zh8cVZhv_AHnSS7GdWCa17c+T2R(S<@6MZCBXrmy9jD{R?DxZc~4kZuGpfk9Li#Yw;T058L@p)+fBe{&rxq>UX3O7sdf`sr-$QRZ0I@fS5FN922$Mq@? zH9<0Jh9ppkd9{_>_yWv)r{F6ypJDcUn3wQU{uShj(;#1bq;lXgzJxF3%Tyj*&R6mZ zzKU1!Z!s_a4l-yhuY!EBnyqJEZF8_<6{KQy@LO0J(MtWbK#v72Zh~q9-olS0OFD2I+ej|C3*b)bIu*hTZ(8 zO6TuD3V4s-=RMey`6KV6U(-gKhI{3w<2|Ym_(PEoAOn63DfJM4!k_YfNOYgkEJzRk zh79oqb_Zrd*87T<(Ixyfq`z-)kHvrZApaL{YJLwn;z!7?Kk;Em^>eXq#P)|_(Ajhj z9l*MM8@`9GdW;^Y zXXrs)jclQ(Av+F(Sw5i6&+vC4Q4a_1_@rBSRj z-h!<9F1=|?GA3Ka<;_(WRC(GOYO`a-G24t3IA+)}a~w0zHj5Kx)NwoEi$^)`V#jpi z7w342>&q&d8yh^ujn$0}Rg3(^&9x2HWfg6$Ri5IQngL9{%@u9+3+k#a^hYZjTgxgc zsv24)rEqp>wrxi9tdfc{Bm|?mu}polV`1e3p_I>Xb~N9LX|}AG2DZ70IJt{D+(sSl zqE7CjdH&cDkIjxnoD4;ZJuxQ(F*^fJlF`7Kom>actZ1ySFSGgfpV^U?YfO1rb6`wo zKs$0yo-uZuH8!dIqfWU+M|s9Nj~i=0&KlcVTUS{X7}uFdaD39!oYEO-P0+HmCTPBc z6Ow|sCV(L)oMsEC5p55#;=tsV zy0VrUoBgSdVdrA%&vKDpY)#W3foVwvIPGWQ3LIt%m5Ffns8ZK-)SWfGqZx&>?J(h- zY$pQ+QENt0sl=Sz#|qptI&&Ly%Ba*SvsfbcPOf9Ilo?0lc7~JN8Fu}$@%AYG*~-C zVNTju^*L|ch`Pp#x=9mCd~?)x=8WRhIqH>gc29a-g4T6Tf~N$fi;gm-c?ox_%}~PW zco!Vy9+hyXMmwK5Wy1JWyX~s4I42+JYgc%Oubs(+(<)1m{z@Wq9WJK~s5{Nh|snNy8r&ahY3Iqa2HDZhb=&WiF^bX0*=+0o=MzUc!o zz9~cKw*^*}@^4iog^D=Yh&pUV9X6s)Hlq3ds*dz+P9qNIk%*_t;k4?A@y&j4PJUsa zx|7XdbyCOiS9hf8s*!Q7rZb@Zkeqz0R=M@nYVl8>I^On(I^`8DG-?-mY8?h@oe{5A z+q#4d=j4tIEbL4+xG3rQ!9~^0RaFgjWet_J6;_>=sa0nW1a(Q_Ty@}6Q_GGPS`BvD zHTc{D6iS@@#}cueP$f=9#_*y&K7Y07N_2pzcY!{SRZAoPjbMhP;>2B-HW6UWTdjijn z#S(dT@)|2mX*(j1ZHYWO17DjCd~No?x7g0X;?5lS7AFQ7Yq34>E$Q^M>#sE5T9Q2Q zIdxK6Xf4&`{7XApupOZ=H?3BE&ReJrRtCN$YLkI)iFzfR-IE@dpmkl6;3+|IMz;Kf zJJn_=;dH!59_21fxKk@SpD81q-FnqmoQ+5N+ST0QYiBayw5mt6w1l$*)mn3EPfP0B zO6Z#0zm?LZvy-daTyWk3fZd;R3w6p1=i1|7I5(n`S~zTvE--agg{ciFCp$l3=J_BC zR@GM5wAOS#zpb$q^H)Q4T~#96+FQfL+EQ}DVLMhhtf|4R?JXQG)+rV4JXfr(xltnJ zIFHQ9$=2ovQv>H@=jg1LlM_}GEaiL3nwuLJw>A0IY!+sGYOQQs++f@4supNYbF#B@ zeN`>37^zyTDwVt3uq{gEhDZ9ejiEo4`?Nm9=7iBUAI`NohiR8juERubxY(!7J1It9 zN=;+qqO$VF3#x<~L<=pis%u=_8N|*>7;jSPLvox#&xscLw9k$xt7}a;qG8Bh1Y_U9 zgVjxUeCsccbq^M-Sl19%2Ai-a(F4A7Ksy_29>$(gHE=Cf9*p$|w5o6db3br9Ry~X} zm`{P_)P?CA9!!iqg_D7^v17p4Etn2G3wz~^bC+KO{|5WzjFXo;fj3|+Y+%j!1?&g; zAoR5c8L|WB2c+XHs4vnq5elni*q{fCeE{3c1eb>F&I9Klj_@zw$2xx%86~9?rc-KN zS*z^pxIh;ze=!YUD~TfI;6_P!E1FuGD96T&s+t>Ul#T1lnio-#gBR5=qH*<#k(NA9 zQe=Sa^O;B&r?YTbk|NfPd33qi%^a0hkhZmJro*EbrXS&CZHYV})=nh#XWRnuGF)DO zA3Ju~-E{cvsik0GP5&ncL$CYg9@I<4%;6asV1*)$i8l2xmg|B1|g1hEt6N zaOY{7J!odfPjvsuc&i3>ikihbIbk|+7Epi-^XXh zKjyK3De(h54dzUk>!F)q!RG*c4#4Lid=7N@csQu>_Q7`_eD}e3H+=UY-acrzJw@sK zGT=>|uD`9h4kD)pU95T7iW4q3*5VZ3jr(NxNp_R};xET>Y1J?OW!N!r-^a)J;dE2~ zG9(q;FRQ=gcnIzMKsB%eH?fyM=pA#guW&Ygz3E*1jzdqGk39w{tK+q-`f6E)w5$ec zO-LN+Q)dH99M(9}o7Tc#%Gc01rp9r>)m5M7(h!_f0|rhyKSlmO!(M`gp7#}M;TxQK zd9b7B!|w8Eo=iU3V*{Rv9X1d4xh{j2%@uq#@XdTP>g87KSoyG<^9i0|AQ#d*W@Fbw z`WK-@Gw?&())10&*|9zjzCY*IK)p!pK2l2vN!WkH6`0U#5qlyHO&4^u|CBJs8Y891 zZn^0H2gwND9lYPSI%xMzJCt8T^}(K(T@rJUnv6)ZbVNE=N2CfJkuCvO_hWxUMx+fo zBK=uMr1y11`bb73K3+y7&Xf^}hscP8(^QN|*o(x7g#AN|NH|Nq##(N*0vZ5I0VP(s zV=lK&SNC#j9Ly=8+vaj>Cfw&(%YEsfN!%KM#1|0v(qOB#4CVotR|BrIZm`z*X8O+Y z&GhybucQ?*!Ng(0Y35!@niz}dWKjBY||To{cDfk zF^5>+!F?EhF7@|o_%zsi+Tqnb!jl)M^GxuR0!Di#cyIRgXeS4Y+tJ{ zjgHV6FESrQPn(ZT}-_+u?V2lE1{=4*qrk zUIXj~ya)Ku=M7x!^ZE>**SjC^rT3t?y@za?FV(l)cQqir1Ny)n@(uEh^SgZGd?kQk zfE?c_b)W9grfA!gI+1dhd`mi;gXaL20?K`xeeJ$`)x1van|(KcUIY6&&|&)XV87Eg zgI>@O*Y9`YOB#}voz7;zE7e zoC7Eiw)*p6HUO654VbA4MgCI%1pm~)hyDu!9|j5l17V--pXaZHxlr8xCh_xMI_LoWSKs_wv9&Kpo)A zK(o+-H8$Px|2gf#B!9$3{#yep0jmLP0k;5d59|nR18fJ}4Y)t>pt_%MXs-pQ1ZM`P z1a>DN@Lq5x>Slj1J!k+@gXw`U0S5zz)a|ut!9Ln9r9Pz0CEt=xM_UQjB-#q*ygc)3 z;9T=O;E`rM@H^&k;BDr!z+t5q#_>%p&Q{+%#ktVIlUNcy8#=NK*%|U1zo7INl^#|~ zSSgK~LaWft&xB%%1jNwOk`!hro~q#|X~^js=01F@kj=X_YRk`a^|+^9td1p88g3ta%!yLg@{PmkHhIFLcb*BAs5RaaY?| zeCcYnuTp%CZP)NuiJje=rcdoznr4>9TBjku*7VzqcaYBan*Oa4-rS|JYQ)!^FTPx% zDKsc<7Hss=68Mjnz#=VyCfiqHah=-FSKO$$R&iW;!r zwkzHtn2*ioey6mudPpX_(^__fq`r;qSpi4}ETTrSw$%Rs3nt)6_mt<963DxFrYlUTQx< z?fsMo(cWcxMeSdy{XZI}tAiz{Jv6m6;|+wAKjvl*x-TMW zV$Vc&YoM#aJQaYQ51s%R4JZPXI&Sfs4VVY0MCxk*Hv!fo4Ov@0|mxUBqS9|iH^+HMdEmqj_x=nFqf zN~6t_cLAqjH%avKPlb+7 z7Ia<27J(MWBxpm-MMy(R4Q~MfR9)kKS@hiF-JD)0@lX;VVfj$P>x zJvY91Zix0woID{!-(kl(#A(eesVMc?(D`X~=q~ICWkLh$G#ZcH zj%vcVuj#naPEJ4xo(}%!qE;?&=oa=G2SW4e44R0weJwP`?C>76PWk$35z6`;XxCln z(7o7o83Y~KV(3_%3!U&K4&8?xjKSF7il9ZzgD%!mhwjIo#Sq*I7p1eHFHui;dqepN zpv?|NeU?BUW4J${k`=22jgdqs_N>+w{dg2~C_}u^>}~euO}xqMZEOJSf_oT#lgz8ld(Ew` z-mdwsmH6#;edq4!Etlk%V-Vs60h&n7vvkED|j0;+I&VT-W`#5Nn&_X7j0_7~`aE+G)xDusnI7@*%XN&pkx0sn9vkmIppUXfH??Rf;O+3RkaejgD2)$YS5$XzcM;&(*?x(@)X4JWXI@u1q6?Jc*Zk_|)hBjcJ zj$QzM1iGOHYGMuWa{fK=mAn>s1^)r~Drk!u&?&tU__z3)lK~A($@7k+Jj-soU(3k_ zZA1gJJtQ@}*KgQmj`u-(b=28Fvq_3W9iKi*6}c?o^NjfG353%c zkea1&dT1!S&C971+BtM>Ye98MdqgXP6s2`1y`nFAGWvw}h@GPQs;&JtZOZ{|$CAl?W7JV2NYdDG)L4OM z+9yGTzMqCu9XY*8NqVkuC@Te*o}yO4CTA|4PeWOwrw@eeFJ&$7!X@{yjx(3~SUN4rpcW#}wRIB>QI8s4M-2Uq5aK>z>% diff --git a/examples/fonts/Asap/Asap-BoldItalic.ttf b/examples/fonts/Asap/Asap-BoldItalic.ttf deleted file mode 100644 index df290231315c2179246bd4c2bdf407afdd394264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66080 zcmb?^2Y_5v)&INi&CHwfruSZVW_z36o$X0>H@%XOLVy&KPz)tN0O>+Nsz3l~77#`B zLq$|n6al6ADPqH}h<>8@`6>Rb0kX6I-?{hA?CfSY*??kZXU@KT_ndRjJ@*L~;2TXy2}3zFn|X7Rf9BfrT#@suR}>lR6} z-LiUJTj!~txo<*}zIh+6-?rn7?R%W7(jSwg|HemCe#iNHQ>ODwS4z?kQXphb-gC+s zi$8wuh$MZtUXs|aPuYI%9w{Paaep&@?>go53s1hhUR#ad#U$ymZKv+qzSDlqi{RzS zX8gYAR1nlZSaxB&OYk{&>KS|A{n&LK$Km(j(GocQtR36G-S?q;CFv_4gg`f)vHjh9 z%cb>{XncD2b%JHgMGB+2!A&N_FmW#d2pPm*3b9mC(f=bT-8CPuI5k)-c~ zALf^g6ZoCnC_gSKl1Z{in&gxuZ(2#SbWklQOvS6(!~U_Xtg+Y1pSmS`OAmYQnVT}V zJR?6oJ|M* z@`bEPRqISFYw7IjG=5l#MVdQ|@xPU<`FeIzy-7A`)-|%vtjYWQ{_#sKW}D`}=psM! zelC)42!3v2LX4!`EM>sCB&l*q`hU_RRr)1Wdav}H^oV58um6kWlIo=e(o+0QzPqcZ zRO%^q5_4e7lm`W$f`LG>v#0V4Q&u{X7;LfGZudLp`#kAxcSCGAQ?O}fTQXBfyX-Ef zyLoA2`O?Ens_ZbS#(1b9YPFjix}1BX&7nH8)#>th>~>qEGd1pG+UH_L?*Z+X@Y<2x_SDwcDZ<5BO z6%{L~EORKv0Ns^($X^Ax+TgFSOh|ciZ~@QNx`6Vyy228R*!%%ghvE&y(uy^yrNW`| zn@4a1y;I!4a;s`S8CuXzhGH$w`hwFs?w-{VRQ*6j_DMfbtApjrGw=h=(n4wFOn%@U zDdAUBTtWG#Z&M3>Q{BOeqf!I5Mcvmw2anJz9V21)6uy2okMNFU{@IL9q5RvoDSQ9S zj$!57)WNIp4I|R=(vCU%2Gh)|z#-3J75wqY?jfMA&|*PbC}Qa{0u_Y4p@b)n zKf==3Kf8&S8s>yD5`F#eD`7jGb?3lnQ%2`z}F-Rz(vVwqL1gcQPPFnRU zlW1dDy1NE#+*iF%hul&_-&Lj0mtKGuB7C$|en0NtEOo(9u_bYbOhBXxjxtl=hHCQr z2#yphm`UDma0I$~yWDPT1u*qbsxIrvguFSW{AV3P?XbK3wm@5*uRBEesm6J@jpMV3>cfJ+!I$Zb{(FIbPRdan^)MPfgy#<@& z;Q!N+AxXAdbGOYa<`bduyX7y2T@Fvs$R(>^6R;~mFZj{<9 zsI{k<1}^lQvgx4yCL1KANEF%@bR1jCs%qBE_F4Qc_5+6|=gLoM%uH`yclNjySrFK;J;O z>?CliNOb_&J>b+Q#iX1hb(Xwh!y*Lh4{#)x^>zaJWV34JXPvS@_WPeZ`(2AlJqvDm z&rAQ)*0XN0YqYhrbFDik@9}%rF6rMIFxb|0pLQ;LprdZy`^zu4H7!Jt!KB8C1M+V9 z85%dBPbtTX1S1Gf_m!uRj2y7C6ANMnQalZ`3rLd_dTuZuC@Z7@{sSGzyO3U6iDJ|$P zR-_elNrAKrARXgckK^xBRh?J-0GADZ0K(}GLFk^HNelj<>lOO(8_z+${BLo1+UTW`jA6u-X0<)Yb+^H@&Md!7mr6m*|8 zI^I1>v?~i6n_XV^=v>h5UECsP){1gur0bnkbC~Hkw+PY%p0`ow(5m5nkm!VLv1+RKA!IAAAC|7I z@i&BQ1-(O#)zAsq3i^EMt9&o62eNIz{gGRof@^`6_>MKJH!)QZu3RUOt;bz{#y6`9 zEf$V&tH8Dtn#oUiV?lu1N%;u#34TYV4{?4QDRBvYqu9!77_4#YSp^>&N3dbB)$rwR z@2^}xBA@=_m@=4^lkx@R*0p@YtzN%vP5I3f?HUa_UTT1j1Ac{L3!Y}aF>76lDT33Pk!c3RpFER z91!0q>5DK&gnsf5(emG-LSt8?k_Y~sN{(NiN-|UV zEoe-Za+rs2>d@*URw7xM zL;SQpT>hJhIpl#vsG&TfkDX*sI$R5Oq>?-7-_~x@A=$(ud}l%SobM=|T^ z#M;?|pG-GC#61lcsg_LQ?3r4 z%{Ifwx2BQ_j{}>XS)?;NV`-kw^f-)WgWce&&ZXkv83?;wH6N0sJotKbqS?gj69?FC z%(IurlE8?>Peo!=*6lOIk}b_9ZnmAab5A!nZMSrk*jH1@a?_5pFa!J!5AhjZ&n;p= zx`}Hv3;W!JJBw(H-x5wHsK4>4$Tji>gaB1hX%!7ci2Y62DowS2$@f51rm1&#r6G~qu@`$M_-18@H?;H1d(XfFO$EFzje`FB)=lU1+iBY ziz(qbdE8b}9+Ug-M!-=1n~&d_y}k&l+otQ1ueB%m{g2m&x^%}`T(9zJBm?J_+ccpj zT4QUt?GpV*;3LDcgo->S{eiE6+=PqER-qT!F@mnktkc=E()URxkf&Mxwfq}+f;eS% zkZ%-;Wf(o)>yuy6jX{s3E21Hapq)bAk+LYu@+q@v$@oK4)yMWVTg{5s?)4%*8H~l^ zedc2q%PT#;C3*Tc{`p${zT^lu7mU83oKAV{2qutRykIy|g5|c@<1> zs1nnDn2@R}MfOGPb>&a_TrscJWO2KEWH5dH6KzI(I5GC?eKeO257TTWpB#-^Z7h>Y zKGRJfVwUdm0gT#!yh;nS+k#QsWYxrNlH&o`O3)h=tAeDbTzj{-RG@qk>VtdiJ3R(w z$SZ^KB$;q`hxQ(){|Qf97yk%i-e|Pi?r=ro4|;!5&pgTUZ7^flaviuOZfew(%5Q_$t$zZ8#Djx+UUr@1_D(j~yl(anVwU~_A3OjjF zp7NUoMXVv4tNih7m_-`d3Ji;}Z7#Mo7X+dF*%W!z?8E^>Kex1#xW%lOzsoI(bHy!_ z=#=Tnamy5R+P4M0Pv5s`HIfv9POI^Sn)}ecEv_GuUekR>GrztAxU9zRN|QoXCL}Oj z8Kx||r)qbm7b^nuDVU~|6|l#O-4)q|6~XzGOjAue3=%6QJJGCsi?5iXs7M~VVh&-5 zS7as`ua!qkCtpX(s_HsA_}^qb(|84C>I81C3OVEAdSoUkrcUq=4q0-1#$j*Rohmaj zD*XmCB6Fnn2y$u~<#Tve4Xchj?n;4zQodiiefj;D-f-Jj9^Q5Gl0}<#Em>r~6U-Mtk0Lo1S~|A>+RRLW}Q4)JE|POq0${i;D1;tMcls{ zbu5X&(rA}L>h^FIZqw{ltXainv1-U44a`#Ao~?Yfi+Bf2cUOUQ&6Q1%U}g^$ z>r9#2JYAZ_ovzps1kbdRN)nYG^Pv8y(7z}OQm6iqAcoWX~Ehx@R1b05~g&egf@ zk_C4}pDJbT^|exdhes&lK0-=`ua)ocIAN)T&8K2RM~W0GPHk@LV9JrBi6vwvhf~>M zgN#3Nmp4a!*6s=OSgOVfC{hzvFkhO`tzap)raB2IA!ky9)tn6NON0_h6(#*A_w$n8 zT|--y{K$3tWIZpVia^fk)FL;CHBmmOZ(y(gFv>(;MlVWwJA+L3;^kje_cUMw@5DdA z&r8U?*HcA~X9Y6o#Nd3CO8UG6b<u?u2)ai%D>MplB1Wt@bmsoEu&ej{iCO0gKcQxA6m)}`B`gl|J4ug zzp67={(3X5F3uOX(<`{0R%l~=J!Tckc*Pq-VHQm-0JzN3O-clV@n zgV;&awfa?lM%gD(Sa5r*l?9(`)y`{f+VJ7ica0i+K6d8#Z*@h>k9+<0x(3u6kex!o z!M4WXF*Lvbeb+p-qt)jbf3m7-S;_ztk01-?mC%pKQ8Dj-!FnhD@XxE)m(;-sWx{^8 zVc+I+I?fI$gPA`R`dd@myaa#G_j^~q>y)z#ucg?HEq(Rrhk(v{N$B2Z=2cWMsYQ>ZKBKL^uDr_Jb1^fxvPwiRQJ z(!9G)?_TS72a+~}+f-=kYiujV2N&Odvf1x*`A3^Ie>mG1&jd6zx@zHtX9V32hrKDS zD$YVET^F!NSB$=Q4@u5Bf#)4DnlBA$8z(yZy`c+bd* z1-n-{r?YAPQez|K3L3Qc-?Mag5yX9~M#j!xYL3)(v;^HlJuA$^VNJQuv|?%bT%=>K z+xG#xrVNHO<;&5veM?X8rwmc=#GCTj;46*HFl@2n^Rcq@F2?Sjo)YJWTOvO2c7#Cj z#WNaKUO2jXxw2|m=dq0u#ehVD;<$0q{`YOV_4pMlohKc?dZ1_;Ik9!!8AbKD(L_Kq zT8u-swx(P5-TCbC=UsC8ijE$bKjR*35Nkbv@kJJzY`;>+AdTGLcdtY9dDzb>$#oxs zM@sm{yna`ziORg=ucm76gE}?e1NasIrmKl=u4vTH=_emTZm0WsPL4POyW*tD_)cWC zcX;&C$iG|GK1A8<*P%WS^4JgYL1~?k-yP(h;0}yO_450%$CEV8GD$=GDbD~Q&3@Q5 zQ6sY8r%!3KA8PaQLPZIJt?^f*#`Fx9Jz|LHsu5heI5!!IxFjpx&aE_qDiv{Ap?5MP ze$S+CSn0>Ho1}DGn9SIS_Nd9vrtDuX7?GQlp-eK|#Kua4>y6FIP&yfI(H^|c+`V(m z;$xqpl;;+c*4;nW&Hjr%tT^{_=#G!VeGIzmYjQDyPO-G0S7K>Fr!^?(Jj$%zhfuqq zS5SL5q$4Q=o$%}?F7@dWGMZu!iG4PdvTQp7x;) zXY1VbZP!xiP{@d;{jqv9?ax)+6^pqKl|+6LK>!Nw#6_4<$$}lV$`U3OnWDaui3o zF#e%hyNdpVC+&}it!Af9R^8s-WL-v0I-Guosu~?e**_;?J(|^Kt#sRKYHhduyt#>7 z^>LJG5WcArV+x-|F{Yr;=P@Svtfk!d^}=R(y+fTn$ehiW$?prdSTEQyJ=C=NXL4t0 z`3Z9DTQBq+5_Hb!*QzpoxXSy8UPBi$QG6$467e0A3fOC}fIp|2II0F@$-J&ntaxId z%COBpZM@Bpi>5o&&8ormC9N|VP8rItS~|L9zkGpDaclANnp_=wIBs#8&EuE)vEQMx zsoaG-1C`^>2x_VY6CR&cYbS`#$^FCs<0|p?V)-~ZmWvD;8eARc8DimlILey69eb>U zwn5YH3@QK8V4O8+sk05qz!GrqZm-LgEI)C}Eo_Bmv1sKl+<7M(NrnyX@qJbV&1~_; z&E?0fHrlm0SA2i`EM}2v6&G{NDd@Bh3i<$BT|=jRP|ydNsJczR&oIjpMw$2x^huRd zj`(OltLPNr1z|m&Caiqjb^WqbWDQ##oYikY_FV2Otd(UkRf@7U*fo5@9yg&Z2*Jp8+A1{AD146$LX-?1wrH||MF3z2x6M}Ga1Tp>o!_tK{_aOu!?$aULYv^Rr zfi@1k8_MFRYoPUqIKi&#SOY%V=@-yV?-J_13|CEu7XZF5cDDG6P$Mj6h%A= zI_UtNJ={7E$iLR5r zUQtrz8Ym?LuZvT%wxCQkC4<7ae@fWs=CG$QB&y5!<|(uklB zN}trP?~?P7NYE#(V*34urAun=PgWuB(;>TR=wuawJ}T|!blwHd`4pYW3t1zlPp@4r zWTIbtdQGZApB}?6lpg2znQj>iQH7^USqS>^$#lLA{T--VBNYj#ThJ#+1f#<;Ez8P0 z74s#Lkif=UlHH1G=t*q#1(q}}+tj;jL4GJ;vWCK1za{2yS=`E3Ey--LG}cgOKi8mo z8^X~QnWnQ(-FoXWDZk%s32!PNw7Q+9-#0{eZ9T33dYUo8(aX@#_c3D%Z0t#HdxHM$ zD!o^_eUh$Rq0_T6r;{y8Kaf7k@7DzhD}R@TEzA7ANBa9==_38_p&{kgL+;-pJEV`I zO2qk6HbH~)q?4s{=8R3Io)8427Zpkz^*HKT_)N8`GV7Vawbe6&>5NCw&&>W|&XcDY zWAEkA<6`ZAw_oT~RM>W^fAui^6K)MZcBXqh=*@}p#6=-ieXd=H{%i^${Z_#4AqRWrckjL~`$gRgl<@0AgUdB$EyVAB+0;NjeRQ}bWhs?^QIVh`Y z#}!>o+HtMUe9)N(UiVmO8+4XGG;aA%s*HVQYE)DH#yi%S{M59N=c>1>3KTj#tM2{qz?A9%6jqr>Dauwy;t$cB%uDHfK+iq@DR3jcAz}v%LukP%X22vCo zjf-a5-pRqG9PWglPD4I^$X@^Fs(d5T`zPgAKB&{PGNPtUoaoP*nwqdWq5$`*p<6T`t8Pi(Z3lz(uUYZ2^c+*5tn-niAP>e%h`;hl$bHEdn|fI8HCo zIpsi+eR9XpnTtnzOtv6^+A`kl7!3~RG9!(x^>q#{k(Iq1qjmWJ(d4h9$MKD5qP%(6 z)}CVTzD)?*n3P5p>nG4P&av_SDk@rtkx8dIq8E^cw7!4ORk!z?!n_fyLGxteSGBa~ z2cj;cL2>&|Xf&U;d;b@cd23x5X3p2JB;m*`=Dq@~z{O#6@jSIqE5uhOdGxGloawCMaT}%x#XdaAdoZNTcvR7L z>Xu1SX;!DpF`%yiK_|Z=lTQLMY)<7HYo0*ZTAI-2-dnE!}snx%Z*7PrvKqhG^Ctbh?n= zbDA}?RZ$FKby`jjTFLO4l;L{oHZZAbYk+h*jdHu|5} zl`MZeVGd;D<urYCTgtC+eRFxb`0NXLC<+bm)c`pS_Ntr;PFkY8 zCgiW7X9XR;qfbUm1wD|J{R^u&iKo`g<}dYyQR^1jzh?4<7o2y`g|$v^y0_bV>PJ5~ z>D+YN|CdZ_`v69#Nbj4~wf!$1u?c!F&J$JE%D?z|QbF&Vg+9P@hz62QK247zR!N&M z#?eI)30TC;dkEKwNr#X)fV*;z!Nc<#Q?qH#BzB5+Wllju0~9%L>SjXfeN}86T(@1A zS@MshD~c9qh83LPr2MMTjX{qVR??{hM!La*t%(^91B&OY8unMzJ5rN@Lixut!{E7S zMxr9fWPngUP>b8d!&?ro7hUd!dQ`B1M@b~N@KjXnrjy>)2O9k+6d>D=A{dy97UmoT z%taUMUpo5VPJLB#tg0La{TmLmUZz5;FC95&F&pY1m~9Q!dY_{N{BIn(m}c-*v>yxK zGdRWfOwxy>8+Bhr>(5)gp0B?{d>-&W@4OLynDD>2dxvbO@py#)1${m;PQq_5<##Vp zeR2x^*Daaj*qNM{TAT#`CUlB%gJ;iGpugFvd+|e5Y+uXtqR`j7ZS90>YhXXqeft!b z)Z`UaA(;&o$uCuhC{ehJho7vTrC|{ex-kUNx%b;iyux2#F|fbPGUjYW2xxV`^8+Fu1n$dodT z?-jkMh)mM4m*{k=pisOeu0M#=X+6>?C}O?>ID|wt20e*GR|E*>C>2v$20bo3u*Ph6 zW|H2Y9EFVYnv-L#Hk+yiTI&44&ZChSt5=t7h|o7IA^_wN2A;yqP{$V1jL4tmOw1JXa`U*e1(dOtliC;Vi7j=NIxMJOjCn~KWMPMMATSl`m2Aw`xMRfuA7Q^Z)K zZF#xJx2VqJU6ddH^w3C?Bcib&G~V<2z@(~~&VgM@a9;pGFzc9`XB`RiI`i!zIV5B~ zgn0?MOR`+5rxj)cwUZF9PVF5hstRg|Fq}>uc#Eh8P^s&;LLKDS3e8PYhyIVQ_2K+6 z=`(}pkP`aql-|Qvw8r&3$;I&Z=xXB4BV!lh<0*0n+ljw2fZi+{_&Ktm)FYm0H+hl_ z>jME1#Y1QVKKv|I5(VVmY7UdJYpaLJ%1_T>A`gF-Og`c)zcABDGCyQ*rc9fz>-mbX z$EB>5+au)@Y9#ewkAlGD+9-`FlnQ=Rd(C%o*9kRs2|L(vDmehe*TW2 zVQ6>;>qTTn~z2)oDDj@1`&!@U8TQ{CC}}qU%H#5?kuD&2vMyJR}FB?J+MKW)3eZ z$ReF$?(d5@&2An4j%sr`^@IuJB=o!iPtVKn7?&!)OQdIY`Ufy-g+3;ITI%9- z7w1OMNiV=V`t|RcydEdd@6@kvmUC6Q!m@lEhO;Wu2Fzwl;sJ%r;{lWg*t!V!zRJW(S$spJbd8m@-J78@Jl zy7`UE1brhPqznLCqDx~6UZ`X&1d;(aZ~;23!H4cji#}$jGe|Go%jq+u^y;9&VTr`T z&F&;7KAw=%ax~snKx+5Lnq$2OfB2ybqw0-h>#2wbw>5)P+B9P+Sbp-6xE zNvs_<&>zXFXenHfY+0FXtwJEkBCwd3&iB;Ff)nNBb9uBr3CyNQP|JK3@yN2-U}BM2 zw8cd{$KqLqSWe234b6vg8!`Kq)(tzt;as@<4Px_zc!0AxnwsPhmLqti{gkjgWwOvu z17GkK$C2NcGIBT4)-wIJdNu9q;_5!CdULu9s0cBmPA{@|*IZ9~ySRRco+83E)68DS z_2{yhvbA!*UInYubV6~)TUA%qq;ys_kulV0qOIn%IeVbKBL9-s@TC0Zew;Jsd^aog z@YFhgEQ=>}PRfaBkMoTx&Be5*Vzr`P;RVKnudVP+;X)<(f(IVneC8GVcb|Fdd*%;N z;&-)q&+Z!@-E;a~51vQ4-CbMHpu3Cvkj|;2xTq+Z^J|NwSzZ03T zt$LQQUz}H_O}$b#z^aDEj&x#~FO?}Gup6f}S(nQ*XTFkCWtE?*Gz(y4-eo;Q4Sf9Z znKjY{%~mALitI2nQ!A;a=luG_`SSwuePX+-bT-dk*{$rm)8>jjHksX{=H}Vj01lCJ zGLy%1+t-TCrE z)08vJpKghoo{w8e`RWRK3j_gn2xZTV-b_b5XP`kn1;cwQ+m!rlZ&Jz-LBz;afv~lR+Ywm-Z zi!J`9%8`wMo|dT|$)VL8)F^bBdQLjjL#^Sl9;)D`H6qZ9yyJwc*C?))i|(9t5o;3 z)wS%DX|twN)!ix%ReNrI&&qCocEDk`29j6oU)CP7+01r)3g7OEcMt3v_^3C{ZrXU) zgGo9+f&Xmbt~ED28jazT9lho8PCNF;U0dv5^Fo7yAKD|==oa{BjRt9FCJl<6SwXM; z0cAPXTLEWN=Yh_n!~pDx*sbZGsAw=qw99W*Rd!v3 z#@z>$8AarJ0Y*(ix3u+V=;kv+ZgxT3W(YWgtsa4`1)_`_f~CCdZ4HlEqM`Cr1f!Se z`ka?;fR&07$V%zyXUK^yAb&2Xgk}V_F90h3=i=N5Dq=VNX<|{Fy(KOy^iJsp+>1vK z>`Ks^1lA;j6|24qd<0M+sSxO3r=`!?3wH#Ry5>kaooX^S7th?(bAmT)F*IlTl8u(; zu5(T>N3xr}&QQ=Fa4DYNzKvVsDQhn7s|yC*25+frGwOk<&gUXrNgN!hSLMipu=!nU7tL^*G}5|mcG+02)U{%}Gd08M(g8V^oK@x- z2K5W?z%{!C0DLb5pR_`k;o0tRwtssYv$t6AdM)B zLdF%rm(7>2uh=P-xUg3gi*UW90sWZ{C_PP0d|xsaGNKHX_u(;|2xhGV zR#P$>N9(;kP)VFD7<2r#!Rfpm;wj>o5lIkH$vO7koU~o#67x4KmswIrn@(N2K&3sz^D^~U{ zSO|J}a2}uwe0#BCNMn@coT`<>t{sXMX4dT=C)L&wpP}C?v2+GqvpcHl3`OLbdcMZO zJElzl*?$3kjJOw4(Ygrnby0vHtEG41{07+`a`Unm`N`U@uL5n*Ko!*_D7m)EC!ily zuad|vp1IlXjc;h#$MA?fSV~5Y;oBfwqC(}DjWUuiNVe&Dnl+i$%RLr$JWQtiSSX$h zvW0m2tiL5Vg83r;1wZ7o(Kk?oZ3Ug8Z$Ym_--1rwT+n$;TD=cpe?hNc|2R${k-Qap zrGIwnxD|{nsD%B+T?9rJTH7MuhVO2Y*rYzlqd+~t2(v42dUU{Esp?21Bxlj)>=|BZEzf!%l8NxXGdbRvmX-MN(TgR|o1UR4|L@TX z$|dcBq%=1;qx=qMN9N@x_$Ro_w=+|3RF8`v^_-2rP;+j)GJ78Jw75gEEAMdfSbl1% z<({`QdDGG^Z*%U*N?!*O-Hg>Qf|?4Bnv#rphXP9zl?+VHdGlYq4XHm~&BoMZXv+JJ z)@&>5ld5ajjpi70zeD4mLmbW)ybYZkrY=|h_EBm^_v9j47e2TWtqb2w(Ym0EXq|lX zQh7BzJofJ?9ZcvOp4+x2+&CP0rK@3irLMZkedL{vjxy9ud(yqPBWPTH*sYH|dBP$3 znyR}O&M)Y)jp(O8gv=CQlguK%7In8jLt?^@{46`59kMj|uZYr+0D3+iS>0qjQ=XUZ zFxE5GfYxT$DoaNgC)&Sf=}5_b_5)1``yVs)H@Md2AM=}?W=px1PPFHNonOQKQ6=E_ z=Xb<8_M(mns&8rk6KC4nc^YE^bat%p9M{}a+}rWf1^}-!)k+$1XqL< zE*D5aKT9m=ga`zEfZ^BG>j@DE`XIwEs`SbG3`;NJ57@--5TE`5l9bL3w`RHh=NY~B)TjQoK0F2RMMuP4xTefC3Ge3(aEmjI-YuOpsR(GR};Dt zS9h?V)m%+hF77>w&45WNUVbXu=xKN2d^?XDx+=%pDVm`OI~{NunDKGME^M2{X(+$- zU5DoJmETt+dcWJNtfF`2=MaTu8i)&VUy}QMy4)g{CFmr#p!Z4FbM6G4BqzEo4_(Ob z0(?!}Rj%%o{=%=O9+#CoR;Rw44E5zmS(alKF&3MLpW@rFT$o*Yk{e$^JTT&EFoNEd zRE)mfaE18tE&}MX%h>3+bnmKn;bgngT*~w>RSmIqt)mN+j4~8UM&*Q|6m52fi^xU@cZm5e+c>(W}^Yu$90a^BfQkV(TR(+F$s4;5piD zaF(iwNVhIMdd`J~x{o+ChsPYYDAwU!1M?f(U6}=|7hQJDiEa5rw!JRtEyYr?!B}eN zOBXkfc@36~FW%uUY+S$B?D806ZL(}|HuSD(h`a2nY|=^ty&Kxt>-CNCM14~%QGRbM z>~I^6MoZd-OqnhdMOXK6&GbqiAx-LZnr}fLkm#pDr&EXK7@o#|B4{<;nea<+j?}gJ zpVAAvuG+fUf=15r)o({n^1*5sC$)4QZ2ynx?TM;>hNN4d3!c%DKZVZUh4?g?+nm-uKD&@$uG&RlSz=C#4xf;cN~`UFV^OrYH}gr zC{H`h$bB^tg*A+v+5x2G4v3SiIXx~%95)xZIBJ^n2b1xkk>dZ&$h^mX_MS_T17Sa{^cj*5A}NL;MTXg6NeXLVWKpuz z${uCC{0WQ5+n~t~&p@HgB8XQq9()CY>f`TtG6EeC^W;2l$|IMvu|c*BwS;3XqvlmJ z2HCEv=6IxFNw&0ktY)J<7z`U_m!Ca4)Mg7TSO7O#0#snLSZ z?8lZSOD*g%mIg023B91i3voh5PnB~jLivh2OQVardzQ>k#S4*mJ$tODd-TLo_lkA> zoozjx?cH=2A6`BpKZl>lS4sR^=YhcSd|i@_e-*u|q!spa%VS0p^Sj5NOd1s%YjGro z`d(xkE|Z^&j{n51S$o}{sMZ$r%MH)$hZj0gR{VF^Y@c_`l0}<$b+p>tfq&a=UT?T)SakeH@l~6* z-LUP$B@ZCR328adVes5>$@=5of8C1XoCcSi>A_PhGDB{*`l3rdhi!&QZd6?w1-_*I zBR*5q5?R}}XTnLb$=q8IPL?JRUnwgWl=>1+6?u5tO%!8{@t#WV4GuG+RZ?$#CFF=c zjs-7&5HFqO*UcSz>do}pY4}PI6Y&!P%0C+jhI)Yr$b%6c0ugJJ4(x%CLXJrA2Pc4n zQ1w7lT0Y6CI^t~}2TLcTy-MOl=lqmoz#dD>EHmtAXg0dF)yJ~;%Shrz1Nl$f5Ds`1 zr+ck6WMs*$znT}fTNIPL><7nd`A`7>Po`7J>>y^%j$DA~=F!c{lpU5uQIPB6s@+R2 zJ7)XtC6}*h>PctPNqRBgdiB-Mz3cY(UiIv0=dHZ+)MYDH-F4bhN(M1Jf}uWq|OWc7{PBLllBxnI77vi9ujzJW+cX?0r0ze5Kw za7R+zU%)3|oj|1(bAqC{vm~~;*$MAYHXU>;Ea8u~77{H1kI9!LU&4JccZKadeK87Mf4ny&8D`UUJxL7=uUMDgP`u121 z%VPo^WmFgutFl_~xa%y3)zH{m*EzU5?$8R}?!mfTy4CM6J7Vu%yJ_cOXGh0^a~4&f z`?}ukl~;E+rE+$wxfrM`tXtfFoX_A})gzwyN>~Bk$@TR z`c_fck5VcXRMr~N%5U0S3fq9T(`Yi%Q~(=!JWMv0c9{+Tg%Zl1!H83#_D{gmU4-2C zq?qwTOW5xrKfaTNy@n@AiL*6)u?qR~Tmg6l)a3JC^-ai>z3ya^d9bN8uV?TKud`_I z2N*_`>%J9^>^XnkZUeyPCVZ7|(gvvU~Z#nF89lKF= zDCMu45}`)fWH;O7ESYX8>@<&m&4#c;;im%r4ciH$Q>vr88CzY-s6gJE6Gu_~X;rNU}Abi?qZ#2gjQ7=9AYfI6LU?Z!B#eY*^g9 zU{$DTZ)x`u_U*2CG}Ta-$T`bji?k)0TSG0I3N2Xg^%DnW2|Pvg@Awf8hSpF;$vR7= z8t}Yvs!n^kqQ6O6t%rII({w5n&938WCH>MmTE*&^6*KC>-i2>fnsf$G>W%3&PStk1 z>a3TuE>`Ddz4n~LDMu{E@_W7b+eX&N#_$aDoW2LcCF-6@EPZ{&!Tdz=tJ@|&puzun zB0)uSFK_5skZjy>os(I$msw*5jmMw2ryah4)fv{J@xI1nZ##}*`GXj#g{5$c;8}_f zD?4muu}@j+Q^xg^#*Q?r$ZPP%<& z!3j%R*@Ilq;gCUduV^SONHp!b!6o~DJAD$%zYuKdJZGI|`B6vOi8*kE?KB6e{`S-T z3La9YNGm4ZkRR0LKws36<)}Q9d#*Dz#oV?}lj-^+P2)84oF2S~qtjBULpVg@BID*m27VCnn(}G-y&HGoZ z;RW8ik8fn}Mc-!&WVnZWjrr0E^t7|1^&N*SP1@MGkC{!xBYB(Tv`M{lcBzlxm&&h9 znbO&`V8l$mB|o9hWG~K>!XM3PE@@BX=%4m?J&yx>r#Om21^$!u|q6kA7E!-|AeceS%$e0^OP8dC#8*ue?cJa2$h62zUu;DY`5Pwe~H)a zFj$van9Jp|SyovUtJ7v*-C+u$^RIJE)7S^R7OQP_$mFtzHjYyk=0_Wg3-e-Tqht& zdP1uFzREl7i+`HfCw&>2PLGbuhUH4J1>6`TW+fUlDns6QbZB+(QmfVKQhV04HwQfR z=d4E_kr4A3{+hTFD7J>L&MACN{S*jVl%Z4-{0*fZndQCgbPI;3|0EQfL}ep_d2-UT zVxdFc=K&k1fIDlNGV%$YW_$@>|6tbOkJ}d1sOM#TedK+qxc9(aZs#jioyd|lv687= z*QiEK(=`vYDggn8y1eFox7G!6G)96CIH~aQU&d`8-TVQ!)8Q#^L-;079E9E?@@J5D zJXY5`ToOk?+%=KaSGt>E?06PIbyDa9xnvAI`34_&q=N1picP%uQ6IGBn!>qxX*^xW z85nD@6zxH;P4QZ{OukLy3m&@Djd&eyv2@x)9;?r_h2H%wy(KMcR&(aUw7mRGmq(7Z z-juZ2jCIy|x#}Bq$+VtHFZU#Enwp;9e@~JX&O8$~1*?$m<5oeqyv8DGXT3VRG@FB% z&2M7m=jchRd_{2V#M3hK9JVtxiec9qIr=>gcElp-M z6b=s8*w@Q;SM##w#`bVKV!(JoYiL}zv~_W3BG(vm*bb>b2Bu;9%!1*~PW3r*|{ zXJ@G1SiaTS5nAmu*yQggoJQ+-81o6XIQ8hs1Hp?7q)A;AWdY-$x23Vi&Mvjt#+#cvuGq#X`f^R2 z&pL4;8o4=GJ7qDk!4_b%$HH!J;rddue}UupL?&amI-U5QTbrY!^Alb3{F8|fD#+%xd zTIU(lB5^s6gTZX#Rq&>Qw;rnTaC;$k2orENDFmuo5l@r_@$e$rU%@5*WwX&b7BIMt zEfh(qUKYs}>WNpE)8Mg8GJAboo|hBjr`L+{=jpsAGa8<#tKlG|)lqB*v(Q*T*BCNb zq%kk4NvH@TB7-H|Ti(@W4}hD?F-gVcU3k6hdIuWsUc2U7es6+>>c_hha&ieJ6oYR zky`);Am`(O9dHCixy*p5mEUVEcD-u{9u{1uu3weTD~-PIxz>Mk_Z=*Mu`A}XYFC#h zu3Pt^6U-(LpNW?z+NHPjdsft+uizVUAozabtejKVuSgXPO+Kf!*uDL9yX$+nmF>E> zZa((TEmngkOt&Q$7i&esJSUE;n16VbK<{ zQ(Vsu%yK<@4A++zPq{vV>+5Q-uTG3s_W~||;cZ_nW2Rc!Pw~9D0G&oDL3+v@b8XO) zy zbXgrY-s8COMzf8mZkJAlN1eh4PQ_pj_(nR3JOln7S`UZ*g1M_4(CEa{^HV6`=gsQy z`q(bFr-r8+ZDwK!a}$O>+*}`05A;x~N?UEsxX*T$olUuatMhJ;b5g(?NoJboAnxVR z??skYD~ph17+Ra6u~x1kogMH@mWt>T4Sd8;DNzgy~N?NM*mUTFoJQ z-HJYeQX$9hu}gg(c!cZ0sJgF4E(*D4JPTTZ3#zh2JkeIqVTbO%0RV9R zfzqOxf+-O6n*8jFXdu;Ix4*qIAB%X%a}& zB!@&%sei9 z`=3d6u@8pp7j>O`j+Ku7WYroGgkqtj*%odWx;n)>YEpDAX0_- zJZ<>&k^EcMkByB}R6MZ@_D*{yKU9Ingq@nlf`@kHOg22TD;G(uG4`V` zevL0w@3&ZvLpi=#txp=8@XW5{$oS(_lv^0zw0y+|zSbummUm+}9>!>7i+uOxhL{1M z;)`k?nnXGSYE7{L7@y}P=vWplJ}PU;tKG;{8}C^guo(C*OQ6Q}}yDi!^VW z2TdvUgN@ryzN0?xXrm(6$Jo^lxBa}U!YvurjMK`U-fJ)V@{b~F3h$H5o$^9e9> zKdipT>o;tKb|%2|E0|Bri&`1T7&(69^NEj$v?u^m@fIsdmR;^( zRB>d{vz0Xmet()fXjZb})()fP-BuGzMmFQX=&!>v^Eu1!_XJFK&24w3BG*zyYT1ho z?z*tKv2C@*CZ~r7k8et6!eRJ5s%Fx6!Sq9uG+oHRW7x`1yeBVJ(N^`102SmDz&b9L zkF$M#T|~)Trd~m>ZvI85?rZ7j$<-=;D49*~sr@b)`6I43LWZ_^A!F+U2o)Z%+12KG zHoK|ijp`T6fVV|#j@b_Q-|K+iFNGX3`{9e+pJT%?qfaiyEu?CPq{^yDx+NCbF+SJb z$kK%FcCkpr5%sz5#CH+i$v?24Ib8R+z46AoD4lbc%d_+zmxJ^seF{C8LBjAjJ6f%w zr6E2;Q=O72k9{g*9CLY0qqsUK*<=g*Ih`(|$^dVL)Ey{%j({a%6Xghms};(HlmFP& zYSb8k3%cr~*nE9PukE8(af(&l_c4nV6!r;+X1@J4i|Ps)gO=NFvjmL+ht+!9?W);b zrNBeRG1GoLQ^k(XK%B&$8cCUWD8b#xY_92+=gs_)7(BFIm80x z#UJC8WSD--hMn%kn99YeTU{PYs6Jx}7d}HD=^u8Qn6yFmu=9~ip}4X#n%;_79bNxC zK!beDf!Fd0>6-V<#_!Ev9-Vwk^Bt2+xc@iw?gj1)`J{_?H0iXpaYN>bkIK8WFks9{ z5$#`Y-{t*}(710k`Qj~JbJKkKaD6{!6dalAIkCQ*ozDkf#$S6P7L%=~hR_+Y8f~56 z$t}U3uwN6#?-5I=R9DCS4j$AFJPzJewDek36eHWquOVlMN2Bc5G`FX`Ow;V@V#nUS z);e{*@&))wlRu|^o+Qn|mO|`hx+{4)+SKaE)nVnTMkD=zK@Dn}$IZTI<~J{YkA=d9 zfYI~~+{f(BtDl!OyKyP*Zp-2B9HUCe6^$UnhD4y?m2OMSLR&T5nrdym4|jJ5IP01N z?It?w1uOwHNAvSomzL;j4X7?D6Aj;Va|G_>YpD=^(C_6LJR=5#JzDiode#@>yR&yP# z3GA16YP{|YXzwQ2(uw>A`B}cp(af6Wc-?rjJA^Nqu^KHFtKDEPH$`Es285sDCs$`Nzi(`&+WvRnoU0=?LX-bBK25A}L!ncnH7wLrE`InVFF{j9n)! zkz?#ciVJdZYv`2H-8DRKmVM6UEZ;;<`!jxLK&kgFak{UEg?RnnNx^5fN@vM`U{7JK z<>=&J4@CyF)`EI;0bdFHh*#>CbZ_kJYVB!0rV$m&Nzi}96<;UB2H_#H;Wn=l^<=VEBf3=_r))?KMeRz+ zk;_|b7R>^}LoI7<%Z_V*;)a_p+ZTvN+s(^2?Y`~d&6n+?p4K<9I;bB^W}o8S!j;Z5 znLUKn!lcfLyA%akEc(7I3|)iqYFjmtGLZkR<&PazhC=D!=!^Q#)xa?Fe)>Ay&EuD) zoNoC_{Raq;^Dh(k$eVF*LX))qvuVGGIdpu>*CG4_eRmzno|ac>`SO3?W3v5-Eyu0P zet196TVJ*qa59a(MB}jIls{XoPRAPtuV3EebXhvXEW;v>K!N9D0uwvs2bESD5j7c1 zE02JLVzK(@hzN$`U`L!P__eyT%j7lLVy!u?!{j%W%w}d(-0IJ*o$*Fn`McK6#0L}X za!;s130YA$@&td8Tw=4Y^~vWaea6JW8{rfj`y0dMQ8Rex%PyOr!CmK6yN1Me+b{B=`cG}xn(^a86Wxy+hgg3 z{PL>Xc3kr0X*-V@xnL>K*%{ldW^=2@T^}32{a&{tQ%Yo@0HZXF_~Tocdpbox-%K{B zN5#64RqLJPmFVjqsR2e{>zc!8HI0ootUP(Ff5py)YfS9F-0^0M%~alipF5)U$!I+b z<2U`dA|I{Ro%GA06=!W) zyRa}g(g@~MsSVH0c)5n-VRYrLoio=w?^rTfW`)P!UHz11_vLKNzku1bLWg!5{;ADe*y`d=%A7yWqYnNrZMC_^x z+wRad5{LZ-;*{OrupS}>`4Vy^&=v*)`_L3$)fhZ2|4u86|4C%4zD!bn=9t^IANT%^IXuR1%f{6gkH*|V zcjTDwIC3L<<{xwM=$TKPbNr2~oG#7gjQEd#VE2_jzNCECn&7dkH(@iCB@^^^7Ni@c z)4FWhBZYx2VaPRVx?kP1^Na^Tq zo_-=rLrwx~Re^MT!HRb;s*PQ1TNmyf{gKVgWaiJUV|H`ErmkDX-+bC;V>g+4H+8Q( zd(dJF_zwQbs@ZIcpMDApd;_`CXQU!!L@=6w22>;BHZD3egn#3fAP(AKTZ3)jACB74 zwW2z6`Nn`eQ0R*!Bh6irQ2fl}yN{!1uSDX3zIZBAXz7ffwfVRlGUxagNNtW~oE~&{ z_jTt-!_oFNTlj-mG;_x3vpc-K9V>d0EvvV%(QqVgx5l(c%uYRGc;I4%rdgt<_71M8Z=bQnR+qwRkIUm@z?Sbvv`Rkf%RQ&v72_XXW_vz!^8>61Je)p`?Q`W3wd?4rDy1xg+5~$Ly@{^ssG#(~_;4 z!5HyetQu_E86M}gp5Mj2zToj#V?ov-{{i2dn&TNB!DLdV?E%cX$qjaQ7sw#t4pr_J zyWK>?7AR7Q*(_bfHfIu1^BS*btr^d*$l$pZ#~9sM8f)c@>#gJ5W(%`g%r&G7!QG|DSJbzSfx7lu2{^;?rwwT9hw>v$t^7oNum!%Q*RkjjVU?;!Hw_tMn z+yZLz$=58*jW!n-){QpRH)O-vhHQj=Rc~CWAE_U1O-FKVsYs4?tg(snr7s~b=*0a3 z5KpWzH%0vShC&f%gkBWM;X3#^O6Mr6kjtUxNbQyWN0ISYSgZIup4)~J`1j)iK=MK* zG$eM5V(~@vCMiW{TUP0G#(c8H=)oIgjXWSK=*KqQ^4V0>$`+)?51`+3W5<61;5u&j zbP6(0{0};#ACUi!Oi&kADs4Y}&m-SVyLR1j z-Huh;21-4bZFexI+ZOT}EH(o(UAALQB7}^g`ATab;{r%ETfHVN@`3C3w+wY{d}iVG zM%Cpsn-V$G0Q>Sy-MREd$TBomfxX!Um{zWe`c`|^M)i}n9`<~_V;VUZm~z{4USq8?UdQQ;gw zK@>$?P%{w`Q3PYjtZsSR7cILnwX(9!Yo%6ZYGt`*W@f2nrRMp4X5RN4 z4(9j%{_!4oW}kVVS>9)!dFGjUKZ(X_n=+!KFKrj=v8CJ&JqSoRdEO$F?K}p5E&Pz5 z`0E@KSg1J2U3EFUgxPTv$uG z>ONtk(~r;PC8F++f67Zld5*{36Yl#c&=L>hmi{1TyJ$((qa`ccP1$k92vUbR{he<) zcL;fcnt*p!kSa+{#mu#%(*ew1I-R6M{l?*z^L%DO{c@bWL@%&x9?TB%&$M03%S#*G zsWcwksW`M3%i*JGq}Dp}v`b&~ho9D`lAekJHIgd(H2448r@+V$Wn`j{aK0>1pLxSV z#hcPL0OPEk?Ow5L}v=FC}7eZQ7SbZT|SMX!nTl zAnm$Wo@ex#W-|DOsnKo+S4oC^M=lki?DD8e5-k{4u$3 zK)eG-@?wI0y~zi^SbJbnf;BB8q`S@E+hjIbR4hzPWkM&%2Utb{OXzn9Wo>Oy5bB!8 z!AFUABF;y{>+^6eYj`$aeQ z?Gc_I(M7vRMBXQg{>tSESS|kqI-vCzUTaBCI_+*ZrVmI)_S~Aihw229c zjIfxxKo=??sDJk!vBvIxT|)efMlU}vUXA-kEj?AEZx_{1@iAJ0gF<~cuEH=_#CtK3 zoa_`<9J&TXXUa4OpFuX_n?Ai0(z=k-+hPa~OO6W(6Gyx?Iy8u#M{h$X8Er0hDBW}; zuE?jasdrp-WJqxDpj^>|U8ehcqrajfa=^GjPQZ&<8)PukAvq$nV>I|@#-=LAVGAij zqWqgEzi@n*ouO*_mQUxc{1`u{%ur4mZZh0$SZsL8@VsFoWXu6x*L&UO^|9ApwToJ; zR;ZKI*VGTx&(uGSUd9mP2;)TKRO4*p&Bpo0&BkrU-Nu8)lcp+Dy=kv`lzG4TxcOHL z!?mp8mUzo-%gvVOEJrQBST1^-yo0^-z3=i~U+QMW4_P$p7rbF*XFn0Z;Rh{zkPm3{eJ0^*X6}7 zuXQ=tvJsR|M z&`Uwi;FRD=!H)&+4L%b5bMS=_V@PGl`Vj6?r~NjS7s4 zj!KToin2!yk9sWXnW&ee-j4bt>Oj=b(GNr~?H<=Xz55&1Vb)XD^D$~nKulCj&zJ{d zmd31(Ssxo0+c$Pb?DE((u^Zz2;v(V_;%3FY8TVm)kNDL10rA7)=f^LJe>T27eshnU z9;H3DCX^){PB@itK2c2!NQ_GCnRsX7Ly4azc_&pSO-^b^+L82S&zPS7=y`9?M|$o_ zR+9sgBa;)8(~<`ymn2svPfp&I{7tWZy`Jc`tJm)-JS8t>NXkD`+EX^CY)jdlaxmp& z@9f@B^*-JEuRf+e!F{ZKQu@s9b90}&`#j%gW1si>e9||gZ(-l(Y{9mLwq>?ewpVO# z+b*V>QiD^wr}j!+mbxl+ed=#%1JX*;D%0B2PNv7FUz@%#{fYFI>Fd(pO23$4%m~bA z$+#uszKlmRj%EgCmS;Yd`FxfoD>N%MYev@fS+`{^$a*|$W!BoPH~WS5yT9LW{px=>9baKXUS z1OFQ27&L9r;z0-U6Z0GMm*&5lf2p9j;Ff~d3VyXm+Nar{wr{cj=E!i2a%?P2EPScR zzi4r>T0Eq^B-3{Zt=LctCXq% zRW(&Jt8S~hzv{)RSF7Hv`k-n@)fZLYRvjDfKR$7M(fHxxXN_+g|JeAq#~+^%I3aIB z&4e2#JTYP4gkL5GPt2LvI&sm&cFA*rX7LB7B)(gEZI`^?m;yS5-|n|^9uxK#YsyYu zI^*>B3Ni#jQegx=lRvIFPqJU&1n^qt*Z(U}7CXNYz(=wkd?ri2B5Y(yUfng|8<`o9 z*t~1B9JS5iFX&z@mkH1qi)DSTrqi{w#p!vp4~C z6y@z?@xt~5=oQ7|G;^>r6nK2X;tbVD5BqXZj_6@uBi;W7f>5r2p2}Z%Mvl0t2q2y{ z1<$?==vNl;e*xxc(XRb@59GfRY-5@HYC%0qRGL}z<-lv4hj?FR{jV?`Z_DL?eny_2 z{}sg86@7%Z5p5vLh+*Nlo&A3T$^h0?0MAC7f5&331o$d*1mcsVE!+S@v4qv z3iO~N^l2>G^>WB$p?Ibn$jqWVz#DEOz@Uc(!}bww9wpO-=;1`c&Np1Z&8V{u_%2e+ z<-i9ZtOu^>9fA7x#lFv^CX+Mt_P59yXS>=BTfZWSj7oj>v!=(_~;X9Ctgq0Wm4bJCkEk5=POoH#i76Bn3;N`4!xb;c z?a&ux8c>g5=e^v9acM()*)T3`=qDSC)FA+NJlZn?;0-r4b|~XEHu^?k1E2|z#E&qG z0L6fMR-xU_LlgKs+zXvY&`$#a;R?gpdspZyIF|yB0l>dFe*)YBSOQoDSO$0>Fc+|r zu}f!QLy+dg3fDP~tzg+$pydGi0{j5o0KEY|03%>3Adk|Wrh7ZlPQ0L0UxrqITL&Khjxp@@~8zui~3=rvF3!jbc!IlsF|(d01JkY*w~J z`$qSOE{dKIT@yVk`p)RZ(aWR%w0c>Ctr6B}YpgZFYP0583#~6$*Tn?kMxBJ1J~5Rs z6Jj8Xj#Xp3#0JKO#74wM$M%XH5<5Azw#P>oxX=X#Hdg2p<**eJn-%mjy-z3TG`_(c zu4A(m*z8jnwq4_t1UEK5(ecrR(c`11L^njwi(Uk5R#>?;$Qo{qv|1%L*_UB63fL^v zu?YY+UESDB1vcUwrFh3~1y0PVinH&Yze_(|S}R8mWU=Ds?Bevj6n07Z>-1k3!JU7H z&T0Nbp9uTVGlvcxT7Bq-L&b;ehx`s+IC$jXxq~wg)-iU__n_gxMaB*s0sL^_n**B< zEN5(gnYvQ;8}5!|D+wYAbm?(^f}i9U`6Y!b*ya>JVWWpa73@-U{FG?LifbI>(NalD zvXX%`8Okx`BA`4>59C*DT|`wCIe%Syox>>#|YL)d6GmQ|rer{K%(ce1Bc(@F2sm(cy(%?Gm2*a_Uuc$)n|5LM!wP6)+O0`4)hQ7Yw7C5@r6G?8j) zCW$e82i-{z&>r3$CrrHPC;FIvrr+one3RS?Jnmx&$0`=I)?xGrYi0xuu;yWM**mvwWJ4IfQ#$CiNb`*tCSMdF3D4U8X zmj+M;Ip`vdpzElC8mWoyqS15@^d^pgHhd2G-BHMnPU3yX-Ik!;-Puo|>1Uyn`YULT z5!!g?SwB)hk4$)*FR%jgWBKIE9ORF^#4eyEL97V#NhQUy3bL}HxYK_W#b8bgWhK;| zjR7AtjuI(}O`v3U4Q7#v)Qe5Rj5C?~g3_h2X@omHD1*}3bn3@$rQvKA<$-DpW8#$3 zy)=&9PvhBrpveoMQTPx|W()C6&j&%rmeO>#gzDI1G>yGP|6tG4jchs1VlUFoYz@t2 zFVIbF9sQGSqPy8^>}^`W-l2tT3*P^!>;`;?>RD<5E$_vC$Bo|CV*J;zdg{;SQ8{+2 zlG!;(WNlQ)=1?(fqJiu>%4f|~z*@-8W`ie~OGDUARLX9qq3j=2#%`n%_Ai>vo}p&; zZ)#=FL1%mw&0(wYEyk7LJKmuC*z3@l-AwnhH)%d-_z5~iC+Qp7Pv6lo+*%gP<7g`n z!FQm7crf>&^Y{j9bs0kZ=RdcXkkawUdz~9kMA;s-xM{)I3yQ9eUU)=5^ zLfQiz)%A8qal60t1xsIh{)oA2)qML}FQY@cjrsOh{Jp#a9qmDm$?d@a6<`EdtL={V z07nHp?Zp-2=e`0BT9HWv!&vL@rA4dmXcbO(v{{cv$s+gKKsZD;lMIJ*#N2giJ|1R& z#kY%(#Q}8J5t$wS!9h6=Akl7d_`?zGs6_>Y%LrE&xO%r!%0_gO{5fV9wX={RL48G2 zq{9g6+q<2otjErX!3#|?6v+Cc$_9f2?KIh*ZwW9dUS5Q{7#wIFlv;>j(Rg@Z1yE8^ zN5#=@a8%EjY>!+Eue}{8tB%6n?Mh01P=Km3^ark9#W6bEwB;iih^(Rr zy=Z}iQ5LQM4F)Xr!r`^IbMd^w-i&SFP|Zs6BD>w*OLr@}+XvxZx|_jm^l+O!+}^_d zYBBJmeEU6kY}_bOe*VbubKA|E{MkB1u_Cx~?phmdB4&?lH@{}E=CgKXQ~2b2>@~my zalweoN1X9BAH->a?K1uP%zW&z$pU(#9%$KDj0OXf&36qt)NDY$bq0&UF}dEp$+_bd zF)g}UB64jCS|`6^?%H^Sd5@U8$+>5vNC3IHRs=a}>d`n;fVh`zyhz!}(Bu09JkI;z znXupM{M~7DCOgBOeu!)XRo8wdHcfC(26PE1F{VQ4CcO7YbvFl%KB>DEyz98bK&CM; zD`<=6_5wdWMt7@_1TMLY@hdC7?pk%w1|OmNylH{7{ZoU{Dy4z6Y-_?uDS)kcFe7$f<-bZ{^k zm=DL&II05YkcHXzD7yeo?i!jz*V1J0Gu1SOYG^9e;+tE$!99Ed-lLA~q3KjlGr;dP z&@9O-%?8)h3@#xXb8aiO(e;=WbD^32BWA`oXfEAIH-Xm}0N&%6b0mVwl69q7`f~JApM6(bMz{JquoUHvOAcVqSlaR)O?}B%G zkKP9t@d0fG$Gwd{1g|&(oW)1rdAEZP-$|d)r|bs2rE}>sa1y(~neV1AXb(7wz2GMH z(U+2Q-wzJqTRK1oA+Pxt9cKSvYjNu46sXB)IzmSU?*RVrdvLH9={WsBC&2Cf$i{-R z_zC>Q&yXC910Q&r&13(hGvEx*(y!pS&(ZI=@8D1H7#G0T{zaF-@sG!v5{D)@XA{^& zb`op)*SP_o2T{3^UBgZ6d)!fJ0Uh=MSK^2BNB%rO=T$;@C^Wmn@eOKx$qJl%6gc

f08W+XCK33*%RQ_gTXO}f>#d*&mIA;Fp}d|2i}8i#tHTWo(OL598Ut*23;Gh zJEFkHJr5558Sroc;H#eouWbRJ9mwpwC!543W5tpW{@(}Z*Vi$B$ktY~HTZt^+f2x3 z#7bo&dmnsd3b@KXyf3?hy$NZQjjiOVSl2wt(|9_##h<~iZvy3c5Zrrr_6WYiv6wvv zn)fWe^1O^^KboOcRLWAZ9X_$J9ykeUc`&}U_OMG@KQdMm+@g(Reg;W$hY807l3DVU={Ke_}FjQ zmpDOPt`YC=JMq@!!T~lN2OiMv?ePdm9O{FO>dokYME?HwM^FAr_3XX zp$rJ=>14IgP6g_4E&XsU{cw-;8j=|qN>O8-p3Sb7dth6rw_l;&NQHVk7V4v;(ACcR zC~*{dl}~SKZqy1c)azR0%B80&)O)GO=373!4P&IaZB|2dTdT2LZxJ~jiVD2TTNm2RHEupx#s=Z$8q9WhXooYK;ukC29|Gh?w_eC8oM}az4 ztIODqx|qjKt*>paZK-cj$JRAhUtjB8)e)&RcyWeWC8Kn&-aEzp)$y`8%Xs&I)ly`q z`IpF;YfJkyG}SZ=FDo=xN!Q@f1%6dB%H<96c$_ONpvr}lE2N90GQ}A#zn_MX%d1Cc zUFPra^7}bDo;jjysGl}=WvuX)caGIM+#Rb`(&bgFJ-V!*SgWwTpKeQHgSS6j_h)K$ zfy*w`{d)R>Y~63yZ9RWMy0KdC#%i7HRdRO4fcTGn}S!&!}piXrUInX!xK+rdd z5Z>D?wN~P<)_SC}>(y}RXgPE=9C|eznU-32`5LBn9do_O9zd z)2Ghk#j(`6%QQ?EbZ&Y_LhT`GnQFbn)?6=}zp`Se7U9s_%aP6NXBg{s1nPB)S1-r9 zNNGz;@8>3433RAU9wVnvZ{k9|9SilQF4RX#p{pzO5#uQK zYU&^gg?jyqT)FgAg?dL8rTR8?A_`4<6KN#S?%m`;0-Lp(H*2+M{#z|F?P`lw=@yTc zZ24QdtaPu2rpCG!L#rTst?uy!!l#YCv{W7KtU|TTqb-W`Iv4fxYU`+Dk=`=e1fE({ z$IwoY|ybJbBNd~K5OwP}QJj#h&?9W^k|aS<7Hjz;+Ac0_9ZSDdNN^(1_H zpA=`SH_GBHH@Zi#mLWUczh1^%J3|gwLHOoMmmqv|Wt7Vs2GHK8^lUlh+0r#SXiK-tNzG={s0(a4tHPE8C@nS9WoMYd z3)a@xO>dnZbX{9hE9S4px`tX;wdLDtE07~4&1Tbb*=({j_~m%B*$U*83V(*7sI9q4 zWJ=Q?nUt~i@4DZtvv+NJy($mN>Z%fxOhpn|wx{gG;t-vgY zw?~c)-|0#jjX?B&Z=&n$?|n`=B$})$Si51Eb%;1A^|kX zjrIgDJi!S0e;T`upI5AYTf2yFEQs_bfCbMJ>qy{_XMc*|=jjE=S2$uh!c&mOPscSB zdt)da>-TR^=a;eeBx-kxQzFeG1-}_9TKRN@5mxv@i&*=KP!r1cVF$5tB*;<(ZXs-p z`%lQPTz?DD&z^r8K3?e2N0Dm@*3JZ}%zBi+0c%)qDT%^JVLvzz(_rWKR00^`JV~Qq zkAb}c8h|x|P%uMtiZmQb}_!2VEuL0jd zzv39+d%$tXvM`oVZ_aL!b=m~Esuxz_%(>5b82K;zId?kuT^8i>Mr%2or=8#94||^S z7cEM0p49$aNrjEn`X4+TYi|Yb1H&6pg?D2D{=(Tc`0I+dV=|-*qV2lLwu_W)XO(T2 zAp1q+5%19im?96!JmL*{6!D_Hxy+-;JO=mzzPpeN8zPeN8@h8*WOp21NIF;Ze7 zQ50_)LyJb^52IYB5H%NIkoa5)W06}M>1h~!oV}opQM}dXpm;FRdy3iLbWA=&OXUNJ z42Kj0_hsnsyC`31kV4Z!Qk)R9(`1ZWL2)KZiZex0oLkWvFG2btD9*Ez;;fbw=Nn0J zj!KGiQBWMnK|ygK`2@v*OcE3aQb$l6ND4u5u-6EB=Y1S-ieYGZ*uTPkUJLU94e|jE z@(Bdsgpc=dKqX*2J)kzI%{pYkwyTp>&2CWhq`yHO0(&^ZHM>ErgnvAs8DXxx)1<$n ztY*tj^#%c^!|E-jVpCaXP%jFxF-l5(nLAYJ;DBMr0t0e5QSXBw?OS-*Y-DQ4B z-KuK#F7saEz&<5czZ%9{GtzYO!+z_NxP5RpCSEXTvq69 zBkGMZED5F+I@|{PHcJFR*a@bab^Br1_aI!e6HE)>7w#1ZbLD+p`a8 znco>qFG&9q%T9|9=JiOq&a_FwVbgJw25;&1r?B5Q?J;TgVbe$O3%8bTk30Q-cbJGf zW%|{!)9izi&%V<2R_PbxQuK*vZ&7bi#!ky_z-GX^fNjE$`Lsc~ z7Uo>#49r=I&=ikUFw4+MWdO`Sl>fkNQ*vQOI5T11q|Abe&3c&7yMy_WG{*{4*(SoF z3@F0mWEy{&Yod%5+F_*Z#@F|VCWz;9Z~3h6?R^T_(SomWLf44 znQMg1HA03zAi{Zt46%#6Y@4jn4I%_Lu!xYovb0xa2||+*T!c)|5P6~7Bg$-$7!H*s1WUIn z(~vZeN({diZnj67GB4XN-Fs!4Oqus7S?h9%bFvJ7N%q13`K*;P{D@3>QMxC{*ldZz zC0QCY_kdNNOyiW#JtE7QBwI%4{t;=hS;Kx7l?!D9)xugqH}^InwCD&~6;o-Mn zvi!GX@7Ic4Y!%-RbD@0Jt1^7KEVEkXf|fF3zmj?H)=T~rA&eygRIXt*?pbEO>l+}W~(DYE=)q`6wA5vS^jy(mLgN%LPaKctrU)*s}9RvJ=KKFmr}E0K%l zLHuro=7M;NXa(`@o?g(IC`L`EK-1+G$W^pBj=qeBoFWI>45Oj>avi%Bd&ydyf|Y7_ z=o;msjmAKOrBFU{z;@R!kl=8pcBJrUm-259)DVkUYgeQ)U2qavWN<75mr= z^*9ydDi&=ozJ^|fHK5SDdPt8mvJmWpr?Nq4gYnQhx*ogii}W}XMx6L&O+IvmCSct- z2ePWgdYl=n%O23XEI?0AgqH3NSb;vQ$62rjPJqP34!xagpo2UYx&WXqvR#C>ZzArc zcR-h75@wkju@Z%hM8^4G>?dJ_6yjE{YoYyd6T6p*lLewZwQn%>gEmM7MolBT8GH6i z*wShU_^>xb1t7}ff&V=&9)ir@;wX*Iio7m!1Hi?Z+3Q_?&q%}c^GsM7S4X6UTC=9z}*t7|qjPi-{BUtO~WBh!$4l}&6g z7;Ivj;VPPKvs(;VEmze9jTKTZOnv~(C1g>!UqhObpOkZ+kl+eVN7#@+N?UMN!msMK zQMVyjlHtC(EoN?!4mWN|Tg;!rFDSgQB{u>}E1nC@Qu8-4&Q}6X;G+s<~-8pxkN z0o{BYQn(M;R`wsZjeW>IVjn|uZ#ATZE1|yBHE$+~NqovS& zdkPv_X6}b`65`xM5uRj2U7|4)%mlSu0GhrOlDdu1czTV!&fb9L)tk_l`iz}~w9Z%4 zYlL26n$#$~26}!qQbyNV3vYptnA3}zb{y}A^0gWf7t(Olj6$LFsI|WZbEOuafH)xu zXQ1v{h>#(RJFT=ZA!oqVIgL&O3Xo1(LSx17w*8!un=>~GVHvpf_!s5+Gz{=WINjhO|6fC z!v^l9+YWl1DRd|I(rpLM{VCA38>HC~W+FXOZxHcFb*J<@biV_BA$8q=KIiC@tuQyC z?>YKr8_d@+1~~fYBbb|^>&PK31y_c%Cy&6qn;@%(ZscPy?}hdwhmPb@nD^5cFh4^L ziMD&xS;&wrvYiajuH)bwAYZ0MNGsc%mExNKu9l8REyY`a(}74O+Ez${MTtH2m8-^PedgVyUcv;ZTpBexIQS-t@XQ?zmFj>SDQ*fr4JYZF3;qb@!e zlXiqzKwBbk4)sms`kdC%Thd-fTcrI8^f^WNM&vTk+ghA-zoW&&t?1gKo^1%O6fqjr zlAJl(RKeRO#xdmB82zGML<~kd^i4NOKZjOpjt8FiA&uBeGJ*39CVc${`#;aY453vp zd&&~D*0q5~T}DAXXvbx=LgFI!lORwhf9zF=Jqcfr=L!r(I~kxM%c&MF!nxT_ZH+fQ w9(B_3pFIsL)nvYZT>2=i~3GV^2Bm zq_NMPKCCFuU!o}N7su^9{S>80N#lNl_`CnOz2_eLvsCy={H;V$9(nZmV|E_xx#Yhm z73GP`@%PT-0np!Y;`4W2!S~AJPdekAuYF?Wf8p=nR1{})@4nqTuj$?K{}kox{{%tT zoV4?tQ~0-Rf53OTzT)JaCmmD&=;$Ya&!-iI+fLbc`Wem*@BT?qUINbkTTeOdm{TT4 zF6va27mfxF5yd`;3PubbW7Hiz?yn_4fE`Gh=KXI~63;XZ7 z%g+>67fV+~%$JqPNqHyrY9$5C6-DP=%3I2p^YHWY@NVU4<;#lAy8boAqhyr%${2nF z>^eKMZIQ4Bq_paec6^COW6@}~t+Q)@i@#`Ty6jLyu+c6@GG0;N>Wg)+)Ljm{O?CFA zlM7rnO>=l#7N^Z?4@suF*6H2o_PT9t9lXHym)gbvqcm3gR?g;H*PN%eV%9>3vxBr*P-4rtX>7YLvQ#EgAad}HDZgCUaj&7dbX9Y;w8YDNYO%`*`fVn zy0qE=^B9Kiv^jN`tHI&m9^J(?_m>WjHF@3ta9;O&l_&DLf0X(yXhEgYrz|)?_rX5! zPqGd633dwn`e4m}Ca?K_*N6QTiMXrI&f`3x*Npl@-2C&Q^}c&b@5kIuV`DVBQFr+~ zOO|*hJaf?b9F1>*AP8V=nZi5=Y5sMgj=OZH zL+E&{yimvO?g4MalZc0#NX~tM&V`Pklbi1zTF`GegpS|gcDefvBRZc%zqQrwm~el9 z?8CoM_vJObPx+>`>SG;*-yyvLRjz}1i!kLS3@Eo2cnNpr$7$tt1@NtT_@MF#EC~5E zi}~dkzfQsWYO`!BtvJ<|Pm==Qr`!Zxi-gG*XI0D9;g`EZ^9GuNAw#v?oZ2a-6$exC zaIN}=^n12@%KW9R<>9`#-2tzsfE_+tDqR!^vZD_4gRaSAJghvNw|O1ve=2^ZQR%@- z0e`0p3nVQQIy@d2xL~1ACh*U%ntF&^6wrbivWQ?ylbYr%Pb8wuyhATu-^cIrPqO-X^UNQUM=Ed<9%&J{4Ud%V1g8}6s#@IXw5iGA_LEOy*HjKvHaGs-e0;@lYscT=<}1~c@9`D< zNs2>qJ^(og31Abye3&ODfbAsh|7n~#=>4AwRzvTfk`ocNN1^&!wCElB z1iHldLe?8_KZ-wpCS*avDI$~bZt$OUK*DK;B)muYN&%c?O2T`U{rH6{u3uvLtfROC zOk6>;U@m6tf2o>9XE;C_bo%a9Dk_F^S}|5wohQIqJw)jS6lRQmIRaN??hY@{hLS|?2Oh1tD=L=oaeE2!#ni>wnF}rK}UxyUv!*fi%2bFQMG{Ps9 z8r-McSD@LnDoA)gX?9L0gheAb`Kr)~yoDfNRbD@!Tv{*=`Kl7$%3}p^@>L~#Q287z z2P_|a)fmR_nC_`2Qy*X*w4Bq*W5QVtgv=j#56(b`A-vX{yV@I{jX%DsAUxJ7>d2Fm z*p4Wl7TC5B zo7T6(nW!IWyyzp(RxOG|&k7{I#P-C^w=+qWc;zRdQjfbi9)9q%Khw4EMZGmdow%FG z!;vqe^KRumF-HBT#gUKuA8EUVZOnN@iws+FRamg z5hBQyUfkso+@%?jw(t_t;C;c+bPwT%WwRn*5{>A|+=r~5)RX)HJrQdyyk8{Hns(`l zv|FI!VJCRR2Y2jj^JW)2fwy`*jb>dA3E=Xw*VAw|zMm~##*QNH3hKowLb)5A_bR5~ z;=tq@!IhNA>FCg;b3ldQmh%5Vhi4kM3V1^-$#DWKkoS0xpO4?9lts9Qs;p!1;^n>B zh*;+dPV4+qaqleTcAc1U3Gc=)NxQ*A)+lh5@Lu?Y1P2W)E3AZs_bFvLxbnvWc)xXh z-Q>8S!NP~PDgPnx>7O)}Hwy3>n1nBFjh|Ag!|4@$x2B0AbdcQsG7;e z%3GkfLQFK}2gRC2Dkz1ELIEp^eai}7eNwVUd3c8M_r+~KAyyN6eRfOdF%u2A_cnDC zvKX*MbIno)3s6X>krK!@mR6}n9&gm=(p@^&k$Q}kc`5Z6@^hEdqm@_^PqV7}98SZY z&nO~rlNm*ai)rrn|7uA;&AGxR-<^Dq?E()XLQdKdXOKb2GeApc+I;p@@#v_Y6;j*g z+INEH3-mhshS)D4qh{6aQy?Q^ocq*^z~gm7MtYSy5T`PtpI2dgc(}s0NycVHo&Y|$ z&R8@;whhs#Dy>|;F$dQTV@$3F z@zWiu?lw+OvGg*URIi^6)Yv@C>(^>n8SeOpA^B~{e3ay+6PcDEIE*|?Wk#*e=uu?< z<(eRTxllA#pfc7Ux-y!$ik+_AL0G{07sEGlO!g1NQoSIICB2F>qyfLlBC>o(w4s9!M_jdiu> z7$Q*3FrXb42#pdB5r0r)0iUkhT6qnjrDnoK4)aZdv;iZzGB^w~RZ_uH73?LNhrLlR zdsBQjt4SiUQsd_&dAo!Jt>e!N2@^O8`%G}k0!ceB;S~Q#c#rbG!alb{Pl*O=oB~e( zw25^=Uf-|$McD0jNGe_50`G2047;XCq(xeo@R^*@((LY>6zb+La@2}xnaPrskWWht z``LRrvc;6l;ENaXse&5~>Q{vRw25_QHvO524`iiaW(tVOsT*`kJvy&b?|*krbGI!= z;;f4HiP=b)Hnl&RseEFtX)efjm2)woypFlxN(X$*&%wv^pvEsVYr<$Ct(P9)vJ^=x z=(ny~aQg+9UA_K}y=zY$*tPA9OLuP5Zn);OC(ge9>MNeuwQS|Bdr#VT|J^5@f;)*3 zh4h?gNl&ei5(%fQf`oT7r2tO;rG)n?{{m()KIoS@_RZ}va%(qnq$7;U{qCnK%!t)x(=Qmn3b;lY{&}M z5bD)OnjbhA8Id_B24*8@Zs~wS(s~t;_>zBWl-JgGK(HR21B$bS>qHI8H&yh{)9f%46*$9!le#P^E$@U zv}}&`>$>ilc)`a$+;a38m$I+N%?p}NTXyOIze^t+I}PHwd8xyL;?(%aZY-&b`gON% z_SUlt2JXN6=~nZ@<3JL;mG}v{y-3Jy7i%ogI$GN$Z%33r=d@0p06q0eH?EgHx6Grb zk?6>k6}!5y)(ifUN>YYCIgQ8a1blE&Qb@>(0>YqAm8n|&nF}wy>XEw^oLFh6tk%wn zFXs5oA0@B+^{Ass6dB`fnc5q!dH$z0RkbLsTsg&Yip5OcjLcbxs!mq!EVp)xyiF)q zVbVuL^6dNL=WpCw`^i#-$$u*vXjz=SrpU^kE!w_)Pul!Ll%10p>gj2EPeghqPr=Wm zs%1g+Y8B%U0bp9EYFM}fMc}5=U%cX5yKcPZ-j_>l^Hqnlq5PLCZ#nVgGgjVq;@;COs;Cy5ruxb>p6L+%ouZ!V*l{MF1*_3*q45*9-juUxb% zZgcf^KjTnC{-=9t&0iw7b#Z2>s;%`eSOaM8l0q6Um(r+wM@XZDQXusHDNraR3fwSL-4hes3fk+6+;x3t20e-PAy|8v!NNGiD7U6#E|=X z|GJ`LKlj?_ujpT!&Nj8rZ|pyLtYT#>Qm)$~dR<4~>SSv}$4K+QzA??~^@fKWYR!_K ziu&SEq-sG&-%;&BugB+VFl@1bp7PpwAW}2hF>-V#C>NLs%CEL4e@aj&;Y7KF_b8u& z9ug%@f|7AuE#%cNLYEL%0weERlTi^LVn6Cca~b}q;|>P%T0JluJ0xIWjro`tfP-Hqoq1SjWDA`rfNnqJum|H}_ zkZOFVOnaNqFj#8oHn7uAWe%M$9P8P-Y^+H0*uS`N-S(rM^(-2KX4kuKyy@C?HoyJz zTb3?9sZ)!VRK{54(hc)ks$DMqr`{7bo4XAB!*09amI_|&aOp1vSB;JD!P3N)(aHDu z(ZEe)|D#sbKGqI314_V1AV@v85|d7(nMvED*_U=Te)Y=Loekstz3b|1VRYBp^;O4I zo^sph_4~e^I(OA)Q-gK3flYN6ou-d(tt^jox6KhMDya^hbn6A*J9iPQ>^(l!TM0R8 zfyT8#^J7x)W#S}PYmmDBna$P{7 zoba|R2Ju$VgCu$GF1!sRCN4uF{8CZ0Ksx?9e7QCu?+2TY@&8Dv@RtP(>yQNhBi)zg zTij1n&~FwRld}me$OI5YDTUw~pI41OY+M~Gfa;K%ZIcqdavHo%F_B4T%CL}{$EB60 zWjY^yTWujz04@EOQ~OynuSMrfGuyhnV^f!{MSN>9KKT)?V|%a9!M+1SZw~3+(Z!k3 zdiH(#)_u~sM2EPO(EQs49bGITzfC%usHUgeJkaPqe$yifU)1s{j} zQznJro9O4pN^E5(tItXJhsEdl5H)nbCt)N z!;Q7h5u4Z@6j-Y!Y5U=G{i+f@T^nSvc)ZM!j>XvVbLP7<(;U|k*?RjM z^uu6$PIB&4ij`&HJ9$?*6H6AC z1wN9hEbmCVblvClm4w|M&t-=vQKY4tlp2Z>^R*=kiw~7_IP6J#TXXibMa#o#nb%k3 zHxOK?y3x^5l=Rz54No|zx}9E~2j)n&4-M{GE#hq1@9uCIPSNk~aH8M6eXiu^Z6oCn zr0tro{Gq_6QKTs0gUTD0OB%Y-F#V3m2*G8)*bOr{Gq*rN2twqVxJ%DAv|#P zQjCZ$1&k;(MvRFeWxXSoA6ym}P%xkmcc@3(!f|J%y3CQ@uLeW@w7N)7@3&Xtet)%B z)>f*+dioYURg3#xSYmTI{AT#5UF^3mJz$2n9?jnH`)%Hd(+~{aa`DCH9zStI;38Jc zFJa!OmT->XhJ@3~DB-uz+?)I}xVI6sqN=1}funs{Fc1_f zP>e1tg|5=xE#Wd|RH8HBV*DO0g3i;h{j$ehDJ>px{q8XU`~ZCNC$5x(`*k;4#+tC3 z!>Kvn{;SvFuzSs)m|sL*2>=!{|Kz0@r2}^##NCUfM>gZ;xT`L1_|ri8OBmk3@K-+R zs&rPm{+32?i9n03VeRrJfB%OU`5dqLUGqy+n-L=mzI|2lP5Fu7kAxH7B)nI-w*XFj zlkh&}^A>zuU?}0_QMj!P9xyK$e?a+2!8qhm$Z=YExByPlE8!!k4^wQTVR7FK4-Ona zcPJ(xDav{9N{M=w%Q6JrcJ`8a3%RD>x4Z3T1G}7xv+<Tv$3eXq zRbcnHMg5xs3kjzPOTv3-XI%bzqF%!Lm2XHmdiMxB2`B223BmOgXS^nAv5PQ%Ms`FX zq81$!LQ3Bd)$gv!f1(dbRH_tR^>Q*T8ORl)4zkfXuOD5!Bx^bXPXQc`e!yjYiF%_-pE`l58Q#1z{8soO#;f%nt;m5>9d<;k^`l=C3EtNO+%erv)G9 z5l|-K#F?nz%*^ozkPDp}hvY(z)5@I%aFPoNA5m@)aM78o4oHch{906J;xVdz^sd`yQ|Gvcwdz^ughCO4cP(b(F&e2-Cwk+*ztXW;+8Pk2y zfPa^(yFIgG{HW{4JBwp3W1jhVpxA3?^C~iz?O8Rta`Q(>;v`Te)HiuAXAS)9A(mT=ecdqueCM8wZ`a-=#5sT#WlczaLOewSEsisTUtGekywHbCJM@lM&MNRvr05eL4{wJa_G%pdWPG}?T0*@uGq&_l-{cr=+tXCl*`3Ms zdTe$bdsY`!zA@)nsA{Wp>;E&OrUgAm$|*};O3+DBjJm`o zCZI^?q=F`81i({?%If8;|p_EJk@mwJJplldMo%iaqisIFmI#C z6Ob!YpYl;_1tRSX97gKg#IC7; z(nF^hOxuh20ZX+g3g)*a&RDothzg)TF#lj{w`Q}I%8iQODp^HkZ|=$eX5<`r14=3KbsG55jn#IE zqEuPF2o_oLqRH>fSiUAaP2sJ`d^5Q-(HOmm2&WoZT#*e?P^j!kMZ#J3nWbZO17!gl zlFqiiB}Er@r}wmtt^52{?uy9rh6PRB74F{Bl3f-X+1Prl~iK-@4gPBnP;e=Hr1I&F?vxx?=P7v!CY3xAYc zP#zUJA>p*|OTv4UTkv-$Og!dC15>N~OyaE!Cq5k}X_~(=wCjRFg zbT!X}&Msad3})Gymil#l1E(!r-B~Q9!PP1J^D|pUx3B8!g-lHI&bQt$-uQQth@(sv zBKLet_2tK}9$B#oIMqQS{>8rwUyS-@DODg=RLDRKU6=G3g?}o_kys$FAe2()mV}b7 zvVUFvp<_-MJbP7LRUo{<=?yG)d2H^uC*UqxX}J9TN39ke=$rMwNybiRQ7060o6 zt^~YCp&e&YQQA-E!Wm&BLBkAFDw-%dz}Xyo^{igV*G0PFt!)!L)Mm2Sa|}H1=+V05 zv|e{>p2_?Uk4N*x&87sHTDayA!%y>i^s0X1djG$2>=smw3mK49QS4bE^a9k{j6Ag} z_Cul$D7jDoPfIvrs2=`xP|uYn%-@^P=yJ*eb!HAUo}>^nP2}gRrd!5eU;Z&zz}wE~ ziI2^{xV84jx~tImJ%9bf_5GzgHoUXHdZ@GpILUjGZvPWFP(1hF0#^yAxJ<&kmAeYy zr1KKqquf&fC;gG|Uc~1VAz&Q7(eiy4DdT|UYbCVZmFAH4QDki@iFx%tBfM!0rNN0J-BAUumjlm4l!`Q z;NUoBEk`+d|Fo}H#m<;K2iSYlH~=$E7ICVGfgXVW0|(e|VljV#%m1$O6!lQ}4nIk6wKJzs|QZqtKzjvp3&30-E>DEP9YTZ@zFiqzn0G ze^#Z>zKtT9eElO9J!JVqbhu1Od(LN{qtrh!-AlK+FQ>e8ItFMqFTL>tRN*jO^T$3= zFUXqF$|)^EA9fj%6_l1`3f@n8lGh{h)8+L-kMh^&EEN27QNfm@ke@EEZso3m(aBGj z@Ijn1B5mtpVOu+?1G~^urv+xptj;tyI2B^5lTXRHrR)b*kDk+;Gq;82MQyLrt-_4? zsx-rvZyrL$C1-*QEK0Z-o2A6|OkN`lbB<4c>N0C>Ed3>hAz_Dd$uj_X|7#Y>fs)jVV-!_t2XOW%!L@e~{@fd%gsT|}y+LuKd= z6mg=+c>rTFk#Nifvk>LfT6ny|Cns>fEFsohvs z%3?uVbuKVkblm8C=CJvvyg6-``;78)Y8w`>H~;8zNBFlzboBDhg?;|Tjo0{d&Ku-4 zhY>u>ynlWGVnT|i`I49xF(bN|Av&(j!j=1lH#q~-{#x=VYcG-y`~84|&7+v)@IVePmFKky(<%{!?W~;k>Js_!9s$D|Apo zyM+cO05!-Lv##y^7hQ`Q01MDcWI_AhC#b}D3feEOZO2|;;$sRYa*}ShDOZY-iv@qD zuo$3oh6HX3$0{U7R(UHs7QeCq-oXAWW+Se2QoqNv8NvRl-1LZ(?G8RWFtmlcH!**t zQ3Z3xZ23)D<`uJZcJnN|u%@Pz=QlpyH2VxZH)9hPsCI*&agH_C;^*MIQUceo4ncGO z40?m4<(4lfqR)d!V_ww?2>hg2Y2BVBvk~W#Nw7i*9g(f^{*!FRJN)5daM?7HrEPDSj4l5Hnip&-C zxmR=`U~en?E!K}Jz;s<6i3O3#WYQ2FlAT;HB>5X;$k=b)Kl`p8DzZh2w5M%xv6Xiz zs?m`^v+b9jTAM$}{^@@89T!frv^(B<9i3$SSo8qRSq95Oc$COIp4Hq! zTYIN3k{7hHL9))V03T$5aLznyO-*i{pXzpb{1gAB8{wd3l0hLxW@9*aE+ryTIxk_R z@{9#PR!~lnDzY)?a&8B_ZZaY;k=Ij~vtQUg8fVee_1LL>xww8Ax?X*GxXRMvKGfe! zR`@TpM@n?%>Icw*`9#D^Mgcx#*A&T^TV}Y8#7t8IU$ThqEk`5YWS+(G?}&R#IK?Rv-fczN5>8%{g!d}9;deF~hp(|R zRUPu2%9;Jm8mTK8;OP}dT5M(|fqm+OD)#ed63Gu)m!J6h!FVtZj^=2hEFAfTl!eRF z#>{E76(-=Q?36zEVPFTL?YV-a8=8n`t+UTYUOvx8EyOdb>9f*j%}tw?VVh5OOhl3J z>SfF?y(`jiO1m94B~MKC4O2Qz^b(tI&yrI+ker7Kx`y-h@H3_-(Pkw|$Q$NF=FXPV z*(h`M!VXsozh0OvGqeM;WmMC)32G~*Rkpik%H|_2Ma!v5#1(Kz7e17isvl)#p*X(uetFc(lYZ710hf0~J<>J#XDK`I}x!`QMViT6MYOb>6 z^q~UY(uys4JEBmm`AzMIIt7oGyL;ex!S9{(Mo$tYyyxm&wZ&^_;s5ol#k}rr!TkPyk%_-?URBNMD+kCaV3%20 zTdqVWTeuBaS70t+5ajxRxQa4E)0`sVX=h2u{(2z%JVsUxL~JVUwPf;{7n5a<{CD}>t; zgX5gd>aryT?I!4nX8$r4$z)h5+B52NeH#ek{Ej86y=nfjYtKe;o@4vS#7attv67Cg zL?Rgpc>|w{6(eW-@#WYOBTm6%rveMa=%O-)TjxWK0>>0e3Ut`&@~~441u_f5CGg3m zKU&OVV;OWdM1s}E+9l>Y2!MCBRO0CbUT2#--rPwNP3PL3DkK_fDP_v$5frD?0ZJaW zg!V)LoqQkBFJUC1i1n0T3+Zo|9G?bnQ!d6>OhNvO9f#{-SH!A9(k*R=+`=qf4`Qhl z^xW>{bB;3XYE?^lbALlqb$iuu%Nkb~Io0BHqM@~=v8E-}yuMp2t@Rd11C5zf%I_)b zs$aA^9`#gtU6mE7s(`Pgy|#5l1Lz9JbVPyT>hWptHsxh8ca&TCU+nnp6>}HL?d_#)_QG8xeIO2ie6bja z?^9o`n0PGEVej46yL$IvSJ%AZljf%=)t#;$%nZ`8l4{hQ>aXf)$d1@MAhcB2dAD_mC0T<1p++(H8SvPenp^`Xd3_ z$5dfqFi6rUs|z!7JA@IPeu{Yj-w%!epWLkH(4I=Ye}MFmG}<8l~oGfydI30~FMb@I(OY#i-uWIy%79 za#>07R9)Ss(h~U%5AP90uBm3P1w*blTQAC6Uky8Js#a`eF8a3N)USF`cmWj>EAn7Y z6jXF6PYNm|oZ@~7@3y=~38xrY!h6YU%#TA6iG=qdA1LADg6bSR*Eu`=zVO&jo*IQb zb~#Ebo6PfW6TeOw!MA*d-_o#LZ`VP%*fmSo!d~}EPRZu3*;yJ47HshD%d&4=C3u$a z>za+XhKm(OHr3rKc`iijCagLEah@ooo6H8;B)Q)^`tP8~UYH~K?%|Wi7f;*CaLths zOSF!IR+_1m|DL(x1)If>WZwRs-#31QGdS^deiQitb9q4i=if4y9n*6{cOKDfPD>CS z;dJKd{U7YM*pB(MoE)*@(|SXaY$cEW9h57Rxx`VyCiGt%0r~gmvq%M*rN14yOq2dn zl@C^FuGs?Hzd@_-nVv(K^-MFI8GP8b?2vhdPaheZtTn(ZylZK_=UaJrY3nJ|AmM`| z8m1L-F<*-n5!so-ZYnr1BCtA$K^K*~sbx{D0(oBZwbs6eq1lbP=#|f{lh^`bJ*z3# zTGbQttiiU2*-U%EvPpX&oZ{I{Q)?D>K3LQZD_0e)h?E16RF8-=py;G-ll=x%W0x@P zaVH;H>~YVn`3T^s?df*g$(+~YIhwr{c7HT>Jzr@%SVHNInw|3A^+e=S3NdbYhxZTIp)oLt?{qF6$Epd*XGEyoY zK@>XQUXAMGBYG;CR$Ec!`H;ObiTl6eL)(wv{L1J;wm1+lzdSg=R{JFyezvN(%zV%f zd63wUZ}DN+4pea%V%I#Jd`;>PI;8WuaP=7J1$^Q#Z{@kKgIKM~> zVP%CMSEhyx#$oW!#sh?5ddoFq8v;KKiwP~xM6_K9xC9GkBgPi*zn z$mEI1JGQdl6kJVGEyo@~!J8>wyu8c}>fFige3#*1e(Sr8S&~KA`@Vcqp|Fo_*PJ%< zAr_)9<~8L$!_GeH^>ANFh2Lc}&l7PNXq00Uepg$xe_miB;gpk*@E&WgmV^`a1n0b8 z>_qayD<=$dSLggtblLEJVY8^#lcLTbCCw_CQ{Ib>;+-k2s0^Bz;}(3&?|m0rAVdZu zzxiD>7(k!$PCO>vRO@5|VT0KFUO&_{veHPcsad#OO{q1u3=WQKZAdLFt;QGbiJE(5 z`ylGiW$_&+R8%e+GAh_&Y<+*G+>xoTu3#tA-gm<2UjlFH=%fnij|5jFoFqcRd##>( z2`8>dc%O22?s^^qWjS~om-YJT<3lPgB&m>4k_tIWE64MN^Kg<12_I2DA>cE2{fyFy z3Alb{eRtH_0Y1G>q4H5H8j+bJk^z~+7!~!4nUXO|v?9wI!ko4u&(x;$(+)C`YwGGC z3qjQj^1WklzeP>}=buYwhNdhU!@u-BZRId&8pY--grON+kv;GDzNU%=>8k9`k&2?P z23+3S5+1DExvaIbqNUy$9a^&Q(z6$z+&k9SRCz zZB2_7RsH3vEt#%aJ+HDZ7I)$R1V??xg5B&_+5YkNzWQo&cc#7=PgB+%W%Gu>ElCf> zVxJaso!dtt;WXD0p4%-T;G$P_p)#&)!*7TvWNV2uk+}d5WbNA zhtDNEnmXS{D%wilum5-PjbvW(M|6opn~^EvpFtP$5I93DNr}vm$Ug`UiItJslk*=a zsY1!E>A57)*TX+EOHXnn(D*j>CgXjU-sQK^$;{uG(W^{d%xGDrx9QEy$6P`ds1GsZ zg}@ovNOnN(1X7Lc$Q-a0izE7xtZWqz6lP8uks^8L;8?#iJ%cqpb0RZXv*KTn(?UE! zbm*X?k0_bysin4)4<-O;PdR>kssj=YsyZb{HfcVG{@p{65DD1Ae$ke?YN}^-%;WP1 z{M*CxZdkBzRtgNG_u-FHqbg74+gQnp5iRwgCB+O#lFDkuYQdE*_AqPZPow**L-f;8 z?<{N%USM|Mfj|>=Q6R7qlPLKh#{~`uITVY>u6phmZmul0I}L9@#mQLQ&J3HT+N1GW zUeuJTa5*_#i1MVSM&s<^zGi25V5}HTV#!!0;LCb#0q*wu>l<3*MQcJ%S8c7&UsOT4 zhz|A$D?%REsf2+W2{k1m(pD5l2Crj9`|c&}Ys$;28q3PrBPVPgJz>j&%5$qzJ%gDf zM!-M(!~9ufGa~p%&w@wagE;9%JTVpL+^Fn6{aJfz;;|xwEp`_b6`QZJpWM%%jZNGX zaPjH{N=dDdA!T1zF1`e2@Z+QbgD!%dF=@+r;;ONs zMMZ(cTLEM1o}JsyzGP%nbGqLSB(%}U()D+o$e+CS>T@6Nw1s(7h?T0*uin1*s(-|dJnyxBI=cPCwqbBqRD8T5u_8RZ>f_j^jdGQ2a<8VnSd*QD9tF%wJN9+%T*=d1s6jI*db zPBN52e)n&fF`qoC*jiJqLO|{@144}N+n8N(-h!@8?JLh;(7L*7LAGvH=Wtf*J8$dt z&us6z@Tlva+O}x%XU;$I*d=#exMw#}>cGo)(Pxe52%aS6*h6C1Df9!{kLS}!ar5bk z`+FWj?BjXM9T@yL671$4_>T3wxGe7Xxz()OIdLZeXiQ4s{|@+~M^k#Imb8P7B8dTk z6N1OcTcv4Frahc>jP!Tb^_3z=A0FDA+_I;t!Tc+$5YFnARX&}w^-G59T4MpD(j88R zS1;&T+iK!zSJ;O__oROD7jRGXl*?L|a1<$5A~N)}m{^q(ltyOYkvqWkKhJgOw&jcZ zca05;LP=?MxU99WuBRwybA$&sRjk`R+|C;3FF&#PH0hmMJ>a{>8#Jtt3BR? zZ~4-`wIQ2(?R+}CmE&pDCi^=$Tt?3pr8aVU+K=_L<0%f4e5KQwuC5GYBW|DhL)}o> zx$cnfK!?fOZ&tA<5wtk%ue z1!6e-xKy{BA90gDvO3nzzeO^S4tHu{vJjIdz_I}FjN}^Atg{EI)8+A0iE48%uW0X& zCT{LN}Xd!{ZEFY)-pdZ4%^Q`TCt-^ZOk+@*yjSsj0h)-t*aMunD3rI!w5 zUgxLZ)|$(67CCuSU1d*gChXE3fwD}XG+ouC+TAP4+va7;SW#8uaFQiGfmEa+n=bKl zhrcA_50{0a4tJ=rA>C@IdTUk7018oT@(Sqr75p(+>1lZhdRiH~!e%b^1QQ+`dl+y0 zF{j-=aa;&b9OJi!D1xGM7;oiI;I|dZ0!UxZK3MsU1GqmImUh|MYevN5v72eUEi0U_ zw;b%Dn#ziJnz+59qN6`j^r?GYn)7E|v)lQ9`ZZOb*zRZb(`VKENz`HE-)^W*lz0LW zN1&)BQ{Gl`zn`W8G|(w(pS5UMI|mvlAUfO$>zGE16n&x!|ifbUgHBRPq zcvTO_2`RT6T9kjR>FWdN5oB(s$29%S>0htuS6}D~m$7f4bC=Ex|BSFJBg#&kL3c#f zg*NdO&UwXL2%eKkDw#gvUpXWr=3>9h*9xcnP`t5Yrr+mZwr08uCu*baIG4G`+5v^n zxqepKN3IDJYlO*cBk3a+u0zfG$Y4!%&7$_kdDW$*<*WO4R}__@2`O9IWyvJ$A0%D4O;BY8;P9>muVjapi+3k21qmWvQB)bL*Dq4{|rc_|22c6{=r}eYyv5ygdXyQJ#acQCAfVxXZdp z3Egpu=@a)GhKFg}HXy;>jX(|g1#m9P?}r|*k-VqW5JD7+1*bm9xuBjrYg!=pG}R=K z@&oQKEVqbmFogIL!h5INS>j!0Xo)R#tMR(MMGY3&@aUZCvzzhBUgS-W0=%{sm$3NO zR$DM`^U_bwYEwgT+uL<#)p3W@R<^y}6X1(i-cUvkcq;1&(AOMVcpW8vcuMLw~1_#-W3#1 zpWw@SnriEd&nRgv3fuM283UVk_w{BwoZKJq)%$Y_Q4{!Hq+(-jRYP0JCa>YFawk&j zhC0@CHuYDzTvJmIT(rr>>}^dL!nDEiJgCc5uX~ z9af6>4-SqNab`Pvg>^&Hy_AOt3IQQjAj6hUw@v@L9S`4)M51bhotXc`UU5Gi#rnK$ zw>$pFI1F3%)Z|%Q zo%|fd0+0_{HgXYnvcusvc+7>@i8rrw8q9|sD0m@R?PXUf4{1M#i@!FPdz`^o#0XUsX+gccye(*WqBx#E=vo(Qa4~Gli~`bjWp!ut%$1>vT9-4- z;~|Hqulv6S+r!)+FDw7jt{&H5XK<}|eH7=_GJgVm`19mh%7nyB6Ko)1rNk?y33yHO zeFakav?K5Xst~NetcWD)1itQIG9mH3ayBG@S6$l_!m)_%?&|@<;8`Ph)&|THl4qep zF40q&1-rV9fnq#g>|Bt(V)b&oo_nR&@8Kh+-%Gc9|F42fi_UlCXcnC~YsDfMbg4N? ztWo^NT6%8TxdLfbjOFz^-+!~oPbD^-Ti?Les6GXWt!Rc=MG7sfjxI_Oa;NpP#?Yev zlNP$%&Y-dR#Eup{{cx`4*j>@jzRaFQUL9%6F3b}QytUQEVz*wI+d)3<53gzokKsDi<-5^sIAg90 zS-b1P?bBeVf@Q^?+r5U*e>=|F)BM+c+JD{k(}A$4mB8TM90r|aNX6gO>;eOvUPsaZ^^~G!ZRr1U0iATS{%KT+k!T!H zqL|h=Vw9X@Q8x$?))R4kc!ySOxSglM-reYN^+#Qr_xkIc4o}p5yU*kH-;Q%D@z2#P zrJST>*zc9miSrbl{eJS1iStnXS;#|d5C0pw1%9V|406ZW?Y(we{aEm zKS>yk@sRR!Tn~>j*pcs_x6<}wb~V5a-M(|@dH4+Il6yhAN zcsdn#+3;JZ+a0fMO3hEiU50~3h$oW?*ZlOnRAYZ-U#bDyCsfo=9%au#61s3kGi4^= zKq1rwS)e2u>k^Oi1ES=$*Ay&oMi)oBi8&$mBTkj=3Hr0Tt!_iu?N|MS3yZr3 zn@%~(#+?R7v1e7pkB9vEV~xo~$7su$J8Ue%I$L_{>pBLT>Jtl`Mjf8N?sg9u8D>T+ zhqCMTxylV^xRiyU1FiT247VR;^?vS^X1&(SEBVq8j-hh=uLs|yQlE3?^J z1M!~ZU1!!UYbr(Uu%~+UiA5|b_E4Drbh1+MX_Dz%7qyPnA)Cgv|GE^epH>@d67>;r(i!#NB9h;<#+T}w} zquIyflZ!dSM^&^dbN3ZU!S`>+{z#~U4H+P)dvuP9C6ZfBaWhU8J z=Jhvd>9@b}V!}~@&*hvE@0#ObHt{2-Yu}?MnZ8rSi;M5Q4D)Vc^1p|9pNF<7;XRQ- zvH~>8A`zE>8k%)0I3s3aeuWUwJSRD4!9Y<`4Rzy*2EJ}*p^OBf)xf z3*vy|K@(>$K1b_HGiX%U^P>Nd&YB^vPbV$6QHhOsLJt^P{DF{Ks{7rc2l4sA5c6W~ zc;0C|==E1U2n#IU4a)f>lt*}!spS77j{x&jMSO!-Vy|+i*%Ll@$Zrlk;=#Fi3@_MC zh~K0j+HS17@L;Sncq}taa%~y#0^5JKfwL z_PTYAU17L_ZgY)RWKSB`-{365leN_Q?LI>b7TfP98f&m$EW*A?V^UNGlBzHqv=u|y zrSzpT{f4WI-RIPU%8AQLAs z?Ubm}iX%~_@n;A|w;uF)1?mh>ywM4F=JT3sk`Vqogz)d)SHz=XaW5VW3qsU*OuPRy z(g~pw*4>l2yN8G-*vaWc!4RgfNq3r9E*5v2Mx{W*TrqWfRsouA@u&u(?O{o@JrGo< z-QNl7bv_RJ&>*}#X(6UrKWY0QC_&)_iiQBlEL-9Z-UC8?_XO24-RBP8TO9J;OP}v? zhkfQoA70r#eBA55*W>XO-BafC-BaW%y0>iNpB_)J)Z+=1f&Kwy2m3xd9&0Ny^TBGY z22-X$Xnz@=6PvRG;*dA^Y;(KJY_4<%Lr$k%)m^sO{L1?I{x_1?4azI*&uoxns}f!u+_V`K!hwrLqtj_*7qz7AzVg9v0s7$s zL9e)QM1j+r$8E+}+`Up+&cm=ZhzcrUGi*WY%46KaytJFdsOVIM-AI!;dDpHUmh2gcqJ z!BE5;w5E`jhN5^3BW?@Mlar2Z4j}#?+L?}+D`7WO&0n)c^V!zm*;76o;m_4T#9itz zAG3u$Wj9CIy((fMPYlZAOGV6YRMa$viumgW9t;qB|3fYV+|PC1adsauUI~l-mtVu5 zXeM6dLaM*zwqviZl)+jdc3?RfX%6SC_X)tZI%#hG$dkF+c- z_iqTq+cTMw>ZTU{>m}D7bLSJwDpm}x+1K7xQMz6+LB#Q>g_Ttb%F-#qM08nOSCP(0=uHccn%}-~`O3wz z|H31?F;1x&Tw9DYS*<^|%dZ#oEIMgJ@8UHln-@`k#%&?$)i^f9zPhG+`L)R!b7Ai9 zT@s+^$|{^PLKTA?w*A;)H9l923+Y0Im=<%fC{*ULugHJ8*+LaTCH0Y5bX+D7|E={m zX0tw+?@NM?J50J}&}UJ}V7nBa;NKyS)9Q(Ye?s3b=}ZjU_V4O(HV06cmf5FD9^r#_?>lzy>Hr7{SVMgK!pe9IG#P*>RV0*gB}m*e+zj^hte zA*z1Cr8>NClIH&d)ObLRv@Wn`xB>ag!gIx_GW#A;p^V%h#3vO88sc8i4j_wdH5^?) zT-X1gdp%tsiMNBKiT{4l1ue(D_@DE1P-2wQ3!p{ZOL$w*Z^Ua!{#$3!#N8I+Dt9n! zRT4DuEv(Vw<24d_b&4X=+s?_~vG@7&=($8*SHVECD_v|xlZ_Rz5FP{?V(&A@k35Nx z$7{dF=KPU29*Q}yS#bU%A7aNEPPh5hh|6G0U3RyJEsaFXuY_5h`Lg*k-C^zI#p-T! zaxGCd(p=;$Lq={Iv>Ht2NAQz2A)W$9?~;;q@OeGLoJ ztfsa1nEsU^hrKkuw5NaI%=}h>#|?h&vP~=7E0-2A#y;UNn74Z4DdL!jVy7=uS64MY zzh`r|)8-#(>RVe^?9OP*YU)Sio`7#28ysCTzbh7S1cD@+{gZ3(ER;tP=g@i|5_vES zUuc^YBx_g3b2^S@Y8pLb*%M*|-jNFjfsxWClWsBrcfnGG{;8T+ zU30X&qN+7rSzgvYUSGc|Vc43g>KijzM_bQ=^7d8ro2r_di!#kET09mn3%ILFLJ6)^xQa7}|Cj$1 z-CjP}3gk6#wvXkpN!&!9v)Wn-qgnc^Wx<87Bt?dZ7M>mR-MwSS-Me<(y>sW?yVhO1 zcI~z6*3#?R)!Uw|AMKZ~F2XrW85S-tui`oXs2 z_x?EUaTFJiZ&;6rph$B>yy#rQ3CY0r8G)^Mp4SKA+Y!K&Nc8A%nQ~6XshX@)I!%VE zOH36$9~AX-9IoACW0e(wsJDqHuor+gdNP$AaoY*y4RuvZw1bjxtcQKroKVPD6O(NkGTeccLB1*YYkJS5nf0Abn@kXa=hI-L7X zg*SNQRD}{Swj&w|+E4WQPS69PXw(+t`|zABhwW4zv;Ar>gx6shC&X;5)y>XuyB_|M#rIxSa*v8Q7>WZFYfvsl0L^PBV2Y1%(JQaw_a z9nCEG|FvCrVAMtSpEuvlcUyWVgs>@ugcLTL9s(r0Nl0kH&_fML2!W7b3W(kz=V3$6 zf_D~FL{vmX49E#~Y@n!|D0)~xKtvG)EQg4i{e5P>-%U2?@BZSldDF|A{^re_`6dm@ z9b_Arn_{D9OEQz414|vruEez5WRULzZSquXw!59JQMi=Z@stZ?T@z_RCsla(uWQ4@ zl{sQOIS_IWLWTKGnqo)KXTHJU;K0{bn#<@U6~%z_n5Q|?l&wmtzdeyOzOj)J_a zc3}o{x!dU?cysy;Ch7rgFWAK*YYz`po6GhF`Y|IMKSGLPQ9H1o#>J z0t}Tm_(d8b4Qo}LlfyclW}0!?1`BT2FA+gzGi6)NCf}~NaeC+8!Dq2`g1gEKZxM9a zMcBu~Zj~bo4Jq1Djkk8cD#9T(mWhVS#I0QSlN>Kf;#RK0hq)n%3rrbY{t1^L#sV=< z%bhFQLwotf`^}b19$*o;JdU{MWNO3R9b!-F-pq^GTD;|jo8Gqy`!l?ZZN$rEoaamX zdc=4gUo_x6?+)P)(vL0UhhTTE6!s(~~&!_Rq%Fya(^j4P?iFj=SM?|8k;`j7^vBG2eJv$1`Docc{%G zVjW(bk~bhWB{vZQz#-wWk)b>*B{nfP)s~nWZ;J@&V+#+(&>-fE+M12I3IneD%^lRW zzP@OO{bE)Ulp**&KrwD$uxif7>mQ*?jDJ8BRyJRpTv)TSqhbRM{j7eFAA@+kiO)1z zENU{2fgxyUgdBp&YECtoxSz?)RdH8nI*(-HzS{pYUn~qVlgW(FB?X7XrTX$nqsbDb zge8Re2M72?6bsHJc(N7jI9giNKl;1qCSuaZ^fhLnYU?{VCS8reJYe>#4Y2x}(ubSD zq!(*N8CwGW%>rqhH#>n`g%)`QmL)^pZhebarH`L6ff;(OZnf*+pK@tfthz;B7) zU;RGt5A>hue?J~g+3vr~{~iB>0r3H=1J(xY3HTu3^MF%BBn;vM_d{4=ZKpl-jB?T{9EMN zKGXYr+UKi2KSYg)8Xq+)>ZYiDQJ+Mei26RNtMAah^ZPFDdtKjK`>yG`q3^c7FZDgy z_dn4T?HAoAIxTu}^mWmrRcrUA4MOFJ`;VhpW3gg-^_mV`(52{WxqB3 zHpPsInHuw1%<2AD+L-M@+Y`1OwpVTYY#+xK#*T=c6k8MfR_upy5pkpAro`37y%e`M z?xT2H{KEL9@dx9N$Dd1J2?Ysd2~`O*6Xqv;ns92si~*k{HYBzsUYmGJ;$4aB6SoXB z4h$JsIq;@|2L>J)csi*nX=c)6$!ceJ&|=f+nGH!`=8mbWWSpe znUj!X&ne6)&zX=jD`!E@-rSVS({Qddvg0h0Df|&*L3l#NQaGY;eBrFZ z1%-aZPW4{~wb7f*>QRQDNU#&bdE@#~AaVy8Y zb%ptgiYuPI;zCtq)!eEFsvfO+vg-5k{l-rpUpM}V34B7wgv}E^n;1B;XyTOGnzAO)j7O)Z~j(;-}WQgmr~Wk6U2UumsE(|* zRgbK$ss2m#%IYVopRV3teQ{dd1js+gA)jXFUzgD+r-e+WC6!>7Ln9A9weuzPgFI_40-<5=3|0Q!k#@O7X8E`>vE7*@<7 z0HF_~DCnCt9p+5H5aFf+UW9^#q-IZto&-px)$acY;6^9jrt@yAO~LXhwbH;|-rHFrHt>8ucm0Z4UC8$KrVeUWki; z6jlW4JObs3U2x z)!7J1aVsPM$6=?l8ywu-SXJkv|0aUVl?eQ?XBO`CBCDjwnM%XiEVcvR$KLJ!j#}Ms z@o@KRY&+J7U=~8b?z0q+eh?2m4Rd{(`=D5rF>f#(pCtM>`ozC6pQb`jg+2~1Im~2b zfKqkYOjREYG_>@AAqo3SA(P$Qc`pTkHkU<>GV#$DVgu3tM zc7(IDK7csgY=bU9xFw-JLnYXmna88v;@M=lCn%wqZ`$35Wu1$BDLfXq`=GQd`4smB?6;w_0kZw_;TqtBm!fcvtk@uZ=^eLn z%-9O#;P(sH@=!Lg12#sul89JVtf7qA#EH73(u5ug8!&=yP$64{H%*r_mHFa~VKBZx zyo|1;+h{dCM4j{wouVJ`owdF^mrvy{@ICx3ehBBGzDm3@Q2D#^xblLsE50*|sev7Hz||ZD5ms2{z+_&Fwliczz?U zPd7Gozy|UGZZXEL1y0PZvKpcH{B?cXwbi{8J_|KBcYxckE22yJ?YrO5b9?_CJ+Il1 z{!8dbA3l2Y=;KGPI$CzrdDQ>NFGs#Oa{kDIBMpol@jGJp{8z?4{{nFG^FyD%@cCNC zJ{zrWknOe-kvCAfNP~aPzkyJ~ue?j)imz}%PlJvY3Rhw@yQV5JiVcraCZML0ltBvQ z!IUiJgmN1CX+_ZNfB2_-ra24UV}|J?Jus)e^d5T2clhRTB<-h9=u>))e#X6y!}KnF zKzrzY$S?m!Zrq88K(C9&c|ts76^G#~>m{rlC*G6URMhAk@I|g?H?o`XwdlLp{p=~$ z$^MD=BwxZU(AU8U89)d4Ft(q4!%ng9*afnZF9lH;MBfteGMk+;sDLKWB$`aqsh$>) z=))`MdiopQ1?`XT{`t_ikW)O1+hFJLK9Y?`@j~7gx1o+e#`SX^&5Q7*ABCQy=h3tL zc^`U%7vS7&6CI^p^dh|i3G5K?X{?yRgTR}NLeInsjoz8V#z9tLG{$%$o6i=rCf3aU z%G&7{+yh?E9>DkX)?q%sfoj=Nyy5vV`wG0BFWJ}Z4EeC1*{{SQmm5KSK<9r?`BX|n zXed>Y3j!45=t^p$W@@1uXd>MN-rX0N8$JM+^*DG(r&%mJ3+b41tUvn}e4`(*zWj)F z!id*_FJgUDu+Et2@AT|T5$aG44Ui+O@QhJxPCJ zn`kN9OxLh&^cS|1Ze-80m+4ma3TDJz82@$bDtzbb5o&{kRto!xRkK;>|Fx`<^4M~! z#F_0Nb{^kKJb|-~lk7UI@2jbXt-&3xAv~Yw;T2dXFXk>joG0;QoXKGY<7s>#z7&?h z)A=Bt%97cC*a!+`4V1%H(KL1^&0+W9o08k{Ysh1+ycVH;-Yq`~)1p z&v5pA2)A|*W3~bpHtznO&{#4PN@Z;O-^s zVpmWM*8XU$Ath`Hm9Z8Y#;&9y)=I^!jht)|Xo96w&aR=6>`yd`{h3CytEqxLM2pzN z)XE;EcJ>%8W*cbypXRxcL(djDd43xG|Y3nQ+w{)vo@T|LL}h~Wow|1=B;*ni(Tb_?!-`JhTh0ogzl*D2Di0Y} z0VNe>R9u|~SIzv{&giYMIy-@~>MBX=R8osVf>f2EJ@EBa3mzH(;Xqu(UZhojCy9JS zRFQ<9v_Qfr6IXx+1D1N?usS=r$gd=gv1d4>x{`dz>2#*(W<@vq!rezVGnkEDX0w;s zD$M^Z1AbKGyb0OHj~D3|T~WQX)4Vf~ZBrB*oF^>ZYNwsVoY9@;XAQO@)~W1_oPCqC z7MQ><6n;hUGd}AJKh3dIhTooDgm>WRh?S=i>u3jSgWm+iUc~C*lY~XVy~DKYVZ=Nt{A`zmNXJQpmP!$@ zScp}+5$l^Lw7_KvT-t%3ja4D9Hb5)ZzKA7@Ae-wcJD5RlS-Ovg&jD}MY6_XS6NC|qqgS|(k zY(JIJ2r8!vtVa!4k3OK$G=|1fB`B6FK)Z|w9X1iG{v=7mWr8}%!df?(rqEPS1vyw* zkF#GurOlw3G>c|~R;i&mR7-VK54o!YpbS0)UD3ckqIuLv^Fhls(LzZFEdoW<3JM?} z>uEc6&|<83Lm&-s3aj4pw3M!zr>_FCH|6GPU=M3xf zgR~Ai>2I(eNf#8=tKGl)WW|(DIBDaB~|_zR{z8F zIUNBn<{>)9{>-+r@$3v{iiz|E9T#)~Xuq#Oo&HK+(@FXUl-wyc8C1i!pdHTAIY{I# zqwm;q_80mdRNoKuBdF=~^b=hGy?>E@0nPdwb%C0%#(ohOEjR}sV>&yH{rYqG!lVyZ zxslD_CiWFx(zIZX^##S@kMF|=@*tfK3B%h!5!l`J0e#*VRCzy8<^9<%_6jKJwV>K# zc^tbBlz1qp;c(F5k)X$;K=DQMc%HxquorMXoyZ4*k~`0nK#>h+uVGKo4>Z~npxz$_ zeTMHDf-d&~bru6k&dHPUg8FRiMv6e&`?9C;Qhgx!W{?QUxdjq@|u?yJ&xtkQ8 z3W_nE4`wUai{LQW*#@40Jq$qQLADA!@2 zT8luvIzhjd@KRpJNAPl9!AJ5@d^8_}9n+`ST^t6zc`N8q7j_t*fJQyUKIUV2rCL0v zwSIBEv7@;$qqMkG)15iGZr62(u4id_u}635c0GJ?o^CJJbv=Hu!&tnqrna@E*;w4t z(9&GLz*5}W*xXQ4+tFTcEH0J06^m|bZO6j7P4!nZNn(*t&G=TzctTS(egX*=CeF z^#VDIjiq`4O0@#$NxG~fda(8#QQNX`VU32jWkh#chVnTzt=96MfLde+L_$WY6<+o4 z((BEYXROe(tI)DjE7}{I>guf{dlK;-<&~RWJGts;S(oZ)S#ICaUO^0_fsh_f77O)M ztd7;fkJZAD^$M>cnU$rKHaFPwk3M~mFSgJqSs@I-aAS>^{n?2SE*0syq4Bx zE#VTqtfihUbH%I^L@S$Nw%|j*d*eM8cktS7w-iwo@l`cO!d-7RK(#&!?hxvrSV1flY7cyo?O>h z-&)_+*rrZyXsuaXZ=KrXsa1GcmO52>=~lgU%JS4|nVhA%yTfWB@*RN{(r4Dl^rn{D zrm>?-%u}Ul#KdC%snW}14e`p(;}$g4gOkT4K+ZB{SsuH;hLFdqd*@tY&-2**T|Ie@ z8$HTj>$}odSSx$`Y7O4)s}<5?RcpMOthh`ourpWJR zcj~$xzc|xaqc>xXKJC@?OnWu;5?^a=PeWO1yPH6*>+W)x-(-fE-y}j9w>fIP#9yuV z3gy&`;nLA^>1eq0Vz{y`_1)=fm^yXLolaxDj%j_b`Ay5%k)3aC=t0xB!E4}H8oJXo z%oFq6yqYLw>T((B8W&l~3(8+8O4^%<{G_H_}`?#Rrw&hJUq zcY#;_z6%;!>+74FYMSdBYt<%Mr)rZn5j1&)Gc*BDnOcS`Uv1XvzPV;mOIv$u%c6Pp zYO@xn*(;8QW>&UR-#rDo)E2LvQ=(UKiC&K-dR3R`J*C9c6nc+wmHD*vObR7>`Aa>q z^iU;wLzZUvwe*@4TJ$Q?ra-5)#cK*|)k@x~m7?``rO0-wZCat*ylS%Tcj0m}eVSUD z8`=!*V&ZG>?q8VrwBF~)(9zB*Q9HcqqEs()X|7L4PZ>+~n$Z^UjM7q1nf0=kmicw` zDr1MIjQYgaAt$~LZQ@&^m0(Fv3Cv48lZ?7VoA{RYcxvremaQ)Jp7``ODa%){mdRPJ z?(V@_g#66FM(HzazU;7K;#(?BV&YpWy*$%O%gAsm+7-Os8Db>~?K-fi727=&}RhEqafhWdOIAlqt#w4dy2zu*J9c2GBwy`f3w?*<&p|}mZ7wxwM9g7=vg`(8M6C9 zmw_D_4!P<%9CoP#rEFtOYirArjzt!!w+dv;(pcBBq**gH)z6hBb!22@n(N!zF;lhI z*GcR$?HVbSY0ovwK8Eo$$1K}JnDj8R&)YLK%%N-blc^(-X)iX*?(G#L%Wqyw%YvFY zEsN`gt1o)!ocgAgB|Sm3lGt%6US{IZE8XGBH_I`bUena>*Q;X)UWB+gYsBs*1ovm$ z*!N(=ihT{k&R{)w5@GO-fLkBz((o20c0L4-Q3KRQ>|_YLkUOE?1#Sd^4ZI!dGvKC^ zJaZxT1K}PY&RqsVO$JASaOyGv>SS=r38yZ9hI%cy<%DyWN1&divrx~|d7ODC@+{=7 zA}t|Zd3#8WL zBFgQ9lACH5wJl-}OA}3l=V5nU5IrL_Ra*Z%yy*z4EC^A$%#TwVJF?kXaw%|RshDiPt$nllc2AIG@lBe z)9^VBpY!lJ-R)x}Um5Qhe2>BR7<>=H_ZZ?GVn>W>olVwVqHGrtM~tIs zP(>V)al{C^3;v?Mxs0R8I0o1t*MO7_G6Z?G2*=sxNtFK-xCkmvIKM-y{D5;VBY1gc z)MWuxGPB^WL7fENnh_kXzcLF(+A65`(S2yA2f(W`gOBqy^57_i=qYjFD2kEBP@^&U zp_j`LqT~V$5}!Z9Sj5(Px&ytBv&~u`#i)kFCqAIL$}+)hk$GsLe9_A}xEQZP)yBI> zKN#ubE{B|P!cb4M(Qn0!GhNO&bL5P3Eox&6xF2H1c|^`QkINb7keqRj%NgfaG2?&_ ziWvu-Ps}*rNn*wU*AX)gIE9#TaE^%i&NmV;2!PSzi*e@b1S9~2n;jtB@kJNuUe1_x z8DIrqCH+lpQd@P%f$mgitD4@V7D{`QS`K|I+%>&PodA0^pcQVOxO1hwC#_aXmwJ@| zGgFtDW}51HgStZ4rTqR%^>$T&kmdKl{V;EYWIw?2rg~6^?YjOP^j+rnRjI?g8}N?q zcM$HL@b7!vgsvV|kDKPJ=KV}eT^GM-lnr~WUns1!0>83@-Il?a8=1G0A z=}#sJ#%9xEV>{q#<7!QZc{$();cr~s9qt}I{Hou(dBOr6NG2ID@YSwylI-E!)T$K-|x0p3A;)3 zOHuctzD2o_hUJLqB^}m5Ux%`5`VrF_X+L7x27LqEHT{TbGwj0r65Kp-cS?ItTAF*8 z>2;LpbxWt^nciU9E9|EC;Ol{pVE)W>On{kLar7p@a$48F#{~Jc=|_vyVLk&tn6+@G zA3fns7d>uL$JKF5mpK6t42T582)lWZxljkYIS1xy^K8ov=D8a9E{5*(*v;kLeq#}C zf^hE+-(_w>dKmlWtIW%wt^nL+zP%e(cAM`vuQzYBykL3N8_ZjDdzaA7+oj)|mVFip zW{h_;#<>||U(;dUt+`qDncwdYhw-nO|H!S|@38r}86fQDZ_MY+=gq%bxFxh3OfdTc z4p=_91fnc9xFrHG@_PYh0ZSg>h-I7(!=RT)^YU)Dk=_1( zBfPXJ)X75S!$e%=fbcblFOCt#%aHyu?sVazKo$(@UVJkUDahP^l{tPSb386{oFQ`@ zBlB{JSXg~Uj)P>H4~kg4RK{8*U1rF2d#rRRcjqD`9};!X0l90e|+ANE5H< z3*54Bzmn)%r7zSWGW^#f41a*XgD{Yzgt}3Lf#{+LStfjyn}jbtDsdPkQsx5$<~&nk zzFA_vQTZFfIAp2sm!-H%q>u9>Q3|)rYq~U#l9=Gc9`0s|VV8{crNnuUbol}=niBii zoe6cHQ1RA-!2cf-has}mNix4H2(xHqeMtnEnV)HX}&6ZLW@j! z1Ah;`t3?WUeN^<2M`da=WvuUHn6G5qTco;EhX04m>zEAxfDE%sx_>Fc@Ds8xl}J2a zm*v_gYvvo7<_Qr7JyX;VqC)K_a^w@GdArPAQo2+iv1*X%&ycxSN%dhF=6;#lR_U@q zst?JKcT4jsq0%sk!*LOkvSp4bGB1~OSt-+bQ<_K0@O@;MSrU@~nfv=PHAo>Ng)K7t zk23rb8NO78|Bv(?B7Ke0JV3@hE6uUe{EIYaNb~c;jBmP#I{!+#hX^xONNmr_^ovEy z!cXLz178)E9;%+04d=B+1>HFR}rQ+OF+dtrHIZTS=4KSRQ=HbpTt_3nbCioc+ zKnXyMBk@{`m+()XiH{kqO)K0im zjFc-BQl5p(iBtZuI4!Tn9%e42LhwC6G0s_vR2{evOt=Ri)C{Sna_r@CTjqdtc^j&O zWA~5yJ{zH?@+VLmIif`|2E?cyhn5g^0p16lQ3v(neWc2Lmf(F@;O`G^!XT7-JWiKe zaE^ZqqzHVAt4nQco~w9*jjeE1L0wlmew>ZH0FI`O9UN6$ZDT)*5WDH;r568(Gb+uB*+1^vgP1NXnqFl25pe05nqR%3^|=P$jIKN`}u%l6$=R) z@lE$BsNZ&+f8VbAspwzg`#8hcDD1ZzSO?C?@6i2>;Dy9P%4az0pc;}$i*Y)?TK6-d z=Ov)06hV?`8upt@z~h2Uyeyv?yV3!W#VkgfPKV^~RoJ=yUH7wK@02u49F!f zg$%(zbUz^hJP_aPbfJgML@&4+J6P~mWSYJh14-yBC3uf-7Nkb5VK=k8bwBM(QMr&J zsY3s0W`Dw2|2^#98ZhycdoHh9Tf1S3v#km?z!o|kHuPz|b7{iPbdu%UHqvAsfyK3@SElO1fHFy zgL5g?D{yNyUDb85MhkbbY6;y>*MmchUCEX~2?uT^S1Nd5D z4f{Lf^X}}Co1=RnGxs1QyUg4lcPGSMic;icM_FR95-h-Mc`N4hd%@w`0ZFN6*>mi9 zNNl|bIjjBbG`M_znmi=r868p*aR%fAYb9^5xAfkvg40ee!A zR#K~f3)V`_U+_T%2c2Pd*IWeeIT|a3<|g>_xK9F`b`sG;g?LhmK|H+21IW{LEiLgr zn#egp;vw8FJrfWo9<^S)(h~@6!b*~bwXg&$*95FVb*P_qtP3l!GTe?e<$j#lYy@9) zJL+i{+T2)4^Xyc!YjN_#@Qy((cmj zF4zUP_8GJ}N1N=1x)W{B(Kc^FeGYwqqmA}LeE~9(99&jVXSk2@52!a1c-)Ywyc_Dx zkecL>t-KfNE%YJO{U{;PJ6>fLyl0E7Cj+GNI4B43rD-+7$~tFwN4dMrgp{-x1)-o8 zNX8KyWRYUB?h_?lL31LU=JST|m;89WHy|}}-b558Luq|poXDV`L*}#*t_IN`(aXf1 z#WO0RLH%G%$Pw|LkO7sw{XyB6t+FSJe*7|etOL@rGw4?Iz@FH?sAo9>;HGH(wA&YN z&)~E{8`mbdjzw8~(I=g7vw${=!oABE5$glmN-s%$8|{+%(~tud?mH06fH(Ptd7Cu9 zqWQzD=u)Ph-YO-{gY)h78%eHKJ$TRVF;_SUhNXUW^N+DrGQbs}H(^!)r#XSWoUQW)Qlzk5#o@I^ab+yBBd_!~Zd=p#E% z*?O{5elaRZU!0G>ckTp0Z8ZN7^Bu(d^v*NRz3{95*nAcKewifMf*;vI+%5ly=?%Q6=M!gcJ>!&I<>*boXNx4s!QH#gIoG!7Z-0=a=RuF>w%uo+ zvU_}FU#BEJ3;J0=GD|9c|5JWSQY4FHlN^#;l6=*AHLDJ(8J(&4sjBR+1qb^~;ekU{ zhuBT`-(Pv?e)*}fIr25*P2&Zb6~^&<9MefQyxk~&O$tc~NeXtfclkQ=`L>WR81Sc3 z$xNnNwWLx^sjbbVEUIEt-n3*=8OxhWlebpwD7@9Krrd1Xi6&K+t?EYEYtiKEb$#q? zo5iaiJg74>v$|@n4c@}Ln2yh-+#n@^xg@D_JsXw2Sb~3~1aD$((igGz{CSgE@=Ep6 zkhBn=sqJX*?CR?5%(nqQiwfK<0O4IQ6wJ4EcJ<5oHvAza9gPs?b@;tOTXifJ4tML` zO#Rw`&1#WDOKRIS*FOWAXMY>aR8|&bFnM%J;YLJGdMfh6=>rhKfz!HUm#DfpafVfu+J7b|p ztiv7j`t7deAA|#bd(2f;X|Y;jwecE@YCb*|6jwMRRY-Zzx7t|Y&aTdm_S#g^l1d6% zL8xl^;H0)!IDO`*-|Y{{7Jo-HGpoXrRp);Idbc<&R+rlp3_F~Tm{kj;YpRm|iqnp3 z-HURq{8h?G{nA=#Gqe|Ec9BqHO-YQ!@)FVD02UmqpqH882NU*BIh4qNUQ_8!JM6m6 zW;dIyP9@T>*H-#!9GY&^Y$nUf55SHa%?^t?SF@{jyUU{}CMbe<=XNU}KOUx(Y`~=@ z8<1tK6E;Bk?U8K&xCfqboiNN6*p-rxY6Lzt*$DiPWwx^>P~+^igsc_rj=ZPB`rJ`$ zz|tu;;3TK2Z43l9+N@5`;Gk#Bbp#VovH(S@CoI5J>W^pvOdpi|&rdM{h2MPu223>p z$HRe=F1&Y)7T^Pu`t(dDpzz1zr+08h6R`X^mYt#+;Ry>Lzyf^0wZ8QjOh9ltH=a3) z_}_K}3-H?&D+T6jYd9l%Q>LJ?7eBD`ej`H5(vj-rMV{La!it@7qF_(>&r z3+sf1p$KS!{5ZzXO6^!0!{}0+QENm$$w_uZwFLYy(y%wBs7QWX)4N7nG>6w@ghVYR zzEwKXQHR@OExc0o5Gau;q9H`0%8nA9&74ko{N3?_ zGJ=}pM?kIewk@@+H z@!Vhd?;m+|rLyqmLR+w4!aKlO z;Kn0b><4&6`zmBgkxoQt-2t2fQWbPuYOC$g5wjUlYp6;N_^VUCHqQA}p!DK&k4Jy} ztaFB%m~W`FPd@)9$ns}dp56ZUGFlPYUa0phkd-asjcOO!Z-Um z^Ifk>OzIqePhKKFD`A&_IKczHgm{3MxI<|iY}oao z)6QraO}NZ9pR2lRTbufY$JxmNkK5^K4|*>>aMk{h#~O*Neq9ej$M{^F?n;C1JzU2G z{Bo?JfH$&j;0NK>#BuA#2FaLTRVjuqMtBsyBj)45Z5JZk!=f)Y0>6rlty)#rx%JDO zn$24sY4PbUhko-lGt%=>ja23PEM}`!>(m~4U>cDn>c}e#Dzfb4 zGU~`e33!H`$}I@3_CBn(3%jVvYn@7U8LO<*;(qU@&wcKu?|p0Y?%kU&5es_gOJ6#) z@zTpLzjW+<(gBQQ`Wr?{(*6^2^fmc0sRUPuuLMsCIOMHS`5ngq`zek=7*-6+Pg+2n z)#(TA7V=#DyoSIP6(tqOv!4-0x@U+~VF(y*Al%kEG|jR068R}Y&!e-Dhctm%b=2m%xIm0-kuwu$Te+kdBDe>bea-C`Z4sk%WM`wN%yM$Ae3 z)jm!epPTR71iXpWm%(Y31iX>el)*`!1U$EW__xG?Wn$iDL!g4!byezbV@aFiQXzfuvg>^fsT54V^7S4e#1ScBOk(0Oce>|u!& z)M6dQE_T~->|sB+NZ&kD5F72Aw2$>n4`SCGUd)fQgZ75Oy29AkQE)yp}bV zY5oK}!x{vfbc9(g) z$k^GD2&?8ClBFERdl2I_Q)~?$@xXybD2{~RCEg&>iunT7#_zmZ9jKP?244ckG`S#g zDOj95a9QDDwp?!a=yu&xXfdXZupe;1S&_(D^#78TB=usB~q0dJD7=a{$ww{r{yJS$zx;X2?puK5C9E8WQ99K$kr#u#Va_yUf( zfsfpVaR?s|DEdYjK6TP{e4KfJzoU@mi}7386C7^Eygr3AU%*?Wo4ABve3k~iiOHHz zdM~6zoltO=77A{Uf^X4jy6m-hNo>Z>_L&Uz1%Lhv^UmeFG%;^8@c}Dh8h?+?!Mp*i zh_S~+CbKlFCADzW?KGzXL$jMaxl_+jJ$`rI+SSi~2D!{^-wh7X2-k29ui-0_Wf~uO zJ!E4MMnmv8u3Qw3}fnN$z?Gqv@CSrhq?S`#lR;gONzELM;?b+)j`h z{EW4_trn}J@V&yLNMNcK8|!0*7bC!7{kWgcm6XVTLm%05_${K%*MWoAh&DMAB`bQm z2Xiu+gun0z4B$t>w8ywfpbE>0izq#xMKhQt1$ybJNI&VINgwkeSF;ooa&td-o(x*gHv=Q;N1HoOD>)lwVw`DfRAs7 zOhr=e8_Mx#KRN2II6sBNqwa-mGxJ~pcu=H?cEM+IUQDHpcEO#f?jGkpcxK{$dklNu z>Es=4$2*!#=iUs$d?^?sW!=OPWd+S7=@>+ihqhq!+e8z$xyA^p>8d{i%4q zZjuI5lq%q@^8cH#DN0VNM)tmFQ$j%=475`p1C?#LlERJ7U}mqEI|Pok9T@6e@;| z+JQah0ts>Ju~zbB5hEWXFJ3wZfiXckj>Ip89idkD{y)iv9S=9fIybP(%PctUMg+Wt zon@?lQI05HvXt#T(56EX8jpR&FA@Z~%+<*EUU+VCPvYkr6`M`}1lNHd8{X$zUkVsI z6H!xh*L#I6F8sadjwnv4B7Y=*LyBXScnkqiOJl}*}9t^dURyA&1W^+2MS;4VcR;c*iU%v94Oqj5W2*%;$Cv!6v-s-N&Z>zvYGb6 z!apm|Lnm|6tVxk>hrvdWK!lVc?}@BQpxRW%SLG`d{T|XiOZMM-#~rusTZ(XZHQ#9b z{cZa7HnSHJ27`<7aD3il4?XngXk2%fXMEKXlVxc+V!$M_H{N1yqFC-i^3gkIrkmRHSd#p0|2aqVP_HUD7_(-})QcIf*f8PgzxpaRZzz zKk8R0s%=gTi>$-t)5gViZ}Xb-R-cr(CBm%j!+m1h&hazkhrr1ocr2MRNIz3jOxU3S zFPd3WwPhTW_xx?iyszB#iGz1-+4#828L)q8=gw_gckbMJ%hp6}A+tU7_~Q>nfh-%L)pLTu}pPS)-|VjE?;G6fUjz=nwC?sJ}B>=UZVy zeJgBQ;-A&CDChoi^T<&5*@9&!Z0uVdFxh6czTlE|oAn!QHHEh$?7(oYz2j$Ot@-R+ z!mc(XY!25}0jIcJz#ENhCGCC%wgji$FS;7}I147h+gJIzF;pt@OKH?pC`V>}pM9V?JA;VNvg_wK>1X>2|d`g6)lox}YzVT`{or zG+@jz?a@IFj1>aL`AK#dwuxW zC_indnweg=RASy^7Oqo*rpL;CRGRo7L?O}fSWT;}f4 z%}vm+EO1&3Tq_V^81Y3lbcx#K46A79b*i(g+MhpkS*4BLXggtT=kknMH`yykch9=z z#*bZhkE`&=r_|5ip{1|7Fcwg(IeVZk`jw|XfB%}7UV?NW$0z%t#k4P)$cGjUEjMqk zJw_!{>@M*0QJ!g9z%3>JG4@!bY!q06Vy1P%L>oLKHj*0UOBv~ES)(EMlV#qIOvOj& z#Yc?UG#@chtd3E+yt_O|G#pPW`J)S5cJuYQ^k5rK_BU>O; z>6lu8_f<^BYV0w;sd1|8+Gm;kvS1Ba79AFc@JLF8AIadOTn2U2FUsL<44F6Za4FV% z4>AGB;bMnsIo583pi&~d9;`hy0sC##@XOQ(w zZBbSARLCzjn+o%H?VZY(s?zur z=LmT7ByQ3kLvW!1IsXYRQ;aF#Exh}JWI}Mc71&UANX?fjSW|<{={%G7oVtY>MXX|w z=__NJ8`+^k|EBYGQNJpMnNwL;k^<_}68PmN(L;0!IHwb`jEuj$4zi3=RS=m(UR4SC zLU|*7=;ukpf0%!1YW38rhcw0hfLiNnR(@&D)X2JggG2H93u5ow^Wz(=tMB`A#v`1nW=DjB>&IQ4|(KDou6HSM;k7L&_%rYRH3n08q_ zu`^7KYK#1NQ+=B^ET7`{?~udZHlT35X7c+BbCZo#zOFR;iq~5hj;!9rp7i@&K*wrh zSMJ_j*kiMTHykf6OAi~e^j*HD0!|tw;92%q8Jsjqz-!r8%HSj$0$#@+H|!b3Szn_{ zUl4u>-E5qlAiRsHraA$gXaJQ&Feg{H{^zr{JYojREeEw)Pu}+2&#mDYz5o0J=CDu4 zSq2wdz_KojSGE1}S8lv5{Iu|hQ?=Pt8H`ySl3xLDrg=d*>M?RZ zY&UwL(1C-tJgP1icOZV175!s%r%z!YEj&z)i+x-5D}|4+GwDqsO5R-#`24Pbw;67I z>@EHhIP$SMj$bo)FpuLR;1s_Jcq3~rgA)$~Jj2=r96TT#1)O-$&3Qmo!3X*H^D%x# zRR1t5uMUzYkb5|dBjfL~KY)kWy%sai#Xy&bK1e@{K%?BU@QMTb!#U@*t!bB;YF6^Y z^<4v%RqI-oF3)MXrflt;SWY9ua>l{^o~TO;=%|LJm(Ou0l55wnI{2sGwX~(`X6!B6m&dkD`ADaO5dFu)#;*aMcN)`$S7eeP33w zS9Yvx-F~X0HQ;foEViPvZ@6OXxtX-OWd12DST@z*3p6`i@xYozKOX5dxg1qVOQLaY zI(~A^>|_n;mB8aw@Z}|}lY%Npn)6q{Uns$wm}dgc`9;sC&?^8qk3v$M-e#=VFXZCq zQdlo5ANOdVuVZfG^WY2D=gH%@%DrWOr*)kC_hyO_OZ2>0!aIv`5&9FI@}M#oJNPagZ~u!EDc^@TrA@9wEN>I%wVn9`ZNYf5L&lgF~eoFlX%MUG6Y zNEv6q@soIG+j40GxOH5;ZO6x&f18@?|M>^v%{QlJ|F8Za%qhx+1sxAD)PzsqwIwbW z#p(;+AsV|eOX~khKyI(FyS)Cgqg}PlwwaD~9D|g6*=bQ74p&4k9qy{gS6}cia09Zi z3K2=abh3o(`LSk(DSqKWOEEo^p-g2;=FB{H)j4lU%9x+|n$jy5m@YBNqifS2kB~=x z;DfNMWNXMb3ENp~_$FZky(Kuefu-jsY;9e+O_l#xdcIZeo3OR=gC%$id>myMo5xR* z*TQ#nQw46~>_=BlCO1Jjjl-I&iE|)|a3wgmC$HssYP(x_kj2xOY1YJPkXOjFG!9p+ z5*v!jDppkucQT_LGMf`&ekx=Ed75;5!q@z&L{E-vbCEA)K83@%PZ6ES!WTVS`dl-& z=?d9&#R@*;(J?qu^rAQc!!s#@4^Z8Zp}whV5&VdhDfmRpLd!%dsakMq#=<@8wHrTi zt;H9em+7nY2S`Wrt^9P(5tq=4j zt=qJ1JvtehoS`JGwU~h@e#@X36qexBIWORie2odCCXUhUqvkj%h(9d4?1hcp6A8`Y z;fvExo0)=&YI%lp7q)%-r&Be7J9$`vJH@mUaIy~qo-y>7c<@)OZY5$7S!xOLyd)uy zQV8w)^s=yzqCUOI{Kwa| zT|W2HIigpae0HB_k;CKZo;5HqJT%ZhEJq&SwEw_?{rj)e z8tUvJhh4F$N!6|>c7MnowKNN_^`nGV>a@)9JnjEKOV~ z7#Z)6$#+)Us|yQDOf0hYOrELii9~t|?{J=`xMgca)JrQ`#J!E7*HuWh^4`{A=Wu}!3~4~#4AKjVcg6EuFG}M~*qFLWwuItc@qDX{-hk5c6z>Xn3)3NM6KT#pBK&cfQ%tu}OrtHPuth zrz5PHL_%GT-Y|E;1jZDZ3XGe%_2U&j_GZz3OGfPjUP+YsfVXg13O|$0-V`XY>@))| zdkgPOvEYU8kOfDqmPJqdEctH~A(qwm;P>DNgO5Js8&J zt=N0ktXZ>KTV~B_+1ESR;qU+GD+5CfgC3=-t*<7quw}`KDxv66pX{EFY_cI-7H~TM> zeam!}_8ccCT=y(Yqx7uvpP;1iT^SbLs8{TjFR|qf31ZA+=m+XlKQ4twWV+; z^odE8#^`OZZxgNf6C=)WfC)-XfiLa-*e&HVHC0gjS^*qxsHnIBc*x$VQ%2<3rMMMw z?G;rSSXET9*xSk-kovB}O1!sNpFSMv+H_g&n1LG#wkqW}%)I}n!?Fi!vIfyK@jl7d zf^z;~8Zbs@mGmU7(3tsemgK8yfJebUBCP&J@O| zeqq3CrK>nxU@`&Epk6>WYB^3?5sgXQnMU)rN&7h*C#R&R%J8W(<{be1Pn0_{#&2a` z#OF*Hr;&Y+^U8-k1a=)0{Aw$4PAcPEv3H(MClu>K7S7Mt!PCN5IEy|#V@JPn=B32w znfmu7@WD>GECbIL-o+(@&kI--{IQ{p#wcIKWg5m6*mWvdd zF6DKRjtKsZ4GA~yZSq=lApxZmVUj%lSK#B4A4UD86>KI?#`lkuBH~+L>T*K{6NOhw zebB2+Nf{t#I8_(ya8*T>dY9K})f6+XP~!kk!mc~5R>iVN$kcL+o!>Ua^a#IfN)=a6 zMP2gP7fbR6eBK&wl)k~&3p>7vwczIF1dY`}_~KjTuo|H9O>6B6TNKMSdcM9sX) z;YnjnJ*@<(2X+kC2V(V59ikl%@5*A*T5ypx6;-aZ;VY2%<^6%hMazSoJTIfFA@+7K z>Izh-kC|X2a`qi)Kdz#BFq-d^#S8&=Lxi)|Soqz%$kfR1o~U7Kywlru@;n;&dJ1f;pC8 zpPU4R$h=PJ-Qh%~KDiX6L_;ngQeDf{k)`rdBwrp&SDLf-G~v!auiOpJQ6CJQ*t|u0 z&VcuF8z#U;8fc!2E5VW4UMC)6rYE>zXaZh4spJW z-&oQ?QViWM9xl^K(8b|0-i(xIe%JD@r5UTq7RZ3Aq$b+}Nlx1;P&2ah2O&CM<_xh> ztk64t6P-2l7v2T2v>L@yTMQSq4hXN zmu;x&)}cPxY(+~Id@0X$xl|Ti*3`UMuUy?!?%}dwTj74gzsA+V9eBBBn3p=^=0syn zV=4#SMj)qe05?NYCQm#GJMPez8A+n5l?{tmVg6iWqSw!ZWHEF_!jh_Av!v+PYbxQ^ zv36n>@)@uxkkfjcnxJ557l5;ig1%x-k(0f=vuFp{-SthJ z#3E;l#mGwipPkvd z>lm_8)95>T4r)4qB9rJfvQPaJ_o)`SE%wsf7^5C?yvua|F=m~n(Q<6Lro+)g6=$lk z{{MMi>#7fyUDBpVkmWS3KV; zQ(jqElnVJ`WMR`Z;u6!mGLpv`NG;lNfb*EDyE z;c?)ygozVd?U9;h^StY(+oB2{pU<0Oix$WnH@Z0rT3JqG`Say8R`7UUJTGWexUH)| zyqY;anehtybd21%Y>LS6BYsoL@5!TaOkRG3B;?owBq7JhPYdU#SnCFcy>dx5XdfZq zEi3`Pk_-r0N=Ue$WuhkD*~L?B!aPhAza<_}^Kc}0|ZEQ&3jtdNfgmY)!B$8p2XoNC(d8$?MivszqGpI=Tq1k=ACRL{j zJFDI9_&Rdxzo2aEdAvA2FhHyw8Y&j6G9xWRb6e+9ZIJDvWUDOA9dDH>zND%tkM|AN zYlM$nwGr#n=A)QzL{&V$f?_!YkMSkh+3Rw=sKq4V`#FlM;8P&AJRxW8bP->zb)v zD`xEJjEfnS8xcOgJ3O*?`KpTsrq9+o-pYOtI%Cjv$<$%AY8v&;&Mt5rQ9GrbWE6%e z#^dqEN!_-vsnRZ|bCv5F+eiCPTHL-iVmIY7sm5$m z$Lv`1s&<>d%I$0lct+zXJutU-WL3TCy@pWOis}itsk+Mb3j2=sj0)`U(3)+O_k&AJmA> z7IJMOV?)~*;lBA9Endqn)(}YaSF~^H+jv%2Q~yx+$%B@g|d(hfA*xlIN zg#=)P{ddp$)-@;OR{9k0rs2U&ZM>v0+}$(B<8!6^8(W*Zn+rbL@ZA)J z_FVe8(oL(N%v3uWdz@!^Q`m!d%h&J<4U$-;Vc`O3<4u}HgHtmDh~13A3kCaV5fd<(TpurlD z1z6TdQ#Ve-r3@pH6F8qImQS0E)@dAg7I1?c5Equoe}){K!u5ZS^cZ?i(1{OSAns5P z8&;lBrF1|DNbnL7-cq6A*x05@8Sn+1jv~BG`bXLG6Z6(h8lPfxF;1&IQsjp`D&LG=J#iY$aMvgVl6K9&p2@Mh zJlfuF_#H5%EGg4a5h=#)az(RJ`1c9!OyOL40B=wXjEI7Brf_Zn$FvA9qj=KuWfTKW z6pL{L#WJk`;1sdrW~*cEqcu^+<~VtCF`RF&ySmDoa%djCZ$^FE0>*yr!yVd9sush3Ih@U92aGx`3y_tA=7K9|lHk>SK) zIm|_oA_4d@GAJ41cwGEN--lxnC+F@p?KibGo0gf(nu(bl%2HFtQD<6eH_NiDSY!(T z4b93vIpR&#nHDOlq5y7QgkyAR;P+F9#jh8B;7E2^e1lGw!#TQ_n$lL6Gij@_u~yx0 zwiVJgtIjf7Ht4Y?^}^5inL2?#Nyepy)*axIA>d?}1U$>mErXL?67X7fVHtd4oH}+M zmn57|WIqCbC=)=tE;{*6^@q3&b{N4k8XBp*L!m{pOoaz<$78$W7F!-)wqjZ1GuhO& zmayO6V*B{*1PfZWcs1Kr0?E47@#cTIRNbQOPnbRS%WW1w{`0?`RzPl!>#mRCfE}HZ zY{jjhPU_ynW%lCCB4JZJFp16x5(Wwc*vra$YJ;cVyz~L3#ho#4aC&8{OFO|_Z7zpBTT+xOY#CPxelX(<0BiT1(`^1SmviIcb>8M@# ziMzb6Fy9|2?qFL~w_Nz6G)`{{*Ee`@8UAh_Vl?O$OW|hzlI9d+6OPv#Jg{B)A=e_rz%@>|N+LOH=nF_qGq_%@$0BEUxJe?h;dqF*rgJjWI@22jd`i_`31t$ z=!l*x-Nm1uS~;03-Ob@sGyHRLCnSwOdEX_=b9o2xRB4ziT?TH*=pk?9JL(#o@sXqu zY9FOF5__!9E)Kzx!Cq1|Dz6qdj3@_ppp`4Xb7jMVtfDxShJ5wPg>vO^cH4J~1o~v?o)+r&AS-E{tmJ18xI|+gDi)OX2KX~&xbi17 zu38+4#xsh=H7nhkw}kChtL&_gwN$lr*RUsQQ*DUHb-$N|EcRfw=0k}poR{~x-N9_4 z&09&kunx>zftmSDkw#vePgGtH*gd25v-u z7g2#;dHHUczQqQ9p^e-bzTX952CgzCK33N#DWy%fGFgkIqZ*T~iLtM-_kN`Q*yHl} zee(Kncd)MUx? z2rZEOaASBIF1x(zWX;p%Aouxa9K5hBT0|FLE?i_$?2bR_K9^6o$QvJj@R3Izd~B0q z<^bKN*)`dJ%}3}$%)QqHWSc|tQN+0zvCN;~6OeT!xfiCB>?M+(6jsi#cY|O1wQ(dF zad>4c7^aRaUPi}RsHXMCN#s<$^>K$IEFYS@zr~z(ScFyZ$6z+qb10N8eXm|MMYNb7 zGEGq&p+H#z^cCRcYQG%Im|L68F8lI%?ADlxdE6by%6lUT@4e>wm@Va(9hzcU{^nLK z?u`V3=4zL{@SE1opxNOXTErT+|8*X=B(ju-r}`hPh@0OGhCGBwRMCW!E4|;^+1tBw zc=)uwzSD-g=g;r%nm4aYUH0ko&inK-@pH|U*IaWY{SUg3(PjTFKTSIWW1aK5ap(r7 zTqk=@6jgU(;qJcI;qbrh2wu&DAQAq^ELP{&&23KG*aIR6Vif7Z_diQnCXC6yXh?_# z_yNw85^M-`OP3qWcA6k`!Tyfgj+oc%i1n?jIr*IEywx9K9zNT}wLXW54Ysx=n%V;% zd%Y`|4s2Q3G`e-i?#gZ#)b9hjf=^^SpHJ!1VPM2M6 zbVrgK*R;*|nmk)a#T`p@Pl<}YvI>e3kwvF#9t0PmM zN{}C1hh-VNT}Rcd3H<%n@ZQ@4awV z+ln*SuRXJ^v8(^W9osq^8ak_*YpX_5?K!izHapy*p1FPAs!;1GE4H4reAc{RWYwZA zXR=r3HZ|vR)!B%v@X$ziU0aO4f_AvAcNlza86Sg|J_Ej@->hhVNSVsDDdjDG-zP`o z+Mu@aqDbewwi7ygPwqSQyesahIn=GpX^(Xz=j4@~M>9=ulrx!K7_W?-uz)icTutKp zB)*Kb^m4CQZWPGE9dIpDRr`6W(`~o1I;+YSxkE5x6;^ZM3Kd%(bQH5176Hy_;Zv=0leial}fAftpR5`9*jp7)w3WqYsB5q(toGZ`72fvv|Ht`R91RpRSr+c z=8JW=MC*HIFY#NEDn#CvC0HCQhSl;K0$);kiXV(OC3{UVYEj=)XDsxnN_)C0S6}aQ zIz2V%s${A`v3TaCyN0Tx@%km5ED>_sB0+3*k^wc4jRq44u-*R7w#o*(to79Q^`oQl zVaVWCXtxK+#FAQLFG+4;xrv?Th*(3W!a-GcSY_R;$ybMUt2%a}!vTp)j*rWaa!x0u zc{6ew3M1S(ZBH>`8e|@oq2)`|Ze~|ILN1TFaG^cqX3G!H>uGCQ_=y!?OMIITKgUk> zu_G)6QSu(X1CV4UN#8GzT`j$p?v|$R zA%;sQD`K&V3dc9=s;lE3#hIx}Ww=MXoz0h@qkRLdu}H-tED6ymcgP=h#yow&bFuJJ z8ETbT=e3NSs5@+`X=5m~(S$Fq);G>E<61WB+_|d8Zug)|d{dRlY1ied?V-^2D%tID znW{FaR@GzQvc-;hP2+E|9l|%5@I4aTBhkK${3wfSh5u2iYzd9I6t`OoMC5~3)fXAt zsakCsdABvJS$dv*nofumC6hjRULp|=S67Ewb2a}RuEw)<(l=R#Jx|YemKMg6I(y;5 zj*dl(I=(4>bu6S<4f}HGQ`kj`-KX%ATnFW{*V(3EE*EU#KiS733nKVe_^|km`7GlH zq}!y6C`K@%5{fqs&z`CX+C1J+$Po=XGsIkiV9E=f$nhEq z*%=1(dzxZJmW_QSs)TiUQvj__Yx^Y)VRvIE2XY+$4BT2)m4%C^lM#}?2D?2`0h&CAF+D$s#yeG6z0#Y z#wEgGi`O0B6~|vKKaA{($Tm&=obGe`^sD*LJDm>wKK!s*?<<4e4*;ISFBZA_YDfc@ zD88PkbWE(JSTkdt49Q~;z~ZKGdXp{(r@~N2J9YB_f4)I9whDO9Nh)8l8?v?ehFukn z5wqK5uR42R=%m(`Tx-rO>wdjetYIXzBGb`TwZUbVEzQn~bkow7)`d;Y-Ka(u7ZocI z7@yB><*N!T#2pSi+mz?gA%*e?Lx}M_Tm)XEPv_2DhEf%!5T)Rtx{>~INXXAe! z{}MYNz6D!!KaDjJ1%X13iM2T5wkoCYeT&bdEwNh_hx}5+r&`CFoeq9d=sY<){$=Q~ zWTC7UHm)Keq1i*SH)MB={qb6-+ge#^b>k7_0NB;ijo1rv+M)FmSP$YBOC0eGB^>N( zT&gM2dCeu4%*SVZ>jtS>h5U87kZcK z&ZYeiR>U}fD2R=p&sZ5YL;>zcgzRywYPqscpcM>>WE&JkU{ebOJYHvOG}eNIgk1vy ziE;$5;WNc59_(N0^e*ZBLPeAVdS*|-u@~)7Oj-qO%)kal3%n`vlapOa4yCrE9JGh_ z)^q35&pp@RtDxuW9=C4Q-D4IHkxFA#fzL0M#^NMQ^wNNtbOa<-j{!zUX_&qAZHaq1 zx*qu-d@zsx{^L5fSp1uiS+(Nkc+ql^YI02_KF~pLLh+B_PhtGlX_OCD7_p9}Gn});z-T9i6{;qfoN`=fr5(rIk|2Aw@>E`^>V;&wr}{&{(ZmZBRSWMWBhdv8c~vdsl17^CLkUjhz2TVU8+eglqZSG_eX!J z`)ods2S0cg3!h?tJ^ZKG^Yp30tSO($;8Vd#pDKxG8=ilL4E{4yK2wL!_-Fo19-n!d zKJ)aX&&bH@^sqzhJ2<(A{8DFz_(MhG^0*0BYX<1E&V4S|K8M3mY2CHUTH(CUPVccc zRrJ2n+2i~qt@kG#VW-W0<4ty(JvtpimI`-kvBydn@R^FMM5~VCd8%zj=&}kv6HuU; zo%Z`GZD*cot17+kiDJ^5ZnWE+VTZuQ8MSNLWC*Qmz0@PU$1ykz2cEU>cRKeUkv1Bq z4V1{nI5T6+c~#|bYJyiYU`Ula*lxs#7fBiRrgYiZm6FQd9RK~;mH119y~~E>H<2NE zL%N)*9qf&_=@VgDVMFr&0{*7-F%Ez8eZr`by(>M9=S9w^n5g9{1=XB`IL_|~Sak~O z*t=eL*XAC?!Jg!jcHM_hSICO=5JoM2n!AlsG}sojGTqxGqCr`)+AJO=y|^vyb$4#* z0ZxwbYot4+JskT#k6ne8ZXJIFwb}WQIb_}OttX-$B$OSV0FFRxIp{L5jBXf!G^&o4 z0B+an3Wm=7@CDn0q0SX_BUm6E&9%~HV1<|4XPsG5Ew}ppZR-BZKYE#(^7~VAl^>Ub z*&K4=V@e2jgI&7b%1-y(4J_tMd)agBJ*k(zA+jj(v|J@A4~57?kQVT}230!LA@&d? zjMzrd54oS#n=8`N-Q5y_S+L>12;zE&#f+B@d#HKV?542h z_{PvsE;l%s+e;tRoT1#Tp4PD4p;?_aM4Rk&2|8HOGsvfJTP(~M_9Q~2XFL61q|y({1t{2{p4VAfPdWt zEd%0cGO|4JnF%l{v}CZdx>~d7@xg|^g`GRs1m!?#VJKRmnSurW7&Lx|H7V*o`{^1aA~xmdr1(50%~ogK)4Q zpPsa&-oDw_Miw^9SeAb0{Rb~>SlJ$T$hAGS3s3YgJGZ}oV9faD^S^se9LWwgDbC97 z_08%9d)*$@<8~)1T>QmFAHf&gy4!-@g)8`bS7k!{Vaei9q}Aa~qg=-%6&0AH4cnoKgM0 z!)ar;KOGF!ZNUvHj;uLWdt%6!9s49)Ir-0rUT4`faRodD`k(8_VZ1c$r{jU;Jx- zlz-6z;f2l}_B7VDr&y07BQi0iKny$({mnoWd-EK~W277tg z>gvi6vyx*gT<_6!hf8x+$FZqF1z^<=a8NyK>*+s%zg@9(ROc@UE2{p{9pD4t7OjF2 z_?1u3Qch{r8>Wp^Ax^ujd918%Qc`v3t@3silc2OK5mEwjDsFII=!X;rQF+@s;V zzT0X0%x7#)x29RNd+xESHiz3`y=R2Ka9Th6S*sHdTh)8-!B zA#2%yb+-Tq%8{fd?~5!5=)`6OXOHML*-PXA-pE;UIm=sB?5?V+!j?PvJGL!M7xabi zNvo-pn*Mr(-bLVBa?&Z_`^K^y4By_Qs>?^uRk4E@YiE`5o4Ld=_ovk~{_*vbY#8TZ zs(>&u5`+l6HpwIyg`b3<@w*xtMaY7q;z_o9{!>*||Lm~^Jnkx6Zm_EAvU%UcWR|=& z=y6wC>pStrwE`xYuxBG&DysevGIuVN-` zQO)pwT7<1EJQUSzs+Hkbd|?|U$=t5EOfKiPgi})u zVl8P)WgrQ^ZRVROLUI!43EwY-z4&J1+nx4sVVl+Bt74zRVw`+#T#sp2US&<%g@C>0TlY%b1a;NQs!;LPCor^x#jg^2wDq=fBGcDv2u4Hv!uQhbH= z;5?=~S2?6by)NDIc_nU7TCciNi|O&=WE7K2$R({7=d-xA7Wca`dMxQ0MI?n^TF6ggP>#ahKnwX_RACDuzS(Y^=){_1MrXKIsXY zT~*4SaN;vIrphK0?lqaGX+F*EmKBR;wvK9c)gE=x59I3AM1x)taJVe0=CxFegxedEP3T^8IPE6w z*B0yx=W7mGvm5uUI2StcZA^BzOUA5eX|e2JYbaJui(Y_<{UI#uQ-w>(D6)rjPqlk- z_zrFrJ^Bxz3<}Odo-O~HeHH6nOZ|hLJbdGe(T^j>B8v}g!dg(QmQPSJo`MKa zcF%%rc5!pZx`u4Fw`Sh@#)a9I^)0PyI+_O4qjPI=ZME#$TysrzZ+*k;;X{dwc8CKc zk)s9S;Mjn>5XX*YhfVgmn(9!}yrZ(!X;u7A@#bzl=nbpuPuY1ecK=Ah8}LT(=9GgK zpPw^EyO&~blFZQSiuc9dCz&0Be`Zo}{Gj|(f!E_WCWvX8GUf z=Qi9hQut@izB3~KyY9n({KpusPm%u}KgG!ZPaeR?n2~Hg|ALf~23O;&Nno24M3!WL ztAoye{KZktUW8tK?t512>oVJem%$y?$P2L%Hmv&`m1#Y#6y8F{#$Nb^l8%xktsdVg zpQAKU_lyb6Wh8H0ETLs2k3PiYhzrZw_Q@lste>imnVl9*clfQI1!i6Skj*K}7FGVD zIin}pZ{?s~bubJwrs+|uOEuXPOGU8o^GLfI&syalhTUe<`={HT;AXMAN@fp&n=(Hw z^&9z@pc#EVqN9Y%T297V0y|o30tmSyiK9j4GWXP`-l46nbpt`xvLqKxRhqQQ`Z+br z_Kwa!tG{QC8fi->J1bjT)P_x?^R~A|{cA57a(HST?zn&RqZeOv+uT*>pA-tz4W^st zqDIw?s_VBA8EWXD$sh}WzFw+_7?LFuk9ca4)_`i{6CE0Pe4jd(M+%+|H6WEZu;Mzj zy3CHCv1qrW%A+|=g&Um}p5@WHOe9*%`c^LQo4s=7>|x{mp7|$!cEP3-=NB%sm{Ae` zWYA(}+Sn;U_Vme3Jv~j?p2E=l6F1FYu<^wC102%Wjc8Ho8UKg!Y8l=GI4{5EgW;Yi zqXRF6GC{Q`US1NX_K-qkSFQIb`q+AEiozIFJ910s^@pA-C=pdgRhW(AxYn#p7_4z z8w`2gP}Mi|;Gj?Y{@5?L?vXJ5QPycx5Zu*OEu5zjUmH7b(uFJWktdhE|SQ5qiuuFr8?$zPE`g+j*J9&!NWu3_{GY7s>-1h7^B3K4=hm=hWM zMR7K=6NKxZ3|vduFcEVZ#)0V1mh`?KSa#qywrFR1sc4_GXR9C81x*Ijp= zKgi}dkUL=hju-+xwflxPf7 zYpJfQU$=Sb-Z{;tN-fw>_@}>a!JfIRE}6UgKhE8B(=xCx8V#R#@9F!0{E@;u`;JJ} zX={4BI(tDSx&-A9K_B_eEuwEaQ$)F|!$^gYF)Mm2I7}E^9^aF(;X81?74FQZK{bK~ zY?v$od82Fj+|+{2vvv&5Yj5n{wXvoPml-8}y{)mH`dwu`)cpRc!qBqz*>l*rbLUex zbvE0yd31EE$D}XmSh3#i*BflTnOIwOshRo-oLuehOIn=I^$bJ4>c>~Jzm+_k@%^Ai zpvSZ5qEj96@W0&~JNtI_zZ?oxW3wAwiXRVz*|nCz?L7lKd!zQq`+r2ktKy|!Bx5b; z;JXCe>7czX)g_?CM3Y-K5qm9Y#`HNVlJ)L&@T53}+L_-?WZut8?+P@Oggf*?9CB>T!YYmSS z%uf$Sh&pClzTU9Fr?OlFE|MRtsIQP! z@6w!wp8d?`&7aw}?cOaNcx=^mD^^^GPpv#@$LTMHTv|M`c=@s=PInc)EfAU%d27>_mkL7T)?W9J;|CO zzr0J@J4F`C6R=nIrP96WbRYdoG&d&_xm<*C~n7?_BnhM|JZdAHV3=mry^r- zM%V~meHFQZ@qeNZbtkgvY0=5s!7t0>p&-(8MTtc+mgJXNyvL(v*s26%>VsBRH11Y8_Y{hH0|$nLxLrly5`YaZ*rC0*&x+H>r|E4oGs zFM-;Y@pI9~`?V<%H1a`{tsrsWwNFs&{E4h3dn>)TwZU>-L=K|M!{`%EgyW5e!cJaxFPT6+f)}OP7 zvb$yvoYf#dyZP?TKgqT>)w;b_cB$qNoTS97uC*R?&u!bzNT{QuDB(TwLae4*^5gOSx1~ASpaNuLOTd zywHZ96lC_=fX!-=LrZGgHQ8dd2G;b-qXT&#E`hD_`Akl;FFycI-Y>UE8{}UJ+r&93 z*f^;)mComDYTDat{L!f27mfJkmV8ZZdwXq7J{$E3AS~ke3V9*!1*Y2srrcJGjVc!r z(kH}FAq`+p)+sI!ojc+}X_aOynk~>Dh;mMbB{WeiP0eAC2Fy z7Wr*_Yt!NR9hV1KOxO|X5$3VcWIGZ+20J24HS#)ES+w%JJB!QOgq5#3jFp$pft7zr zSow+1oioYGm(~$h9&NYNto&m~xAL-7hn2jjXrqhLAoS`mHah>W+31oq!VX}*Np>K8 z7(0Nf=K`q!KZ0W;d~eeg;(02b>E-5XM~eq73gXM<=zFXm8%_ti{oaXht%lK&=EI7< zSG1y>))}nm-bq$8>YLe$N@jT>J5yNEW4)2Z_kUnTrAAqk4q^uhyF?UBu>?Z_6GMpt z?Q+qyQ`XRlTxDyEQO8_nxbFW%^Cj4hUC55@LS|3Ke^uBWNz%#6oA^!nlUn}YMro&X zuJjw}uj~|jW9WzE^AWxMP1&LR(sa7%0@LND8%>`y-Di5ote8(XUtoUD{1WaqAF?d5 zthYRDdCKxF%iEUsRGT`gu2eUuJJhq){pus?*VXT;KUIHYU1r^E{jP1q_HEmbY`?U< zZF^5MYr3{sJ6-!X?R(nKwBKrf(Z=jG_S5X=+4tFRwcl<3g8fPR^Y)kRui1ZZ|GPtW zxE%eCg^tr57dRetJmL6>)8njg);ZgqgU+j*Uvnw0Ue|orYS$Lmhg}!CzUq#4_#Pu_p}7W&Th z-R1j{-|tWO8~xq>IsTjdxBKr8gaWBRb6|GhiooN6{|p9#)xlGOX9X_~-V=N{_*C#) z!B>L62>v1XZYUob2rUY&3vCbW3hfPD6?!4`lhCij&hYy1DdDri7l#jopAY{j{LhFQ z@kQd1Y@{nP5;-e!apXYcDfXEUa0YeB$c<*H)30v}(0AUMRSVhT363Au}RyR}x zM2icSDsEIQ6{R*TRkWzsT1u@IwWw4P0Vyg{#SKxA_xsGb_azVX+uuL*C1>W$neEJS zXU@!=J9psxfr|z{G4Rl!{6Uijy?(`nE2dwu@`}BQJrV~b`VyxnRwgzjUYocoab4mg ziBBayn;07G7(90Hor7N(d}#2Aq<%?5l4d7eo3tuvUD6{-PbEE@bZW@xA=eDKe#q(} z_YT=OWXq6~L(U|dl7}Q`Cg&$lO5T#ZYiQr0D~A42r77jwlvOF~QXWaY zEOlV&u+-+%=TiTY`evFdZBE*~Y5UU74YLmGKCJ(+p~L16TQuzEVTXsE9CpTSa!+ zEJMralF=t)a7KE@qKqdpc4X|%crD{V#<7f38Rs&SGqW;%nUgbTWj1C$o%wv`p3Jv0 z4`rUnJe?)8VzPQ?C1!cDMrEzaT9@_6a2g&xeD3h=!@tYcvh%X%Wv|UXnA0<7dd|%` zTXH_h9iF=&cXRHs5sndMBkmmW`w_=RCXNh@+&rr9sK-ZZqi-F3W=zJIk}-8-ZXEOL zF}ufno)?!lI`4_RU3sB=e|~-b%KYv5Z{?rxnS2v`_xN`D4*4y9w||0vo`1Q2ga7?N z-#}iVDsWBU`oL|0Cjx&BywUj=>>gYeJX8=YxS`oxUEK*gLIEDDUf+$rUE*KwBANq zup>d9f%~{HYZFAN+$Ty+<+`o#NYFkq-0y?m{|&?=T?JX%cgQ1G6fvm0Do}=tfW8p} z{x86MU4}XxDz6Y-e-`$N$?{S`oru+1g#BWWmB@RTaQs)O#mz()1KJsJ{QnhH-&O5{ zx>0q&X$-(F#s3JjbTLeU%n?QOZ{hhl!1pmd@K2W|uoVt4#p@vZfPQIUC~hMeisvus zCym>sL{S8~;3@BT{r2_P$uJSX{=VAaomX7>?;8T?0B0yUfgSWtc z1J!ro?^o@>b$~P^>ggx22L0>-Ksuli^)Uuo-%~}VdWz_p#U@T8cK;`4q%+@DV&NV?q*F<;ix5aO7;wW26r-b6WsxQXrL$o z#8WE9i&TtNn3XYZ2=Fo(kVNmoei!wTg14+v&_{}p&Q;KgzY1Dq`;qQ`ocrz(P8Cni zz&!nfK35a8is?A&uBRABr!aQI4;n%iHJ{OEC>O>*%t>WT<*Et zGGjd~?hwm`89gx??)G#XlVXy(`KU=j!c>cO|$IU2a#dE8yDV+BTs3 zfP?{w1JVYR4wyY)U4l$7Cv-{ZmVi$jCB!ETO&FIjH=+89SI4=d?);tq&-tyPrSL(IQom4_Q1toB z&TBt@`6GJpg})=Gb@!2d%0BYgkt0VoA6asw;E3-?%;B?#KRSH+@PflNLL81hZ2Ax{ zsC@Vl;L{I3_;A;U4-0W{qInay+ZsgPM9@w`lYSyk$dmG%Jg-R&2PEpJY_w2M4R1Df z{sr$0lBc`qHJf6zbauegI#(2o?N*YP}R7=8LduQ&y|2xIVEjDud#bbRZwTvUh! z;#RRrtj1T(){6(l??t=#gLp=~NC(jB|B_eGemPpaDNf+l!Y{=c!uu`Mm3q)XO2oZp zZt_qrmC`htPFG^*WC5u@yn=3}U(h?UKfMc0-T%<`Cnu%?kal{8lb!B1$vqG;Y7%UQ-sU0mv$L;r(JkXQqeoJ z@HTTHG(;z1{FyHniF(l}ZV_#CR@{X96Cc9+JsUB8ucS(GM7)KW<74rm__z2(d`?l~ zdvT5=^`pz^a>)8$Q4R%Z1dXC%=*pd=BD#j^sS(;LE2#|cc=iwtjMPrkw?+OPqD&BT`-n(7eUM= zrIY}f$|WXHFU%hKVZjig}bN zswsogMJ){%H_~L$KqE0~OcE=oMBGL*#O*XotihPP7WbImO>@OvbhWq>n)E{ zD|8`;itofMaW(pXrKqE9v7DyhoGe+K7QMuAoOpaHZpQq650#4zR3}Hs9GNXgNuSJ@ ze&{kL$su@;W|(x#R5@5CN{>vF$rAG=?jaqEz3>{!5_iySv5qRl12kXkpj*UNjP2X$ zX7MMwTXYkLF>W5kX!#!Axjl$enGbLW&LNCe?_&(zFRbDnoR7Qb# z7Na1Sdc=>=;|qxl48ED9H2%*+MvE6y@O7Pq6cCFEdxSJvT!VR|h4L}4_(U^gf~BP1 z$to1r(FAclO%%&$y!bUWi^r%%{04LCZ*dd)6SP=t#?8E&Ab0$c)`;inHnEFt7cbBq z7{gD{=X8=jpo8>pI!?!Af*eSDWDnU*c9*!0jeel-B-AG*7;6_Pc`>*RHQH>8i|Ov@ zX6+j5Z>Jsw?KXcwd6mx}4F4CjI~Aq9o4>ry=PziFWmg=#+VhH*ZZ+rmwnka}j9c=2 z+he1my7}9?`{%aD0n7jkz*X+^w|Dgy!_i()JZtIps3?nyq{uL~#zyJh<_>Q&&<<~# z;f<74?5*8^sAyKE11(y*&76nKeA}bDIPHG0yN$@^$0dNdelXE)_s0T>^H-w)3bO#~ z0xY$iQl3R4@z38JY!^MobswgxA{|EeVX5shWe47yFh!wCCV<&k6xn3*qn_sa^6Xu$ zT2vIFEG9o{2dM@?R+Wbgi@}l^Wz_ubCV%<-xxT(z;qTfecZS}1Rt1QPiqexHe`Osi zrvi*eapm>pMwX}K8OS)eg;?TbDD+*(9ZC+xhvMKl88_wVKPzrPg7joUlZbNw9fuA! zTskgA48~U-*Ra-b8MrATOqA%2=U5M87CkW@>Ua#shUK-hRJ4dXQNt$!E{yM$ z7^7SmGu#+QJeYlSY&d397w)fVz|2#Jd32#@gI_+qLuM2foj@XJ9 z_2sN`q)~-C?Ct++qYuJ_$LD z2Nq5v^@5nwn5$N#qxf+}#Qpj3>ine*oaAEm90sl`xfPm0YelXq9aRTSpw_^D3d%VV zy+8u+3+;jXXMR#qCE9q9_#5VYjd0pTX0qV+8th$=UCA?|0<-k*D2BRVrmm!};%UsA z-7w!JV%7*@=IxHzX)t7-p5jx;484@pfHheX=HkB8Pwb_5>Q62jfO-8@$VG$1-*F#Z zBG2SQFgu=soRo~2d?=+*D(21<%;vA*YgGThH(`DtkJzW)RK%>FDN->5reS6uM%j=_ zav_t9q*0jh(C=t8^m_6rpL~!}0(cU<;tdLlH>rTe(l{EAIjRP8)IVt=O`^#(1yamZ z$TKC7&B`DNOk=q&9TH0h=D+DQgJwcf$ii%UOq_+}HixdFt7$IemvXA0N~*&5O0w~` z$~?@YwU8U$p<1e=`H=7GsexsqW=KgbkP31z$F|WzT7+3}1m0Z!3^U;$X(=tEYwfZm?M7yd9;q!Le{v8?#Ak8omfEk(l2Q}{fh1r z^>n|OPaEg~dQdcCx1j;6l812%bb&Ze0(15wv=KMioxqxj=uvu%9*1n(OuwN`n8$xh zPe9Uq67OoX(iW^-uc4>0mT9N0v<>~X5O3CRrycYMNMg_6J^!8PPwzs4d=5|2VkMD6 zGS~$f^hJ7!c4L?7Wvpi>L&kmuQo~RiUf3c)~5E8*5`j8G|kLK5OR9r8%iW1zrUW%3eM|4cd z3XlgshQxY~KA}(P1f;vquv2{!()8z$rcYtTeFJ@o)#FX{6(qs0agW2dbeg`yTln8Y zrZ@{Z_D4Do34a#Wka+nLYy8<5Nl#)8|GYHeeKE7Nh&j?KKF0f8c8t4DNFgz@3sw_d z4cVlJ>?tq9itlpB?0q1q_k*O~U+fkyL%Mz#lKcRfARd5p9tR1zCuH+pkkNZX3hXNf z;tkd-#4hm-q`bk9?oP`jNNHolpRvy92f6JjNc4|EX6p($`ccT%cF5J;u>UtiTqWjW zg^~w(-zm0>ZP*7&7MsPB;zjY2_>0)VE0bsOt>~dL1yW?1941zX7qDC67Mr98>zMV@ zE7KtzegXM>C&rvRA+7fpzZCc4i^0Fe2=+McAb3z_$V{;Tw@Lj*X5mhItJs6JM>eF@ zT*$ryAw#YfE5+?%4OZv3h^xgXa)f{cDo2WZNVB6M$>u?#^+Bc$$RNIQGFFb0<7J_o zAScR6SVg^$6~`gSplczc`mySG4|3}V;$L#IoMO(eXsKRQZCTh@=LzNqb=#L^*lxr2 z8g_jh6hTnx@9;1@`=wy2hIF%7ty!mi!>w z4ot)?l?xl@)mJaE`>UGT$}20Y8{1S$Zcl+nxBVIBKxH`+g4NPg&c2?YoB2Q}=F{!* zXPSdtEOU?p>)iN^()kT;{RVe_qjdfZd+>s6JwcyQAYZ;EXcQo*7r;o;?-<+3wR3D` zQ$s_!&bNJRM_Q(F73D3Cah(D6$leS&zR)~AqW=9xz4@~()Dydx7F2GRXYkh z6LC(6$jzvo;pT~4m*$CFZs){^Af}06$Oy;9LOtc1C+p!S>)|Iygx8tO$k2j~HAXa_ zc}fJg0i*r`MkNJ|dJGu7BM`10Jihojy%zSeR2XT}{RA9+Z!etrESD^%w=n@|(*dY9(ltKA06%)>+!1QAY(vodv_CH%c80MweYs z+A^cGWqSKZm8s{&T*ke?JY6r#^v<%_rdQQfw^X;*wVJ2bw3IKZcFgSb)GNFo!#tC{ z45!gL1=;3VoSc1DM~Brz`fTb3U)lfA;u?h)CAX{}9p?pR${03WogAKn?41cD*x+8s^Q=h@P z&u6JNIIX^5eA6@bX686*I@xsAL_9e5nvOJ0wQ8KJ?F^`A;>|SIF}JoluKv>E3A%^h zs4stxtebDCGZ?5dM!Y)i>nfz%n?BqzzcX3qf{6T`3u;=bs~hXf8>{Ln&GlTT=6Zb~ zsE-I|st2E(n#Z4GZq)0(vAnsdwXLP8xwhKesK;rHh@-Qak*QU841s=gQ$)`R7*!lF z>M>wcb-?H;fpAk8J;q-U)zmpC1dQ?r!?BD|0iz*@>WGU*ygSyJIRl^Hd|9-h0_sIh!u@)tkA)S1)AP zX|73YZFPGbHQaKH$3{HbOz0Z=R?kJZQj-M0Q&Qkp2Jg~J6#_K-T3q;KWA=z zbaA_VJgdUy4&?P@hV2X+WWnmXn%cJ7?$<1AYQy~1SW{meF1DUZKKB%_+pWiPyE!$u zxxcyH`8=hce>6w zY`uQc4JOjv`8Mv}5iv5NYnz%Dlvgw@s#a7ddT2#;ebeI3AbLsMcm1EC_oF!(e)_V?fv~D1|v4`{sl*nCoHQh`n>d zDa_+AH(@O-v1a@N_-Q%~?XpCffxPX2IGhWqn*cO~!fF{9bYih%pxZ+X+YN4|+Odb} zMO;-51$M0W*9sXZm{=-e>dV{IE{_RxbyI?}B}{V!Mb$?XYQ$RE+}bR>y1AgbrBP(- zWry*UO*urI2DPsMYAy#PJwCvbku^VF5*O65m zE5MF$YK^IMe>L$2$WPH0?7c|%O3+gfCdS~mC+Y(^U~T^a>f!fTmlCyy5LQ;H5b|-< z7;^v`Vf7ycm;O^!E5gKxw{X%i5A+O9vxk`0@u&8;!hdT~bCG{ru*Q{0Z$175R;3Rh zh2Nv?qtNy*KnFnWQ=uP;PeVs(Z0KVe4=BQ0+9j~3!QL1;Da`OW37?bjISrqa9X=Lv za=fGPJqq8W@I3(EqlkADni78&ar8RiUpQfh1{uocL`kK%fy=NHr(RK5lZ()S&{2fH z=oflDbl@UV*cq?K2z?p)7=JkNC3I5%3kOv;LK}Z@NUX#)>?sI5B{Q+VFdKip z#2oxxjwfa=_8L@O_2Ihe%XQ`Ax=Q4hP;u0=H5;aiLmWpvRqNre>Q{0cjpLZ$QeWh_ z9OYFZD53=yfj7-=^#K?s6)jQ40%(Z~gfB@i1bA@3qGZ(UZnYA12B6EddFEdZ;pjW84)vFyCYAZFAXroA0yrwyrThtZ?fMa6fMTllgV%?>`B6hVef^ zmtWEL0^T$42OPHawe+;~wZ_Aawe&R~2b=_aWw9G}U-KEb&%@uuU{~QS33_<8b$60| zizUyJ3rGVLSTYzduuQaN>tH!*DYj)>Pg$-5%m7>kr~=FngQc1MZiagiK-ntn&CY4I zGk^-G%d*z88nDT-1#mCh8yHvajmm9Hw58Y*E!%8~RsmRV*~#t~L3;)6B{r`u+vc_W z0C*K}6!3=S6Ws>>4&b1oTRu_YkcQXtc{t9`(7w^bsIcL5t-?9YYR1)g2LT1vURF1t z8z9~~2zav9Z5wS1*hX8k03)qF#>ZJF+XB{7z%1vLI#}mg=K-&GUTNP5`$6knfExfU zfF%qonD!{}^#I-fLYneh15mc|SK%H7e@_6O2JA54dFxw_CDymBy8(L`UQ=${a^UBb zZ7Z}D*$S-(0EYm_tS4+m)>GEgwiw3>Ta5LbEe2s?Y|>^`Zd;t;j^n5d54S<{3Ud_02 zuT^emoU^wx&N0uhPoY04_grV3eG}{zPM0&$>9QXMYyrFq*k*r&?VZ411iYf?_BT{G zq~o%`6OQvUw1avW6*io%W1jsJ`{(v=>_0fnj(C6oIPs!tyrY*R-kIX`I#V2j0LczF z<5`Z8POrlU80VbU0h5830%iejaQ1fG;Ft$E0hr5B&$Jc5mvs35l-2@&gyFCVhw@Kz ztO48wSZ~0CctdFi;8DO63{SIrH}Ka0yPes1XDQpU2k;u;Eyn@pXuQvK%qbn`04D&a z6gW;RH~N8eUdguVm#RHf-KcV_bUOM9!Wex8bKY>R0j8JD4DDm!?`e=`=_T#2Fw-?9 z6%P+(Djd3aL>YQW`3>+aHS^J{On;r}Lzyy^DGQiV!*S~ruBoqn5o>Fa_6hrrViO;@ zKsw_&WFCjvf^YW`Gyy@^Hgjr&Ipk0__c4b%n8PIIFoo$uIE5jMCvz-Zb%vCa6h+*{ zcnSM1=8&ZvW*O6`F};!TYn3l#b#N>0;kdtG^G+RCzGAJ8bC}x|PA*PUk9EJ+2#SN# zS%^1X38$jqMBIxTI7oY+^R4Fa^Hp4JE{ADQxa`5D`+-Z>$faxM^qV<+6XT6+wy;^x zW{5di$^2(B#lv_S<7ph%%ch4zo>z479h=`Ox;Up(7(dG)p+}%N=M-X?63b>KQ|7T* ztK&?sV!TQ(9plxULIvZ`G5rO`|H$UEZ1!cdH=CDpI=whnHd8!oX0e&8d?7V~E19f^ zXY(>PdvIK(??BX@DfNukaK5#S*D?-`S%hg+IO`C@WIFUzLC;`3j#KE$W(=DzaJ%f3 ztqAjwHVUSlbL`2!R*w6q>bJCsQ|QX3QqCair$TDU+^YvEiZ+;iyRjL~rqXpKdY*IH z!0B(}yt;5MUD!8<(~sdaoqAlxqZJ+Jfhw;*a4fuo4m?Hk!|bC{5Ub_;FmF?_#0uSH z`YP4;#7a(W1LL=_?I7jIS`Co4KSfGv1$l2XQ*_jQ3-6fHLJ(oa0Me1N|BA&-~mbpGRJ| z;|nq*Zeo0OC>{76Y_5QTg2Alht=Y6^_n{O))RWCPHorW729$1`&Z*F!fx9?l98>U#1W+zxd?4ff6fTtJE=vM}sBx1J^cS&4;vEo`8v481mm#d7C=E~m!2Sm10wx0T0E(t? z1;qep=;QQQ;7nQBYNe%i5n_?PO7U^*eV{ihZlqefn!ygQu&}=B3Csok7VLwlb876Q zC|U|N9`)p{os@)K`r&uI&pxJ+69x32WFQpns zvFpKmHv(te*|-N`1kSUgc^}FP-)uk;U^E~AmFiA~A$AC-B2Kg<~6gRX8ocA1i)$C3rtNWeH~4wa%Upy91*D)c3rJjddFJ5aFW zqD+lV!Mc+!-KE^!9C(JqR`zORdtil=;iJ$i2BHQ>ga293{90tt&DcjA1l_7J z(7l?CRr+FsZb5HT-+a$w4Z8WzNzv1>Vkae0q(f(G3U);nK+jC4+psG!7<*PewER`j zw1Q;8{M)f7l7u_j{9-C}AsWQB2Hk-jh#_dt0CZjELci-egYLwROtKgb4V@C`TQotl zMNcOh&)!hxL$hY8s;{fj?3dP@hch27773SYJuk6| z<6f<=I`q{N*i2^A%VsW{J~j*4EM{{Sn-y%%XS0RPWeqhg3$&Xrt=ZI4)u^q$v}P+> zUt4>LO&qpp_o^e8OK2`^Zq+ulUQ!cdlG?dbW0$^%hwoZ2uHFeEo(I%kIgi~uhpC-w z?7yn9oJTZen+>Tk)nxq(w3^|Tvp@|t)HT5Ep?56{Rb!}ik#oDJQIw|I2hkN4y>=pByHZJZJyJVHOC{1NI_ zcKZ#tAMRIB2hX6*CE8>U%$;a^iMH7b^Lg|Ei8gu_<}T=cO0)zd7qrGNVXmU}Fjv#B zVBQ8zPzim}`(fTrZ^Kk~2NJ%Qp!Z_54f=qR!&E()plK*E+XKV<`%?yg z`)P+Sp_OeBH8L4xak9*%WXNuqu@msVs|KCat@I+>+o0L1+}okss%)jLZ=#oUKgM6y z{W&*Xb2i*tRUN9YF%VK#HKNWW)M%#RX;VJk0K9@^AM|$U-tOd(5*og_5qz#eNGsx4 zQG0Rp8?=~%ehae)P9!B-q_gyHj8PYjAQ5B7MPmi?qE62Qp8Z&y)Tk4g=!kq3hpOk8 z_=L0?m^w%4%nfyoCw&m4X;s(iowvwmEV92{1wK{(Fj1<3N!%9^$vOH3>moxRS%pJs K7eISE_x}Nc7Gf9x diff --git a/examples/fonts/Asap/Asap-MediumItalic.ttf b/examples/fonts/Asap/Asap-MediumItalic.ttf deleted file mode 100644 index 6f8d9060683f59233fa8ff5c40d1a58b9945b6fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66228 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%0Deh1z$r~$Z?9{ z!PiSj{D|^wnPij4F6ZgwXO?u%BWfEw$gVP-6OEv_R7!;lqLbx6SdN7*s_aa)1mjf> zJJSu&(HF{hY(7{!=?+&Y$j|d9l%&!~ z{7N+kRB*tW0Dw9y2%0>iN+IP@@U0SM^;Cq)WErt^Zx6pO#Jl%I}NPr@jm+?m!m_VI#t%gQ^G! zol^Drav}pYR3jHqTK%JN$$uS@6^7SPVGuRPyZjzzJNcTo-n8+(dq&PmtK1)8XU;xU zq&$Dr?QtZkQG7t&ii!`FHKr%M?2elcd}&9zHWu{HzH5F1;GnCt9FDC0DDYz%+d<*^S!Z<>$(kBZ$7{MW-k``-*|uXc-tGr zA{54MpV^6J?uau-X_el&Gx*2($`C_Dx?t=j}JY?|Vtxuk}s~h?X0u* z;K$k6aEfaSqCP9Kjti)!htVu3I_Sts)z{0UR~d>yD&swP;e{(kqABkX?+iBB`n>tR zRg_u#d8$7((0x(VBIxRM(`Q6&&VkNvn>^@$qN}d($2dEX9!Ryc{aD1O1cl(v{gOMC zX9RZ&=tLz=j`u4cwZ=mh)HF^}{@Y}n3!jRr%_&8WcQHiDh?|ngQv3kijJ$6IRaBAd zkX>YCO0sS6VTJWAmWQ)KG(#YOs8DO^Pn?>F2UV26$6I^X_6-Kxh8xHCP1moFgwig1 zRBh@UT%K%i?-*;D*gK{BeBRJnClAC%d$KKYM=-lAH+gzj$ZPuCIg^L-?bX>>Ak?rd zxAL4EVK1N)*aHiZTl=!WQjSv$CCB@fyC9o{O{>6$JS)KyhpQDI6Si#2Sj)v*eTyo{ z@yxo|QDD!m21;kX&d4q=Q#?J%+rGWe2Md*a)(8Wkic1v(~h4ic04&ZvtYan zUI^`YAN29tvE$UW0x8Nxihe^;6GQhRT&LI_aWu?>kS>dm2p#vG#{%l)Xy3-kv1;9G zd++q*=B=9=Y0_R%fP*St=@U_(vRZFA}= zcU3lU`R#A{=7pPCAhv9%X9`qlv2Ng_+ z{-&t4l+g73Kcd9wQ+B^a58~FAxuAd|rAX*5>Uup#6T$P^B8NLDk-{D78z@!}EIs!~q0jcuClJTYf$r*DCF8G(r$C$INpA&-mK9Z{Yvn}@u7IZr1)MeHMcGP94q~aukS(Cb`9{5irxwb=nK4LnKnBfj866TRn+Ah zbBS^f857(uua!Eti-G(A$gn9WoeLkGUD z_Z>X}N?Mfq5u`R7wW`@t`%$ba$Hzox9O-?jE&IU>l%qdfw0cPG$FC2xJ1;zaanR}v zE~)-dTOw<)< z0XQYLg3+8+m_d~f{?!j`?_oUW%i1r|RNv1XSx=k2@ZXO5I_^hnu)`J&6&&py?3GYJ zbIo4tC*KV=U%e6YQr-vik_SQaqRfGbNwFDokZooEl-DcR2!uiSz42 z{p{gT(B_%F)amfEYcIX5@J6RY+I&DKEbD{VO^T$i5L6OSi+rveA7rb`#>s-p@gcUR zY@B#Vj*qYmQFZS3pz1bYpOxLc)PXDrfM5*Dn7UPU)jDuipH&6&Vb|;jwRj>DjM`uS z3A5hY<@)8rT6Z)Qw%`1u9uN4r-{EF6tl5AB=+5VV;q}{ng&!3@<59yX3-iLPQzN=l z&1N}PrcM)7mliDy?WJS6bo%?j)W@(|2&-N5X`{j4?0PXJtFu_6PprB93Qg!gW4fS1fd)!ufr|84ml6eQa(ST zTw68|c^z`RlLyMiiKFHCBz4Dw0(O|7E$}KUMU@zR?Nr|-@{@}UcD6j_WYA+{UnqRS zaC$xLo4VItm|}a;c~hup448Hv!eO^yHGQ1@gWF^D64!{C1qB|nDDWEri5#aGPmcG~ zIiAw>nLjuQoX1O=#X{Ru+6k@ek5ac|=4qaVB_8M}M{$CO;K_*I@SVSlPe5mvNiR=BrtqpIGKf zZERX~LRhz>z}v3YbhM@>5=phqh0=Cm|3%ji1*?43Dcu|1c3w^0@(J_FnKtweJRitp z>#A8l@diOuAt>{NMVY4r1>`t+q;h;vd0*K$QAUmrDet$&XE@bt_=Vi99M4lk84l5g=DE|Z zHb=JED%Q3(uId`vvwU*E8Hnj=yDNrcEI!-Y98J0P%T7%#kGVHG0u`a)j*iZz9cxaz z{e+%Gm_^#R{>~lq*jc14ebw3fCzfAF+$>@G32^=%XrY4J9ul;c^zp*A@{FyctG=$sqv{TE#`f1oy@&jj zCHj9#Ck;Q|*DlWA27;l0&29Ild#ehbW83woO1nEKvTTyN)9Nt6(~wVA&k9DgM&9uqmbIi?)mN zk<;fb+Mh~Kce0n}Ef@cIX?zTw=ai>N!w*}7*$1fqXVJOH!`f!@-^sQtX{u*rCDP|x zt{%!E+MCk`Tn65`c#C5-VFO~-?E9uk!qV)r3|>fuj38<$=%&6HoX6KBN3U& zj<&0=Xy2BO+`8z*wy|}|JI?D*jPzgJTwi$a@=Dk0i4jxIM{mEZW{YTC-)t^S9NtRV52|RG?fGuIugx#u7K;n-_#{fWMa<)D>g9n(a>F(Ns<>hDuflKsSzBvi_?oj)5HWhWZ8i9S6 zO^>+^r_GV~1^vKWu0fdpUSbZ9m1IVaKU*5_SNb>~co*gf8i~2nQ#uSs_zSE!zQJOG@%IEwQexbz|@F-kF(v1?{-2$L~iW>H6k@ zjVpV4(<|ch9{3q+PP^^ImY3r3tp$gd9PsJX)o0C&jYHjVbUDAuIsX<;B=ix_k-!K8 zitUf0Gm<`|*`S_kf53yq|d+J@CL>mE}Px0 zMZKN?(n^Ng-@j*VzJCS&=b1a#z30PwckjAwm9sTtgiO?!+&*2?9V%0q+pIDohL`EH zFI;@l)a1plTQ&h26U{yhnx$|?gS`D^>}hntb1}XeVY&g{TCXI-#rSC7%ncrKfwG@UcK-iL#Cn>(i z^VmvbY-nF|{j<8dK*yba@jD-E z8Li%a^6xfIXL~ABfLg9j^89x|L($u}1vYY=qA)q$hsc_+l;b4la=c%;uWX!TNRAIG zcVj*qJm5qjy<6rpu@sVR#6grCje&MxaB!}Gj zLCeSAsBA0lyi`u}H4mYy{vAwUTT}(n;OiHDa(u||U0f3(AHVR-@z8+%(embZP*>IA zDkM6ukN7?KjFO83hss`y~lH>qx z3_;~_fk$r{*P?5>lz!5qC5OJW7wpMWU!kX@rWfiXdw0ezdk}wMsqc~_k3vmJbuXZ0 zxYEHzNrzW1(CB~W+&>gf`o}6GyIJ44J>rE3Yt^{{Vm|#_zDJ(-DGLpofavYY!8Xr{NPQSOt=WtRjU%bBy)FKOty&|He zQ9+c6`=dronQI;NyMr#>AE=0#&m9Yv)5*#e-Doupx)M#5zF_}RAU%&AWZS}SSa2jm zm*%TH2#-nhW%YEA@?N69)rnOeEdk@QFxm18*}J-&Hn*)VQ@<)s$D55vSVm6i&27uZ zn8W5pO53mMc3V?K@6LNSwO(G=FubhrQ`7A>_~TT0Xl+?G5?s~vhA_E)Mluv6E-gZ} z3^A+a+k;CE0*>sM*sC9rBB6TF)+r9+U_q{Zu~CY_{e|*`dC7?GCP4^H~&m`U>Bm zxJ9_vL}R(GTT~Yhvr^Ae^Xp4Z3LG&ftLM=CEJC>Pd;?L$i_sia6Rgp`IT{UN)JvY4 zH9GiD8m+=8)x(OTL$A@Ogmr#&_+=Vx##v#aT#717@SD4o>xCpo1V!he82P?62eWh7 zAF-awJLxPYyyG6|sJ{yOM=X-?hl4F{tX@{8LR%J^`()v@pIjA~~b#F7D5EnO!@km`WHAGWiB&dndr zS1WVX=!Ev6AlK67(HyVH9*hL!X!{Zw($eAW+4n9b2EbwN(YY4o_k!BB3#rYQ;*(oS z-0}Ef_~5ngEZl0yuvq8&+)XFOwWVf`9yFhItg?8j7tN7?l_iB7W`xWRqJ)q0+3*v7 z54%u@F^U2TXk%Tu~sXbteA;6CXSMoL^7HYG7 z!`ME2wmaD6%yvDP%d`S2o`aCY9{9@G6@v8;0uNbINZTbMEi4e|#-g1!TiY^%*B z0<&nPi7hpJQ5PF!k%&L;=cCLUlBTX1mZ;O`!4-?{*{d` z&R+1TzJMMr{COT}BE>I4Hm_FU4zm?RlEOcuHqaP#ceM$=;mo{^CmHU+UB>19hvkkC z&jBVl$ViBe8GeXgn0idYSc!y}1g;Gd+Y<27K(NR-uSyraVF zt~$BxLVxwC?aRj89+9h(nZENXZ#suf4GtBaBM*gbN!JgI75+0$A1BZo+?*~Pi4tIp}V)$wWq zIl|I9>xfsT2-uQKRrE}1;_1oCb&jdgslxMdkBgnK&ItGc=CrG=W8SeM=>ok0BU!~x zAzgXp^TbJWe--23Q&PO=Gtgc1+tSm>Fyq)NQ-br11WrF`V`QjWB!yl`|P+N z0QPC?s80}}p|KqVD73Df6xg(=G3a9pI+o^ur-UWqGyyfLCRdXMqI?+#%%&2vviO$6 zpDxxziQC?OZtvw=ckRCMsa`)81n)%Tuz@~tQDJNK^pHeb4TYVyiG%O+_JIUCV^ zz@q!t#M*M4ydpW?M{#fIdGd?o_#nk6Ru*rwmBo{pTA90pELh1rjpUZx#f4csvN@KV zXbUi4?>JKJzIQQ>{IC`J*+&j0fxu`6^ekdTIXUtURd~kpm~ks7*CXx+6(_5OfJbC{ z?kLVAFGTI3Qu0l7882CNUjn;gu1&TWFRHCCgSJH!s5!YsgtTIR{rr&CTIidW#zucLoue=->G*VaTi0U!<2Dx@pU8CU`E!=gNL|7b zEEjT^$2$uo)H7T_$K-f9j`Po#s=80Y7nF8d9vbJ8B4?QAIug%0m@QQ>7?5v|AkyziRuG*8C!P)FwEe|wpU%l<^Csv1k5p%nE zdux?&la@Q%I?%#!&UF*hk9nh{qiO+#4Us|>^-n-Y$rgV^dcudWfC5YaN-2Sm>y7LX z-gdYuv8uUgb#7qy)Utsns|Xv+m^>{#6_2#!pLV;{Tgk63{JN>JNtAaxer}qq`s%X| z432*r;T=`dQ5V1tR^r?~mY26V^#rjR8lx>g28Rvc<>;l|LRT&OyKedDfM%#w` z*d?o*Cc?UI#GISZxpB(cfgGARxZ5yiH^gGhXls?rC&LlL_sQY()n~03ADbqBmGa{k zWBppOx~SvJc%--mH~~(%a!N9?lPaT5HS9|d2gy?pN9sWKcomCH)bl_j7-}~rhYG(U zul&@mng*{qU}}w>;9j`~aW(WKWyyw!r{ySl+;X%(g3-BI#KZ)T97X(xE@XPT5!I3M z@h;^$%*B)n#Cd!1Jp5Z>Ux>dYDt8N3;Z}*;l>{w~UQ9P7GdbG7QNr3PV4Wgb6Ie&?ML9e!tfIa4gk}Ck2T9}YCmk9$ zmICO@wwaBK8&uRGR$X^Uge{!*3a?h;G`L1$8WK23OetQM9S9ZJFWQ&lc$e}+v0u0) zfz9Cra-WnndNqomMA#C45`1xvhtz+TVsmovi*X2o_;h+a)U6Hg9Nu{LNN;c7_`b;+ zoaL?VT{X~Ifx3;erZ3sm&^}`8rdkbdFZ*r(DV-}<^_(72z3cm?&+YemZMik0o&Bsz z>q_Qpn;QB$3L!dGT&xtKvqGFr#>!Y4F>QINh}Ne95$-o#SRukuWmy_hqgAEa(CkAZ zyMjAI*&x4xv>fqhK(v$KtP;&F^Z0^C)204-E8BAGWTm|k=b#%67K<9qt~D+#7Votu zBH=*TM!|DJ?;Z~@9WB-Qv)pFU8|X#iS>B=!E_v$R$Q34+){aIw23`<6X+5LLTToSrZw}j`^%mwzw@; z>z;+bb>0k0qeu{LQu1fJEpEk3>AFv1xJzSX3Ii5`{V)`dkN>9Tc z&U;bhlM24ILbHDZ(<*bt6i?Y%^5>5O@drxzp0XU!tH;a3B&k%9JNFpkdAyFWsF;tF z+3!1=5TA`62Xzix*89kDP|4ETRb|#<)ass+R)KN=Qg4sp{33b3uz-96EFG$x3%Z-? zkJ{b|vX+R<*A?5FW{zk9;Vf0l970)tld<{m77^+_Vu-9l$wuLvf`!$Da&wvOqR3og zJ88uw8Th%s2DX*(`NSD=czQKF63yX6r11Je!k4|%b^-75X6-$0yVG79I>%A(XtRCa zQJ>;|_A`eq=>N-jW%HKSzxe&Qg)M`6u{EwWF~da`P#YO@7ir#0D{)z9eJx(2lsXKVNB*?K_mbXW^v-ET}T zW4(bu;j7`MVK(Lu*bRW@@rahRSMbocsehKB&A>HctM5 z93P^tvC{M82gvaeU+dI- z4)=fm)^NHUn(JL2@4Ms)^}4xE_5vV5jDUOi#q|Q{D9-2UsJhr-YkS!Xw!)X3&EBkj z#+54e*ESL2QO~80tdqO3le1gGxD4WP_8eNr{Dm!JiGp|ogPfi4r`VSKFPT-7<3vXq z=X_A~7s^qhq8uH%jA&SdusBB-Ly4y}UYr{~*4p_|^0DNKo$TjjPZL+mxhKyi&O`qg z`%i8V_UIGA>2wh{AjV#BIvscN#Y{Wv`-8a0X}fON3g1xX=u6?|IzN81*X!ls+IqiB zE8OPws^J=fM9xj{z0Jb?V*(O6PLZ@6@3(qhGzIPe)0!40Fo07&1aAN?_KXtZfKZVsixF6qm|d%ni^_H;&u4K-SNWtvTrYHR#iqGtI1A}n>B0( ze))H5^>kfb6+<_nkcSqo2&hqu6#Vd%z=rOmRzxH&$LVDH{PRQ;IZh|ji{sV6t2o}p zL4||n2TxovKT13yXQ6ZH^W(%5a-7bk)A-`kIaA6bVtjG^b&Bpf!1IgigHw2uAdSmR z4sn3YFH8x3Ud$I$xPcF}P~4zF4*7*n+z+`@#?`&~9^%Sk+AIpKV5N5{yoU2a<{3F% z`K@>wih@gc;|L7WutYese))NW?a5`SOxMm8)yW5ghPN)py&Y#PZ)xuvNRO}1U3ALc z@myQ?cyG&TEzKUit7iJ@=lTXZmiyI;#_7i3Xj5)ePLJDy8P%4kYuV6Kl}dzkyR$vt zwziY~sv}=t*O{-+6wY564cobdx;OlSoey(?z3cHj+5-m@je5`M zh&zBqcP+FYp^UT%Ra%7W_U|DIyS3Q7nMEgO;cv%IBR)6Z#7QlkueJVbL@UvZ_+8Rw z821yB93zv%??cb;D82~}pxoc0E?;_Q3MF&IyHl*DN|6*HCs#gBmIarh9^3-l()?5E z1a4WXQTS)nH_ZRGs5clr!{`uR+&PRqk%0D9el4J#0_cl#9prB#U33(HmgSLnygb>_ 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

Vqd;ONoameo@| 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 diff --git a/examples/fonts/Asap/Asap-SemiBold.ttf b/examples/fonts/Asap/Asap-SemiBold.ttf deleted file mode 100644 index 5328f3f689b09e5c546ee0959a951b34f44624c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63140 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 P=qt-`;A}p$y9@pwCm(So diff --git a/examples/fonts/Asap/Asap-SemiBoldItalic.ttf b/examples/fonts/Asap/Asap-SemiBoldItalic.ttf deleted file mode 100644 index 6415ef2bee6a709cbea33f78e29b8358bb766e40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66240 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

& 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 max_temp: - max_temp = corr_temperature + 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 - range_string = f"{min_temp:.0f}-{max_temp:.0f}" + 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("icons/temperature.png") + temp_icon = Image.open(path + "/icons/temperature.png") img.paste(temp_icon, (margin, 18), mask=temp_icon) # Humidity @@ -382,7 +395,7 @@ def describe_light(light): 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("icons/humidity-" + humidity_desc.lower() + ".png") + humidity_icon = Image.open(path + "/icons/humidity-" + humidity_desc.lower() + ".png") img.paste(humidity_icon, (margin, 48), mask=humidity_icon) # Light @@ -392,7 +405,7 @@ def describe_light(light): 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("icons/bulb-" + light_desc.lower() + ".png") + light_icon = Image.open(path + "/icons/bulb-" + light_desc.lower() + ".png") img.paste(humidity_icon, (80, 18), mask=light_icon) # Pressure @@ -404,7 +417,7 @@ def describe_light(light): 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("icons/weather-" + pressure_desc.lower() + ".png") + pressure_icon = Image.open(path + "/icons/weather-" + pressure_desc.lower() + ".png") img.paste(pressure_icon, (80, 48), mask=pressure_icon) # Display image diff --git a/examples/weather.py b/examples/weather.py index 50360210..66f18e0a 100755 --- a/examples/weather.py +++ b/examples/weather.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import time from bme280 import BME280 diff --git a/library/setup.cfg b/library/setup.cfg index ed7cef7f..9375d3cd 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -33,6 +33,10 @@ install_requires = ltr559 st7735 ads1015 + fonts + font-roboto + astral + pytz [flake8] exclude = diff --git a/library/setup.py b/library/setup.py index 08ebdc56..784db513 100755 --- a/library/setup.py +++ b/library/setup.py @@ -32,5 +32,5 @@ setup( packages=['enviroplus'], - install_requires=['setuptools>={}'.format(minimum_version), 'pimoroni-bme280', 'pms5003', 'ltr559', 'st7735', 'ads1015'] + install_requires=['setuptools>={}'.format(minimum_version), 'pimoroni-bme280', 'pms5003', 'ltr559', 'st7735', 'ads1015', 'fonts', 'font-roboto', 'astral', 'pytz'] ) From bde3840195e7b9c8b32759a2b801e6d31263218f Mon Sep 17 00:00:00 2001 From: Ross Lockwood <4527806+spincrisis@users.noreply.github.com> Date: Wed, 4 Mar 2020 08:30:18 -0700 Subject: [PATCH 030/122] Fix launch bug in all-in-one-no-pm.py Launching all-in-one-no-pm.py results in an error due to line 57. FontSize should be replaced with the variable font_size, to run line 57 without error. --- examples/all-in-one-no-pm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index db1ca8c9..d9b1069c 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -54,7 +54,7 @@ draw = ImageDraw.Draw(img) path = os.path.dirname(os.path.realpath(__file__)) font_size = 20 -font = ImageFont.truetype(UserFont, FontSize) +font = ImageFont.truetype(UserFont, font_size) message = "" From 04565414bfd12051093ad7fa1a5815e06953edda Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 19 Aug 2019 11:02:52 +0100 Subject: [PATCH 031/122] Improvements for #28 --- examples/all-in-one-no-pm.py | 8 ++++---- examples/all-in-one.py | 8 ++++---- examples/luftdaten.py | 9 +++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index d9b1069c..959303e3 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -14,7 +14,6 @@ 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 @@ -91,9 +90,10 @@ 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, universal_newlines=True) - output, _error = process.communicate() - 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 diff --git a/examples/all-in-one.py b/examples/all-in-one.py index c0423e6d..8b84a444 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -15,7 +15,6 @@ 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 @@ -96,9 +95,10 @@ 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, universal_newlines=True) - output, _error = process.communicate() - 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 diff --git a/examples/luftdaten.py b/examples/luftdaten.py index d2d65621..3270f7d1 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -71,11 +71,12 @@ def read_values(): return values -# Get CPU temperature to use for compensation +# 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("'")]) + with open("/sys/class/thermal/thermal_zone0/temp", "r") as f: + temp = f.read() + temp = int(temp) / 1000.0 + return temp # Get Raspberry Pi serial number to use as ID From 7614d188751888be4c165ce86b49be74db82dd9e Mon Sep 17 00:00:00 2001 From: Ross Fowler <37389643+roscoe81@users.noreply.github.com> Date: Fri, 13 Mar 2020 09:24:29 +1100 Subject: [PATCH 032/122] Address Issue #55 --- examples/weather-and-light.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index fde1d961..a26ec7b0 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -12,6 +12,7 @@ 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 @@ -79,19 +80,21 @@ def x_from_sun_moon_time(progress, period, x_range): return x -def sun_moon_time(dt, city_name, time_zone): +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 - today = dt.date() - dt = pytz.timezone(time_zone).localize(dt) + 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 yesterfay, today, tomorrow + # 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) @@ -103,29 +106,29 @@ def sun_moon_time(dt, city_name, time_zone): sunrise_tomorrow = sun_tomorrow["sunrise"] # Work out lengths of day or night period and progress through period - if sunrise_today < dt < sunset_today: + if sunrise_today < local_dt < sunset_today: day = True period = sunset_today - sunrise_today mid = sunrise_today + (period / 2) - progress = dt - sunrise_today + progress = local_dt - sunrise_today - elif dt > sunset_today: + elif local_dt > sunset_today: day = False period = sunrise_tomorrow - sunset_today mid = sunset_today + (period / 2) - progress = dt - sunset_today + progress = local_dt - sunset_today else: day = False period = sunrise_today - sunset_yesterday mid = sunset_yesterday + (period / 2) - progress = dt - sunset_yesterday + progress = local_dt - sunset_yesterday # Convert time deltas to seconds progress = progress.total_seconds() period = period.total_seconds() - return (progress, period, day) + return (progress, period, day, local_dt) def draw_background(progress, period, day): @@ -139,7 +142,7 @@ def draw_background(progress, period, day): if day: x = WIDTH - x - # Calculate position on sun/moon's curve + # Calculate position on sun/moon's curve centre = WIDTH / 2 y = calculate_y_pos(x, centre) @@ -319,7 +322,6 @@ def describe_light(light): # Margins margin = 3 -dt = datetime.now() # Set up BME280 weather sensor bus = SMBus(1) @@ -346,14 +348,13 @@ def describe_light(light): while True: path = os.path.dirname(os.path.realpath(__file__)) - dt = datetime.now() - progress, period, day = sun_moon_time(dt, city_name, time_zone) + 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 = dt.strftime("%d %b %y").lstrip('0') - time_string = dt.strftime("%H:%M") + 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) From 6d6d3dd296978d367df80193a9fc842cd79a662d Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 13 Mar 2020 10:43:39 +0000 Subject: [PATCH 033/122] Prep for v0.0.2 --- library/CHANGELOG.txt | 7 +++++++ library/enviroplus/__init__.py | 2 +- library/setup.cfg | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 0f98d125..65825f28 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,10 @@ +0.0.2 +----- + +* Add support for extra ADC channel in Gas +* Handle breaking change in new ltr559 library +* Add Noise functionality + 0.0.1 ----- diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py index b8023d8b..d18f4098 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.1' +__version__ = '0.0.2' diff --git a/library/setup.cfg b/library/setup.cfg index 362646dc..c8a3ace3 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.1 +version = 0.0.2 author = Philip Howard author_email = phil@pimoroni.com description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi" From f8d88341a5c4dbc710ada549f5d7a9dc272c4040 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 13 Mar 2020 10:44:13 +0000 Subject: [PATCH 034/122] Update README for 0.0.2 --- library/README.rst | 84 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/library/README.rst b/library/README.rst index f90e0720..6bf510d3 100644 --- a/library/README.rst +++ b/library/README.rst @@ -1,29 +1,87 @@ -Enviro+ pHAT -============ +Enviro+ +======= -`Build Status `__ -`Coverage -Status `__ -`PyPi Package `__ `Python -Versions `__ +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 ========== -Stable library from PyPi: +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) +------------------------------- + +:: -- Just run ``sudo pip install enviroplus`` + curl -sSL https://get.pimoroni.com/enviroplus | bash -(**Note** that you’re best using the git clone / install.sh method below -if you want all of the UART serial configuration for the PMS5003 -particulate matter sensor to run automatically) +**Note** report issues with one-line installer here: +https://github.com/pimoroni/get -Latest/development library from GitHub: +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.2 +----- + +* Add support for extra ADC channel in Gas +* Handle breaking change in new ltr559 library +* Add Noise functionality + 0.0.1 ----- From 97ee1d88e84df79d82c3ca20eb89378c65197f9a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 16 Mar 2020 11:37:40 +0000 Subject: [PATCH 035/122] Fix noise_floor bug --- library/CHANGELOG.txt | 5 +++++ library/README.rst | 5 +++++ library/enviroplus/__init__.py | 2 +- library/enviroplus/noise.py | 2 +- library/setup.cfg | 14 +++++++++----- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 65825f28..81d41368 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.0.3 +----- + +* Fix "self.noise_floor" bug in get_noise_profile + 0.0.2 ----- diff --git a/library/README.rst b/library/README.rst index 6bf510d3..bd74b9d4 100644 --- a/library/README.rst +++ b/library/README.rst @@ -75,6 +75,11 @@ Help & Support .. |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 ----- diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py index d18f4098..ffcc925a 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.2' +__version__ = '0.0.3' diff --git a/library/enviroplus/noise.py b/library/enviroplus/noise.py index 2e7472d1..6830bd0d 100644 --- a/library/enviroplus/noise.py +++ b/library/enviroplus/noise.py @@ -73,7 +73,7 @@ def get_noise_profile(self, 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_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 = (low + mid + high) / 3.0 diff --git a/library/setup.cfg b/library/setup.cfg index c8a3ace3..83f89fa1 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.2 +version = 0.0.3 author = Philip Howard author_email = phil@pimoroni.com description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi" @@ -33,10 +33,10 @@ install_requires = ltr559 st7735 ads1015 - fonts - font-roboto - astral - pytz + fonts + font-roboto + astral + pytz sounddevice [flake8] @@ -56,12 +56,16 @@ py2deps = 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 From be4d0fc9022240b5047654523aab877cbaa8a73d Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 17 Mar 2020 11:37:53 +0000 Subject: [PATCH 036/122] Test tweaks and linting I've re-written the tests to use conftest.py to set up and tear down mock modules via fixtures. I have also linted the examples, removing redundant linebreaks, commenting out unused variables and attempting to simplify long lines. --- examples/all-in-one-no-pm.py | 15 +- examples/all-in-one.py | 16 +- examples/combined.py | 344 ++++++++++++++++---------------- examples/luftdaten.py | 51 ++--- examples/noise-amps-at-freqs.py | 10 +- examples/noise-profile.py | 10 +- examples/weather-and-light.py | 24 +-- library/tests/conftest.py | 56 ++++++ library/tests/test_setup.py | 49 +---- 9 files changed, 297 insertions(+), 278 deletions(-) create mode 100644 library/tests/conftest.py diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index d9b1069c..de8ab067 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -67,8 +67,9 @@ 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]] + 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) @@ -76,14 +77,12 @@ def display_text(variable, data, unit): 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)] + 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.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)) + 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) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index c0423e6d..6dda6077 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -2,7 +2,6 @@ import time import colorsys -import os import sys import ST7735 try: @@ -72,8 +71,9 @@ 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]] + 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) @@ -81,14 +81,12 @@ def display_text(variable, data, unit): 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)] + 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.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)) + 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) diff --git a/examples/combined.py b/examples/combined.py index c2fd397e..4b8fbdda 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -2,7 +2,6 @@ import time import colorsys -import os import sys import ST7735 try: @@ -107,23 +106,23 @@ # 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]] +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 +palette = [(0, 0, 255), # Dangerously Low + (0, 255, 255), # Low + (0, 255, 0), # Normal + (255, 255, 0), # High + (255, 0, 0)] # Dangerously High values = {} @@ -133,8 +132,9 @@ 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]] + 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) @@ -142,18 +142,17 @@ def display_text(variable, data, unit): 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)] + 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.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)) + 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] @@ -168,24 +167,23 @@ def save_data(idx, data): def display_everything(): draw.rectangle((0, 0, WIDTH, HEIGHT), (0, 0, 0)) column_count = 2 - row_count = (len(variables)/column_count) + row_count = (len(variables) / column_count) for i in range(len(variables)): variable = variables[i] data_value = values[variable][-1] unit = units[i] - x = x_offset + ((WIDTH/column_count) * (i / row_count)) - y = y_offset + ((HEIGHT/row_count) * (i % row_count)) + x = x_offset + ((WIDTH / column_count) * (i / row_count)) + y = y_offset + ((HEIGHT / row_count) * (i % row_count)) message = "{}: {:.1f} {}".format(variable[:4], data_value, unit) lim = limits[i] rgb = palette[0] for j in range(len(lim)): if data_value > lim[j]: - rgb = palette[j+1] + 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) @@ -193,156 +191,158 @@ def get_cpu_temperature(): 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 = 10 # The starting mode -last_page = 0 -light = 1 +def main(): + # Tuning factor for compensation. Decrease this number to adjust the + # temperature down, and increase to adjust up + factor = 2.25 + + cpu_temps = [get_cpu_temperature()] * 5 + + delay = 0.5 # Debounce the proximity tap + mode = 10 # The starting mode + last_page = 0 + + for v in variables: + values[v] = [1] * WIDTH + + # The main loop + try: + while True: + proximity = ltr559.get_proximity() + + # If the proximity crosses the threshold, toggle the mode + if proximity > 1500 and time.time() - last_page > delay: + mode += 1 + mode %= (len(variables) + 1) + last_page = time.time() + + # One mode for each variable + if mode == 0: + # variable = "temperature" + unit = "C" + cpu_temp = get_cpu_temperature() + # Smooth out with some averaging to decrease jitter + cpu_temps = cpu_temps[1:] + [cpu_temp] + avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) + raw_temp = bme280.get_temperature() + data = raw_temp - ((avg_cpu_temp - raw_temp) / factor) + display_text(variables[mode], data, unit) -for v in variables: - values[v] = [1] * WIDTH + if mode == 1: + # variable = "pressure" + unit = "hPa" + data = bme280.get_pressure() + display_text(variables[mode], data, unit) -# 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)) + if mode == 2: + # variable = "humidity" + unit = "%" + data = bme280.get_humidity() 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)) + 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 == 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)) + if mode == 4: + # variable = "oxidised" + unit = "kO" + data = gas.read_all() + data = data.oxidising / 1000 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() + 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) + if mode == 7: + # variable = "pm1" + unit = "ug/m3" + try: + data = pms5003.read() + except pmsReadTimeoutError: + logging.warn("Failed to read PMS5003") + else: + data = float(data.pm_ug_per_m3(1.0)) + display_text(variables[mode], data, unit) + + if mode == 8: + # variable = "pm25" + unit = "ug/m3" + try: + data = pms5003.read() + except pmsReadTimeoutError: + logging.warn("Failed to read PMS5003") + else: + data = float(data.pm_ug_per_m3(2.5)) + display_text(variables[mode], data, unit) + + if mode == 9: + # variable = "pm10" + unit = "ug/m3" + try: + data = pms5003.read() + except pmsReadTimeoutError: + logging.warn("Failed to read PMS5003") + else: + data = float(data.pm_ug_per_m3(10)) + display_text(variables[mode], data, unit) + if mode == 10: + # Everything on one screen + cpu_temp = get_cpu_temperature() + # Smooth out with some averaging to decrease jitter + cpu_temps = cpu_temps[1:] + [cpu_temp] + avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) + raw_temp = bme280.get_temperature() + raw_data = raw_temp - ((avg_cpu_temp - raw_temp) / factor) + save_data(0, raw_data) + display_everything() + raw_data = bme280.get_pressure() + save_data(1, raw_data) + display_everything() + raw_data = bme280.get_humidity() + save_data(2, raw_data) + if proximity < 10: + raw_data = ltr559.get_lux() + else: + raw_data = 1 + save_data(3, raw_data) + display_everything() + gas_data = gas.read_all() + save_data(4, gas_data.oxidising / 1000) + save_data(5, gas_data.reducing / 1000) + save_data(6, gas_data.nh3 / 1000) + display_everything() + pms_data = None + try: + pms_data = pms5003.read() + except pmsReadTimeoutError: + logging.warn("Failed to read PMS5003") + else: + save_data(7, float(pms_data.pm_ug_per_m3(1.0))) + save_data(8, float(pms_data.pm_ug_per_m3(2.5))) + save_data(9, float(pms_data.pm_ug_per_m3(10))) + display_everything() + + # Exit cleanly + except KeyboardInterrupt: + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/examples/luftdaten.py b/examples/luftdaten.py index d2d65621..84f11177 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -115,32 +115,35 @@ 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")) - resp_1 = requests.post("https://api.luftdaten.info/v1/push-sensor-data/", - json={ - "software_version": "enviro-plus 0.0.1", - "sensordatavalues": [{"value_type": key, "value": val} for - key, val in pm_values.items()] - }, - headers={ - "X-PIN": "1", - "X-Sensor": id, - "Content-Type": "application/json", - "cache-control": "no-cache" - } + 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": [{"value_type": key, "value": val} for - key, val in temp_values.items()] - }, - headers={ - "X-PIN": "11", - "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: diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index 8b1ddd57..4c14c585 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -15,11 +15,11 @@ noise = Noise() disp = ST7735.ST7735( - port=0, - cs=ST7735.BG_SPI_CS_FRONT, - dc=9, - backlight=12, - rotation=90) + port=0, + cs=ST7735.BG_SPI_CS_FRONT, + dc=9, + backlight=12, + rotation=90) disp.begin() diff --git a/examples/noise-profile.py b/examples/noise-profile.py index 1afdff5b..40844391 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -13,11 +13,11 @@ noise = Noise() disp = ST7735.ST7735( - port=0, - cs=ST7735.BG_SPI_CS_FRONT, - dc=9, - backlight=12, - rotation=90) + port=0, + cs=ST7735.BG_SPI_CS_FRONT, + dc=9, + backlight=12, + rotation=90) disp.begin() diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index a26ec7b0..bccf7cc0 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -109,19 +109,19 @@ def sun_moon_time(city_name, time_zone): if sunrise_today < local_dt < sunset_today: day = True period = sunset_today - sunrise_today - mid = sunrise_today + (period / 2) + # 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) + # mid = sunset_today + (period / 2) progress = local_dt - sunset_today else: day = False period = sunrise_today - sunset_yesterday - mid = sunset_yesterday + (period / 2) + # mid = sunset_yesterday + (period / 2) progress = local_dt - sunset_yesterday # Convert time deltas to seconds @@ -151,7 +151,7 @@ def draw_background(progress, period, day): # New image for background colour img = Image.new('RGBA', (WIDTH, HEIGHT), color=background) - draw = ImageDraw.Draw(img) + # draw = ImageDraw.Draw(img) # New image for sun/moon overlay overlay = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0)) @@ -216,12 +216,12 @@ def analyse_pressure(pressure, t): 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)]) + 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 + # variance_per_hour = variance * 60 * 60 mean_pressure = numpy.mean(pressure_vals) @@ -244,10 +244,10 @@ def analyse_pressure(pressure, t): change_per_hour = 0 trend = "-" -# time.sleep(interval) - + # time.sleep(interval) return (mean_pressure, change_per_hour, trend) + def describe_pressure(pressure): """Convert pressure into barometer-type description.""" if pressure < 970: @@ -385,7 +385,7 @@ def describe_light(light): else: range_string = "------" img = overlay_text(img, (68, 18 + spacing), range_string, font_sm, align_right=True, rectangle=True) - temp_icon = Image.open(path + "/icons/temperature.png") + temp_icon = Image.open(f"{path}/icons/temperature.png") img.paste(temp_icon, (margin, 18), mask=temp_icon) # Humidity @@ -396,7 +396,7 @@ def describe_light(light): 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(path + "/icons/humidity-" + humidity_desc.lower() + ".png") + humidity_icon = Image.open(f"{path}/icons/humidity-{humidity_desc.lower()}.png") img.paste(humidity_icon, (margin, 48), mask=humidity_icon) # Light @@ -406,7 +406,7 @@ def describe_light(light): 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(path + "/icons/bulb-" + light_desc.lower() + ".png") + light_icon = Image.open(f"{path}/icons/bulb-{light_desc.lower()}.png") img.paste(humidity_icon, (80, 18), mask=light_icon) # Pressure @@ -418,7 +418,7 @@ def describe_light(light): 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(path + "/icons/weather-" + pressure_desc.lower() + ".png") + pressure_icon = Image.open(f"{path}/icons/weather-{pressure_desc.lower()}.png") img.paste(pressure_icon, (80, 48), mask=pressure_icon) # Display image diff --git a/library/tests/conftest.py b/library/tests/conftest.py new file mode 100644 index 00000000..8a3ffb44 --- /dev/null +++ b/library/tests/conftest.py @@ -0,0 +1,56 @@ +"""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=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'] + diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 7c25d946..95080b6b 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -1,32 +1,15 @@ import sys import mock -from i2cdevice import MockSMBus -class SMBusFakeDevice(MockSMBus): - def __init__(self, i2c_bus): - MockSMBus.__init__(self, i2c_bus) - self.regs[0x00:0x01] = 0x0f, 0x00 - - -def test_gas_setup(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_setup(GPIO, smbus): from enviroplus import gas gas._is_setup = False gas.setup() gas.setup() -def test_gas_read_all(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_all(GPIO, smbus): from enviroplus import gas gas._is_setup = False result = gas.read_all() @@ -43,12 +26,7 @@ def test_gas_read_all(): assert "Oxidising" in str(result) -def test_gas_read_each(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_each(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -57,12 +35,7 @@ def test_gas_read_each(): assert int(gas.read_nh3()) == 16813 -def test_gas_read_adc(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_adc(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -71,12 +44,7 @@ def test_gas_read_adc(): assert gas.read_adc() == 0.255 -def test_gas_read_adc_default_gain(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_adc_default_gain(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -84,12 +52,7 @@ def test_gas_read_adc_default_gain(): assert gas.read_adc() == 0.255 -def test_gas_read_adc_str(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_adc_str(GPIO, smbus): from enviroplus import gas gas._is_setup = False From e9c93677beeb6c6416d8f15aa2bd29ce3cd4b726 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 17 Mar 2020 12:31:33 +0000 Subject: [PATCH 037/122] Test noise, fix gas --- library/tests/conftest.py | 17 ++++++++ library/tests/test_noise.py | 78 +++++++++++++++++++++++++++++++++++++ library/tests/test_setup.py | 34 +++++++++++++++- 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 library/tests/test_noise.py diff --git a/library/tests/conftest.py b/library/tests/conftest.py index 8a3ffb44..b026172c 100644 --- a/library/tests/conftest.py +++ b/library/tests/conftest.py @@ -54,3 +54,20 @@ def 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'] diff --git a/library/tests/test_noise.py b/library/tests/test_noise.py new file mode 100644 index 00000000..c93f8ccb --- /dev/null +++ b/library/tests/test_noise.py @@ -0,0 +1,78 @@ +import sys +import mock +import pytest + + +def force_reimport(module): + """Force the module under test to be re-imported. + + Because pytest runs all tests within the same scope (this makes me cry) + we have to do some manual housekeeping to avoid tests polluting each other. + + Since conftest.py already does some sys.modules mangling I see no reason not to + do the same thing here. + """ + if "." in module: + steps = module.split(".") + else: + steps = [module] + + for i in range(len(steps)): + module = ".".join(steps[0:i + 1]) + try: + del sys.modules[module] + except KeyError: + pass + + +def test_noise_setup(sounddevice, numpy): + force_reimport('enviroplus.noise') + 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): + # Ippity zippidy what is this farce + # a curious function that makes my tests pass? + force_reimport('enviroplus.noise') + 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): + # Ippity zippidy what is this farce + # a curious function that makes my tests pass? + force_reimport('enviroplus.noise') + from enviroplus.noise import Noise + + 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') + + +def test_get_amplitude_at_frequency_range(sounddevice, numpy): + # Ippity zippidy what is this farce + # a curious function that makes my tests pass? + force_reimport('enviroplus.noise') + 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) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 95080b6b..6b6658cb 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -2,6 +2,28 @@ import mock +def force_reimport(module): + """Force the module under test to be re-imported. + + Because pytest runs all tests within the same scope (this makes me cry) + we have to do some manual housekeeping to avoid tests polluting each other. + + Since conftest.py already does some sys.modules mangling I see no reason not to + do the same thing here. + """ + if "." in module: + steps = module.split(".") + else: + steps = [module] + + for i in range(len(steps)): + module = ".".join(steps[0:i + 1]) + try: + del sys.modules[module] + except KeyError: + pass + + def test_gas_setup(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -49,7 +71,8 @@ def test_gas_read_adc_default_gain(GPIO, smbus): gas._is_setup = False gas.enable_adc(True) - assert gas.read_adc() == 0.255 + gas.set_adc_gain(gas.MICS6814_GAIN) + assert gas.read_adc() == 0.765 def test_gas_read_adc_str(GPIO, smbus): @@ -59,3 +82,12 @@ def test_gas_read_adc_str(GPIO, smbus): gas.enable_adc(True) gas.set_adc_gain(2.048) assert 'ADC' in str(gas.read_all()) + + +def test_gas_cleanup(GPIO, smbus): + force_reimport('enviroplus.gas') + from enviroplus import gas + + gas.cleanup() + + GPIO.output.assert_called_with(gas.MICS6814_HEATER_PIN, 0) From 4cc6c622eb92bbc223db977e37bd351f84ef2ee5 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 24 Mar 2020 11:23:30 +0000 Subject: [PATCH 038/122] Move package and requires to setup.cfg --- library/setup.cfg | 1 + library/setup.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/library/setup.cfg b/library/setup.cfg index 83f89fa1..dddc8afd 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -27,6 +27,7 @@ classifiers = Topic :: System :: Hardware [options] +packages = enviroplus install_requires = pimoroni-bme280 pms5003 diff --git a/library/setup.py b/library/setup.py index 784db513..40d6dbc3 100755 --- a/library/setup.py +++ b/library/setup.py @@ -30,7 +30,4 @@ if parse_version(__version__) < minimum_version: raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version)) -setup( - packages=['enviroplus'], - install_requires=['setuptools>={}'.format(minimum_version), 'pimoroni-bme280', 'pms5003', 'ltr559', 'st7735', 'ads1015', 'fonts', 'font-roboto', 'astral', 'pytz'] -) +setup() From e72e5682757a38fb94317d68758b874c61d55dd1 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 29 Apr 2020 11:56:42 +0100 Subject: [PATCH 039/122] Drop Python 2.6 --- library/setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/setup.cfg b/library/setup.cfg index dddc8afd..af306be5 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -4,7 +4,7 @@ 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" +description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi long_description = file: README.rst keywords = Raspberry Pi url = https://www.pimoroni.com @@ -19,7 +19,6 @@ classifiers = Operating System :: POSIX :: Linux License :: OSI Approved :: MIT License Intended Audience :: Developers - Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Topic :: Software Development From 5a376ddbb3ec3399eb6c5872f4ed2ac88cd75afc Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 29 Apr 2020 12:50:37 +0100 Subject: [PATCH 040/122] Catch #61 with tests and fix --- library/enviroplus/noise.py | 2 +- library/tests/conftest.py | 17 +++++++++++++++++ library/tests/test_noise.py | 38 ++++--------------------------------- library/tests/test_setup.py | 27 -------------------------- 4 files changed, 22 insertions(+), 62 deletions(-) diff --git a/library/enviroplus/noise.py b/library/enviroplus/noise.py index 6830bd0d..7b6d5e28 100644 --- a/library/enviroplus/noise.py +++ b/library/enviroplus/noise.py @@ -76,7 +76,7 @@ def get_noise_profile(self, 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 = (low + mid + high) / 3.0 + amp_total = (amp_low + amp_mid + amp_high) / 3.0 return amp_low, amp_mid, amp_high, amp_total diff --git a/library/tests/conftest.py b/library/tests/conftest.py index b026172c..8a5c54c5 100644 --- a/library/tests/conftest.py +++ b/library/tests/conftest.py @@ -14,6 +14,23 @@ def __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.""" diff --git a/library/tests/test_noise.py b/library/tests/test_noise.py index c93f8ccb..3778c166 100644 --- a/library/tests/test_noise.py +++ b/library/tests/test_noise.py @@ -1,32 +1,7 @@ -import sys -import mock import pytest -def force_reimport(module): - """Force the module under test to be re-imported. - - Because pytest runs all tests within the same scope (this makes me cry) - we have to do some manual housekeeping to avoid tests polluting each other. - - Since conftest.py already does some sys.modules mangling I see no reason not to - do the same thing here. - """ - if "." in module: - steps = module.split(".") - else: - steps = [module] - - for i in range(len(steps)): - module = ".".join(steps[0:i + 1]) - try: - del sys.modules[module] - except KeyError: - pass - - def test_noise_setup(sounddevice, numpy): - force_reimport('enviroplus.noise') from enviroplus.noise import Noise noise = Noise(sample_rate=16000, duration=0.1) @@ -34,9 +9,6 @@ def test_noise_setup(sounddevice, numpy): def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): - # Ippity zippidy what is this farce - # a curious function that makes my tests pass? - force_reimport('enviroplus.noise') from enviroplus.noise import Noise noise = Noise(sample_rate=16000, duration=0.1) @@ -49,11 +21,10 @@ def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): def test_noise_get_noise_profile(sounddevice, numpy): - # Ippity zippidy what is this farce - # a curious function that makes my tests pass? - force_reimport('enviroplus.noise') 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, @@ -63,11 +34,10 @@ def test_noise_get_noise_profile(sounddevice, numpy): 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): - # Ippity zippidy what is this farce - # a curious function that makes my tests pass? - force_reimport('enviroplus.noise') from enviroplus.noise import Noise noise = Noise(sample_rate=16000, duration=0.1) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 6b6658cb..2aa7b492 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -1,29 +1,3 @@ -import sys -import mock - - -def force_reimport(module): - """Force the module under test to be re-imported. - - Because pytest runs all tests within the same scope (this makes me cry) - we have to do some manual housekeeping to avoid tests polluting each other. - - Since conftest.py already does some sys.modules mangling I see no reason not to - do the same thing here. - """ - if "." in module: - steps = module.split(".") - else: - steps = [module] - - for i in range(len(steps)): - module = ".".join(steps[0:i + 1]) - try: - del sys.modules[module] - except KeyError: - pass - - def test_gas_setup(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -85,7 +59,6 @@ def test_gas_read_adc_str(GPIO, smbus): def test_gas_cleanup(GPIO, smbus): - force_reimport('enviroplus.gas') from enviroplus import gas gas.cleanup() From 0c5c9465f1a8cecddbeb654b6dbacaedc4ba0e39 Mon Sep 17 00:00:00 2001 From: mendhak Date: Fri, 8 May 2020 20:38:27 +0100 Subject: [PATCH 041/122] Include python cffi in setup dependencies --- library/setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/setup.cfg b/library/setup.cfg index af306be5..d2909c13 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -56,6 +56,7 @@ py2deps = python-numpy python-smbus python-pil + python-cffi python-spidev python-rpi.gpio libportaudio2 @@ -64,6 +65,7 @@ py3deps = python3-numpy python3-smbus python3-pil + python3-cffi python3-spidev python3-rpi.gpio libportaudio2 From 230698ad4272177850319668510c217fb8e699ab Mon Sep 17 00:00:00 2001 From: Sumit Kumar Maitra Date: Thu, 4 Jun 2020 00:45:47 +0100 Subject: [PATCH 042/122] Added supported board images and example --- Enviro-Plus-pHAT.jpg | Bin 0 -> 47407 bytes Enviro-mini-pHAT.jpg | Bin 0 -> 46494 bytes README.md | 8 +- examples/all-in-one-enviro-mini.py | 166 +++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 Enviro-Plus-pHAT.jpg create mode 100644 Enviro-mini-pHAT.jpg create mode 100755 examples/all-in-one-enviro-mini.py diff --git a/Enviro-Plus-pHAT.jpg b/Enviro-Plus-pHAT.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f0947a093c5d7447072352e3835964f54189b6ed GIT binary patch literal 47407 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 Kt`Fd^x&H!!qS0~y literal 0 HcmV?d00001 diff --git a/Enviro-mini-pHAT.jpg b/Enviro-mini-pHAT.jpg new file mode 100644 index 0000000000000000000000000000000000000000..120469f420abe79fe9db2d997afd5e776449340d GIT binary patch literal 46494 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 ZtGD^ym>Wfzkxm#%?`!F?kp1`be*sDckKh0R literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 5fb9fe6b..843de1f3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# Enviro+ +# Enviro+ Designed for environmental monitoring, Enviro+ lets you measure air quality (pollutant gases and particulates), temperature, pressure, humidity, light, and noise level. Learn more - https://shop.pimoroni.com/products/enviro-plus + [![Build Status](https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master)](https://travis-ci.com/pimoroni/enviroplus-python) [![Coverage Status](https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/enviroplus-python?branch=master) [![PyPi Package](https://img.shields.io/pypi/v/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus) @@ -11,6 +12,11 @@ Designed for environmental monitoring, Enviro+ lets you measure air quality (pol 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. +**Note** The code in this repository supports both the Enviro+ and Enviro Mini boards. _The Enviro Mini board does not have the Gas sensor or the breakout for the PM sensor._ + +![Enviro Plus pHAT](./Enviro-Plus-pHAT.jpg) +![Enviro Mini pHAT](./Enviro-mini-pHAT.jpg) + ## One-line (Installs from GitHub) ``` diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py new file mode 100755 index 00000000..d7a001ff --- /dev/null +++ b/examples/all-in-one-enviro-mini.py @@ -0,0 +1,166 @@ +#!/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"] + +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) + +# Exit cleanly +except KeyboardInterrupt: + sys.exit(0) From 6a89566fe3e2227d60780c65ef3bb023ea0e1362 Mon Sep 17 00:00:00 2001 From: Peter McDonald Date: Wed, 17 Jun 2020 23:44:38 +0100 Subject: [PATCH 043/122] Correcting examples --- examples/all-in-one.py | 6 +++--- examples/combined.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 6dda6077..f7933a9e 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -198,7 +198,7 @@ def get_cpu_temperature(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(1.0)) display_text(variables[mode], data, unit) @@ -209,7 +209,7 @@ def get_cpu_temperature(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(2.5)) display_text(variables[mode], data, unit) @@ -220,7 +220,7 @@ def get_cpu_temperature(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(10)) display_text(variables[mode], data, unit) diff --git a/examples/combined.py b/examples/combined.py index 4b8fbdda..b417ccf1 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -12,7 +12,7 @@ import ltr559 from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError +from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError, SerialTimeoutError from enviroplus import gas from subprocess import PIPE, Popen from PIL import Image @@ -276,7 +276,7 @@ def main(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(1.0)) display_text(variables[mode], data, unit) @@ -287,7 +287,7 @@ def main(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(2.5)) display_text(variables[mode], data, unit) @@ -298,7 +298,7 @@ def main(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(10)) display_text(variables[mode], data, unit) @@ -331,8 +331,8 @@ def main(): pms_data = None try: pms_data = pms5003.read() - except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + except (SerialTimeoutError, pmsReadTimeoutError): + logging.warning("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))) From f5335ba6681d5268d306ab438571bb369d682c29 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 18 Jun 2020 09:35:06 +0100 Subject: [PATCH 044/122] Adds mqtt example (#68) Adds mqtt example by @robmarkcole - see also: https://github.com/robmarkcole/rpi-enviro-mqtt --- examples/mqtt-all.py | 212 +++++++++++++++++++++++++++++++++++++++++++ library/setup.cfg | 1 + 2 files changed, 213 insertions(+) create mode 100755 examples/mqtt-all.py diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py new file mode 100755 index 00000000..26f46bb3 --- /dev/null +++ b/examples/mqtt-all.py @@ -0,0 +1,212 @@ +""" +Run mqtt broker on localhost: sudo apt-get install mosquitto mosquitto-clients + +Example run: python3 mqtt-all.py --broker 192.168.1.164 --topic enviro +""" +#!/usr/bin/env python3 + +import argparse +import ST7735 +import time +from bme280 import BME280 +from pms5003 import PMS5003, ReadTimeoutError +from enviroplus import gas + +try: + # Transitional fix for breaking change in LTR559 + from ltr559 import LTR559 + + ltr559 = LTR559() +except ImportError: + import ltr559 + +from subprocess import PIPE, Popen, check_output +from PIL import Image, ImageDraw, ImageFont +from fonts.ttf import RobotoMedium as UserFont +import json + +import paho.mqtt.client as mqtt +import paho.mqtt.publish as publish + +try: + from smbus2 import SMBus +except ImportError: + from smbus import SMBus + + +DEFAULT_MQTT_BROKER_IP = "localhost" +DEFAULT_MQTT_BROKER_PORT = 1883 +DEFAULT_MQTT_TOPIC = "enviroplus" + +# mqtt callbacks +def on_connect(client, userdata, flags, rc): + print(f"CONNACK received with code {rc}") + if rc == 0: + print("connected OK") + else: + print("Bad connection Returned code=", rc) + + +def on_publish(client, userdata, mid): + print("mid: " + str(mid)) + + +# Read values from BME280 and PMS5003 and return as dict +def read_values(bme280, pms5003): + # Compensation factor for temperature + comp_factor = 2.25 + + values = {} + cpu_temp = get_cpu_temperature() + raw_temp = bme280.get_temperature() # float + comp_temp = raw_temp - ((cpu_temp - raw_temp) / comp_factor) + values["temperature"] = int(comp_temp) + values["pressure"] = round( + int(bme280.get_pressure() * 100), -1 + ) # round to nearest 10 + values["humidity"] = int(bme280.get_humidity()) + try: + pm_values = pms5003.read() # int + values["pm1"] = pm_values.pm_ug_per_m3(1) + values["pm25"] = pm_values.pm_ug_per_m3(2.5) + values["pm10"] = pm_values.pm_ug_per_m3(10) + except ReadTimeoutError: + pms5003.reset() + pm_values = pms5003.read() + values["pm1"] = pm_values.pm_ug_per_m3(1) + values["pm25"] = pm_values.pm_ug_per_m3(2.5) + values["pm10"] = pm_values.pm_ug_per_m3(10) + data = gas.read_all() + values["oxidised"] = int(data.oxidising / 1000) + values["reduced"] = int(data.reducing / 1000) + values["nh3"] = int(data.nh3 / 1000) + values["lux"] = int(ltr559.get_lux()) + 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(disp, mqtt_broker): + # Width and height to calculate text position + WIDTH = disp.width + HEIGHT = disp.height + # Text settings + font_size = 16 + font = ImageFont.truetype(UserFont, font_size) + + 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: {}\nmqtt-broker: {}".format(id, wifi_status, mqtt_broker) + 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 main(): + parser = argparse.ArgumentParser(description="Publish enviroplus values over mqtt") + parser.add_argument( + "--broker", default=DEFAULT_MQTT_BROKER_IP, type=str, help="mqtt broker IP", + ) + parser.add_argument( + "--port", default=DEFAULT_MQTT_BROKER_PORT, type=int, help="mqtt broker port", + ) + parser.add_argument( + "--topic", default=DEFAULT_MQTT_TOPIC, type=str, help="mqtt topic" + ) + args = parser.parse_args() + + print( + """mqtt-all.py - Reads temperature, pressure, humidity, + PM2.5, and PM10 from Enviro plus and sends data over mqtt. + + broker: {} + port: {} + topic: {} + + Press Ctrl+C to exit! + + """.format( + args.broker, args.port, args.topic + ) + ) + + mqtt_client = mqtt.Client() + mqtt_client.on_connect = on_connect + mqtt_client.on_publish = on_publish + mqtt_client.connect(args.broker, port=args.port) + + 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() + + # Raspberry Pi ID + device_serial_number = get_serial_number() + id = "raspi-" + device_serial_number + + # 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")) + print("MQTT broker IP: {}".format(args.broker)) + + time_since_update = 0 + update_time = time.time() + + # Main loop to read data, display, and send over mqtt + mqtt_client.loop_start() + while True: + try: + time_since_update = time.time() - update_time + values = read_values(bme280, pms5003) + values["serial"] = device_serial_number + print(values) + mqtt_client.publish(args.topic, json.dumps(values)) + if time_since_update > 145: + update_time = time.time() + display_status(disp, args.broker) + except Exception as e: + print(e) + + +if __name__ == "__main__": + main() diff --git a/library/setup.cfg b/library/setup.cfg index d2909c13..c59250c8 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -38,6 +38,7 @@ install_requires = astral pytz sounddevice + paho-mqtt [flake8] exclude = From f984ea196d3c96763d5048aac9b5656a404a9d65 Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Thu, 18 Jun 2020 09:40:51 +0100 Subject: [PATCH 045/122] Added user projects section to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 843de1f3..bcb1aa61 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ And install additional dependencies: sudo apt install python-numpy python-smbus python-pil python-setuptools ``` +## Alternate Software & User Projects + +* enviro monitor - https://github.com/roscoe81/enviro-monitor +* mqtt-all - https://github.com/robmarkcole/rpi-enviro-mqtt - now upstream: [see examples/mqtt-all.py](examples/mqtt-all.py) +* adafruit_io.py - https://github.com/dedSyn4ps3/enviroplus-python/blob/master/examples/adafruit_io.py - uses Adafruit Blinka and BME280 libraries to publish to Adafruit IO + ## Help & Support * GPIO Pinout - https://pinout.xyz/pinout/enviro_plus From 1d82eac50317410ae4dbbbccbde8cdf65e5d0ba6 Mon Sep 17 00:00:00 2001 From: Tijmen van den Brink Date: Sat, 27 Jun 2020 08:06:05 +0200 Subject: [PATCH 046/122] Added user project: enviroplus_exporter --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bcb1aa61..edcc8373 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ sudo apt install python-numpy python-smbus python-pil python-setuptools * enviro monitor - https://github.com/roscoe81/enviro-monitor * mqtt-all - https://github.com/robmarkcole/rpi-enviro-mqtt - now upstream: [see examples/mqtt-all.py](examples/mqtt-all.py) * adafruit_io.py - https://github.com/dedSyn4ps3/enviroplus-python/blob/master/examples/adafruit_io.py - uses Adafruit Blinka and BME280 libraries to publish to Adafruit IO +* enviroplus_exporter - https://github.com/tijmenvandenbrink/enviroplus_exporter - Prometheus exporter (with added support for Luftdaten and InfluxDB Cloud) ## Help & Support From a1a12d7adabe5fd7dfb5710596fb44cb17dcf062 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sat, 11 Jul 2020 11:22:04 +0100 Subject: [PATCH 047/122] Update mqtt-all.py --- examples/mqtt-all.py | 106 +++++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 40 deletions(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 26f46bb3..0eff47a1 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -9,7 +9,7 @@ import ST7735 import time from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError +from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError from enviroplus import gas try: @@ -37,10 +37,10 @@ DEFAULT_MQTT_BROKER_IP = "localhost" DEFAULT_MQTT_BROKER_PORT = 1883 DEFAULT_MQTT_TOPIC = "enviroplus" +DEFAULT_READ_INTERVAL = 5 # mqtt callbacks def on_connect(client, userdata, flags, rc): - print(f"CONNACK received with code {rc}") if rc == 0: print("connected OK") else: @@ -51,11 +51,10 @@ def on_publish(client, userdata, mid): print("mid: " + str(mid)) -# Read values from BME280 and PMS5003 and return as dict -def read_values(bme280, pms5003): +# Read values from BME280 and return as dict +def read_bme280(bme280): # Compensation factor for temperature comp_factor = 2.25 - values = {} cpu_temp = get_cpu_temperature() raw_temp = bme280.get_temperature() # float @@ -65,6 +64,17 @@ def read_values(bme280, pms5003): int(bme280.get_pressure() * 100), -1 ) # round to nearest 10 values["humidity"] = int(bme280.get_humidity()) + data = gas.read_all() + values["oxidised"] = int(data.oxidising / 1000) + values["reduced"] = int(data.reducing / 1000) + values["nh3"] = int(data.nh3 / 1000) + values["lux"] = int(ltr559.get_lux()) + return values + + +# Read values PMS5003 and return as dict +def read_pms5003(pms5003): + values = {} try: pm_values = pms5003.read() # int values["pm1"] = pm_values.pm_ug_per_m3(1) @@ -76,17 +86,14 @@ def read_values(bme280, pms5003): values["pm1"] = pm_values.pm_ug_per_m3(1) values["pm25"] = pm_values.pm_ug_per_m3(2.5) values["pm10"] = pm_values.pm_ug_per_m3(10) - data = gas.read_all() - values["oxidised"] = int(data.oxidising / 1000) - values["reduced"] = int(data.reducing / 1000) - values["nh3"] = int(data.nh3 / 1000) - values["lux"] = int(ltr559.get_lux()) return values # Get CPU temperature to use for compensation def get_cpu_temperature(): - process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True) + process = Popen( + ["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True + ) output, _error = process.communicate() return float(output[output.index("=") + 1 : output.rindex("'")]) @@ -113,14 +120,16 @@ def display_status(disp, mqtt_broker): WIDTH = disp.width HEIGHT = disp.height # Text settings - font_size = 16 + font_size = 12 font = ImageFont.truetype(UserFont, font_size) 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: {}\nmqtt-broker: {}".format(id, wifi_status, mqtt_broker) + device_serial_number = get_serial_number() + message = "{}\nWi-Fi: {}\nmqtt-broker: {}".format( + device_serial_number, wifi_status, mqtt_broker + ) img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) size_x, size_y = draw.textsize(message, font) @@ -132,34 +141,50 @@ def display_status(disp, mqtt_broker): def main(): - parser = argparse.ArgumentParser(description="Publish enviroplus values over mqtt") + parser = argparse.ArgumentParser( + description="Publish enviroplus values over mqtt" + ) parser.add_argument( - "--broker", default=DEFAULT_MQTT_BROKER_IP, type=str, help="mqtt broker IP", + "--broker", + default=DEFAULT_MQTT_BROKER_IP, + type=str, + help="mqtt broker IP", ) parser.add_argument( - "--port", default=DEFAULT_MQTT_BROKER_PORT, type=int, help="mqtt broker port", + "--port", + default=DEFAULT_MQTT_BROKER_PORT, + type=int, + help="mqtt broker port", ) parser.add_argument( "--topic", default=DEFAULT_MQTT_TOPIC, type=str, help="mqtt topic" ) + parser.add_argument( + "--interval", + default=DEFAULT_READ_INTERVAL, + type=int, + help="the read interval in seconds", + ) args = parser.parse_args() + # Raspberry Pi ID + device_serial_number = get_serial_number() + device_id = "raspi-" + device_serial_number + print( - """mqtt-all.py - Reads temperature, pressure, humidity, - PM2.5, and PM10 from Enviro plus and sends data over mqtt. + f"""mqtt-all.py - Reads Enviro plus data and sends over mqtt. - broker: {} - port: {} - topic: {} + broker: {args.broker} + client_id: {device_id} + port: {args.port} + topic: {args.topic} Press Ctrl+C to exit! - """.format( - args.broker, args.port, args.topic - ) + """ ) - mqtt_client = mqtt.Client() + mqtt_client = mqtt.Client(client_id=device_id) mqtt_client.on_connect = on_connect mqtt_client.on_publish = on_publish mqtt_client.connect(args.broker, port=args.port) @@ -177,33 +202,34 @@ def main(): # Initialize display disp.begin() - # Create PMS5003 instance - pms5003 = PMS5003() - - # Raspberry Pi ID - device_serial_number = get_serial_number() - id = "raspi-" + device_serial_number + # Try to create PMS5003 instance + HAS_PMS = False + try: + pms5003 = PMS5003() + pm_values = pms5003.read() + HAS_PMS = True + print("PMS5003 sensor is connected") + except SerialTimeoutError: + print("No PMS5003 sensor connected") # Display Raspberry Pi serial and Wi-Fi status - print("Raspberry Pi serial: {}".format(get_serial_number())) + print("RPi serial: {}".format(device_serial_number)) print("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) print("MQTT broker IP: {}".format(args.broker)) - time_since_update = 0 - update_time = time.time() - # Main loop to read data, display, and send over mqtt mqtt_client.loop_start() while True: try: - time_since_update = time.time() - update_time - values = read_values(bme280, pms5003) + values = read_bme280(bme280) + if HAS_PMS: + pms_values = read_pms5003(pms5003) + values.update(pms_values) values["serial"] = device_serial_number print(values) mqtt_client.publish(args.topic, json.dumps(values)) - if time_since_update > 145: - update_time = time.time() display_status(disp, args.broker) + time.sleep(args.interval) except Exception as e: print(e) From c161c52b91df55e59441551353551c5b539121ad Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 30 Jul 2020 10:47:04 +0100 Subject: [PATCH 048/122] Fix combined.py indentation for Python 3.x --- examples/combined.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/combined.py b/examples/combined.py index b417ccf1..6c4ab6f5 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -26,7 +26,7 @@ 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("""combined.py - Displays readings from all of Enviro plus' sensors Press Ctrl+C to exit! @@ -172,7 +172,7 @@ def display_everything(): variable = variables[i] data_value = values[variable][-1] unit = units[i] - x = x_offset + ((WIDTH / column_count) * (i / row_count)) + 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] From 3e4b64c3fcb517c2224341ac0ca8ca60a0b48685 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 30 Jul 2020 11:53:10 +0100 Subject: [PATCH 049/122] Experimental fix to communicate Py version reqs for #78 --- examples/weather-and-light.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index bccf7cc0..cd8ae962 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +f"Sorry! This program requires Python >= 3.6 😅" import os import time From ddb2f5d8a7a50ce5524b0013742c35eb4dba2fb5 Mon Sep 17 00:00:00 2001 From: Jaroslav Lichtblau Date: Wed, 5 Aug 2020 12:18:26 +0200 Subject: [PATCH 050/122] Minute instead month in backup file name Fix for the DATESTAMP variable, to show proper file name. --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 3e5b8984..6837697f 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #!/bin/bash CONFIG=/boot/config.txt -DATESTAMP=`date "+%Y-%M-%d-%H-%M-%S"` +DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false USER_HOME=/home/$SUDO_USER From 124f49bc4cfc130ff31890767b8c994b0686cde0 Mon Sep 17 00:00:00 2001 From: Ross Fowler Date: Mon, 10 Aug 2020 08:51:43 +1000 Subject: [PATCH 051/122] Update luftdaten.py Add Logging, PMS5003 Checksum exception and Luftdaten exception handling as per Issue #81 --- examples/luftdaten.py | 133 ++++++++++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 50 deletions(-) diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 84f11177..7701a4ae 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -4,7 +4,7 @@ import ST7735 import time from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError +from pms5003 import PMS5003, ReadTimeoutError, ChecksumMismatchError from subprocess import PIPE, Popen, check_output from PIL import Image, ImageDraw, ImageFont from fonts.ttf import RobotoMedium as UserFont @@ -13,20 +13,26 @@ from smbus2 import SMBus except ImportError: from smbus import SMBus +import logging -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. +logging.basicConfig( + format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + level=logging.INFO, + datefmt='%Y-%m-%d %H:%M:%S') -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. +logging.info("""luftdaten.py - Reads temperature, pressure, humidity, +#PM2.5, and PM10 from Enviro plus and sends data to Luftdaten, +#the citizen science air quality project. -Press Ctrl+C to exit! +#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) @@ -63,7 +69,8 @@ def read_values(): 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: + except(ReadTimeoutError, ChecksumMismatchError): + logging.info("Failed to read PMS5003. Reseting and retrying.") pms5003.reset() pm_values = pms5003.read() values["P2"] = str(pm_values.pm_ug_per_m3(2.5)) @@ -117,37 +124,63 @@ def send_to_luftdaten(values, id): 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 + resp1_exception = False + resp2_exception = False + try: + 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" + }, + timeout=5 + ) + except requests.exceptions.ConnectionError as e: + resp1_exception = True + logging.info('Luftdaten PM Connection Error: ' + str(e)) + except requests.exceptions.Timeout as e: + resp1_exception = True + logging.info('Luftdaten PM Timeout Error: ' + str(e)) + except requests.exceptions.RequestException as e: + resp1_exception = True + logging.info('Luftdaten PM Request Error: ' + str(e)) + + try: + 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" + }, + timeout=5 + ) + except requests.exceptions.ConnectionError as e: + resp2_exception = True + logging.info('Luftdaten Climate Connection Error: ' + str(e)) + except requests.exceptions.Timeout as e: + resp2_exception = True + logging.info('Luftdaten Climate Timeout Error: ' + str(e)) + except requests.exceptions.RequestException as e: + resp2_exception = True + logging.info('Luftdaten Climate Request Error: ' + str(e)) + + if not resp1_exception and not resp2_exception: + if resp_1.ok and resp_2.ok: + return True + else: + return False else: return False @@ -166,9 +199,9 @@ def send_to_luftdaten(values, id): 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")) +# Log Raspberry Pi serial and Wi-Fi status +logging.info("Raspberry Pi serial: {}".format(get_serial_number())) +logging.info("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) time_since_update = 0 update_time = time.time() @@ -176,13 +209,13 @@ def send_to_luftdaten(values, id): # 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) + logging.info(values) + time_since_update = time.time() - update_time if time_since_update > 145: resp = send_to_luftdaten(values, id) update_time = time.time() - print("Response: {}\n".format("ok" if resp else "failed")) + logging.info("Luftdaten Response: {}\n".format("ok" if resp else "failed")) display_status() except Exception as e: - print(e) + logging.info("Main Loop Exception: " + str(e)) From 1a10d4bfb81d15b9355fbb668902bcadfc33dc5a Mon Sep 17 00:00:00 2001 From: Ross Fowler Date: Fri, 14 Aug 2020 20:07:31 +1000 Subject: [PATCH 052/122] Update luftdaten.py Added suggested changes but removed "logging.info('Luftdaten Climate Success', values)", since I found it redundant, given the use of "logging.info("Luftdaten Response: OK")" in Line 215. --- examples/luftdaten.py | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 7701a4ae..0c13771b 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -124,10 +124,12 @@ def send_to_luftdaten(values, id): 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()] - resp1_exception = False - resp2_exception = False + + resp_pm = None + resp_bmp = None + try: - resp_1 = requests.post( + resp_pm = requests.post( "https://api.luftdaten.info/v1/push-sensor-data/", json={ "software_version": "enviro-plus 0.0.1", @@ -142,17 +144,14 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - resp1_exception = True - logging.info('Luftdaten PM Connection Error: ' + str(e)) + logging.warning('Luftdaten PM Connection Error: {}'.format(e)) except requests.exceptions.Timeout as e: - resp1_exception = True - logging.info('Luftdaten PM Timeout Error: ' + str(e)) + logging.warning('Luftdaten PM Timeout Error: {}'.format(e)) except requests.exceptions.RequestException as e: - resp1_exception = True - logging.info('Luftdaten PM Request Error: ' + str(e)) + logging.warning('Luftdaten PM Request Error: {}'.format(e)) try: - resp_2 = requests.post( + resp_bmp = requests.post( "https://api.luftdaten.info/v1/push-sensor-data/", json={ "software_version": "enviro-plus 0.0.1", @@ -167,19 +166,17 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - resp2_exception = True - logging.info('Luftdaten Climate Connection Error: ' + str(e)) + logging.warning('Luftdaten Climate Connection Error: {}'.format(e)) except requests.exceptions.Timeout as e: - resp2_exception = True - logging.info('Luftdaten Climate Timeout Error: ' + str(e)) + logging.warning('Luftdaten Climate Timeout Error: {}'.format(e)) except requests.exceptions.RequestException as e: - resp2_exception = True - logging.info('Luftdaten Climate Request Error: ' + str(e)) + logging.warning('Luftdaten Climate Request Error: {}'.format(e)) - if not resp1_exception and not resp2_exception: - if resp_1.ok and resp_2.ok: + if resp_pm is not None and resp_bmp is not None: + if resp_pm.ok and resp_bmp.ok: return True else: + logging.warning('Luftdaten Error. PM: {}, Climate: {}'.format(resp_pm.reason, resp_bmp.reason)) return False else: return False @@ -210,12 +207,14 @@ def send_to_luftdaten(values, id): while True: try: values = read_values() - logging.info(values) time_since_update = time.time() - update_time if time_since_update > 145: - resp = send_to_luftdaten(values, id) + logging.info(values) update_time = time.time() - logging.info("Luftdaten Response: {}\n".format("ok" if resp else "failed")) + if send_to_luftdaten(values, id): + logging.info("Luftdaten Response: OK") + else: + logging.warning("Luftdaten Response: Failed") display_status() except Exception as e: - logging.info("Main Loop Exception: " + str(e)) + logging.warning('Main Loop Exception: {}'.format(e)) From 8553be8ddd4ba9886fc9eb5a93730aad3b96cb44 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Sat, 14 Nov 2020 17:30:36 +0000 Subject: [PATCH 053/122] Add GitHub actions workflow --- .github/workflows/test.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..d8187f0a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Python Tests + +on: + pull_request: + push: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python: [2.7, 3.4, 3.5, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + python -m pip install --upgrade setuptools tox + - name: Run Tests + working-directory: library + run: | + tox -e py + - name: Coverage + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: library + run: | + python -m pip install coveralls + coveralls + if: ${{ matrix.python == '3.8' }} + From 051aec5225cdb45d259b93cf5256ff0e54c85091 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Sat, 14 Nov 2020 17:38:07 +0000 Subject: [PATCH 054/122] Remove .travis.yml --- .travis.yml | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 79decd6f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: python -sudo: false -cache: pip - -git: - submodules: true - -matrix: - include: - - python: "2.7" - env: TOXENV=py27 - - python: "3.5" - env: TOXENV=py35 - -install: - - pip install --ignore-installed --upgrade setuptools pip tox coveralls - -script: - - cd library - - tox -vv - -after_success: if [ "$TOXENV" == "py35" ]; then coveralls; fi - -notifications: - email: false From f7229e09ecc0da158d9b7c02f5ab13ad219c5edb Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 17 Dec 2020 11:46:47 -0500 Subject: [PATCH 055/122] Catching errors With no PMS sensor attached you cannot flip through the screens ( tapping the light sensor until you reach the PMS page will throw uncaught exception. Added exception to the three cases, in the same manner as the main page --- examples/combined.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/combined.py b/examples/combined.py index 6c4ab6f5..556bcb0c 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -275,7 +275,7 @@ def main(): unit = "ug/m3" try: data = pms5003.read() - except pmsReadTimeoutError: + except (SerialTimeoutError, pmsReadTimeoutError): logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(1.0)) @@ -286,7 +286,7 @@ def main(): unit = "ug/m3" try: data = pms5003.read() - except pmsReadTimeoutError: + except (SerialTimeoutError, pmsReadTimeoutError): logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(2.5)) @@ -297,7 +297,7 @@ def main(): unit = "ug/m3" try: data = pms5003.read() - except pmsReadTimeoutError: + except (SerialTimeoutError, pmsReadTimeoutError): logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(10)) From aeaad174b8da7aa5dabc31a126fa2e287c98f18e Mon Sep 17 00:00:00 2001 From: Peter Armstrong Date: Sun, 20 Dec 2020 12:46:19 +0000 Subject: [PATCH 056/122] adds username and password parameters to mqtt-all --- examples/mqtt-all.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 0eff47a1..2e9df340 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -1,7 +1,7 @@ """ Run mqtt broker on localhost: sudo apt-get install mosquitto mosquitto-clients -Example run: python3 mqtt-all.py --broker 192.168.1.164 --topic enviro +Example run: python3 mqtt-all.py --broker 192.168.1.164 --topic enviro --username xxx --password xxxx """ #!/usr/bin/env python3 @@ -165,6 +165,19 @@ def main(): type=int, help="the read interval in seconds", ) + parser.add_argument( + "--username", + default=None, + type=str, + help="mqtt username", + ) + parser.add_argument( + "--password", + default=None, + type=str, + help="mqtt password", + ) + args = parser.parse_args() # Raspberry Pi ID @@ -178,6 +191,8 @@ def main(): client_id: {device_id} port: {args.port} topic: {args.topic} + username: {args.username} + password: {args.password} Press Ctrl+C to exit! @@ -185,6 +200,7 @@ def main(): ) mqtt_client = mqtt.Client(client_id=device_id) + mqtt_client.username_pw_set(args.username, args.password) mqtt_client.on_connect = on_connect mqtt_client.on_publish = on_publish mqtt_client.connect(args.broker, port=args.port) From d2f4688195b6aa5b1beb4745623672ed854ef63c Mon Sep 17 00:00:00 2001 From: Peter Armstrong Date: Mon, 4 Jan 2021 12:16:13 +0000 Subject: [PATCH 057/122] adds username and password check --- examples/mqtt-all.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 2e9df340..07eecccc 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -200,7 +200,8 @@ def main(): ) mqtt_client = mqtt.Client(client_id=device_id) - mqtt_client.username_pw_set(args.username, args.password) + if username and password: + mqtt_client.username_pw_set(args.username, args.password) mqtt_client.on_connect = on_connect mqtt_client.on_publish = on_publish mqtt_client.connect(args.broker, port=args.port) From 34aec98724f8c0e6fa5a20f509744809234bf319 Mon Sep 17 00:00:00 2001 From: Sergio Rubio Date: Tue, 26 Jan 2021 17:26:12 +0100 Subject: [PATCH 058/122] Add two third party projects to the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index edcc8373..4bce5f5b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ sudo apt install python-numpy python-smbus python-pil python-setuptools * mqtt-all - https://github.com/robmarkcole/rpi-enviro-mqtt - now upstream: [see examples/mqtt-all.py](examples/mqtt-all.py) * adafruit_io.py - https://github.com/dedSyn4ps3/enviroplus-python/blob/master/examples/adafruit_io.py - uses Adafruit Blinka and BME280 libraries to publish to Adafruit IO * enviroplus_exporter - https://github.com/tijmenvandenbrink/enviroplus_exporter - Prometheus exporter (with added support for Luftdaten and InfluxDB Cloud) +* homekit-enviroplus - https://github.com/sighmon/homekit-enviroplus - An Apple HomeKit accessory for the Pimoroni Enviro+ +* go-enviroplus - https://github.com/rubiojr/go-enviroplus - Go modules to read Enviro+ sensors ## Help & Support From a9a34aa1c9b9f9f182ed56b53d9042b63ea6023f Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Tue, 26 Jan 2021 17:18:36 +0000 Subject: [PATCH 059/122] Update python versions for CI (#96) * Drop Python 3.4 - GitHub reports it as not found, despite it being in the list it links to!?!?!? * Fix Coveralls --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8187f0a..89154d53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [2.7, 3.4, 3.5, 3.7, 3.8] + python: [2.7, 3.5, 3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 @@ -32,6 +32,6 @@ jobs: working-directory: library run: | python -m pip install coveralls - coveralls + coveralls --service=github if: ${{ matrix.python == '3.8' }} From ae3e87dc933d1e94548f40e1903027fc7b2e7096 Mon Sep 17 00:00:00 2001 From: James Sutton <1068763+jpwsutton@users.noreply.github.com> Date: Fri, 19 Feb 2021 20:07:48 +0000 Subject: [PATCH 060/122] Adding MQTT Username / Password & TLS Config Signed-off-by: James Sutton <1068763+jpwsutton@users.noreply.github.com> --- examples/mqtt-all.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 0eff47a1..d531120e 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -8,6 +8,7 @@ import argparse import ST7735 import time +import ssl from bme280 import BME280 from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError from enviroplus import gas @@ -38,6 +39,9 @@ DEFAULT_MQTT_BROKER_PORT = 1883 DEFAULT_MQTT_TOPIC = "enviroplus" DEFAULT_READ_INTERVAL = 5 +DEFAULT_TLS_MODE = False +DEFAULT_USERNAME = None +DEFAULT_PASSWORD = None # mqtt callbacks def on_connect(client, userdata, flags, rc): @@ -165,6 +169,24 @@ def main(): type=int, help="the read interval in seconds", ) + parser.add_argument( + "--tls", + default=DEFAULT_TLS_MODE, + type=bool, + help="enable TLS" + ) + parser.add_argument( + "--username", + default=DEFAULT_USERNAME, + type=str, + help="mqtt username" + ) + parser.add_argument( + "--password", + default=DEFAULT_PASSWORD, + type=str, + help="mqtt password" + ) args = parser.parse_args() # Raspberry Pi ID @@ -178,6 +200,9 @@ def main(): client_id: {device_id} port: {args.port} topic: {args.topic} + tls: {args.tls} + username: {args.username} + password: {args.password} Press Ctrl+C to exit! @@ -187,6 +212,13 @@ def main(): mqtt_client = mqtt.Client(client_id=device_id) mqtt_client.on_connect = on_connect mqtt_client.on_publish = on_publish + + if args.tls is True: + mqtt_client.tls_set(tls_version=ssl.PROTOCOL_TLSv1_2) + + if args.username is not None: + mqtt_client.username_pw_set(args.username, password=args.password) + mqtt_client.connect(args.broker, port=args.port) bus = SMBus(1) From 2dbabe56cd8cddb2a8c641a9b545eacc80e0c4f0 Mon Sep 17 00:00:00 2001 From: James Sutton <1068763+jpwsutton@users.noreply.github.com> Date: Fri, 19 Feb 2021 20:23:24 +0000 Subject: [PATCH 061/122] Tweaking the arguments for MQTT TLS Signed-off-by: James Sutton <1068763+jpwsutton@users.noreply.github.com> --- examples/mqtt-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index d531120e..9735ddcd 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -172,7 +172,7 @@ def main(): parser.add_argument( "--tls", default=DEFAULT_TLS_MODE, - type=bool, + action='store_true', help="enable TLS" ) parser.add_argument( From 2ef6bafef176ccffbd4371a464854372cc8687a6 Mon Sep 17 00:00:00 2001 From: Lucas Dodgson Date: Mon, 8 Mar 2021 16:22:49 +0100 Subject: [PATCH 062/122] Added a file with the functionality of combined, while also sharing the data with luftdaten --- examples/luftdaten_combined.py | 445 +++++++++++++++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 examples/luftdaten_combined.py diff --git a/examples/luftdaten_combined.py b/examples/luftdaten_combined.py new file mode 100644 index 00000000..1d920db4 --- /dev/null +++ b/examples/luftdaten_combined.py @@ -0,0 +1,445 @@ +import logging +import sys +import requests +import ST7735 +import time +import colorsys +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 +from enviroplus import gas + +try: + from smbus2 import SMBus +except ImportError: + from smbus import SMBus +try: + # Transitional fix for breaking change in LTR559 + from ltr559 import LTR559 + ltr559 = LTR559() +except ImportError: + import ltr559 + +print("""luftdaten_combined.py - This combines the functionality of luftdaten.py and combined.py +================================================================================================ +Luftdaten INFO +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! + +======================================================================== + +Combined INFO: +Displays readings from all of Enviro plus' sensors + +Press Ctrl+C to exit! + +""") + +logging.basicConfig( + format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + level=logging.INFO, + datefmt='%Y-%m-%d %H:%M:%S') + +logging.info(""" """) +bus = SMBus(1) + +# Create BME280 instance +bme280 = BME280(i2c_dev=bus) + + +# Create PMS5003 instance +pms5003 = PMS5003() + +# 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, 25, 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_lcd = {} + + +# Read values from BME280 and PMS5003 and return as dict +def read_values(comp_temp, mod_press, raw_humid, raw_pm25, raw_pm10): + values = {} + values["temperature"] = "{:.2f}".format(comp_temp) + values["pressure"] = "{:.2f}".format(mod_press) + values["humidity"] = "{:.2f}".format(raw_humid) + values["P2"] = str(raw_pm25) + values["P1"] = str(raw_pm10) + 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 + + +# Create ST7735 LCD display class +st7735 = ST7735.ST7735( + port=0, + cs=1, + dc=9, + backlight=12, + rotation=270, + spi_speed_hz=10000000 +) + +# Initialize display +st7735.begin() + +WIDTH = st7735.width +HEIGHT = st7735.height + +# Set up canvas and font +img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +draw = ImageDraw.Draw(img) +font_size_small = 10 +font_size_large = 20 +font = ImageFont.truetype(UserFont, font_size_large) +smallfont = ImageFont.truetype(UserFont, font_size_small) +x_offset = 2 +y_offset = 2 +message = "" + +# The position of the top bar +top_pos = 25 + +# 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_lcd[variable] = values_lcd[variable][1:] + [data] + unit = units[idx] + message = "{}: {:.1f} {}".format(variable[:4], data, unit) + logging.info(message) + + +# Displays data and text on the 0.96" LCD +def display_text(variable, data, unit): + # Maintain length of list + values_lcd[variable] = values_lcd[variable][1:] + [data] + # Scale the values for the variable between 0 and 1 + vmin = min(values_lcd[variable]) + vmax = max(values_lcd[variable]) + colours = [(v - vmin + 1) / (vmax - vmin + 1) + for v in values_lcd[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) + +# Displays all the text on the 0.96" LCD + + +def display_everything(): + draw.rectangle((0, 0, WIDTH, HEIGHT), (0, 0, 0)) + column_count = 2 + row_count = (len(variables) / column_count) + for i in range(len(variables)): + variable = variables[i] + data_value = values_lcd[variable][-1] + unit = units[i] + x = x_offset + ((WIDTH // column_count) * (i // row_count)) + y = y_offset + ((HEIGHT / row_count) * (i % row_count)) + message = "{}: {:.1f} {}".format(variable[:4], data_value, unit) + lim = limits[i] + rgb = palette[0] + for j in range(len(lim)): + if data_value > lim[j]: + rgb = palette[j + 1] + draw.text((x, y), message, font=smallfont, fill=rgb) + st7735.display(img) + + +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 = 1 + +# Raspberry Pi ID to send to Luftdaten +id = "raspi-" + get_serial_number() + + +# Added for state +delay = 0.5 # Debounce the proximity tap +mode = 10 # The starting mode +last_page = 0 +light = 1 + + +for v in variables: + values_lcd[v] = [1] * WIDTH + + +# Text settings +font_size = 16 +font = ImageFont.truetype(UserFont, font_size) +cpu_temps = [get_cpu_temperature()] * 5 + +# 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() +cpu_temps_len = float(len(cpu_temps)) + +# Main loop to read data, display, and send to Luftdaten +while True: + try: + curtime = time.time() + time_since_update = curtime - update_time + + # Calculate these things once, not twice + 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) / cpu_temps_len + raw_temp = bme280.get_temperature() + comp_temp = raw_temp - ((avg_cpu_temp - raw_temp) / comp_factor) + + raw_press = bme280.get_pressure() + raw_humid = bme280.get_humidity() + try: + pm_values = pms5003.read() + raw_pm25 = pm_values.pm_ug_per_m3(2.5) + raw_pm10 = pm_values.pm_ug_per_m3(10) + except ReadTimeoutError: + pms5003.reset() + pm_values = pms5003.read() + raw_pm25 = pm_values.pm_ug_per_m3(2.5) + raw_pm10 = pm_values.pm_ug_per_m3(10) + + if time_since_update > 145: + values = read_values(comp_temp, raw_press*100, + raw_humid, raw_pm25, raw_pm10) + resp = send_to_luftdaten(values, id) + update_time = curtime + print("Response: {}\n".format("ok" if resp else "failed")) + + # Now comes the combined.py functionality: + # If the proximity crosses the threshold, toggle the mode + proximity = ltr559.get_proximity() + if proximity > 1500 and curtime - last_page > delay: + mode = (mode + 1) % 11 + last_page = curtime + # One mode for each variable + if mode == 0: + # variable = "temperature" + unit = "C" + display_text(variables[mode], comp_temp, unit) + + if mode == 1: + # variable = "pressure" + unit = "hPa" + display_text(variables[mode], raw_press, unit) + + if mode == 2: + # variable = "humidity" + unit = "%" + display_text(variables[mode], raw_humid, 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" + data = float(pm_values.pm_ug_per_m3(1.0)) + display_text(variables[mode], data, unit) + + if mode == 8: + # variable = "pm25" + unit = "ug/m3" + display_text(variables[mode], float(raw_pm25), unit) + + if mode == 9: + # variable = "pm10" + unit = "ug/m3" + display_text(variables[mode], float(raw_pm10), unit) + + if mode == 10: + # Everything on one screen + save_data(0, comp_temp) + save_data(1, raw_press) + display_everything() + save_data(2, raw_humid) + 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 + save_data(7, float(pm_values.pm_ug_per_m3(1.0))) + save_data(8, float(raw_pm25)) + save_data(9, float(raw_pm10)) + display_everything() + except Exception as e: + print(e) From 8e62b4f76481ff38831beef1b86d2f1cca77a4f2 Mon Sep 17 00:00:00 2001 From: Peter Armstrong Date: Tue, 13 Apr 2021 13:02:36 +0100 Subject: [PATCH 063/122] fix checking for username and password --- examples/mqtt-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 07eecccc..5f25e393 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -200,7 +200,7 @@ def main(): ) mqtt_client = mqtt.Client(client_id=device_id) - if username and password: + if args.username and args.password: mqtt_client.username_pw_set(args.username, args.password) mqtt_client.on_connect = on_connect mqtt_client.on_publish = on_publish From ba1042d0b2245f9ad5d0d12ac55c01f7b66fa223 Mon Sep 17 00:00:00 2001 From: James Sutton <1068763+jpwsutton@users.noreply.github.com> Date: Fri, 19 Feb 2021 20:07:48 +0000 Subject: [PATCH 064/122] Adding MQTT Username / Password & TLS Config Signed-off-by: James Sutton <1068763+jpwsutton@users.noreply.github.com> --- examples/mqtt-all.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 5f25e393..895a8a26 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -8,6 +8,7 @@ import argparse import ST7735 import time +import ssl from bme280 import BME280 from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError from enviroplus import gas @@ -38,6 +39,9 @@ DEFAULT_MQTT_BROKER_PORT = 1883 DEFAULT_MQTT_TOPIC = "enviroplus" DEFAULT_READ_INTERVAL = 5 +DEFAULT_TLS_MODE = False +DEFAULT_USERNAME = None +DEFAULT_PASSWORD = None # mqtt callbacks def on_connect(client, userdata, flags, rc): @@ -165,19 +169,24 @@ def main(): type=int, help="the read interval in seconds", ) + parser.add_argument( + "--tls", + default=DEFAULT_TLS_MODE, + type=bool, + help="enable TLS" + ) parser.add_argument( "--username", - default=None, + default=DEFAULT_USERNAME, type=str, - help="mqtt username", + help="mqtt username" ) parser.add_argument( "--password", - default=None, + default=DEFAULT_PASSWORD, type=str, - help="mqtt password", + help="mqtt password" ) - args = parser.parse_args() # Raspberry Pi ID @@ -191,6 +200,7 @@ def main(): client_id: {device_id} port: {args.port} topic: {args.topic} + tls: {args.tls} username: {args.username} password: {args.password} @@ -204,6 +214,13 @@ def main(): mqtt_client.username_pw_set(args.username, args.password) mqtt_client.on_connect = on_connect mqtt_client.on_publish = on_publish + + if args.tls is True: + mqtt_client.tls_set(tls_version=ssl.PROTOCOL_TLSv1_2) + + if args.username is not None: + mqtt_client.username_pw_set(args.username, password=args.password) + mqtt_client.connect(args.broker, port=args.port) bus = SMBus(1) From d3c7e731ec6d57f16a3d4983f1cec9529bb37077 Mon Sep 17 00:00:00 2001 From: James Sutton <1068763+jpwsutton@users.noreply.github.com> Date: Fri, 19 Feb 2021 20:23:24 +0000 Subject: [PATCH 065/122] Tweaking the arguments for MQTT TLS Signed-off-by: James Sutton <1068763+jpwsutton@users.noreply.github.com> --- examples/mqtt-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 895a8a26..b725a6c8 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -172,7 +172,7 @@ def main(): parser.add_argument( "--tls", default=DEFAULT_TLS_MODE, - type=bool, + action='store_true', help="enable TLS" ) parser.add_argument( From 27ab20d43a7ebd8722aecf98d86d48b7891b530c Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Tue, 31 Aug 2021 17:08:34 +0100 Subject: [PATCH 066/122] Add support for ADS1115 Use the auto-detect feature of the ADS1015 library to support reading the gas sensor via an ADS1115. --- library/enviroplus/gas.py | 8 ++++++-- library/setup.cfg | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/library/enviroplus/gas.py b/library/enviroplus/gas.py index 584317b6..f5eb2ab6 100644 --- a/library/enviroplus/gas.py +++ b/library/enviroplus/gas.py @@ -41,15 +41,19 @@ def __repr__(self): def setup(): - global adc, _is_setup + global adc, adc_type, _is_setup if _is_setup: return _is_setup = True adc = ads1015.ADS1015(i2c_addr=0x49) + adc_type = adc.detect_chip_type() adc.set_mode('single') adc.set_programmable_gain(MICS6814_GAIN) - adc.set_sample_rate(1600) + if adc_type == 'ADS1115': + adc.set_sample_rate(128) + else: + adc.set_sample_rate(1600) GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) diff --git a/library/setup.cfg b/library/setup.cfg index c59250c8..598d88ca 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -32,7 +32,7 @@ install_requires = pms5003 ltr559 st7735 - ads1015 + ads1015 >= 0.0.7 fonts font-roboto astral From 1f68a8eef6b638f91516c19428507a1cf90a67a0 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 2 Sep 2021 11:36:53 +0100 Subject: [PATCH 067/122] Add support for ADS1115 Switch README to markdown (drop dependency on pandoc) --- Makefile | 10 ++-- library/CHANGELOG.txt | 6 +++ library/MANIFEST.in | 2 +- library/README.md | 94 ++++++++++++++++++++++++++++++++++ library/README.rst | 93 --------------------------------- library/enviroplus/__init__.py | 2 +- library/setup.cfg | 5 +- 7 files changed, 110 insertions(+), 102 deletions(-) create mode 100644 library/README.md delete mode 100644 library/README.rst diff --git a/Makefile b/Makefile index d2bba498..948282df 100644 --- a/Makefile +++ b/Makefile @@ -36,14 +36,14 @@ check: tag: git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" -python-readme: library/README.rst +python-readme: library/README.md python-license: library/LICENSE.txt -library/README.rst: README.md library/CHANGELOG.txt - pandoc --from=markdown --to=rst -o library/README.rst README.md - echo "" >> library/README.rst - cat library/CHANGELOG.txt >> library/README.rst +library/README.md: README.md library/CHANGELOG.txt + cp README.md library/README.md + printf "\n# Changelog\n" >> library/README.md + cat library/CHANGELOG.txt >> library/README.md library/LICENSE.txt: LICENSE cp LICENSE library/LICENSE.txt diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 81d41368..4e59b220 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,9 @@ +0.0.4 +----- + +* Add support for ads1015 >= v0.0.7 (ADS1115 ADCs) +* Packaging tweaks + 0.0.3 ----- diff --git a/library/MANIFEST.in b/library/MANIFEST.in index 43329d99..478b3f30 100644 --- a/library/MANIFEST.in +++ b/library/MANIFEST.in @@ -1,5 +1,5 @@ include CHANGELOG.txt include LICENSE.txt -include README.rst +include README.md include setup.py recursive-include enviroplus *.py diff --git a/library/README.md b/library/README.md new file mode 100644 index 00000000..43572bf1 --- /dev/null +++ b/library/README.md @@ -0,0 +1,94 @@ +# Enviro+ + +Designed for environmental monitoring, Enviro+ lets you measure air quality (pollutant gases and particulates), temperature, pressure, humidity, light, and noise level. Learn more - https://shop.pimoroni.com/products/enviro-plus + + +[![Build Status](https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master)](https://travis-ci.com/pimoroni/enviroplus-python) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/enviroplus-python?branch=master) +[![PyPi Package](https://img.shields.io/pypi/v/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus) +[![Python Versions](https://img.shields.io/pypi/pyversions/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus) + +# Installing + +You're best using the "One-line" install method if you want all of the UART serial configuration for the PMS5003 particulate matter sensor to run automatically. + +**Note** The code in this repository supports both the Enviro+ and Enviro Mini boards. _The Enviro Mini board does not have the Gas sensor or the breakout for the PM sensor._ + +![Enviro Plus pHAT](./Enviro-Plus-pHAT.jpg) +![Enviro Mini pHAT](./Enviro-mini-pHAT.jpg) + +## 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 +``` + +## Alternate Software & User Projects + +* enviro monitor - https://github.com/roscoe81/enviro-monitor +* mqtt-all - https://github.com/robmarkcole/rpi-enviro-mqtt - now upstream: [see examples/mqtt-all.py](examples/mqtt-all.py) +* adafruit_io.py - https://github.com/dedSyn4ps3/enviroplus-python/blob/master/examples/adafruit_io.py - uses Adafruit Blinka and BME280 libraries to publish to Adafruit IO +* enviroplus_exporter - https://github.com/tijmenvandenbrink/enviroplus_exporter - Prometheus exporter (with added support for Luftdaten and InfluxDB Cloud) +* homekit-enviroplus - https://github.com/sighmon/homekit-enviroplus - An Apple HomeKit accessory for the Pimoroni Enviro+ +* go-enviroplus - https://github.com/rubiojr/go-enviroplus - Go modules to read Enviro+ sensors + +## Help & Support + +* GPIO Pinout - https://pinout.xyz/pinout/enviro_plus +* Support forums - http://forums.pimoroni.com/c/support +* Discord - https://discord.gg/hr93ByC + +# Changelog +0.0.4 +----- + +* Add support for ads1015 >= v0.0.7 (ADS1115 ADCs) +* Packaging tweaks + +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 diff --git a/library/README.rst b/library/README.rst deleted file mode 100644 index bd74b9d4..00000000 --- a/library/README.rst +++ /dev/null @@ -1,93 +0,0 @@ -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 diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py index ffcc925a..156d6f9a 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.3' +__version__ = '0.0.4' diff --git a/library/setup.cfg b/library/setup.cfg index 598d88ca..a6ba0bab 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.3 +version = 0.0.4 author = Philip Howard author_email = phil@pimoroni.com description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi -long_description = file: README.rst +long_description = file: README.md +long_description_content_type = text/markdown keywords = Raspberry Pi url = https://www.pimoroni.com project_urls = From 582c7757a38492eb1e4d0932490422d936f90eb9 Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Tue, 2 Nov 2021 10:17:46 +0000 Subject: [PATCH 068/122] Drop Python2 support. --- .github/workflows/test.yml | 4 ++-- README.md | 2 +- install.sh | 11 +++-------- library/CHANGELOG.txt | 5 +++++ library/README.md | 8 +++++++- library/enviroplus/__init__.py | 2 +- library/setup.cfg | 12 ++---------- library/tox.ini | 2 +- 8 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89154d53..5fac95b2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [2.7, 3.5, 3.6, 3.7, 3.8] + python: [3.6, 3.7, 3.9] steps: - uses: actions/checkout@v2 @@ -33,5 +33,5 @@ jobs: run: | python -m pip install coveralls coveralls --service=github - if: ${{ matrix.python == '3.8' }} + if: ${{ matrix.python == '3.9' }} diff --git a/README.md b/README.md index 4bce5f5b..ad90197c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ curl -sSL https://get.pimoroni.com/enviroplus | bash ## Or... Install from PyPi and configure manually: -* Run `sudo pip install enviroplus` +* Run `sudo python3 -m pip install enviroplus` **Note** this wont perform any of the required configuration changes on your Pi, you may additionally need to: diff --git a/install.sh b/install.sh index 6837697f..a8513b6d 100755 --- a/install.sh +++ b/install.sh @@ -139,14 +139,6 @@ printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" cd library -printf "Installing for Python 2..\n" -apt_pkg_install "${PY2_DEPS[@]}" -python setup.py install > /dev/null -if [ $? -eq 0 ]; then - success "Done!\n" - echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER -fi - if [ -f "/usr/bin/python3" ]; then printf "Installing for Python 3..\n" apt_pkg_install "${PY3_DEPS[@]}" @@ -155,6 +147,9 @@ if [ -f "/usr/bin/python3" ]; then success "Done!\n" echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER fi +else + printf "/usr/bin/python3 not found. Unable to install!\n" + exit 1 fi cd $WD diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 4e59b220..4dd54d57 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.0.5 +----- + +* Drop Python 2.x support + 0.0.4 ----- diff --git a/library/README.md b/library/README.md index 43572bf1..a1136e8e 100644 --- a/library/README.md +++ b/library/README.md @@ -35,7 +35,7 @@ curl -sSL https://get.pimoroni.com/enviroplus | bash ## Or... Install from PyPi and configure manually: -* Run `sudo pip install enviroplus` +* Run `sudo python3 -m pip install enviroplus` **Note** this wont perform any of the required configuration changes on your Pi, you may additionally need to: @@ -70,6 +70,12 @@ sudo apt install python-numpy python-smbus python-pil python-setuptools * Discord - https://discord.gg/hr93ByC # Changelog + +0.0.5 +----- + +* Drop Python 2.x support + 0.0.4 ----- diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py index 156d6f9a..eead3198 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.4' +__version__ = '0.0.5' diff --git a/library/setup.cfg b/library/setup.cfg index a6ba0bab..591de903 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.4 +version = 0.0.5 author = Philip Howard author_email = phil@pimoroni.com description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi @@ -20,13 +20,13 @@ classifiers = 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] +python_requires = >= 3.6 packages = enviroplus install_requires = pimoroni-bme280 @@ -54,14 +54,6 @@ ignore = [pimoroni] py2deps = - python-pip - python-numpy - python-smbus - python-pil - python-cffi - python-spidev - python-rpi.gpio - libportaudio2 py3deps = python3-pip python3-numpy diff --git a/library/tox.ini b/library/tox.ini index aa962163..fcee0794 100644 --- a/library/tox.ini +++ b/library/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,35},qa +envlist = py{36, 37, 38, 39},qa skip_missing_interpreters = True [testenv] From 47089ae867ac6aa9b4a3f23a02f10631fcd10aa5 Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Tue, 2 Nov 2021 10:40:37 +0000 Subject: [PATCH 069/122] Linting fixes --- examples/luftdaten.py | 4 ++-- examples/mqtt-all.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 0c13771b..d0bee376 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -127,7 +127,7 @@ def send_to_luftdaten(values, id): resp_pm = None resp_bmp = None - + try: resp_pm = requests.post( "https://api.luftdaten.info/v1/push-sensor-data/", @@ -150,7 +150,7 @@ def send_to_luftdaten(values, id): except requests.exceptions.RequestException as e: logging.warning('Luftdaten PM Request Error: {}'.format(e)) - try: + try: resp_bmp = requests.post( "https://api.luftdaten.info/v1/push-sensor-data/", json={ diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index b725a6c8..8220c678 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -1,9 +1,9 @@ +#!/usr/bin/env python3 """ Run mqtt broker on localhost: sudo apt-get install mosquitto mosquitto-clients Example run: python3 mqtt-all.py --broker 192.168.1.164 --topic enviro --username xxx --password xxxx """ -#!/usr/bin/env python3 import argparse import ST7735 @@ -43,6 +43,7 @@ DEFAULT_USERNAME = None DEFAULT_PASSWORD = None + # mqtt callbacks def on_connect(client, userdata, flags, rc): if rc == 0: @@ -99,7 +100,7 @@ def get_cpu_temperature(): ["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True ) output, _error = process.communicate() - return float(output[output.index("=") + 1 : output.rindex("'")]) + return float(output[output.index("=") + 1:output.rindex("'")]) # Get Raspberry Pi serial number to use as ID @@ -240,7 +241,7 @@ def main(): HAS_PMS = False try: pms5003 = PMS5003() - pm_values = pms5003.read() + _ = pms5003.read() HAS_PMS = True print("PMS5003 sensor is connected") except SerialTimeoutError: From 7f40ecfda25117e585f1e95a51e4af65a679b433 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 18 Jun 2020 10:42:25 +0100 Subject: [PATCH 070/122] Add available() method to gas sensor This change catches an IOError when setting up the gas sensor and provides an `available()` method for determining if a sensor is present. --- library/enviroplus/gas.py | 26 +++-- library/tests/conftest.py | 189 +++++++++++++++++++----------------- library/tests/test_noise.py | 96 +++++++++--------- library/tests/test_setup.py | 13 +++ 4 files changed, 179 insertions(+), 145 deletions(-) diff --git a/library/enviroplus/gas.py b/library/enviroplus/gas.py index f5eb2ab6..54c240f6 100644 --- a/library/enviroplus/gas.py +++ b/library/enviroplus/gas.py @@ -10,6 +10,7 @@ ads1015.I2C_ADDRESS_DEFAULT = ads1015.I2C_ADDRESS_ALTERNATE _is_setup = False +_is_available = False _adc_enabled = False _adc_gain = 6.148 @@ -41,13 +42,19 @@ def __repr__(self): def setup(): - global adc, adc_type, _is_setup + global adc, adc_type, _is_setup, _is_available if _is_setup: return _is_setup = True - adc = ads1015.ADS1015(i2c_addr=0x49) - adc_type = adc.detect_chip_type() + try: + adc = ads1015.ADS1015(i2c_addr=0x49) + adc_type = adc.detect_chip_type() + _is_available = True + except IOError: + _is_available = False + return + adc.set_mode('single') adc.set_programmable_gain(MICS6814_GAIN) if adc_type == 'ADS1115': @@ -62,6 +69,11 @@ def setup(): atexit.register(cleanup) +def available(): + setup() + return _is_available + + def enable_adc(value=True): """Enable reading from the additional ADC pin.""" global _adc_enabled @@ -81,6 +93,10 @@ def cleanup(): def read_all(): """Return gas resistence for oxidising, reducing and NH3""" setup() + + if not _is_available: + raise RuntimeError("Gas sensor not connected.") + ox = adc.get_voltage('in0/gnd') red = adc.get_voltage('in1/gnd') nh3 = adc.get_voltage('in2/gnd') @@ -119,7 +135,6 @@ def read_oxidising(): Eg chlorine, nitrous oxide """ - setup() return read_all().oxidising @@ -128,17 +143,14 @@ def read_reducing(): 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 diff --git a/library/tests/conftest.py b/library/tests/conftest.py index 8a5c54c5..d076a6c3 100644 --- a/library/tests/conftest.py +++ b/library/tests/conftest.py @@ -1,90 +1,99 @@ -"""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'] +"""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 mocksmbus(): + """Mock smbus module.""" + smbus = mock.MagicMock() + 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'] diff --git a/library/tests/test_noise.py b/library/tests/test_noise.py index 3778c166..75aa89c2 100644 --- a/library/tests/test_noise.py +++ b/library/tests/test_noise.py @@ -1,48 +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) +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) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 2aa7b492..89d3a7bd 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -5,6 +5,19 @@ def test_gas_setup(GPIO, smbus): gas.setup() +def test_gas_unavailable(GPIO, mocksmbus): + from enviroplus import gas + mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh noes!") + gas._is_setup = False + assert gas.available() == False + + +def test_gas_available(GPIO, mocksmbus): + from enviroplus import gas + gas._is_setup = False + assert gas.available() == True + + def test_gas_read_all(GPIO, smbus): from enviroplus import gas gas._is_setup = False From 18f582c6d956eb39371e65488d0f3277ef8357b5 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Sat, 14 Nov 2020 17:42:49 +0000 Subject: [PATCH 071/122] Trigger tests From be638668c8c917947754b0a6af9224cc59aad6e4 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Sat, 14 Nov 2020 17:47:39 +0000 Subject: [PATCH 072/122] Test read_all throws exception when unavailable --- library/tests/test_setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 89d3a7bd..162f1bce 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -1,3 +1,6 @@ +import pytest + + def test_gas_setup(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -11,6 +14,9 @@ def test_gas_unavailable(GPIO, mocksmbus): gas._is_setup = False assert gas.available() == False + with pytest.raises(RuntimeError): + gas.read_all() + def test_gas_available(GPIO, mocksmbus): from enviroplus import gas From e3df80ec28c41d1e8e0c996442c86642e970fcbe Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Tue, 2 Nov 2021 11:04:33 +0000 Subject: [PATCH 073/122] Fixup tests for ADC detect. --- library/tests/conftest.py | 16 ++++++++++++++++ library/tests/test_setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/library/tests/conftest.py b/library/tests/conftest.py index d076a6c3..b3fa3766 100644 --- a/library/tests/conftest.py +++ b/library/tests/conftest.py @@ -14,6 +14,12 @@ def __init__(self, i2c_bus): self.regs[0x00:0x01] = 0x0f, 0x00 +class SMBusFakeDeviceNoTimeout(MockSMBus): + def __init__(self, i2c_bus): + MockSMBus.__init__(self, i2c_bus) + self.regs[0x00:0x01] = 0x0f, 0x80 + + @pytest.fixture(scope='function', autouse=True) def cleanup(): yield None @@ -63,6 +69,16 @@ def smbus(): del sys.modules['smbus'] +@pytest.fixture(scope='function', autouse=False) +def smbus_notimeout(): + """Mock smbus module.""" + smbus = mock.MagicMock() + smbus.SMBus = SMBusFakeDeviceNoTimeout + sys.modules['smbus'] = smbus + yield smbus + del sys.modules['smbus'] + + @pytest.fixture(scope='function', autouse=False) def mocksmbus(): """Mock smbus module.""" diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 162f1bce..40bf80d6 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -18,7 +18,7 @@ def test_gas_unavailable(GPIO, mocksmbus): gas.read_all() -def test_gas_available(GPIO, mocksmbus): +def test_gas_available(GPIO, smbus_notimeout): from enviroplus import gas gas._is_setup = False assert gas.available() == True From ac66a840b3d4a3e6ee04354e6c758e44386ab31e Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Tue, 2 Nov 2021 11:11:03 +0000 Subject: [PATCH 074/122] Update changelog & readme --- library/CHANGELOG.txt | 1 + library/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 4dd54d57..d5d7bc88 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -2,6 +2,7 @@ ----- * Drop Python 2.x support +* Add "available()" method for gas sensor 0.0.4 ----- diff --git a/library/README.md b/library/README.md index a1136e8e..f637ae85 100644 --- a/library/README.md +++ b/library/README.md @@ -75,6 +75,7 @@ sudo apt install python-numpy python-smbus python-pil python-setuptools ----- * Drop Python 2.x support +* Add "available()" method for gas sensor 0.0.4 ----- From 05c735acbc7d40124630f375a83fe6d32c53861f Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Tue, 2 Nov 2021 11:13:01 +0000 Subject: [PATCH 075/122] Drop Python2 from Makefile --- Makefile | 3 +-- README.md | 2 +- library/README.md | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 948282df..12afc903 100644 --- a/Makefile +++ b/Makefile @@ -50,10 +50,9 @@ library/LICENSE.txt: LICENSE python-wheels: python-readme python-license cd library; python3 setup.py bdist_wheel - cd library; python setup.py bdist_wheel python-sdist: python-readme python-license - cd library; python setup.py sdist + cd library; python3 setup.py sdist python-clean: -rm -r library/dist diff --git a/README.md b/README.md index ad90197c..f6b0d628 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Enviro+ +# 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 diff --git a/library/README.md b/library/README.md index f637ae85..67360fda 100644 --- a/library/README.md +++ b/library/README.md @@ -1,4 +1,4 @@ -# Enviro+ +# 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 @@ -70,7 +70,6 @@ sudo apt install python-numpy python-smbus python-pil python-setuptools * Discord - https://discord.gg/hr93ByC # Changelog - 0.0.5 ----- From 01f6f2772e4557d71e3b2232eddf580080e687c1 Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Thu, 4 Nov 2021 10:22:57 +0000 Subject: [PATCH 076/122] Correct README to python3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6b0d628..9c32ab56 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ And if you're using a PMS5003 sensor you will need to: And install additional dependencies: ``` -sudo apt install python-numpy python-smbus python-pil python-setuptools +sudo apt install python3-numpy python3-smbus python3-pil python3-setuptools ``` ## Alternate Software & User Projects From 3afa06d1db501c7c9ae9046b96bee198e35f1ad0 Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Thu, 4 Nov 2021 11:08:58 +0000 Subject: [PATCH 077/122] Direct users toward Python3, install explicitly --- README.md | 2 ++ install.sh | 3 ++- library/setup.cfg | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c32ab56..76054ecb 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ You're best using the "One-line" install method if you want all of the UART seri ![Enviro Plus pHAT](./Enviro-Plus-pHAT.jpg) ![Enviro Mini pHAT](./Enviro-mini-pHAT.jpg) +:warning: This library now supports Python 3 only, Python 2 is EOL - https://www.python.org/doc/sunset-python-2/ + ## One-line (Installs from GitHub) ``` diff --git a/install.sh b/install.sh index a8513b6d..60f2a19d 100755 --- a/install.sh +++ b/install.sh @@ -184,5 +184,6 @@ if [ -d "examples" ]; then fi success "\nAll done!" -inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" +warning "If this is your first time installing you should --reboot-- for hardware changes to take effect.\n" +warning "This library is installed for Python 3 *only* make sure to use \"python3\" when running examples.\n" inform "Find uninstall steps in $UNINSTALLER\n" diff --git a/library/setup.cfg b/library/setup.cfg index 591de903..a5fcae51 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -55,6 +55,7 @@ ignore = [pimoroni] py2deps = py3deps = + python3 python3-pip python3-numpy python3-smbus From ffb7a0334f2a814ecc3d38f34b9070dc1991e5d9 Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Thu, 4 Nov 2021 11:43:51 +0000 Subject: [PATCH 078/122] Add check-install.py debug script --- check-install.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100755 check-install.py diff --git a/check-install.py b/check-install.py new file mode 100755 index 00000000..ea5eecd6 --- /dev/null +++ b/check-install.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +f"Sorry! This program requires Python >= 3.6 😅. Run with \"python3 check-install.py\"" + +CONFIG_FILE = "/boot/config.txt" + +print("""Checking Enviro+ install, please wait...""") + +errors = 0 +check_apt = False + +try: + import apt + check_apt = True +except ImportErorr: + print("⚠️ Could not import \"apt\". Unable to verify system dependencies.") + + +apt_deps = { + "python3", + "python3-pip", + "python3-numpy", + "python3-smbus", + "python3-pil", + "python3-cffi", + "python3-spidev", + "python3-rpi.gpio", + "libportaudio2" +} + +deps = { + "bme280": None, + "pms5003": None, + "ltr559": None, + "ST7735": None, + "ads1015": "0.0.7", + "fonts": None, + "font_roboto": None, + "astral": None, + "pytz": None, + "sounddevice": None, + "paho.mqtt": None +} + +config = { + "dtparam=i2c_arm=on", + "dtparam=spi=on", + "dtoverlay=adau7002-simple", + "dtoverlay=pi3-miniuart-bt", + "enable_uart=1" +} + +if check_apt: + print("\nSystem dependencies...") + print(" Retrieving cache...") + cache = apt.Cache() + + for dep in apt_deps: + installed = False + print(f" Checking for {dep}".ljust(35), end="") + try: + installed = cache[dep].is_installed + except KeyError: + pass + + if installed: + print("✅") + else: + print("⚠️ Missing!") + errors += 1 + +print("\nPython dependencies...") + +for dep, version in deps.items(): + print(f" Checking for {dep}".ljust(35), end="") + try: + __import__(dep) + print("✅") + except ImportError: + print("⚠️ Missing!") + errors += 1 + +print("\nSystem config...") + +config_txt = open(CONFIG_FILE, "r").read().split("\n") + +def check_config(line): + global errors + print(f" Checking for {line} in {CONFIG_FILE}: ", end="") + for cline in config_txt: + if cline.startswith(line): + print("✅") + return + print("⚠️ Missing!") + errors += 1 + +for line in config: + check_config(line) + +if errors > 0: + print("\n⚠️ Config errors were found! Something might be awry.") +else: + print("\n✅ Looks good from here!") + +print("\nHave you?") +print(" • Rebooted after installing") +print(" • Made sure to run examples with \"python3\"") +print(" • Checked for any errors when running \"sudo ./install.sh\"") From fbcd83aa555fec60ad21290e4c39e72d121e6297 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 8 Nov 2021 12:09:37 +0000 Subject: [PATCH 079/122] Add Bullseye installer --- install-bullseye.sh | 254 ++++++++++++++++++++++++++++++++++++++++++++ install.sh | 41 ++++++- 2 files changed, 294 insertions(+), 1 deletion(-) create mode 100755 install-bullseye.sh diff --git a/install-bullseye.sh b/install-bullseye.sh new file mode 100755 index 00000000..016c7ee9 --- /dev/null +++ b/install-bullseye.sh @@ -0,0 +1,254 @@ +#!/bin/bash +CONFIG=/boot/config.txt +DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` +CONFIG_BACKUP=false +APT_HAS_UPDATED=false +USER_HOME=/home/$SUDO_USER +RESOURCES_TOP_DIR=$USER_HOME/Pimoroni +WD=`pwd` +USAGE="sudo ./install.sh (--unstable)" +POSITIONAL_ARGS=() +UNSTABLE=false +PYTHON="/usr/bin/python3" +CODENAME=`lsb_release -sc` + +distro_check() { + if [[ $CODENAME != "bullseye" ]]; then + printf "This installer is for Raspberry Pi OS: Bullseye only, current distro: $CODENAME\n" + exit 1 + fi +} + +user_check() { + if [ $(id -u) -ne 0 ]; then + printf "Script must be run as root. Try 'sudo ./install.sh'\n" + exit 1 + fi +} + +confirm() { + if [ "$FORCE" == '-y' ]; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} + +prompt() { + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi +} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +function do_config_backup { + if [ ! $CONFIG_BACKUP == true ]; then + CONFIG_BACKUP=true + FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" + inform "Backing up $CONFIG to /boot/$FILENAME\n" + cp $CONFIG /boot/$FILENAME + mkdir -p $RESOURCES_TOP_DIR/config-backups/ + cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME + if [ -f "$UNINSTALLER" ]; then + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER + fi + fi +} + +function apt_pkg_install { + PACKAGES=() + PACKAGES_IN=("$@") + for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do + PACKAGE="${PACKAGES_IN[$i]}" + if [ "$PACKAGE" == "" ]; then continue; fi + printf "Checking for $PACKAGE\n" + dpkg -L $PACKAGE > /dev/null 2>&1 + if [ "$?" == "1" ]; then + PACKAGES+=("$PACKAGE") + fi + done + PACKAGES="${PACKAGES[@]}" + if ! [ "$PACKAGES" == "" ]; then + echo "Installing missing packages: $PACKAGES" + if [ ! $APT_HAS_UPDATED ]; then + apt update + APT_HAS_UPDATED=true + fi + apt install -y $PACKAGES + if [ -f "$UNINSTALLER" ]; then + echo "apt uninstall -y $PACKAGES" + fi + fi +} + +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -u|--unstable) + UNSTABLE=true + shift + ;; + -p|--python) + PYTHON=$2 + shift + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: $1\n"; + printf "Usage: $USAGE\n"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +distro_check +user_check + +if [ ! -f "$PYTHON" ]; then + printf "Python path $PYTHON not found!\n" + exit 1 +fi + +PYTHON_VER=`$PYTHON --version` + +inform "Installing. Please wait..." + +$PYTHON -m pip install --upgrade configparser + +CONFIG_VARS=`$PYTHON - < $UNINSTALLER +printf "It's recommended you run these steps manually.\n" +printf "If you want to run the full script, open it in\n" +printf "an editor and remove 'exit 1' from below.\n" +exit 1 +EOF + +printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" + +if $UNSTABLE; then + warning "Installing unstable library from source.\n\n" +else + printf "Installing stable library from pypi.\n\n" +fi + +cd library + +printf "Installing for $PYTHON_VER...\n" +apt_pkg_install "${PY3_DEPS[@]}" +if $UNSTABLE; then + $PYTHON setup.py install > /dev/null +else + $PYTHON -m pip install --upgrade $LIBRARY_NAME +fi +if [ $? -eq 0 ]; then + success "Done!\n" + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER +fi + +cd $WD + +for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do + CMD="${SETUP_CMDS[$i]}" + # Attempt to catch anything that touches /boot/config.txt and trigger a backup + if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then + do_config_backup + fi + eval $CMD +done + +for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do + CONFIG_LINE="${CONFIG_TXT[$i]}" + if ! [ "$CONFIG_LINE" == "" ]; then + do_config_backup + inform "Adding $CONFIG_LINE to $CONFIG\n" + sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG + if ! grep -q "^$CONFIG_LINE" $CONFIG; then + printf "$CONFIG_LINE\n" >> $CONFIG + fi + fi +done + +if [ -d "examples" ]; then + if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then + inform "Copying examples to $RESOURCES_DIR" + cp -r examples/ $RESOURCES_DIR + echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER + success "Done!" + fi +fi + +printf "\n" + +if [ -f "/usr/bin/pydoc" ]; then + printf "Generating documentation.\n" + pydoc -w $LIBRARY_NAME > /dev/null + if [ -f "$LIBRARY_NAME.html" ]; then + cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html + rm -f $LIBRARY_NAME.html + inform "Documentation saved to $RESOURCES_DIR/docs.html" + success "Done!" + else + warning "Error: Failed to generate documentation." + fi +fi + +success "\nAll done!" +warning "If this is your first time installing you should --reboot-- for hardware changes to take effect.\n" +warning "This library is installed for python 3 *only* make sure to use \"python3\" when running examples.\n" diff --git a/install.sh b/install.sh index 60f2a19d..baa9d5af 100755 --- a/install.sh +++ b/install.sh @@ -7,6 +7,15 @@ APT_HAS_UPDATED=false USER_HOME=/home/$SUDO_USER RESOURCES_TOP_DIR=$USER_HOME/Pimoroni WD=`pwd` +USAGE="sudo ./install.sh (--unstable)" +POSITIONAL_ARGS=() +UNSTABLE=false +CODENAME=`lsb_release -sc` + +if [[ $CODENAME == "bullseye" ]]; then + bash ./install-bullseye.sh + exit $? +fi user_check() { if [ $(id -u) -ne 0 ]; then @@ -68,6 +77,7 @@ function apt_pkg_install { PACKAGES_IN=("$@") for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" + if [ "$PACKAGE" == "" ]; then continue; fi printf "Checking for $PACKAGE\n" dpkg -L $PACKAGE > /dev/null 2>&1 if [ "$?" == "1" ]; then @@ -88,6 +98,24 @@ function apt_pkg_install { fi } +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -u|--unstable) + UNSTABLE=true + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: $1\n"; + printf "Usage: $USAGE\n"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + user_check apt_pkg_install python-configparser @@ -137,12 +165,22 @@ EOF printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" +if $UNSTABLE; then + warning "Installing unstable library from source.\n\n" +else + printf "Installing stable library from pypi.\n\n" +fi + cd library if [ -f "/usr/bin/python3" ]; then printf "Installing for Python 3..\n" apt_pkg_install "${PY3_DEPS[@]}" - python3 setup.py install > /dev/null + if $UNSTABLE; then + python3 setup.py install > /dev/null + else + pip3 install --upgrade $LIBRARY_NAME + fi if [ $? -eq 0 ]; then success "Done!\n" echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER @@ -180,6 +218,7 @@ if [ -d "examples" ]; then inform "Copying examples to $RESOURCES_DIR" cp -r examples/ $RESOURCES_DIR echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER + success "Done!" fi fi From 40edd6b13ea21f2d93d2148f14dc64f6ff954cdd Mon Sep 17 00:00:00 2001 From: Rajko Zschiegner Date: Sun, 6 Feb 2022 09:52:42 +0100 Subject: [PATCH 080/122] Name change of Luftdaten.info to Sensor.Community API endpoints changed to reflect name change of Luftdaten.info to Sensor.Community --- examples/luftdaten.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/luftdaten.py b/examples/luftdaten.py index d0bee376..7b6c43bb 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -130,7 +130,7 @@ def send_to_luftdaten(values, id): try: resp_pm = requests.post( - "https://api.luftdaten.info/v1/push-sensor-data/", + "https://api.sensor.community/v1/push-sensor-data/", json={ "software_version": "enviro-plus 0.0.1", "sensordatavalues": pm_values_json @@ -144,15 +144,15 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning('Luftdaten PM Connection Error: {}'.format(e)) + logging.warning('Sensor.Community (Luftdaten) PM Connection Error: {}'.format(e)) except requests.exceptions.Timeout as e: - logging.warning('Luftdaten PM Timeout Error: {}'.format(e)) + logging.warning('Sensor.Community (Luftdaten) PM Timeout Error: {}'.format(e)) except requests.exceptions.RequestException as e: - logging.warning('Luftdaten PM Request Error: {}'.format(e)) + logging.warning('Sensor.Community (Luftdaten) PM Request Error: {}'.format(e)) try: resp_bmp = requests.post( - "https://api.luftdaten.info/v1/push-sensor-data/", + "https://api.sensor.community/v1/push-sensor-data/", json={ "software_version": "enviro-plus 0.0.1", "sensordatavalues": temp_values_json @@ -166,11 +166,11 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning('Luftdaten Climate Connection Error: {}'.format(e)) + logging.warning('Sensor.Community (Luftdaten) Climate Connection Error: {}'.format(e)) except requests.exceptions.Timeout as e: - logging.warning('Luftdaten Climate Timeout Error: {}'.format(e)) + logging.warning('Sensor.Community (Luftdaten) Climate Timeout Error: {}'.format(e)) except requests.exceptions.RequestException as e: - logging.warning('Luftdaten Climate Request Error: {}'.format(e)) + logging.warning('Sensor.Community (Luftdaten) Climate Request Error: {}'.format(e)) if resp_pm is not None and resp_bmp is not None: if resp_pm.ok and resp_bmp.ok: From bb38109e947e4fbce5b3993101a2952f1ea19bfc Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 9 Feb 2022 18:56:38 +0000 Subject: [PATCH 081/122] Pass --unstable to bullseye installer --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index baa9d5af..65f33422 100755 --- a/install.sh +++ b/install.sh @@ -13,7 +13,7 @@ UNSTABLE=false CODENAME=`lsb_release -sc` if [[ $CODENAME == "bullseye" ]]; then - bash ./install-bullseye.sh + bash ./install-bullseye.sh $@ exit $? fi From 599615d0aca427acfca081a303f60d8acdfe3e34 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 11 Feb 2022 16:53:14 +0000 Subject: [PATCH 082/122] Fix audio capture for #91 --- library/enviroplus/noise.py | 1 + library/tests/test_noise.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/enviroplus/noise.py b/library/enviroplus/noise.py index 7b6d5e28..0d413bde 100644 --- a/library/enviroplus/noise.py +++ b/library/enviroplus/noise.py @@ -83,6 +83,7 @@ def get_noise_profile(self, def _record(self): return sounddevice.rec( int(self.duration * self.sample_rate), + device='adau7002', samplerate=self.sample_rate, blocking=True, channels=1, diff --git a/library/tests/test_noise.py b/library/tests/test_noise.py index 75aa89c2..4949a3d3 100644 --- a/library/tests/test_noise.py +++ b/library/tests/test_noise.py @@ -17,7 +17,7 @@ def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): (501, 1000) ]) - sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64') + sounddevice.rec.assert_called_with(0.1 * 16000, device='adau7002', samplerate=16000, blocking=True, channels=1, dtype='float64') def test_noise_get_noise_profile(sounddevice, numpy): @@ -32,7 +32,7 @@ def test_noise_get_noise_profile(sounddevice, numpy): mid=0.36, high=None) - sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64') + sounddevice.rec.assert_called_with(0.1 * 16000, device='adau7002', samplerate=16000, blocking=True, channels=1, dtype='float64') assert amp_total == 10.0 From 78b620a209ee0f5ded4e002e50745c235639f2c0 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 3 Mar 2022 13:00:10 +0000 Subject: [PATCH 083/122] Prep for v0.0.6 --- library/CHANGELOG.txt | 5 +++++ library/README.md | 9 ++++++++- library/enviroplus/__init__.py | 2 +- library/setup.cfg | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index d5d7bc88..21f175aa 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.0.6 +----- + +* Fix noise by specifying adau7002 device + 0.0.5 ----- diff --git a/library/README.md b/library/README.md index 67360fda..14988143 100644 --- a/library/README.md +++ b/library/README.md @@ -17,6 +17,8 @@ You're best using the "One-line" install method if you want all of the UART seri ![Enviro Plus pHAT](./Enviro-Plus-pHAT.jpg) ![Enviro Mini pHAT](./Enviro-mini-pHAT.jpg) +:warning: This library now supports Python 3 only, Python 2 is EOL - https://www.python.org/doc/sunset-python-2/ + ## One-line (Installs from GitHub) ``` @@ -51,7 +53,7 @@ And if you're using a PMS5003 sensor you will need to: And install additional dependencies: ``` -sudo apt install python-numpy python-smbus python-pil python-setuptools +sudo apt install python3-numpy python3-smbus python3-pil python3-setuptools ``` ## Alternate Software & User Projects @@ -70,6 +72,11 @@ sudo apt install python-numpy python-smbus python-pil python-setuptools * Discord - https://discord.gg/hr93ByC # Changelog +0.0.6 +----- + +* Fix noise by specifying adau7002 device + 0.0.5 ----- diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py index eead3198..fa9c4ec2 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.5' +__version__ = '0.0.6' diff --git a/library/setup.cfg b/library/setup.cfg index a5fcae51..68d9d3f6 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.5 +version = 0.0.6 author = Philip Howard author_email = phil@pimoroni.com description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi From bd58ed8fde017088969593aa77a8d4788d6948f2 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Fri, 4 Mar 2022 23:55:35 +0000 Subject: [PATCH 084/122] trivial updates to correct for different script names minor README tweaks add link to homebridge plugin --- README.md | 14 +++++++------- install-bullseye.sh | 4 ++-- install.sh | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 76054ecb..2314c815 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Designed for environmental monitoring, Enviro+ lets you measure air quality (pollutant gases and particulates), temperature, pressure, humidity, light, and noise level. Learn more - https://shop.pimoroni.com/products/enviro-plus - [![Build Status](https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master)](https://travis-ci.com/pimoroni/enviroplus-python) [![Coverage Status](https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/enviroplus-python?branch=master) [![PyPi Package](https://img.shields.io/pypi/v/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus) @@ -10,7 +9,7 @@ Designed for environmental monitoring, Enviro+ lets you measure air quality (pol # 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 are 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. **Note** The code in this repository supports both the Enviro+ and Enviro Mini boards. _The Enviro Mini board does not have the Gas sensor or the breakout for the PM sensor._ @@ -21,7 +20,7 @@ You're best using the "One-line" install method if you want all of the UART seri ## One-line (Installs from GitHub) -``` +```bash curl -sSL https://get.pimoroni.com/enviroplus | bash ``` @@ -33,13 +32,13 @@ curl -sSL https://get.pimoroni.com/enviroplus | bash * `cd enviroplus-python` * `sudo ./install.sh` -**Note** Raspbian Lite users may first need to install git: `sudo apt install git` +**Note** Raspbian/Raspberry Pi OS Lite users may first need to install git: `sudo apt install git` ## Or... Install from PyPi and configure manually: * Run `sudo python3 -m pip install enviroplus` -**Note** this wont perform any of the required configuration changes on your Pi, you may additionally need to: +**Note** this will not 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` @@ -52,7 +51,7 @@ And if you're using a PMS5003 sensor you will need to: And install additional dependencies: -``` +```bash sudo apt install python3-numpy python3-smbus python3-pil python3-setuptools ``` @@ -64,9 +63,10 @@ sudo apt install python3-numpy python3-smbus python3-pil python3-setuptools * enviroplus_exporter - https://github.com/tijmenvandenbrink/enviroplus_exporter - Prometheus exporter (with added support for Luftdaten and InfluxDB Cloud) * homekit-enviroplus - https://github.com/sighmon/homekit-enviroplus - An Apple HomeKit accessory for the Pimoroni Enviro+ * go-enviroplus - https://github.com/rubiojr/go-enviroplus - Go modules to read Enviro+ sensors +* homebridge-enviroplus - https://github.com/mhawkshaw/homebridge-enviroplus - a Homebridge plugin to add the Enviro+ to HomeKit via Homebridge ## Help & Support * GPIO Pinout - https://pinout.xyz/pinout/enviro_plus -* Support forums - http://forums.pimoroni.com/c/support +* Support forums - https://forums.pimoroni.com/c/support * Discord - https://discord.gg/hr93ByC diff --git a/install-bullseye.sh b/install-bullseye.sh index 016c7ee9..9780175b 100755 --- a/install-bullseye.sh +++ b/install-bullseye.sh @@ -6,7 +6,7 @@ APT_HAS_UPDATED=false USER_HOME=/home/$SUDO_USER RESOURCES_TOP_DIR=$USER_HOME/Pimoroni WD=`pwd` -USAGE="sudo ./install.sh (--unstable)" +USAGE="sudo $0 (--unstable)" POSITIONAL_ARGS=() UNSTABLE=false PYTHON="/usr/bin/python3" @@ -21,7 +21,7 @@ distro_check() { user_check() { if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./install.sh'\n" + printf "Script must be run as root. Try 'sudo $0'\n" exit 1 fi } diff --git a/install.sh b/install.sh index 65f33422..66c9e4d7 100755 --- a/install.sh +++ b/install.sh @@ -7,7 +7,7 @@ APT_HAS_UPDATED=false USER_HOME=/home/$SUDO_USER RESOURCES_TOP_DIR=$USER_HOME/Pimoroni WD=`pwd` -USAGE="sudo ./install.sh (--unstable)" +USAGE="sudo $0 (--unstable)" POSITIONAL_ARGS=() UNSTABLE=false CODENAME=`lsb_release -sc` @@ -19,7 +19,7 @@ fi user_check() { if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./install.sh'\n" + printf "Script must be run as root. Try 'sudo $0'\n" exit 1 fi } From 611234cf8ec1abe7836aec0891085db75c9a79ac Mon Sep 17 00:00:00 2001 From: idotj Date: Fri, 3 Jun 2022 12:23:09 +0200 Subject: [PATCH 085/122] Add forked project Add a new web interface version, well documented and fully responsive. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2314c815..e0f23066 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ sudo apt install python3-numpy python3-smbus python3-pil python3-setuptools * homekit-enviroplus - https://github.com/sighmon/homekit-enviroplus - An Apple HomeKit accessory for the Pimoroni Enviro+ * go-enviroplus - https://github.com/rubiojr/go-enviroplus - Go modules to read Enviro+ sensors * homebridge-enviroplus - https://github.com/mhawkshaw/homebridge-enviroplus - a Homebridge plugin to add the Enviro+ to HomeKit via Homebridge +* Enviro Plus Web - https://gitlab.com/idotj/enviroplusweb - Simple Flask application serves a web page with the current sensor readings and a graph over a specified time period ## Help & Support From f1b282005af299c935f2c7dc7fcaf09b774ab839 Mon Sep 17 00:00:00 2001 From: Ed Rutherford <49820403+dedSyn4ps3@users.noreply.github.com> Date: Mon, 25 Jul 2022 12:34:05 -0400 Subject: [PATCH 086/122] Contribution Updates (#117) Update contribution links Co-authored-by: EddieSneed --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2314c815..24e4a78b 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,10 @@ sudo apt install python3-numpy python3-smbus python3-pil python3-setuptools ## Alternate Software & User Projects +* Enviro Plus Dashboard - https://gitlab.com/dedSyn4ps3/enviroplus-dashboard - A React-based web dashboard for viewing sensor data +* Enviro+ Example Projects - https://gitlab.com/dedSyn4ps3/enviroplus-python-projects - Includes original examples plus code to stream to Adafruit IO (more projects coming soon) * enviro monitor - https://github.com/roscoe81/enviro-monitor * mqtt-all - https://github.com/robmarkcole/rpi-enviro-mqtt - now upstream: [see examples/mqtt-all.py](examples/mqtt-all.py) -* adafruit_io.py - https://github.com/dedSyn4ps3/enviroplus-python/blob/master/examples/adafruit_io.py - uses Adafruit Blinka and BME280 libraries to publish to Adafruit IO * enviroplus_exporter - https://github.com/tijmenvandenbrink/enviroplus_exporter - Prometheus exporter (with added support for Luftdaten and InfluxDB Cloud) * homekit-enviroplus - https://github.com/sighmon/homekit-enviroplus - An Apple HomeKit accessory for the Pimoroni Enviro+ * go-enviroplus - https://github.com/rubiojr/go-enviroplus - Go modules to read Enviro+ sensors From 40553f165647d7bd73594ef92544de528e09a5eb Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Thu, 18 Aug 2022 11:32:21 +0100 Subject: [PATCH 087/122] mqtt-all.py example: use retained publications Per [the change I suggested to the Pico-based Enviro code](https://github.com/pimoroni/enviro/pull/2), with this change, the MQTT messages will be published with the retain flag set, so that if a consumer is not subscribed, the most recent set of readings can still be read by a future subscriber later. This supports the [homebridge-enviroplus plugin](https://github.com/mhawkshaw/homebridge-enviroplus/issues/2#issuecomment-1215872947) better, so that a publication is more likely to exist on the topic even if the Pi with Enviroplus / this example code is not running at the time. Additional update to consider would be to align the message data formats between this sample and the newer Enviro product line. --- examples/mqtt-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 8220c678..2338f0fe 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -262,7 +262,7 @@ def main(): values.update(pms_values) values["serial"] = device_serial_number print(values) - mqtt_client.publish(args.topic, json.dumps(values)) + mqtt_client.publish(args.topic, json.dumps(values), retain=True) display_status(disp, args.broker) time.sleep(args.interval) except Exception as e: From 81f6d6ea52b8a45dea23e3591c2e429d1f820012 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 25 Jul 2023 12:55:04 +0100 Subject: [PATCH 088/122] CI: Fix outdated Python versions and broken tests. --- .github/workflows/test.yml | 4 ++-- library/tox.ini | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5fac95b2..e8e63580 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [3.6, 3.7, 3.9] + python: ['3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 @@ -33,5 +33,5 @@ jobs: run: | python -m pip install coveralls coveralls --service=github - if: ${{ matrix.python == '3.9' }} + if: ${{ matrix.python == '3.11' }} diff --git a/library/tox.ini b/library/tox.ini index fcee0794..1b757864 100644 --- a/library/tox.ini +++ b/library/tox.ini @@ -1,11 +1,11 @@ [tox] -envlist = py{36, 37, 38, 39},qa +envlist = py,qa skip_missing_interpreters = True [testenv] commands = python setup.py install - coverage run -m py.test -v -r wsx + coverage run -m pytest -v -r wsx coverage report deps = mock From 39d365f24b7b14e7828923be89b1d3d64bd3b645 Mon Sep 17 00:00:00 2001 From: Justin Pauley Date: Mon, 24 Jul 2023 19:03:54 -0400 Subject: [PATCH 089/122] Adjust Script To Prevent PMS Sensor Buffering --- examples/mqtt-all.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 2338f0fe..5e564afe 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -252,6 +252,9 @@ def main(): print("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) print("MQTT broker IP: {}".format(args.broker)) + # Set an initial update time + update_time = time.time() + # Main loop to read data, display, and send over mqtt mqtt_client.loop_start() while True: @@ -260,11 +263,13 @@ def main(): if HAS_PMS: pms_values = read_pms5003(pms5003) values.update(pms_values) - values["serial"] = device_serial_number - print(values) - mqtt_client.publish(args.topic, json.dumps(values), retain=True) - display_status(disp, args.broker) - time.sleep(args.interval) + time_since_update = time.time() - update_time + if time_since_update >= args.interval: + update_time = time.time() + values["serial"] = device_serial_number + print(values) + mqtt_client.publish(args.topic, json.dumps(values), retain=True) + display_status(disp, args.broker) except Exception as e: print(e) From 550fac120543f945c38733a072c958a02705eea2 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:27:10 +0100 Subject: [PATCH 090/122] Repackage to latest boilerplate. --- .github/workflows/build.yml | 41 +++ .github/workflows/qa.yml | 36 +++ .github/workflows/test.yml | 22 +- library/CHANGELOG.txt => CHANGELOG.md | 0 library/MANIFEST.in => MANIFEST.in | 0 Makefile | 89 +++--- check-install.py | 109 -------- check.sh | 87 ++++++ .../enviroplus => enviroplus}/__init__.py | 0 {library/enviroplus => enviroplus}/gas.py | 0 {library/enviroplus => enviroplus}/noise.py | 0 install-bullseye.sh | 254 ------------------ install.sh | 183 +++++++++---- library/LICENSE.txt | 21 -- library/README.md | 107 -------- library/setup.cfg | 76 ------ library/setup.py | 33 --- library/tox.ini | 24 -- pyproject.toml | 145 ++++++++++ requirements-dev.txt | 9 + {library/tests => tests}/conftest.py | 0 {library/tests => tests}/test_noise.py | 0 {library/tests => tests}/test_setup.py | 0 tox.ini | 34 +++ uninstall.sh | 83 ++++-- 25 files changed, 590 insertions(+), 763 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/qa.yml rename library/CHANGELOG.txt => CHANGELOG.md (100%) rename library/MANIFEST.in => MANIFEST.in (100%) delete mode 100755 check-install.py create mode 100755 check.sh rename {library/enviroplus => enviroplus}/__init__.py (100%) rename {library/enviroplus => enviroplus}/gas.py (100%) rename {library/enviroplus => enviroplus}/noise.py (100%) delete mode 100755 install-bullseye.sh delete mode 100644 library/LICENSE.txt delete mode 100644 library/README.md delete mode 100644 library/setup.cfg delete mode 100755 library/setup.py delete mode 100644 library/tox.ini create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt rename {library/tests => tests}/conftest.py (100%) rename {library/tests => tests}/test_noise.py (100%) rename {library/tests => tests}/test_setup.py (100%) create mode 100644 tox.ini diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..87200efb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Python ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + + env: + RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python }} + + - name: Install Dependencies + run: | + make dev-deps + + - name: Build Packages + run: | + make build + + - name: Upload Packages + uses: actions/upload-artifact@v3 + with: + name: ${{ env.RELEASE_FILE }} + path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 00000000..4f858832 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,36 @@ +name: QA + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: linting & spelling + runs-on: ubuntu-latest + + env: + TERM: xterm-256color + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + + - name: Set up Python '3,11' + uses: actions/setup-python@v3 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + make dev-deps + + - name: Run Quality Assurance + run: | + make qa + + - name: Run Code Checks + run: | + make check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e8e63580..016a6780 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,37 +1,41 @@ -name: Python Tests +name: Tests on: pull_request: push: branches: - - master + - main jobs: test: + name: Python ${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: python: ['3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - name: Checkout Code + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} + - name: Install Dependencies run: | - python -m pip install --upgrade setuptools tox + make dev-deps + - name: Run Tests - working-directory: library run: | - tox -e py + make pytest + - name: Coverage + if: ${{ matrix.python == '3.9' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - working-directory: library run: | python -m pip install coveralls coveralls --service=github - if: ${{ matrix.python == '3.11' }} diff --git a/library/CHANGELOG.txt b/CHANGELOG.md similarity index 100% rename from library/CHANGELOG.txt rename to CHANGELOG.md diff --git a/library/MANIFEST.in b/MANIFEST.in similarity index 100% rename from library/MANIFEST.in rename to MANIFEST.in diff --git a/Makefile b/Makefile index 12afc903..9e0c15c5 100644 --- a/Makefile +++ b/Makefile @@ -1,69 +1,60 @@ -LIBRARY_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}') -LIBRARY_NAME=$(shell grep name library/setup.cfg | awk -F" = " '{print $$2}') +LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) +LIBRARY_VERSION := $(shell hatch version 2> /dev/null) -.PHONY: usage install uninstall +.PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy usage: +ifdef LIBRARY_NAME @echo "Library: ${LIBRARY_NAME}" @echo "Version: ${LIBRARY_VERSION}\n" +else + @echo "WARNING: You should 'make dev-deps'\n" +endif @echo "Usage: make , where target is one of:\n" - @echo "install: install the library locally from source" - @echo "uninstall: uninstall the local library" - @echo "check: peform basic integrity checks on the codebase" - @echo "python-readme: generate library/README.rst from README.md" - @echo "python-wheels: build python .whl files for distribution" - @echo "python-sdist: build python source distribution" - @echo "python-clean: clean python build and dist directories" - @echo "python-dist: build all python distribution files" - @echo "python-testdeploy: build all and deploy to test PyPi" - @echo "tag: tag the repository with the current version" + @echo "install: install the library locally from source" + @echo "uninstall: uninstall the local library" + @echo "dev-deps: install Python dev dependencies" + @echo "check: perform basic integrity checks on the codebase" + @echo "qa: run linting and package QA" + @echo "pytest: run Python test fixtures" + @echo "clean: clean Python build and dist directories" + @echo "build: build Python distribution files" + @echo "testdeploy: build and upload to test PyPi" + @echo "deploy: build and upload to PyPi" + @echo "tag: tag the repository with the current version\n" install: - ./install.sh + ./install.sh --unstable uninstall: ./uninstall.sh -check: - @echo "Checking for trailing whitespace" - @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO - @echo "Checking for DOS line-endings" - @! grep -IUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile - @echo "Checking library/CHANGELOG.txt" - @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} - @echo "Checking library/${LIBRARY_NAME}/__init__.py" - @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" - -tag: - git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" +dev-deps: + python3 -m pip install -r requirements-dev.txt + sudo apt install dos2unix -python-readme: library/README.md - -python-license: library/LICENSE.txt +check: + @bash check.sh -library/README.md: README.md library/CHANGELOG.txt - cp README.md library/README.md - printf "\n# Changelog\n" >> library/README.md - cat library/CHANGELOG.txt >> library/README.md +qa: + tox -e qa -library/LICENSE.txt: LICENSE - cp LICENSE library/LICENSE.txt +pytest: + tox -e py -python-wheels: python-readme python-license - cd library; python3 setup.py bdist_wheel +nopost: + @bash check.sh --nopost -python-sdist: python-readme python-license - cd library; python3 setup.py sdist +tag: + git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" -python-clean: - -rm -r library/dist - -rm -r library/build - -rm -r library/*.egg-info +build: check + @hatch build -python-dist: python-clean python-wheels python-sdist - ls library/dist +clean: + -rm -r dist -python-testdeploy: python-dist - twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* +testdeploy: build + twine upload --repository testpypi dist/* -python-deploy: check python-dist - twine upload library/dist/* +deploy: nopost build + twine upload dist/* diff --git a/check-install.py b/check-install.py deleted file mode 100755 index ea5eecd6..00000000 --- a/check-install.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -f"Sorry! This program requires Python >= 3.6 😅. Run with \"python3 check-install.py\"" - -CONFIG_FILE = "/boot/config.txt" - -print("""Checking Enviro+ install, please wait...""") - -errors = 0 -check_apt = False - -try: - import apt - check_apt = True -except ImportErorr: - print("⚠️ Could not import \"apt\". Unable to verify system dependencies.") - - -apt_deps = { - "python3", - "python3-pip", - "python3-numpy", - "python3-smbus", - "python3-pil", - "python3-cffi", - "python3-spidev", - "python3-rpi.gpio", - "libportaudio2" -} - -deps = { - "bme280": None, - "pms5003": None, - "ltr559": None, - "ST7735": None, - "ads1015": "0.0.7", - "fonts": None, - "font_roboto": None, - "astral": None, - "pytz": None, - "sounddevice": None, - "paho.mqtt": None -} - -config = { - "dtparam=i2c_arm=on", - "dtparam=spi=on", - "dtoverlay=adau7002-simple", - "dtoverlay=pi3-miniuart-bt", - "enable_uart=1" -} - -if check_apt: - print("\nSystem dependencies...") - print(" Retrieving cache...") - cache = apt.Cache() - - for dep in apt_deps: - installed = False - print(f" Checking for {dep}".ljust(35), end="") - try: - installed = cache[dep].is_installed - except KeyError: - pass - - if installed: - print("✅") - else: - print("⚠️ Missing!") - errors += 1 - -print("\nPython dependencies...") - -for dep, version in deps.items(): - print(f" Checking for {dep}".ljust(35), end="") - try: - __import__(dep) - print("✅") - except ImportError: - print("⚠️ Missing!") - errors += 1 - -print("\nSystem config...") - -config_txt = open(CONFIG_FILE, "r").read().split("\n") - -def check_config(line): - global errors - print(f" Checking for {line} in {CONFIG_FILE}: ", end="") - for cline in config_txt: - if cline.startswith(line): - print("✅") - return - print("⚠️ Missing!") - errors += 1 - -for line in config: - check_config(line) - -if errors > 0: - print("\n⚠️ Config errors were found! Something might be awry.") -else: - print("\n✅ Looks good from here!") - -print("\nHave you?") -print(" • Rebooted after installing") -print(" • Made sure to run examples with \"python3\"") -print(" • Checked for any errors when running \"sudo ./install.sh\"") diff --git a/check.sh b/check.sh new file mode 100755 index 00000000..cbb15653 --- /dev/null +++ b/check.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# This script handles some basic QA checks on the source + +NOPOST=$1 +LIBRARY_NAME=`hatch project metadata name` +LIBRARY_VERSION=`hatch version | awk -F "." '{print $1"."$2"."$3}'` +POST_VERSION=`hatch version | awk -F "." '{print substr($4,0,length($4))}'` + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -p|--nopost) + NOPOST=true + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: $1\n"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" + +inform "Checking for trailing whitespace..." +grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO +if [[ $? -eq 0 ]]; then + warning "Trailing whitespace found!" + exit 1 +else + success "No trailing whitespace found." +fi +printf "\n" + +inform "Checking for DOS line-endings..." +grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile +if [[ $? -eq 0 ]]; then + warning "DOS line-endings found!" + exit 1 +else + success "No DOS line-endings found." +fi +printf "\n" + +inform "Checking CHANGELOG.md..." +cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 +if [[ $? -eq 1 ]]; then + warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." + exit 1 +else + success "Changes found for version ${LIBRARY_VERSION}." +fi +printf "\n" + +inform "Checking for git tag ${LIBRARY_VERSION}..." +git tag -l | grep -E "${LIBRARY_VERSION}$" +if [[ $? -eq 1 ]]; then + warning "Missing git tag for version ${LIBRARY_VERSION}" +fi +printf "\n" + +if [[ $NOPOST ]]; then + inform "Checking for .postN on library version..." + if [[ "$POST_VERSION" != "" ]]; then + warning "Found .$POST_VERSION on library version." + inform "Please only use these for testpypi releases." + exit 1 + else + success "OK" + fi +fi diff --git a/library/enviroplus/__init__.py b/enviroplus/__init__.py similarity index 100% rename from library/enviroplus/__init__.py rename to enviroplus/__init__.py diff --git a/library/enviroplus/gas.py b/enviroplus/gas.py similarity index 100% rename from library/enviroplus/gas.py rename to enviroplus/gas.py diff --git a/library/enviroplus/noise.py b/enviroplus/noise.py similarity index 100% rename from library/enviroplus/noise.py rename to enviroplus/noise.py diff --git a/install-bullseye.sh b/install-bullseye.sh deleted file mode 100755 index 9780175b..00000000 --- a/install-bullseye.sh +++ /dev/null @@ -1,254 +0,0 @@ -#!/bin/bash -CONFIG=/boot/config.txt -DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` -CONFIG_BACKUP=false -APT_HAS_UPDATED=false -USER_HOME=/home/$SUDO_USER -RESOURCES_TOP_DIR=$USER_HOME/Pimoroni -WD=`pwd` -USAGE="sudo $0 (--unstable)" -POSITIONAL_ARGS=() -UNSTABLE=false -PYTHON="/usr/bin/python3" -CODENAME=`lsb_release -sc` - -distro_check() { - if [[ $CODENAME != "bullseye" ]]; then - printf "This installer is for Raspberry Pi OS: Bullseye only, current distro: $CODENAME\n" - exit 1 - fi -} - -user_check() { - if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo $0'\n" - exit 1 - fi -} - -confirm() { - if [ "$FORCE" == '-y' ]; then - true - else - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi - fi -} - -prompt() { - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi -} - -success() { - echo -e "$(tput setaf 2)$1$(tput sgr0)" -} - -inform() { - echo -e "$(tput setaf 6)$1$(tput sgr0)" -} - -warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" -} - -function do_config_backup { - if [ ! $CONFIG_BACKUP == true ]; then - CONFIG_BACKUP=true - FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" - inform "Backing up $CONFIG to /boot/$FILENAME\n" - cp $CONFIG /boot/$FILENAME - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME - if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER - fi - fi -} - -function apt_pkg_install { - PACKAGES=() - PACKAGES_IN=("$@") - for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do - PACKAGE="${PACKAGES_IN[$i]}" - if [ "$PACKAGE" == "" ]; then continue; fi - printf "Checking for $PACKAGE\n" - dpkg -L $PACKAGE > /dev/null 2>&1 - if [ "$?" == "1" ]; then - PACKAGES+=("$PACKAGE") - fi - done - PACKAGES="${PACKAGES[@]}" - if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" - if [ ! $APT_HAS_UPDATED ]; then - apt update - APT_HAS_UPDATED=true - fi - apt install -y $PACKAGES - if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" - fi - fi -} - -while [[ $# -gt 0 ]]; do - K="$1" - case $K in - -u|--unstable) - UNSTABLE=true - shift - ;; - -p|--python) - PYTHON=$2 - shift - shift - ;; - *) - if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; - printf "Usage: $USAGE\n"; - exit 1 - fi - POSITIONAL_ARGS+=("$1") - shift - esac -done - -distro_check -user_check - -if [ ! -f "$PYTHON" ]; then - printf "Python path $PYTHON not found!\n" - exit 1 -fi - -PYTHON_VER=`$PYTHON --version` - -inform "Installing. Please wait..." - -$PYTHON -m pip install --upgrade configparser - -CONFIG_VARS=`$PYTHON - < $UNINSTALLER -printf "It's recommended you run these steps manually.\n" -printf "If you want to run the full script, open it in\n" -printf "an editor and remove 'exit 1' from below.\n" -exit 1 -EOF - -printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" - -if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" -else - printf "Installing stable library from pypi.\n\n" -fi - -cd library - -printf "Installing for $PYTHON_VER...\n" -apt_pkg_install "${PY3_DEPS[@]}" -if $UNSTABLE; then - $PYTHON setup.py install > /dev/null -else - $PYTHON -m pip install --upgrade $LIBRARY_NAME -fi -if [ $? -eq 0 ]; then - success "Done!\n" - echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER -fi - -cd $WD - -for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do - CMD="${SETUP_CMDS[$i]}" - # Attempt to catch anything that touches /boot/config.txt and trigger a backup - if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then - do_config_backup - fi - eval $CMD -done - -for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do - CONFIG_LINE="${CONFIG_TXT[$i]}" - if ! [ "$CONFIG_LINE" == "" ]; then - do_config_backup - inform "Adding $CONFIG_LINE to $CONFIG\n" - sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG - if ! grep -q "^$CONFIG_LINE" $CONFIG; then - printf "$CONFIG_LINE\n" >> $CONFIG - fi - fi -done - -if [ -d "examples" ]; then - if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then - inform "Copying examples to $RESOURCES_DIR" - cp -r examples/ $RESOURCES_DIR - echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER - success "Done!" - fi -fi - -printf "\n" - -if [ -f "/usr/bin/pydoc" ]; then - printf "Generating documentation.\n" - pydoc -w $LIBRARY_NAME > /dev/null - if [ -f "$LIBRARY_NAME.html" ]; then - cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html - rm -f $LIBRARY_NAME.html - inform "Documentation saved to $RESOURCES_DIR/docs.html" - success "Done!" - else - warning "Error: Failed to generate documentation." - fi -fi - -success "\nAll done!" -warning "If this is your first time installing you should --reboot-- for hardware changes to take effect.\n" -warning "This library is installed for python 3 *only* make sure to use \"python3\" when running examples.\n" diff --git a/install.sh b/install.sh index 66c9e4d7..bee710c7 100755 --- a/install.sh +++ b/install.sh @@ -1,31 +1,28 @@ #!/bin/bash - +LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` CONFIG=/boot/config.txt DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false -USER_HOME=/home/$SUDO_USER -RESOURCES_TOP_DIR=$USER_HOME/Pimoroni +RESOURCES_TOP_DIR=$HOME/Pimoroni +VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh +VENV_DIR=$RESOURCES_TOP_DIR/venv WD=`pwd` -USAGE="sudo $0 (--unstable)" +USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() +FORCE=false UNSTABLE=false -CODENAME=`lsb_release -sc` - -if [[ $CODENAME == "bullseye" ]]; then - bash ./install-bullseye.sh $@ - exit $? -fi +PYTHON="python" user_check() { - if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo $0'\n" + if [ $(id -u) -eq 0 ]; then + printf "Script should not be run as root. Try './install.sh'\n" exit 1 fi } confirm() { - if [ "$FORCE" == '-y' ]; then + if $FORCE; then true else read -r -p "$1 [y/N] " response < /dev/tty @@ -58,12 +55,52 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } +venv_bash_snippet() { + if [ ! -f $VENV_BASH_SNIPPET ]; then + cat << EOF > $VENV_BASH_SNIPPET +# Add \`source $RESOURCES_TOP_DIR/auto_venv.sh\` to your ~/.bashrc to activate +# the Pimoroni virtual environment automagically! +PY_ENV_DIR=~/Pimoroni/venv +if [ ! -f \$PY_ENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$PY_ENV_DIR, please wait...\n" + mkdir -p \$PY_ENV_DIR + python3 -m venv --system-site-packages --prompt Pimoroni \$PY_ENV_DIR +fi +printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" +source \$PY_ENV_DIR/bin/activate +EOF + fi +} + +venv_check() { + PYTHON_BIN=`which $PYTHON` + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + if confirm "Would you like us to create one for you?"; then + if [ ! -f $VENV_DIR/bin/activate ]; then + inform "Creating virtual Python environment in $VENV_DIR, please wait...\n" + mkdir -p $VENV_DIR + /usr/bin/python3 -m venv $VENV_DIR --system-site-packages --prompt Pimoroni + venv_bash_snippet + else + inform "Found existing virtual Python environment in $VENV_DIR\n" + fi + inform "Activating virtual Python environment in $VENV_DIR..." + inform "source $VENV_DIR/bin/activate\n" + source $VENV_DIR/bin/activate + + else + exit 1 + fi + fi +} + function do_config_backup { if [ ! $CONFIG_BACKUP == true ]; then CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG to /boot/$FILENAME\n" - cp $CONFIG /boot/$FILENAME + sudo cp $CONFIG /boot/$FILENAME mkdir -p $RESOURCES_TOP_DIR/config-backups/ cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME if [ -f "$UNINSTALLER" ]; then @@ -88,16 +125,20 @@ function apt_pkg_install { if ! [ "$PACKAGES" == "" ]; then echo "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then - apt update + sudo apt update APT_HAS_UPDATED=true fi - apt install -y $PACKAGES + sudo apt install -y $PACKAGES if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" + echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER fi fi } +function pip_pkg_install { + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" +} + while [[ $# -gt 0 ]]; do K="$1" case $K in @@ -105,6 +146,15 @@ while [[ $# -gt 0 ]]; do UNSTABLE=true shift ;; + -f|--force) + FORCE=true + shift + ;; + -p|--python) + PYTHON=$2 + shift + shift + ;; *) if [[ $1 == -* ]]; then printf "Unrecognised option: $1\n"; @@ -117,28 +167,31 @@ while [[ $# -gt 0 ]]; do done user_check +venv_check -apt_pkg_install python-configparser - -CONFIG_VARS=`python - < $UNINSTALLER @@ -161,33 +221,25 @@ printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" exit 1 +source $VIRTUAL_ENV/bin/activate EOF -printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" - if $UNSTABLE; then warning "Installing unstable library from source.\n\n" else printf "Installing stable library from pypi.\n\n" fi -cd library - -if [ -f "/usr/bin/python3" ]; then - printf "Installing for Python 3..\n" - apt_pkg_install "${PY3_DEPS[@]}" - if $UNSTABLE; then - python3 setup.py install > /dev/null - else - pip3 install --upgrade $LIBRARY_NAME - fi - if [ $? -eq 0 ]; then - success "Done!\n" - echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER - fi +inform "Installing for $PYTHON_VER...\n" +apt_pkg_install "${APT_PACKAGES[@]}" +if $UNSTABLE; then + pip_pkg_install . else - printf "/usr/bin/python3 not found. Unable to install!\n" - exit 1 + pip_pkg_install $LIBRARY_NAME +fi +if [ $? -eq 0 ]; then + success "Done!\n" + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER fi cd $WD @@ -206,9 +258,9 @@ for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup inform "Adding $CONFIG_LINE to $CONFIG\n" - sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG + sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG if ! grep -q "^$CONFIG_LINE" $CONFIG; then - printf "$CONFIG_LINE\n" >> $CONFIG + printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG fi fi done @@ -222,7 +274,20 @@ if [ -d "examples" ]; then fi fi +printf "\n" + +if confirm "Would you like to generate documentation?"; then + pip_pkg_install pdoc + printf "Generating documentation.\n" + $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null + if [ $? -eq 0 ]; then + inform "Documentation saved to $RESOURCES_DIR/docs" + success "Done!" + else + warning "Error: Failed to generate documentation." + fi +fi + success "\nAll done!" -warning "If this is your first time installing you should --reboot-- for hardware changes to take effect.\n" -warning "This library is installed for Python 3 *only* make sure to use \"python3\" when running examples.\n" +inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" inform "Find uninstall steps in $UNINSTALLER\n" diff --git a/library/LICENSE.txt b/library/LICENSE.txt deleted file mode 100644 index aed751a0..00000000 --- a/library/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -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. diff --git a/library/README.md b/library/README.md deleted file mode 100644 index 14988143..00000000 --- a/library/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# Enviro+ - -Designed for environmental monitoring, Enviro+ lets you measure air quality (pollutant gases and particulates), temperature, pressure, humidity, light, and noise level. Learn more - https://shop.pimoroni.com/products/enviro-plus - - -[![Build Status](https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master)](https://travis-ci.com/pimoroni/enviroplus-python) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/enviroplus-python?branch=master) -[![PyPi Package](https://img.shields.io/pypi/v/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus) -[![Python Versions](https://img.shields.io/pypi/pyversions/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus) - -# Installing - -You're best using the "One-line" install method if you want all of the UART serial configuration for the PMS5003 particulate matter sensor to run automatically. - -**Note** The code in this repository supports both the Enviro+ and Enviro Mini boards. _The Enviro Mini board does not have the Gas sensor or the breakout for the PM sensor._ - -![Enviro Plus pHAT](./Enviro-Plus-pHAT.jpg) -![Enviro Mini pHAT](./Enviro-mini-pHAT.jpg) - -:warning: This library now supports Python 3 only, Python 2 is EOL - https://www.python.org/doc/sunset-python-2/ - -## 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 python3 -m 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 python3-numpy python3-smbus python3-pil python3-setuptools -``` - -## Alternate Software & User Projects - -* enviro monitor - https://github.com/roscoe81/enviro-monitor -* mqtt-all - https://github.com/robmarkcole/rpi-enviro-mqtt - now upstream: [see examples/mqtt-all.py](examples/mqtt-all.py) -* adafruit_io.py - https://github.com/dedSyn4ps3/enviroplus-python/blob/master/examples/adafruit_io.py - uses Adafruit Blinka and BME280 libraries to publish to Adafruit IO -* enviroplus_exporter - https://github.com/tijmenvandenbrink/enviroplus_exporter - Prometheus exporter (with added support for Luftdaten and InfluxDB Cloud) -* homekit-enviroplus - https://github.com/sighmon/homekit-enviroplus - An Apple HomeKit accessory for the Pimoroni Enviro+ -* go-enviroplus - https://github.com/rubiojr/go-enviroplus - Go modules to read Enviro+ sensors - -## Help & Support - -* GPIO Pinout - https://pinout.xyz/pinout/enviro_plus -* Support forums - http://forums.pimoroni.com/c/support -* Discord - https://discord.gg/hr93ByC - -# Changelog -0.0.6 ------ - -* Fix noise by specifying adau7002 device - -0.0.5 ------ - -* Drop Python 2.x support -* Add "available()" method for gas sensor - -0.0.4 ------ - -* Add support for ads1015 >= v0.0.7 (ADS1115 ADCs) -* Packaging tweaks - -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 diff --git a/library/setup.cfg b/library/setup.cfg deleted file mode 100644 index 68d9d3f6..00000000 --- a/library/setup.cfg +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -[metadata] -name = enviroplus -version = 0.0.6 -author = Philip Howard -author_email = phil@pimoroni.com -description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi -long_description = file: README.md -long_description_content_type = text/markdown -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 :: 3 - Topic :: Software Development - Topic :: Software Development :: Libraries - Topic :: System :: Hardware - -[options] -python_requires = >= 3.6 -packages = enviroplus -install_requires = - pimoroni-bme280 - pms5003 - ltr559 - st7735 - ads1015 >= 0.0.7 - fonts - font-roboto - astral - pytz - sounddevice - paho-mqtt - -[flake8] -exclude = - .tox, - .eggs, - .git, - __pycache__, - build, - dist -ignore = - E501 - -[pimoroni] -py2deps = -py3deps = - python3 - python3-pip - python3-numpy - python3-smbus - python3-pil - python3-cffi - 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 diff --git a/library/setup.py b/library/setup.py deleted file mode 100755 index 40d6dbc3..00000000 --- a/library/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/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() diff --git a/library/tox.ini b/library/tox.ini deleted file mode 100644 index 1b757864..00000000 --- a/library/tox.ini +++ /dev/null @@ -1,24 +0,0 @@ -[tox] -envlist = py,qa -skip_missing_interpreters = True - -[testenv] -commands = - python setup.py install - coverage run -m pytest -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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..0d296bf2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,145 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "enviroplus" +dynamic = ["version", "readme"] +description = "Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi" +license = {file = "LICENSE"} +requires-python = ">= 3.7" +authors = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +maintainers = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +keywords = [ + "Pi", + "Raspberry", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Hardware", +] +dependencies = [ + "pimoroni-bme280", + "pms5003", + "ltr559", + "st7735", + "ads1015 >= 0.0.7", + "fonts", + "font-roboto", + "astral", + "pytz", + "sounddevice", + "paho-mqtt" +] + +[project.urls] +GitHub = "https://www.github.com/pimoroni/enviroplus-python" +Homepage = "https://www.pimoroni.com" + +[tool.hatch.version] +path = "enviroplus/__init__.py" + +[tool.hatch.build] +include = [ + "enviroplus", + "README.md", + "CHANGELOG.md", + "LICENSE" +] + +[tool.hatch.build.targets.sdist] +include = [ + "*" +] +exclude = [ + ".*", + "dist" +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [ + { path = "README.md" }, + { text = "\n" }, + { path = "CHANGELOG.md" } +] + +[tool.ruff] +exclude = [ + '.tox', + '.egg', + '.git', + '__pycache__', + 'build', + 'dist' +] +line-length = 200 + +[tool.codespell] +skip = """ +./.tox,\ +./.egg,\ +./.git,\ +./__pycache__,\ +./build,\ +./dist.\ +""" + +[tool.isort] +line_length = 200 + +[tool.check-manifest] +ignore = [ + '.stickler.yml', + 'boilerplate.md', + 'check.sh', + 'install.sh', + 'uninstall.sh', + 'Makefile', + 'tox.ini', + 'tests/*', + 'examples/*', + '.coveragerc', + 'requirements-dev.txt' +] + +[pimoroni] +apt_packages = [ + "python3", + "python3-pip", + "python3-numpy", + "python3-smbus", + "python3-pil", + "python3-cffi", + "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", + "raspi-config nonint set_config_var enable_uart 1 $CONFIG" +] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..525b0427 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,9 @@ +check-manifest +ruff +codespell +isort +twine +hatch +hatch-fancy-pypi-readme +tox +pdoc diff --git a/library/tests/conftest.py b/tests/conftest.py similarity index 100% rename from library/tests/conftest.py rename to tests/conftest.py diff --git a/library/tests/test_noise.py b/tests/test_noise.py similarity index 100% rename from library/tests/test_noise.py rename to tests/test_noise.py diff --git a/library/tests/test_setup.py b/tests/test_setup.py similarity index 100% rename from library/tests/test_setup.py rename to tests/test_setup.py diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..44c86546 --- /dev/null +++ b/tox.ini @@ -0,0 +1,34 @@ +[tox] +envlist = py,qa +skip_missing_interpreters = True +isolated_build = true +minversion = 4.0.0 + +[testenv] +commands = + coverage run -m pytest -v -r wsx + coverage report +deps = + mock + pytest>=3.1 + pytest-cov + build + +[testenv:qa] +commands = + check-manifest + python -m build --no-isolation + python -m twine check dist/* + isort --check . + ruff . + codespell . +deps = + check-manifest + ruff + codespell + isort + twine + build + hatch + hatch-fancy-pypi-readme + diff --git a/uninstall.sh b/uninstall.sh index e3174449..f213fc52 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,33 +1,72 @@ #!/bin/bash -LIBRARY_VERSION=`cat library/setup.cfg | grep version | awk -F" = " '{print $2}'` -LIBRARY_NAME=`cat library/setup.cfg | grep name | awk -F" = " '{print $2}'` +FORCE=false +LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME +PYTHON="python" -printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n" -if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" - exit 1 -fi +venv_check() { + PYTHON_BIN=`which $PYTHON` + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + exit 1 + fi +} -cd library +user_check() { + if [ $(id -u) -eq 0 ]; then + printf "Script should not be run as root. Try './uninstall.sh'\n" + exit 1 + fi +} -printf "Unnstalling for Python 2..\n" -pip uninstall $LIBRARY_NAME +confirm() { + if $FORCE; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} -if [ -f "/usr/bin/pip3" ]; then - printf "Uninstalling for Python 3..\n" - pip3 uninstall $LIBRARY_NAME -fi +prompt() { + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi +} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} -cd .. +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} -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 "$LIBRARY_NAME Python Library: Uninstaller\n\n" + +user_check +venv_check + +printf "Uninstalling for Python 3...\n" +$PYTHON -m pip uninstall $LIBRARY_NAME + +if [ -d $RESOURCES_DIR ]; then + if confirm "Would you like to delete $RESOURCES_DIR?"; then + rm -r $RESOURCES_DIR + fi +fi printf "Done!\n" From 1f2df5a68d914c3e3e7f33aa7f911808f52ca527 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:29:52 +0100 Subject: [PATCH 091/122] QA: Fix isort suggestions. --- enviroplus/gas.py | 3 ++- enviroplus/noise.py | 2 +- examples/adc.py | 3 ++- examples/all-in-one-enviro-mini.py | 16 +++++++++------- examples/all-in-one-no-pm.py | 14 ++++++++------ examples/all-in-one.py | 17 ++++++++++------- examples/combined.py | 20 ++++++++++++-------- examples/compensated-temperature.py | 1 + examples/gas.py | 3 ++- examples/lcd.py | 5 +++-- examples/light.py | 3 ++- examples/luftdaten.py | 10 ++++++---- examples/luftdaten_combined.py | 12 +++++++----- examples/mqtt-all.py | 12 +++++++----- examples/noise-amps-at-freqs.py | 1 + examples/noise-profile.py | 1 + examples/particulates.py | 3 ++- examples/weather-and-light.py | 17 ++++++++--------- examples/weather.py | 1 + tests/conftest.py | 1 + 20 files changed, 86 insertions(+), 59 deletions(-) diff --git a/enviroplus/gas.py b/enviroplus/gas.py index 54c240f6..6e96d8e8 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -1,7 +1,8 @@ """Read the MICS6814 via an ads1015 ADC""" -import time import atexit +import time + import ads1015 import RPi.GPIO as GPIO diff --git a/enviroplus/noise.py b/enviroplus/noise.py index 0d413bde..873fc26b 100644 --- a/enviroplus/noise.py +++ b/enviroplus/noise.py @@ -1,5 +1,5 @@ -import sounddevice import numpy +import sounddevice class Noise(): diff --git a/examples/adc.py b/examples/adc.py index a345d232..6a86242c 100755 --- a/examples/adc.py +++ b/examples/adc.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 +import logging import time + from enviroplus import gas -import logging logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py index d7a001ff..9e87784c 100755 --- a/examples/all-in-one-enviro-mini.py +++ b/examples/all-in-one-enviro-mini.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 -import time import colorsys import os import sys +import time + import ST7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -12,14 +14,14 @@ except ImportError: import ltr559 -from bme280 import BME280 -from enviroplus import gas +import logging from subprocess import PIPE, Popen -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont + +from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont + +from enviroplus import gas logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index ec1ed1f3..aa67fef7 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 -import time import colorsys import os import sys +import time + import ST7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -12,13 +14,13 @@ except ImportError: import ltr559 +import logging + from bme280 import BME280 -from enviroplus import gas -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont + +from enviroplus import gas logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/all-in-one.py b/examples/all-in-one.py index ac539998..54986036 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -import time import colorsys import sys +import time + import ST7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -11,14 +13,15 @@ except ImportError: import ltr559 +import logging + from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError -from enviroplus import gas -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003 +from pms5003 import ReadTimeoutError as pmsReadTimeoutError + +from enviroplus import gas logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/combined.py b/examples/combined.py index 556bcb0c..1ffaccb6 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -import time import colorsys import sys +import time + import ST7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -11,15 +13,17 @@ except ImportError: import ltr559 -from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError, SerialTimeoutError -from enviroplus import gas +import logging from subprocess import PIPE, Popen -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont + +from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003 +from pms5003 import ReadTimeoutError as pmsReadTimeoutError +from pms5003 import SerialTimeoutError + +from enviroplus import gas logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index b648f576..1d36140c 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import time + from bme280 import BME280 try: diff --git a/examples/gas.py b/examples/gas.py index 5d72cb93..a107696a 100755 --- a/examples/gas.py +++ b/examples/gas.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 +import logging import time + from enviroplus import gas -import logging logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/lcd.py b/examples/lcd.py index 10413b91..0c0606e0 100755 --- a/examples/lcd.py +++ b/examples/lcd.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 +import logging + import ST7735 -from PIL import Image, ImageDraw, ImageFont from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/light.py b/examples/light.py index db61e6a2..ead18a20 100755 --- a/examples/light.py +++ b/examples/light.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 -import time import logging +import time + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 diff --git a/examples/luftdaten.py b/examples/luftdaten.py index a78909fe..9e58dba8 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -1,18 +1,20 @@ #!/usr/bin/env python3 +import time +from subprocess import PIPE, Popen, check_output + import requests import ST7735 -import time from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError, ChecksumMismatchError -from subprocess import PIPE, Popen, check_output -from PIL import Image, ImageDraw, ImageFont from fonts.ttf import RobotoMedium as UserFont +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003, ChecksumMismatchError, ReadTimeoutError try: from smbus2 import SMBus except ImportError: from smbus import SMBus + import logging logging.basicConfig( diff --git a/examples/luftdaten_combined.py b/examples/luftdaten_combined.py index 1d920db4..19e9fdc9 100644 --- a/examples/luftdaten_combined.py +++ b/examples/luftdaten_combined.py @@ -1,14 +1,16 @@ +import colorsys import logging import sys +import time +from subprocess import PIPE, Popen, check_output + import requests import ST7735 -import time -import colorsys 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 +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003, ReadTimeoutError + from enviroplus import gas try: diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 5e564afe..efda0b7b 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -6,11 +6,13 @@ """ import argparse -import ST7735 -import time import ssl +import time + +import ST7735 from bme280 import BME280 from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError + from enviroplus import gas try: @@ -21,13 +23,13 @@ except ImportError: import ltr559 -from subprocess import PIPE, Popen, check_output -from PIL import Image, ImageDraw, ImageFont -from fonts.ttf import RobotoMedium as UserFont import json +from subprocess import PIPE, Popen, check_output import paho.mqtt.client as mqtt import paho.mqtt.publish as publish +from fonts.ttf import RobotoMedium as UserFont +from PIL import Image, ImageDraw, ImageFont try: from smbus2 import SMBus diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index 4c14c585..ddf1a315 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -1,5 +1,6 @@ import ST7735 from PIL import Image, ImageDraw + from enviroplus.noise import Noise print("""noise-amps-at-freqs.py - Measure amplitude from specific frequency bins diff --git a/examples/noise-profile.py b/examples/noise-profile.py index 40844391..c37ba35c 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -1,5 +1,6 @@ import ST7735 from PIL import Image, ImageDraw + from enviroplus.noise import Noise print("""noise-profile.py - Get a simple noise profile. diff --git a/examples/particulates.py b/examples/particulates.py index 04a49500..88d30e0c 100755 --- a/examples/particulates.py +++ b/examples/particulates.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 +import logging import time + from pms5003 import PMS5003, ReadTimeoutError -import logging logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index cd8ae962..926c07b8 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -3,22 +3,21 @@ f"Sorry! This program requires Python >= 3.6 😅" +import colorsys import os import time -import numpy -import colorsys -from PIL import Image, ImageDraw, ImageFont, ImageFilter -from fonts.ttf import RobotoMedium as UserFont +from datetime import datetime, timedelta +import numpy +import pytz import ST7735 +from astral.geocoder import database, lookup +from astral.sun import sun from bme280 import BME280 +from fonts.ttf import RobotoMedium as UserFont from ltr559 import LTR559 - -import pytz +from PIL import Image, ImageDraw, ImageFilter, ImageFont 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 diff --git a/examples/weather.py b/examples/weather.py index 66f18e0a..23a6a86e 100755 --- a/examples/weather.py +++ b/examples/weather.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import time + from bme280 import BME280 try: diff --git a/tests/conftest.py b/tests/conftest.py index b3fa3766..a7b3b333 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ that might otherwise have runtime side-effects. """ import sys + import mock import pytest from i2cdevice import MockSMBus From 207d6eb18cf79343bce05a5b0f6e54be96d9e173 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:35:41 +0100 Subject: [PATCH 092/122] QA: Apply ruff suggestions. --- examples/all-in-one-enviro-mini.py | 2 -- examples/luftdaten.py | 2 +- examples/luftdaten_combined.py | 1 - examples/mqtt-all.py | 1 - examples/weather-and-light.py | 3 --- tests/test_setup.py | 10 +++++----- 6 files changed, 6 insertions(+), 13 deletions(-) diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py index 9e87784c..449c49d8 100755 --- a/examples/all-in-one-enviro-mini.py +++ b/examples/all-in-one-enviro-mini.py @@ -21,8 +21,6 @@ from fonts.ttf import RobotoMedium as UserFont from PIL import Image, ImageDraw, ImageFont -from enviroplus import gas - logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', level=logging.INFO, diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 9e58dba8..f71ee43c 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import time -from subprocess import PIPE, Popen, check_output +from subprocess import check_output import requests import ST7735 diff --git a/examples/luftdaten_combined.py b/examples/luftdaten_combined.py index 19e9fdc9..978ce283 100644 --- a/examples/luftdaten_combined.py +++ b/examples/luftdaten_combined.py @@ -1,6 +1,5 @@ import colorsys import logging -import sys import time from subprocess import PIPE, Popen, check_output diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index efda0b7b..16e8f0df 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -27,7 +27,6 @@ from subprocess import PIPE, Popen, check_output import paho.mqtt.client as mqtt -import paho.mqtt.publish as publish from fonts.ttf import RobotoMedium as UserFont from PIL import Image, ImageDraw, ImageFont diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index 926c07b8..12b43def 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -f"Sorry! This program requires Python >= 3.6 😅" - import colorsys import os import time @@ -17,7 +15,6 @@ from fonts.ttf import RobotoMedium as UserFont from ltr559 import LTR559 from PIL import Image, ImageDraw, ImageFilter, ImageFont -from pytz import timezone try: from smbus2 import SMBus diff --git a/tests/test_setup.py b/tests/test_setup.py index 40bf80d6..9b300e1f 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -12,7 +12,7 @@ def test_gas_unavailable(GPIO, mocksmbus): from enviroplus import gas mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh noes!") gas._is_setup = False - assert gas.available() == False + assert gas.available() is False with pytest.raises(RuntimeError): gas.read_all() @@ -21,7 +21,7 @@ def test_gas_unavailable(GPIO, mocksmbus): def test_gas_available(GPIO, smbus_notimeout): from enviroplus import gas gas._is_setup = False - assert gas.available() == True + assert gas.available() is True def test_gas_read_all(GPIO, smbus): @@ -29,13 +29,13 @@ def test_gas_read_all(GPIO, smbus): gas._is_setup = False result = gas.read_all() - assert type(result.oxidising) == float + assert isinstance(result.oxidising, float) assert int(result.oxidising) == 16641 - assert type(result.reducing) == float + assert isinstance(result.reducing, float) assert int(result.reducing) == 16727 - assert type(result.nh3) == float + assert isinstance(result.nh3, float) assert int(result.nh3) == 16813 assert "Oxidising" in str(result) From fc5a49bbacae740aa431aa9bc6fd9215616f6f54 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:38:44 +0100 Subject: [PATCH 093/122] QA: Apply spelling corrections. --- enviroplus/gas.py | 2 +- enviroplus/noise.py | 4 ++-- examples/luftdaten.py | 2 +- tests/test_setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/enviroplus/gas.py b/enviroplus/gas.py index 6e96d8e8..fe60fb89 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -92,7 +92,7 @@ def cleanup(): def read_all(): - """Return gas resistence for oxidising, reducing and NH3""" + """Return gas resistance for oxidising, reducing and NH3""" setup() if not _is_available: diff --git a/enviroplus/noise.py b/enviroplus/noise.py index 873fc26b..e9288b2e 100644 --- a/enviroplus/noise.py +++ b/enviroplus/noise.py @@ -39,7 +39,7 @@ 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)) + raise ValueError("Maximum frequency is {}".format(n)) recording = self._record() magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate)) @@ -50,7 +50,7 @@ def get_noise_profile(self, low=0.12, mid=0.36, high=None): - """Returns a noise charateristic profile. + """Returns a noise characteristic profile. Bins all frequencies into 3 weighted groups expressed as a percentage of the total frequency range. diff --git a/examples/luftdaten.py b/examples/luftdaten.py index f71ee43c..969c9f16 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -72,7 +72,7 @@ def read_values(): values["P2"] = str(pm_values.pm_ug_per_m3(2.5)) values["P1"] = str(pm_values.pm_ug_per_m3(10)) except(ReadTimeoutError, ChecksumMismatchError): - logging.info("Failed to read PMS5003. Reseting and retrying.") + logging.info("Failed to read PMS5003. Resetting and retrying.") pms5003.reset() pm_values = pms5003.read() values["P2"] = str(pm_values.pm_ug_per_m3(2.5)) diff --git a/tests/test_setup.py b/tests/test_setup.py index 9b300e1f..ac254e02 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -10,7 +10,7 @@ def test_gas_setup(GPIO, smbus): def test_gas_unavailable(GPIO, mocksmbus): from enviroplus import gas - mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh noes!") + mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh no!") gas._is_setup = False assert gas.available() is False From 6e9e4318af70449a81b5537cd0cb4182aea03a17 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:49:48 +0100 Subject: [PATCH 094/122] Install: Fix for do_serial showing a UI. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d296bf2..a39b5514 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,6 +140,6 @@ commands = [ "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", - "raspi-config nonint set_config_var enable_uart 1 $CONFIG" + "sudo raspi-config nonint do_serial_cons 1", + "sudo raspi-config nonint do_serial_hw 1" ] From 342cf4ee4be92c805c0d44ee8a1039dd6e0e0aa2 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 15 Nov 2023 11:24:23 +0000 Subject: [PATCH 095/122] Bump requirement versions. --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a39b5514..bf030cc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,11 +35,11 @@ classifiers = [ "Topic :: System :: Hardware", ] dependencies = [ - "pimoroni-bme280", - "pms5003", - "ltr559", - "st7735", - "ads1015 >= 0.0.7", + "pimoroni-bme280 >= 1.0.0", + "pms5003 >= 1.0.0", + "ltr559 >= 1.0.0", + "st7735 >= 1.0.0", + "ads1015 >= 1.0.0", "fonts", "font-roboto", "astral", From 2cfed377ddcf5af73dae26dd01c60a879343e0fa Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 15 Nov 2023 13:01:04 +0000 Subject: [PATCH 096/122] Port to new libraries. Fixup examples and tests. --- enviroplus/__init__.py | 2 +- enviroplus/gas.py | 58 +++++++++------- enviroplus/noise.py | 18 ++--- examples/adc.py | 4 +- examples/all-in-one-enviro-mini.py | 24 +++---- examples/all-in-one-no-pm.py | 18 ++--- examples/all-in-one.py | 18 ++--- examples/combined.py | 26 +++---- examples/compensated-temperature.py | 15 ++-- examples/gas.py | 4 +- examples/lcd.py | 19 ++--- examples/light.py | 10 +-- examples/luftdaten.py | 80 +++++++++++---------- examples/luftdaten_combined.py | 49 +++++++------ examples/mqtt-all.py | 28 +++++--- examples/noise-amps-at-freqs.py | 16 +++-- examples/noise-profile.py | 16 +++-- examples/particulates.py | 4 +- examples/weather-and-light.py | 50 +++++++------ examples/weather.py | 21 +++--- pyproject.toml | 3 + tests/conftest.py | 104 ++++++++++++++-------------- tests/test_noise.py | 4 +- tests/test_setup.py | 23 +++--- 24 files changed, 312 insertions(+), 302 deletions(-) diff --git a/enviroplus/__init__.py b/enviroplus/__init__.py index fa9c4ec2..034f46c3 100644 --- a/enviroplus/__init__.py +++ b/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.6' +__version__ = "0.0.6" diff --git a/enviroplus/gas.py b/enviroplus/gas.py index fe60fb89..e1c4b76d 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -4,20 +4,29 @@ import time import ads1015 -import RPi.GPIO as GPIO +import gpiod +import gpiodevice +from gpiod.line import Direction, Value -MICS6814_HEATER_PIN = 24 MICS6814_GAIN = 6.144 +OUTH = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE) +PLATFORMS = { + "Radxa ROCK 5B": {"heater": ("PIN_18", OUTH)}, + "Raspberry Pi 5": {"heater": ("PIN18", OUTH)}, + "Raspberry Pi 4": {"heater": ("GPIO24", OUTH)} +} + ads1015.I2C_ADDRESS_DEFAULT = ads1015.I2C_ADDRESS_ALTERNATE _is_setup = False _is_available = False _adc_enabled = False _adc_gain = 6.148 +_heater = None class Mics6814Reading(object): - __slots__ = 'oxidising', 'reducing', 'nh3', 'adc' + __slots__ = "oxidising", "reducing", "nh3", "adc" def __init__(self, ox, red, nh3, adc=None): self.oxidising = ox @@ -26,24 +35,20 @@ def __init__(self, ox, red, nh3, adc=None): self.adc = adc def __repr__(self): - fmt = """Oxidising: {ox:05.02f} Ohms -Reducing: {red:05.02f} Ohms -NH3: {nh3:05.02f} Ohms""" + fmt = f"""Oxidising: {self.oxidising:05.02f} Ohms +Reducing: {self.reducing:05.02f} Ohms +NH3: {self.nh3:05.02f} Ohms""" if self.adc is not None: - fmt += """ -ADC: {adc:05.02f} Volts + fmt += f""" +ADC: {self.adc:05.02f} Volts """ - return fmt.format( - ox=self.oxidising, - red=self.reducing, - nh3=self.nh3, - adc=self.adc) + return fmt __str__ = __repr__ def setup(): - global adc, adc_type, _is_setup, _is_available + global adc, adc_type, _is_setup, _is_available, _heater if _is_setup: return _is_setup = True @@ -56,17 +61,15 @@ def setup(): _is_available = False return - adc.set_mode('single') + adc.set_mode("single") adc.set_programmable_gain(MICS6814_GAIN) - if adc_type == 'ADS1115': + if adc_type == "ADS1115": adc.set_sample_rate(128) else: 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) + _heater = gpiodevice.get_pins_for_platform(PLATFORMS)[0] + atexit.register(cleanup) @@ -88,7 +91,10 @@ def set_adc_gain(value): def cleanup(): - GPIO.output(MICS6814_HEATER_PIN, 0) + if _heater is None: + return + lines, offset = _heater + lines.set_value(offset, Value.INACTIVE) def read_all(): @@ -98,9 +104,9 @@ def read_all(): if not _is_available: raise RuntimeError("Gas sensor not connected.") - ox = adc.get_voltage('in0/gnd') - red = adc.get_voltage('in1/gnd') - nh3 = adc.get_voltage('in2/gnd') + 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) @@ -121,11 +127,11 @@ def read_all(): if _adc_enabled: if _adc_gain == MICS6814_GAIN: - analog = adc.get_voltage('ref/gnd') + analog = adc.get_voltage("ref/gnd") else: adc.set_programmable_gain(_adc_gain) time.sleep(0.05) - analog = adc.get_voltage('ref/gnd') + analog = adc.get_voltage("ref/gnd") adc.set_programmable_gain(MICS6814_GAIN) return Mics6814Reading(ox, red, nh3, analog) diff --git a/enviroplus/noise.py b/enviroplus/noise.py index e9288b2e..261c3abf 100644 --- a/enviroplus/noise.py +++ b/enviroplus/noise.py @@ -2,10 +2,8 @@ import sounddevice -class Noise(): - def __init__(self, - sample_rate=16000, - duration=0.5): +class Noise: + def __init__(self, sample_rate=16000, duration=0.5): """Noise measurement. :param sample_rate: Sample rate in Hz @@ -39,17 +37,13 @@ def get_amplitude_at_frequency_range(self, start, end): """ n = self.sample_rate // 2 if start > n or end > n: - raise ValueError("Maximum frequency is {}".format(n)) + raise ValueError(f"Maximum frequency is {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): + def get_noise_profile(self, noise_floor=100, low=0.12, mid=0.36, high=None): """Returns a noise characteristic profile. Bins all frequencies into 3 weighted groups expressed as a percentage of the total frequency range. @@ -83,9 +77,9 @@ def get_noise_profile(self, def _record(self): return sounddevice.rec( int(self.duration * self.sample_rate), - device='adau7002', + device="adau7002", samplerate=self.sample_rate, blocking=True, channels=1, - dtype='float64' + dtype="float64" ) diff --git a/examples/adc.py b/examples/adc.py index 6a86242c..983aec3a 100755 --- a/examples/adc.py +++ b/examples/adc.py @@ -6,9 +6,9 @@ from enviroplus import gas logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""adc.py - Print readings from the MICS6814 Gas sensor. diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py index 449c49d8..01491f37 100755 --- a/examples/all-in-one-enviro-mini.py +++ b/examples/all-in-one-enviro-mini.py @@ -5,7 +5,7 @@ import sys import time -import ST7735 +import st7735 try: # Transitional fix for breaking change in LTR559 @@ -22,11 +22,11 @@ from PIL import Image, ImageDraw, ImageFont logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + 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 Enviro plus" sensors Press Ctrl+C to exit! """) @@ -34,11 +34,11 @@ bme280 = BME280() # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -50,7 +50,7 @@ HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +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 @@ -71,7 +71,7 @@ def display_text(variable, data, unit): 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) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -90,9 +90,9 @@ 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, universal_newlines=True) + process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() - return float(output[output.index('=') + 1:output.rindex("'")]) + return float(output[output.index("=") + 1:output.rindex("'")]) # Tuning factor for compensation. Decrease this number to adjust the @@ -131,7 +131,7 @@ def get_cpu_temperature(): # One mode for each variable if mode == 0: # variable = "temperature" - unit = "C" + unit = "°C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter cpu_temps = cpu_temps[1:] + [cpu_temp] diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index aa67fef7..c2d9078d 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -5,7 +5,7 @@ import sys import time -import ST7735 +import st7735 try: # Transitional fix for breaking change in LTR559 @@ -23,9 +23,9 @@ from enviroplus import gas logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + 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! @@ -35,11 +35,11 @@ bme280 = BME280() # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -51,7 +51,7 @@ HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +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 @@ -72,7 +72,7 @@ def display_text(variable, data, unit): 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) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -136,7 +136,7 @@ def get_cpu_temperature(): # One mode for each variable if mode == 0: # variable = "temperature" - unit = "C" + unit = "°C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter cpu_temps = cpu_temps[1:] + [cpu_temp] diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 54986036..9b7121ea 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -4,7 +4,7 @@ import sys import time -import ST7735 +import st7735 try: # Transitional fix for breaking change in LTR559 @@ -24,9 +24,9 @@ from enviroplus import gas logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors @@ -41,11 +41,11 @@ pms5003 = PMS5003() # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -57,7 +57,7 @@ HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) font_size = 20 font = ImageFont.truetype(UserFont, font_size) @@ -77,7 +77,7 @@ def display_text(variable, data, unit): 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) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -144,7 +144,7 @@ def get_cpu_temperature(): # One mode for each variable if mode == 0: # variable = "temperature" - unit = "C" + unit = "°C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter cpu_temps = cpu_temps[1:] + [cpu_temp] diff --git a/examples/combined.py b/examples/combined.py index 1ffaccb6..eaab10f3 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -4,7 +4,7 @@ import sys import time -import ST7735 +import st7735 try: # Transitional fix for breaking change in LTR559 @@ -26,9 +26,9 @@ from enviroplus import gas logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""combined.py - Displays readings from all of Enviro plus' sensors @@ -44,11 +44,11 @@ time.sleep(1.0) # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -60,7 +60,7 @@ HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) font_size_small = 10 font_size_large = 20 @@ -140,7 +140,7 @@ def display_text(variable, data, unit): 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) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -163,7 +163,7 @@ def save_data(idx, data): # Maintain length of list values[variable] = values[variable][1:] + [data] unit = units[idx] - message = "{}: {:.1f} {}".format(variable[:4], data, unit) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) @@ -178,7 +178,7 @@ def display_everything(): 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) + message = f"{variable[:4]}: {data_value:.1f} {unit}" lim = limits[i] rgb = palette[0] for j in range(len(lim)): @@ -190,9 +190,9 @@ def display_everything(): # Get the temperature of the CPU for compensation def get_cpu_temperature(): - process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) + process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() - return float(output[output.index('=') + 1:output.rindex("'")]) + return float(output[output.index("=") + 1:output.rindex("'")]) def main(): @@ -223,7 +223,7 @@ def main(): # One mode for each variable if mode == 0: # variable = "temperature" - unit = "C" + unit = "°C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter cpu_temps = cpu_temps[1:] + [cpu_temp] diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index 1d36140c..fb692f07 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -1,20 +1,15 @@ #!/usr/bin/env python3 +import logging import time from bme280 import BME280 - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus - -import logging +from smbus2 import SMBus logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""compensated-temperature.py - Use the CPU temperature to compensate temperature readings from the BME280 sensor. @@ -50,5 +45,5 @@ def get_cpu_temperature(): 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)) + logging.info(f"Compensated temperature: {comp_temp:05.2f} °C") time.sleep(1.0) diff --git a/examples/gas.py b/examples/gas.py index a107696a..c5fce5fc 100755 --- a/examples/gas.py +++ b/examples/gas.py @@ -6,9 +6,9 @@ from enviroplus import gas logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""gas.py - Print readings from the MICS6814 Gas sensor. diff --git a/examples/lcd.py b/examples/lcd.py index 0c0606e0..cb2af2f3 100755 --- a/examples/lcd.py +++ b/examples/lcd.py @@ -2,14 +2,14 @@ import logging -import ST7735 +import st7735 from fonts.ttf import RobotoMedium as UserFont from PIL import Image, ImageDraw, ImageFont logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""lcd.py - Hello, World! example on the 0.96" LCD. @@ -18,11 +18,11 @@ """) # Create LCD class instance. -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -35,7 +35,7 @@ HEIGHT = disp.height # New canvas to draw on. -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) # Text settings. @@ -45,7 +45,10 @@ back_colour = (0, 170, 170) message = "Hello, World!" -size_x, size_y = draw.textsize(message, font) + +x1, y1, x2, y2 = font.getbbox(message) +size_x = x2 - x1 +size_y = y2 - y1 # Calculate text position x = (WIDTH - size_x) / 2 diff --git a/examples/light.py b/examples/light.py index ead18a20..70414db3 100755 --- a/examples/light.py +++ b/examples/light.py @@ -12,9 +12,9 @@ logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""light.py - Print readings from the LTR559 Light & Proximity sensor. @@ -26,9 +26,9 @@ while True: lux = ltr559.get_lux() prox = ltr559.get_proximity() - logging.info("""Light: {:05.02f} Lux -Proximity: {:05.02f} -""".format(lux, prox)) + logging.info(f"""Light: {lux:05.02f} Lux +Proximity: {prox:05.02f} +""") time.sleep(1.0) except KeyboardInterrupt: pass diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 969c9f16..ca1e4c9b 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -1,40 +1,35 @@ #!/usr/bin/env python3 +import logging import time from subprocess import check_output import requests -import ST7735 +import st7735 from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont from PIL import Image, ImageDraw, ImageFont from pms5003 import PMS5003, ChecksumMismatchError, ReadTimeoutError - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus - -import logging +from smbus2 import SMBus logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""luftdaten.py - Reads temperature, pressure, humidity, -#PM2.5, and PM10 from Enviro plus and sends data to Luftdaten, -#the citizen science air quality project. +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. +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! +Press Ctrl+C to exit! -#""") +""") bus = SMBus(1) @@ -42,11 +37,11 @@ bme280 = BME280(i2c_dev=bus) # Create LCD instance -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -64,9 +59,9 @@ def read_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()) + values["temperature"] = f"{comp_temp:.2f}" + values["pressure"] = f"{bme280.get_pressure() * 100:.2f}" + values["humidity"] = f"{bme280.get_humidity():.2f}" try: pm_values = pms5003.read() values["P2"] = str(pm_values.pm_ug_per_m3(2.5)) @@ -90,15 +85,15 @@ def get_cpu_temperature(): # Get Raspberry Pi serial number to use as ID def get_serial_number(): - with open('/proc/cpuinfo', 'r') as f: + with open("/proc/cpuinfo", "r") as f: for line in f: - if line[0:6] == 'Serial': + if line.startswith("Serial"): return line.split(":")[1].strip() # Check for Wi-Fi connection def check_wifi(): - if check_output(['hostname', '-I']): + if check_output(["hostname", "-I"]): return True else: return False @@ -110,10 +105,12 @@ def display_status(): 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)) + message = f"{id}\nWi-Fi: {wifi_status}" + img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) - size_x, size_y = draw.textsize(message, font) + x1, y1, x2, y2 = font.getbbox(message) + size_x = x2 - x1 + size_y = y2 - y1 x = (WIDTH - size_x) / 2 y = (HEIGHT / 2) - (size_y / 2) draw.rectangle((0, 0, 160, 80), back_colour) @@ -147,11 +144,11 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning('Sensor.Community (Luftdaten) PM Connection Error: {}'.format(e)) + logging.warning(f"Sensor.Community (Luftdaten) PM Connection Error: {e}") except requests.exceptions.Timeout as e: - logging.warning('Sensor.Community (Luftdaten) PM Timeout Error: {}'.format(e)) + logging.warning(f"Sensor.Community (Luftdaten) PM Timeout Error: {e}") except requests.exceptions.RequestException as e: - logging.warning('Sensor.Community (Luftdaten) PM Request Error: {}'.format(e)) + logging.warning(f"Sensor.Community (Luftdaten) PM Request Error: {e}") try: resp_bmp = requests.post( @@ -169,17 +166,17 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning('Sensor.Community (Luftdaten) Climate Connection Error: {}'.format(e)) + logging.warning(f"Sensor.Community (Luftdaten) Climate Connection Error: {e}") except requests.exceptions.Timeout as e: - logging.warning('Sensor.Community (Luftdaten) Climate Timeout Error: {}'.format(e)) + logging.warning(f"Sensor.Community (Luftdaten) Climate Timeout Error: {e}") except requests.exceptions.RequestException as e: - logging.warning('Sensor.Community (Luftdaten) Climate Request Error: {}'.format(e)) + logging.warning(f"Sensor.Community (Luftdaten) Climate Request Error: {e}") if resp_pm is not None and resp_bmp is not None: if resp_pm.ok and resp_bmp.ok: return True else: - logging.warning('Luftdaten Error. PM: {}, Climate: {}'.format(resp_pm.reason, resp_bmp.reason)) + logging.warning(f"Luftdaten Error. PM: {resp_pm.reason}, Climate: {resp_bmp.reason}") return False else: return False @@ -200,8 +197,9 @@ def send_to_luftdaten(values, id): font = ImageFont.truetype(UserFont, font_size) # Log Raspberry Pi serial and Wi-Fi status -logging.info("Raspberry Pi serial: {}".format(get_serial_number())) -logging.info("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) +logging.info(f"Raspberry Pi serial: {get_serial_number()}") +wifi_status = "connected" if check_wifi() else "disconnected" +logging.info(f"Wi-Fi: {wifi_status}\n") time_since_update = 0 update_time = time.time() @@ -220,4 +218,4 @@ def send_to_luftdaten(values, id): logging.warning("Luftdaten Response: Failed") display_status() except Exception as e: - logging.warning('Main Loop Exception: {}'.format(e)) + logging.warning(f"Main Loop Exception: {e}") diff --git a/examples/luftdaten_combined.py b/examples/luftdaten_combined.py index 978ce283..293e414d 100644 --- a/examples/luftdaten_combined.py +++ b/examples/luftdaten_combined.py @@ -4,18 +4,15 @@ from subprocess import PIPE, Popen, check_output import requests -import ST7735 +import st7735 from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont from PIL import Image, ImageDraw, ImageFont from pms5003 import PMS5003, ReadTimeoutError +from smbus2 import SMBus from enviroplus import gas -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -48,9 +45,9 @@ """) logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info(""" """) bus = SMBus(1) @@ -120,9 +117,9 @@ # Read values from BME280 and PMS5003 and return as dict def read_values(comp_temp, mod_press, raw_humid, raw_pm25, raw_pm10): values = {} - values["temperature"] = "{:.2f}".format(comp_temp) - values["pressure"] = "{:.2f}".format(mod_press) - values["humidity"] = "{:.2f}".format(raw_humid) + values["temperature"] = f"{comp_temp:.2f}" + values["pressure"] = f"{mod_press:.2f}" + values["humidity"] = f"{raw_humid:.2f}" values["P2"] = str(raw_pm25) values["P1"] = str(raw_pm10) return values @@ -130,34 +127,34 @@ def read_values(comp_temp, mod_press, raw_humid, raw_pm25, raw_pm10): # Get CPU temperature to use for compensation def get_cpu_temperature(): - process = Popen(['vcgencmd', 'measure_temp'], + process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() - return float(output[output.index('=') + 1:output.rindex("'")]) + 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: + with open("/proc/cpuinfo", "r") as f: for line in f: - if line[0:6] == 'Serial': + if line.startswith("Serial"): return line.split(":")[1].strip() # Check for Wi-Fi connection def check_wifi(): - if check_output(['hostname', '-I']): + if check_output(["hostname", "-I"]): return True else: return False # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -169,7 +166,7 @@ def check_wifi(): HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) font_size_small = 10 font_size_large = 20 @@ -190,7 +187,7 @@ def save_data(idx, data): # Maintain length of list values_lcd[variable] = values_lcd[variable][1:] + [data] unit = units[idx] - message = "{}: {:.1f} {}".format(variable[:4], data, unit) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) @@ -204,7 +201,7 @@ def display_text(variable, data, unit): colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values_lcd[variable]] # Format the variable name and value - message = "{}: {:.1f} {}".format(variable[:4], data, unit) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -235,7 +232,7 @@ def display_everything(): 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) + message = f"{variable[:4]}: {data_value:.1f} {unit}" lim = limits[i] rgb = palette[0] for j in range(len(lim)): @@ -312,8 +309,9 @@ def send_to_luftdaten(values, id): cpu_temps = [get_cpu_temperature()] * 5 # 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")) +print(f"Raspberry Pi serial: {get_serial_number()}") +wifi_status = "connected" if check_wifi() else "disconnected" +print(f"Wi-Fi: {wifi_status}\n") time_since_update = 0 update_time = time.time() @@ -350,7 +348,8 @@ def send_to_luftdaten(values, id): raw_humid, raw_pm25, raw_pm10) resp = send_to_luftdaten(values, id) update_time = curtime - print("Response: {}\n".format("ok" if resp else "failed")) + status = "ok" if resp else "failed" + print(f"Response: {status}\n") # Now comes the combined.py functionality: # If the proximity crosses the threshold, toggle the mode diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 16e8f0df..7238d3b0 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -9,7 +9,7 @@ import ssl import time -import ST7735 +import st7735 from bme280 import BME280 from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError @@ -133,12 +133,12 @@ def display_status(disp, mqtt_broker): text_colour = (255, 255, 255) back_colour = (0, 170, 170) if check_wifi() else (85, 15, 15) device_serial_number = get_serial_number() - message = "{}\nWi-Fi: {}\nmqtt-broker: {}".format( - device_serial_number, wifi_status, mqtt_broker - ) + message = f"{device_serial_number}\nWi-Fi: {wifi_status}\nmqtt-broker: {mqtt_broker}" img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) - size_x, size_y = draw.textsize(message, font) + x1, y1, x2, y2 = font.getbbox(message) + size_x = x2 - x1 + size_y = y2 - y1 x = (WIDTH - size_x) / 2 y = (HEIGHT / 2) - (size_y / 2) draw.rectangle((0, 0, 160, 80), back_colour) @@ -174,7 +174,7 @@ def main(): parser.add_argument( "--tls", default=DEFAULT_TLS_MODE, - action='store_true', + action="store_true", help="enable TLS" ) parser.add_argument( @@ -231,8 +231,13 @@ def main(): 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 + disp = st7735.ST7735( + port=0, + cs=1, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + rotation=270, + spi_speed_hz=10000000 ) # Initialize display @@ -249,9 +254,10 @@ def main(): print("No PMS5003 sensor connected") # Display Raspberry Pi serial and Wi-Fi status - print("RPi serial: {}".format(device_serial_number)) - print("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) - print("MQTT broker IP: {}".format(args.broker)) + print(f"RPi serial: {device_serial_number}") + wifi_status = "connected" if check_wifi() else "disconnected" + print(f"Wi-Fi: {wifi_status}\n") + print(f"MQTT broker IP: {args.broker}") # Set an initial update time update_time = time.time() diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index ddf1a315..825f53cf 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -1,4 +1,4 @@ -import ST7735 +import st7735 from PIL import Image, ImageDraw from enviroplus.noise import Noise @@ -15,16 +15,18 @@ noise = Noise() -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, - cs=ST7735.BG_SPI_CS_FRONT, - dc=9, - backlight=12, - rotation=90) + cs=1, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + rotation=270, + spi_speed_hz=10000000 +) disp.begin() -img = Image.new('RGB', (disp.width, disp.height), color=(0, 0, 0)) +img = Image.new("RGB", (disp.width, disp.height), color=(0, 0, 0)) draw = ImageDraw.Draw(img) diff --git a/examples/noise-profile.py b/examples/noise-profile.py index c37ba35c..17ada829 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -1,4 +1,4 @@ -import ST7735 +import st7735 from PIL import Image, ImageDraw from enviroplus.noise import Noise @@ -13,16 +13,18 @@ noise = Noise() -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, - cs=ST7735.BG_SPI_CS_FRONT, - dc=9, - backlight=12, - rotation=90) + cs=1, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + rotation=270, + spi_speed_hz=10000000 +) disp.begin() -img = Image.new('RGB', (disp.width, disp.height), color=(0, 0, 0)) +img = Image.new("RGB", (disp.width, disp.height), color=(0, 0, 0)) draw = ImageDraw.Draw(img) diff --git a/examples/particulates.py b/examples/particulates.py index 88d30e0c..6ecaf707 100755 --- a/examples/particulates.py +++ b/examples/particulates.py @@ -6,9 +6,9 @@ from pms5003 import PMS5003, ReadTimeoutError logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""particulates.py - Print readings from the PMS5003 particulate sensor. diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index 12b43def..efbd2a88 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -8,18 +8,14 @@ import numpy import pytz -import ST7735 +import st7735 from astral.geocoder import database, lookup from astral.sun import sun from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont from ltr559 import LTR559 from PIL import Image, ImageDraw, ImageFilter, ImageFont - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus +from smbus2 import SMBus def calculate_y_pos(x, centre): @@ -44,7 +40,7 @@ def circle_coordinates(x, y, radius): 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.""" + 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 @@ -81,7 +77,7 @@ def x_from_sun_moon_time(progress, period, x_range): 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'.""" + night) from the last sunrise or sunset, given a datetime object "t".""" city = lookup(city_name, database()) @@ -137,11 +133,11 @@ def draw_background(progress, period, day): # x-coordinate for sun/moon x = x_from_sun_moon_time(progress, period, WIDTH) - # If it's day, then move right to left + # If it"s day, then move right to left if day: x = WIDTH - x - # Calculate position on sun/moon's curve + # Calculate position on sun/moon"s curve centre = WIDTH / 2 y = calculate_y_pos(x, centre) @@ -149,11 +145,11 @@ def draw_background(progress, period, day): background = map_colour(x, 80, mid_hue, day_hue, day) # New image for background colour - img = Image.new('RGBA', (WIDTH, HEIGHT), color=background) + 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 = Image.new("RGBA", (WIDTH, HEIGHT), color=(0, 0, 0, 0)) overlay_draw = ImageDraw.Draw(overlay) # Draw the sun/moon @@ -166,9 +162,19 @@ def draw_background(progress, period, day): return composite +def text_width(font, text): + x1, y1, x2, y2 = font.getbbox(text) + return x2 - x1 + + +def text_size(font, text): + x1, y1, x2, y2 = font.getbbox(text) + return x2 - x1, y2 - y1 + + def overlay_text(img, position, text, font, align_right=False, rectangle=False): draw = ImageDraw.Draw(img) - w, h = font.getsize(text) + w, h = text_size(font, text) if align_right: x, y = position x -= w @@ -179,7 +185,7 @@ def overlay_text(img, position, text, font, align_right=False, rectangle=False): 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_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)) @@ -287,11 +293,11 @@ def describe_light(light): # Initialise the LCD -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -352,7 +358,7 @@ def describe_light(light): # Time. time_elapsed = time.time() - start_time - date_string = local_dt.strftime("%d %b %y").lstrip('0') + 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) @@ -378,7 +384,7 @@ def describe_light(light): 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 + spacing = text_width(font_lg, temp_string) + 1 if min_temp is not None and max_temp is not None: range_string = f"{min_temp:.0f}-{max_temp:.0f}" else: @@ -392,7 +398,7 @@ def describe_light(light): 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 + spacing = text_width(font_lg, humidity_string) + 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") @@ -402,7 +408,7 @@ def describe_light(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 + spacing = text_width(font_lg, light_string.replace(",", "")) + 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") @@ -415,7 +421,7 @@ def describe_light(light): 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 + spacing = text_width(font_lg, pressure_string.replace(",", "")) + 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) diff --git a/examples/weather.py b/examples/weather.py index 23a6a86e..0b671d31 100755 --- a/examples/weather.py +++ b/examples/weather.py @@ -1,20 +1,15 @@ #!/usr/bin/env python3 +import logging import time from bme280 import BME280 - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus - -import logging +from smbus2 import SMBus logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""weather.py - Print readings from the BME280 weather sensor. @@ -29,8 +24,8 @@ 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)) + logging.info(f"""Temperature: {temperature:05.2f} °C +Pressure: {pressure:05.2f} hPa +Relative humidity: {humidity:05.2f} % +""") time.sleep(1) diff --git a/pyproject.toml b/pyproject.toml index bf030cc2..e7346f14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,9 @@ skip = """ [tool.isort] line_length = 200 +[tool.black] +line-length = 200 + [tool.check-manifest] ignore = [ '.stickler.yml', diff --git a/tests/conftest.py b/tests/conftest.py index a7b3b333..6ad5557c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,96 +21,96 @@ def __init__(self, i2c_bus): self.regs[0x00:0x01] = 0x0f, 0x80 -@pytest.fixture(scope='function', autouse=True) +@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) + modules = "enviroplus", "enviroplus.noise", "enviroplus.gas", "ads1015", "i2cdevice" + for module in modules: + try: + del sys.modules[module] + except KeyError: + pass + + +@pytest.fixture(scope="function", autouse=False) +def gpiod(): + sys.modules["gpiod"] = mock.Mock() + sys.modules["gpiod.line"] = mock.Mock() + yield sys.modules["gpiod"] + del sys.modules["gpiod.line"] + del sys.modules["gpiod"] + + +@pytest.fixture(scope="function", autouse=False) +def gpiodevice(): + gpiodevice = mock.Mock() + gpiodevice.get_pins_for_platform.return_value = [(mock.Mock(), 0)] + + sys.modules["gpiodevice"] = gpiodevice + yield gpiodevice + del sys.modules["gpiodevice"] + + +@pytest.fixture(scope="function", autouse=False) def spidev(): """Mock spidev module.""" spidev = mock.MagicMock() - sys.modules['spidev'] = spidev + sys.modules["spidev"] = spidev yield spidev - del sys.modules['spidev'] + del sys.modules["spidev"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def smbus(): - """Mock smbus module.""" + """Mock smbus2 module.""" smbus = mock.MagicMock() smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus + sys.modules["smbus2"] = smbus yield smbus - del sys.modules['smbus'] + del sys.modules["smbus2"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def smbus_notimeout(): - """Mock smbus module.""" + """Mock smbus2 module.""" smbus = mock.MagicMock() smbus.SMBus = SMBusFakeDeviceNoTimeout - sys.modules['smbus'] = smbus + sys.modules["smbus2"] = smbus yield smbus - del sys.modules['smbus'] + del sys.modules["smbus2"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def mocksmbus(): - """Mock smbus module.""" + """Mock smbus2 module.""" smbus = mock.MagicMock() - sys.modules['smbus'] = smbus + sys.modules["smbus2"] = smbus yield smbus - del sys.modules['smbus'] + del sys.modules["smbus2"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def atexit(): """Mock atexit module.""" atexit = mock.MagicMock() - sys.modules['atexit'] = atexit + sys.modules["atexit"] = atexit yield atexit - del sys.modules['atexit'] + del sys.modules["atexit"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def sounddevice(): """Mock sounddevice module.""" sounddevice = mock.MagicMock() - sys.modules['sounddevice'] = sounddevice + sys.modules["sounddevice"] = sounddevice yield sounddevice - del sys.modules['sounddevice'] + del sys.modules["sounddevice"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def numpy(): """Mock numpy module.""" numpy = mock.MagicMock() - sys.modules['numpy'] = numpy + sys.modules["numpy"] = numpy yield numpy - del sys.modules['numpy'] + del sys.modules["numpy"] diff --git a/tests/test_noise.py b/tests/test_noise.py index 4949a3d3..a5eb7da0 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -17,7 +17,7 @@ def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): (501, 1000) ]) - sounddevice.rec.assert_called_with(0.1 * 16000, device='adau7002', samplerate=16000, blocking=True, channels=1, dtype='float64') + sounddevice.rec.assert_called_with(0.1 * 16000, device="adau7002", samplerate=16000, blocking=True, channels=1, dtype="float64") def test_noise_get_noise_profile(sounddevice, numpy): @@ -32,7 +32,7 @@ def test_noise_get_noise_profile(sounddevice, numpy): mid=0.36, high=None) - sounddevice.rec.assert_called_with(0.1 * 16000, device='adau7002', samplerate=16000, blocking=True, channels=1, dtype='float64') + sounddevice.rec.assert_called_with(0.1 * 16000, device="adau7002", samplerate=16000, blocking=True, channels=1, dtype="float64") assert amp_total == 10.0 diff --git a/tests/test_setup.py b/tests/test_setup.py index ac254e02..fa7fb939 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,14 +1,14 @@ import pytest -def test_gas_setup(GPIO, smbus): +def test_gas_setup(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False gas.setup() gas.setup() -def test_gas_unavailable(GPIO, mocksmbus): +def test_gas_unavailable(gpiod, gpiodevice, mocksmbus): from enviroplus import gas mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh no!") gas._is_setup = False @@ -18,13 +18,13 @@ def test_gas_unavailable(GPIO, mocksmbus): gas.read_all() -def test_gas_available(GPIO, smbus_notimeout): +def test_gas_available(gpiod, gpiodevice, smbus_notimeout): from enviroplus import gas gas._is_setup = False assert gas.available() is True -def test_gas_read_all(GPIO, smbus): +def test_gas_read_all(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False result = gas.read_all() @@ -41,7 +41,7 @@ def test_gas_read_all(GPIO, smbus): assert "Oxidising" in str(result) -def test_gas_read_each(GPIO, smbus): +def test_gas_read_each(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False @@ -50,7 +50,7 @@ def test_gas_read_each(GPIO, smbus): assert int(gas.read_nh3()) == 16813 -def test_gas_read_adc(GPIO, smbus): +def test_gas_read_adc(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False @@ -59,7 +59,7 @@ def test_gas_read_adc(GPIO, smbus): assert gas.read_adc() == 0.255 -def test_gas_read_adc_default_gain(GPIO, smbus): +def test_gas_read_adc_default_gain(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False @@ -68,18 +68,19 @@ def test_gas_read_adc_default_gain(GPIO, smbus): assert gas.read_adc() == 0.765 -def test_gas_read_adc_str(GPIO, smbus): +def test_gas_read_adc_str(gpiod, gpiodevice, 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()) + assert "ADC" in str(gas.read_all()) -def test_gas_cleanup(GPIO, smbus): +def test_gas_cleanup(gpiod, gpiodevice, smbus): from enviroplus import gas gas.cleanup() - GPIO.output.assert_called_with(gas.MICS6814_HEATER_PIN, 0) + gas.setup() + gas.cleanup() From f6ee18bc0f4286dfe0e9dd2376d22264821de6cd Mon Sep 17 00:00:00 2001 From: "Michael (MicroD)" <72285408+michaeldufault@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:19:58 +0000 Subject: [PATCH 097/122] Update Luftdaten examples to Sensor.Community Co-authored-by: Phil Howard --- examples/{luftdaten.py => sensorcommunity.py} | 40 +++++++++---------- ...ombined.py => sensorcommunity_combined.py} | 28 ++++++------- 2 files changed, 34 insertions(+), 34 deletions(-) rename examples/{luftdaten.py => sensorcommunity.py} (82%) rename examples/{luftdaten_combined.py => sensorcommunity_combined.py} (94%) diff --git a/examples/luftdaten.py b/examples/sensorcommunity.py similarity index 82% rename from examples/luftdaten.py rename to examples/sensorcommunity.py index ca1e4c9b..7128ad72 100755 --- a/examples/luftdaten.py +++ b/examples/sensorcommunity.py @@ -17,15 +17,15 @@ level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S") -logging.info("""luftdaten.py - Reads temperature, pressure, humidity, -PM2.5, and PM10 from Enviro plus and sends data to Luftdaten, +logging.info("""sensorcommunity.py - Reads temperature, pressure, humidity, +PM2.5, and PM10 from Enviro plus and sends data to Sensor.Community, the citizen science air quality project. -Note: you'll need to register with Luftdaten at: -https://meine.luftdaten.info/ and enter your Raspberry Pi +Note: you'll need to register with Sensor.Community at: +https://devices.sensor.community/ 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. +Sensor.Community map. Press Ctrl+C to exit! @@ -118,7 +118,7 @@ def display_status(): disp.display(img) -def send_to_luftdaten(values, id): +def send_to_sensorcommunity(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")) @@ -132,7 +132,7 @@ def send_to_luftdaten(values, id): resp_pm = requests.post( "https://api.sensor.community/v1/push-sensor-data/", json={ - "software_version": "enviro-plus 0.0.1", + "software_version": "enviro-plus 1.0.0", "sensordatavalues": pm_values_json }, headers={ @@ -144,17 +144,17 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning(f"Sensor.Community (Luftdaten) PM Connection Error: {e}") + logging.warning(f"Sensor.Community PM Connection Error: {e}") except requests.exceptions.Timeout as e: - logging.warning(f"Sensor.Community (Luftdaten) PM Timeout Error: {e}") + logging.warning(f"Sensor.Community PM Timeout Error: {e}") except requests.exceptions.RequestException as e: - logging.warning(f"Sensor.Community (Luftdaten) PM Request Error: {e}") + logging.warning(f"Sensor.Community PM Request Error: {e}") try: resp_bmp = requests.post( "https://api.sensor.community/v1/push-sensor-data/", json={ - "software_version": "enviro-plus 0.0.1", + "software_version": "enviro-plus 1.0.0", "sensordatavalues": temp_values_json }, headers={ @@ -166,17 +166,17 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning(f"Sensor.Community (Luftdaten) Climate Connection Error: {e}") + logging.warning(f"Sensor.Community Climate Connection Error: {e}") except requests.exceptions.Timeout as e: - logging.warning(f"Sensor.Community (Luftdaten) Climate Timeout Error: {e}") + logging.warning(f"Sensor.Community Climate Timeout Error: {e}") except requests.exceptions.RequestException as e: - logging.warning(f"Sensor.Community (Luftdaten) Climate Request Error: {e}") + logging.warning(f"Sensor.Community Climate Request Error: {e}") if resp_pm is not None and resp_bmp is not None: if resp_pm.ok and resp_bmp.ok: return True else: - logging.warning(f"Luftdaten Error. PM: {resp_pm.reason}, Climate: {resp_bmp.reason}") + logging.warning(f"Sensor.Community Error. PM: {resp_pm.reason}, Climate: {resp_bmp.reason}") return False else: return False @@ -185,7 +185,7 @@ def send_to_luftdaten(values, id): # Compensation factor for temperature comp_factor = 2.25 -# Raspberry Pi ID to send to Luftdaten +# Raspberry Pi ID to send to Sensor.Community id = "raspi-" + get_serial_number() # Width and height to calculate text position @@ -204,7 +204,7 @@ def send_to_luftdaten(values, id): time_since_update = 0 update_time = time.time() -# Main loop to read data, display, and send to Luftdaten +# Main loop to read data, display, and send to Sensor.Community while True: try: values = read_values() @@ -212,10 +212,10 @@ def send_to_luftdaten(values, id): if time_since_update > 145: logging.info(values) update_time = time.time() - if send_to_luftdaten(values, id): - logging.info("Luftdaten Response: OK") + if send_to_sensorcommunity(values, id): + logging.info("Sensor.Community Response: OK") else: - logging.warning("Luftdaten Response: Failed") + logging.warning("Sensor.Community Response: Failed") display_status() except Exception as e: logging.warning(f"Main Loop Exception: {e}") diff --git a/examples/luftdaten_combined.py b/examples/sensorcommunity_combined.py similarity index 94% rename from examples/luftdaten_combined.py rename to examples/sensorcommunity_combined.py index 293e414d..8dac7232 100644 --- a/examples/luftdaten_combined.py +++ b/examples/sensorcommunity_combined.py @@ -20,18 +20,18 @@ except ImportError: import ltr559 -print("""luftdaten_combined.py - This combines the functionality of luftdaten.py and combined.py +print("""sensorcommunity_combined.py - This combines the functionality of sensorcommunity.py and combined.py ================================================================================================ -Luftdaten INFO +Sensor.Community INFO Reads temperature, pressure, humidity, -PM2.5, and PM10 from Enviro plus and sends data to Luftdaten, +PM2.5, and PM10 from Enviro plus and sends data to Sensor.Community, the citizen science air quality project. -Note: you'll need to register with Luftdaten at: -https://meine.luftdaten.info/ and enter your Raspberry Pi +Note: you'll need to register with Sensor.Community at: +https://devices.sensor.community/ 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. +Sensor.Community map. Press Ctrl+C to exit! @@ -242,7 +242,7 @@ def display_everything(): st7735.display(img) -def send_to_luftdaten(values, id): +def send_to_sensorcommunity(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")) @@ -252,9 +252,9 @@ def send_to_luftdaten(values, id): for key, val in temp_values.items()] resp_1 = requests.post( - "https://api.luftdaten.info/v1/push-sensor-data/", + "https://api.sensor.community/v1/push-sensor-data/", json={ - "software_version": "enviro-plus 0.0.1", + "software_version": "enviro-plus 1.0.0", "sensordatavalues": pm_values_json }, headers={ @@ -266,9 +266,9 @@ def send_to_luftdaten(values, id): ) resp_2 = requests.post( - "https://api.luftdaten.info/v1/push-sensor-data/", + "https://api.sensor.community/v1/push-sensor-data/", json={ - "software_version": "enviro-plus 0.0.1", + "software_version": "enviro-plus 1.0.0", "sensordatavalues": temp_values_json }, headers={ @@ -288,7 +288,7 @@ def send_to_luftdaten(values, id): # Compensation factor for temperature comp_factor = 1 -# Raspberry Pi ID to send to Luftdaten +# Raspberry Pi ID to send to Sensor.Community id = "raspi-" + get_serial_number() @@ -317,7 +317,7 @@ def send_to_luftdaten(values, id): update_time = time.time() cpu_temps_len = float(len(cpu_temps)) -# Main loop to read data, display, and send to Luftdaten +# Main loop to read data, display, and send to Sensor.Community while True: try: curtime = time.time() @@ -346,7 +346,7 @@ def send_to_luftdaten(values, id): if time_since_update > 145: values = read_values(comp_temp, raw_press*100, raw_humid, raw_pm25, raw_pm10) - resp = send_to_luftdaten(values, id) + resp = send_to_sensorcommunity(values, id) update_time = curtime status = "ok" if resp else "failed" print(f"Response: {status}\n") From 11c218874f7cf81ad4898557b07d49a3ea72686d Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 15 Nov 2023 13:49:21 +0000 Subject: [PATCH 098/122] Fix for config.txt location. --- check.sh | 1 + install.sh | 64 +++++++++++++++++++++++++++++++++----------------- pyproject.toml | 15 ++++-------- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/check.sh b/check.sh index cbb15653..4395d89c 100755 --- a/check.sh +++ b/check.sh @@ -6,6 +6,7 @@ NOPOST=$1 LIBRARY_NAME=`hatch project metadata name` LIBRARY_VERSION=`hatch version | awk -F "." '{print $1"."$2"."$3}'` POST_VERSION=`hatch version | awk -F "." '{print substr($4,0,length($4))}'` +TERM=${TERM:="xterm-256color"} success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" diff --git a/install.sh b/install.sh index bee710c7..38f19e9a 100755 --- a/install.sh +++ b/install.sh @@ -1,12 +1,13 @@ #!/bin/bash LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` -CONFIG=/boot/config.txt +CONFIG_FILE=config.txt +CONFIG_DIR="/boot/firmware" DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false RESOURCES_TOP_DIR=$HOME/Pimoroni -VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh -VENV_DIR=$RESOURCES_TOP_DIR/venv +VENV_BASH_SNIPPET=$RESOURCES_DIR/auto_venv.sh +VENV_DIR=$HOME/.virtualenvs/pimoroni WD=`pwd` USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() @@ -14,6 +15,7 @@ FORCE=false UNSTABLE=false PYTHON="python" + user_check() { if [ $(id -u) -eq 0 ]; then printf "Script should not be run as root. Try './install.sh'\n" @@ -55,19 +57,35 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } +find_config() { + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + CONFIG_DIR="/boot" + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE"]; then + warning "Could not find $CONFIG_FILE!" + exit 1 + fi + else + if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then + warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" + warning "You might want to fix this!" + fi + fi + inform "Using $CONFIG_FILE in $CONFIG_DIR" +} + venv_bash_snippet() { if [ ! -f $VENV_BASH_SNIPPET ]; then cat << EOF > $VENV_BASH_SNIPPET -# Add \`source $RESOURCES_TOP_DIR/auto_venv.sh\` to your ~/.bashrc to activate +# Add `source $RESOURCES_DIR/auto_venv.sh` to your ~/.bashrc to activate # the Pimoroni virtual environment automagically! -PY_ENV_DIR=~/Pimoroni/venv -if [ ! -f \$PY_ENV_DIR/bin/activate ]; then - printf "Creating user Python environment in \$PY_ENV_DIR, please wait...\n" - mkdir -p \$PY_ENV_DIR - python3 -m venv --system-site-packages --prompt Pimoroni \$PY_ENV_DIR +VENV_DIR="$VENV_DIR" +if [ ! -f \$VENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$VENV_DIR, please wait...\n" + mkdir -p \$VENV_DIR + python3 -m venv --system-site-packages \$VENV_DIR fi printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" -source \$PY_ENV_DIR/bin/activate +source \$VENV_DIR/bin/activate EOF fi } @@ -80,7 +98,7 @@ venv_check() { if [ ! -f $VENV_DIR/bin/activate ]; then inform "Creating virtual Python environment in $VENV_DIR, please wait...\n" mkdir -p $VENV_DIR - /usr/bin/python3 -m venv $VENV_DIR --system-site-packages --prompt Pimoroni + /usr/bin/python3 -m venv $VENV_DIR --system-site-packages venv_bash_snippet else inform "Found existing virtual Python environment in $VENV_DIR\n" @@ -99,12 +117,12 @@ function do_config_backup { if [ ! $CONFIG_BACKUP == true ]; then CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" - inform "Backing up $CONFIG to /boot/$FILENAME\n" - sudo cp $CONFIG /boot/$FILENAME + inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" + sudo cp $CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME + cp $CONFIG_DIR/$CONFIG_FILE $RESOURCES_TOP_DIR/config-backups/$FILENAME if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> $UNINSTALLER fi fi } @@ -185,7 +203,7 @@ pip_pkg_install toml CONFIG_VARS=`$PYTHON - < Date: Tue, 21 Nov 2023 16:01:41 +0000 Subject: [PATCH 099/122] Fix VENV_DIR path. --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 38f19e9a..9f11aca9 100755 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false RESOURCES_TOP_DIR=$HOME/Pimoroni -VENV_BASH_SNIPPET=$RESOURCES_DIR/auto_venv.sh +VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh VENV_DIR=$HOME/.virtualenvs/pimoroni WD=`pwd` USAGE="./install.sh (--unstable)" @@ -76,7 +76,7 @@ find_config() { venv_bash_snippet() { if [ ! -f $VENV_BASH_SNIPPET ]; then cat << EOF > $VENV_BASH_SNIPPET -# Add `source $RESOURCES_DIR/auto_venv.sh` to your ~/.bashrc to activate +# Add `source $VENV_BASH_SNIPPET` to your ~/.bashrc to activate # the Pimoroni virtual environment automagically! VENV_DIR="$VENV_DIR" if [ ! -f \$VENV_DIR/bin/activate ]; then From f1508a4939279be7c74efc666b32970b76a8f4f8 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 22 Nov 2023 10:42:39 +0000 Subject: [PATCH 100/122] Fix auto_venv.sh creation. --- install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 9f11aca9..6c9b4742 100755 --- a/install.sh +++ b/install.sh @@ -74,9 +74,11 @@ find_config() { } venv_bash_snippet() { + inform "Checking for $VENV_BASH_SNIPPET\n" if [ ! -f $VENV_BASH_SNIPPET ]; then + inform "Creating $VENV_BASH_SNIPPET\n" cat << EOF > $VENV_BASH_SNIPPET -# Add `source $VENV_BASH_SNIPPET` to your ~/.bashrc to activate +# Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate # the Pimoroni virtual environment automagically! VENV_DIR="$VENV_DIR" if [ ! -f \$VENV_DIR/bin/activate ]; then From a1f67c9f9f0856626b25869a19508fef64753451 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 22 Nov 2023 17:15:30 +0000 Subject: [PATCH 101/122] examples/weather-and-light.py: fix bug with vertical label spacing. --- examples/weather-and-light.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index efbd2a88..fcfb5338 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -162,11 +162,6 @@ def draw_background(progress, period, day): return composite -def text_width(font, text): - x1, y1, x2, y2 = font.getbbox(text) - return x2 - x1 - - def text_size(font, text): x1, y1, x2, y2 = font.getbbox(text) return x2 - x1, y2 - y1 @@ -384,7 +379,8 @@ def describe_light(light): temp_string = f"{corr_temperature:.0f}°C" img = overlay_text(img, (68, 18), temp_string, font_lg, align_right=True) - spacing = text_width(font_lg, temp_string) + 1 + _, text_height = text_size(font_lg, temp_string) + spacing = text_height + 1 if min_temp is not None and max_temp is not None: range_string = f"{min_temp:.0f}-{max_temp:.0f}" else: @@ -398,7 +394,8 @@ def describe_light(light): 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 = text_width(font_lg, humidity_string) + 1 + _, text_height = text_size(font_lg, humidity_string) + spacing = text_height + 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") @@ -408,7 +405,8 @@ def describe_light(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 = text_width(font_lg, light_string.replace(",", "")) + 1 + _, text_height = text_size(font_lg, light_string.replace(",", "")) + spacing = text_height + 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") @@ -421,7 +419,8 @@ def describe_light(light): 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 = text_width(font_lg, pressure_string.replace(",", "")) + 1 + _, text_height = text_size(font_lg, pressure_string.replace(",", "")) + spacing = text_height + 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) From e6cd19c57b210000b3fb01e6e4b133a57d16c042 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 23 Nov 2023 10:18:07 +0000 Subject: [PATCH 102/122] install.sh: rework for better error reporting and fix some bugs. Fix a bug where auto_venv.sh was being created in a non-existent directory. Trap exit codes for some commands and add some help text + GitHUb url at the end of the install process. Try to comment what some sections do, and insert linebreaks so they are more logically broken up in the installer output. Try to be more consistent with colours. Try to be more friendly with colours- remove full red warning text in favour of a prefix so the errors/warnings are easier to read. Return a failure exit code if bits of the script have failed. Try to re-order output so it's more logical. Re-word venv creation message. --- install.sh | 144 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 42 deletions(-) diff --git a/install.sh b/install.sh index 6c9b4742..d42c48d0 100755 --- a/install.sh +++ b/install.sh @@ -5,21 +5,21 @@ CONFIG_DIR="/boot/firmware" DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false -RESOURCES_TOP_DIR=$HOME/Pimoroni -VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh -VENV_DIR=$HOME/.virtualenvs/pimoroni +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" WD=`pwd` USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() FORCE=false UNSTABLE=false PYTHON="python" +CMD_ERRORS=false user_check() { if [ $(id -u) -eq 0 ]; then - printf "Script should not be run as root. Try './install.sh'\n" - exit 1 + fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -54,29 +54,34 @@ inform() { } warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" + echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" +} + +fatal() { + echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" + exit 1 } find_config() { if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then CONFIG_DIR="/boot" - if [ ! -f "$CONFIG_DIR/$CONFIG_FILE"]; then - warning "Could not find $CONFIG_FILE!" - exit 1 + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + fatal "Could not find $CONFIG_FILE!" + fi + else + if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then + warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" + warning "You might want to fix this!" fi - else - if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then - warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" - warning "You might want to fix this!" - fi fi - inform "Using $CONFIG_FILE in $CONFIG_DIR" + inform "Using $CONFIG_FILE in $CONFIG_DIR" } venv_bash_snippet() { - inform "Checking for $VENV_BASH_SNIPPET\n" + inform "Checking for $VENV_BASH_SNIPPET\n" if [ ! -f $VENV_BASH_SNIPPET ]; then - inform "Creating $VENV_BASH_SNIPPET\n" + inform "Creating $VENV_BASH_SNIPPET\n" + mkdir -p $RESOURCES_TOP_DIR cat << EOF > $VENV_BASH_SNIPPET # Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate # the Pimoroni virtual environment automagically! @@ -96,23 +101,32 @@ venv_check() { PYTHON_BIN=`which $PYTHON` if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then printf "This script should be run in a virtual Python environment.\n" - if confirm "Would you like us to create one for you?"; then + if confirm "Would you like us to create and/or use a default one?"; then + printf "\n" if [ ! -f $VENV_DIR/bin/activate ]; then - inform "Creating virtual Python environment in $VENV_DIR, please wait...\n" + inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" mkdir -p $VENV_DIR /usr/bin/python3 -m venv $VENV_DIR --system-site-packages venv_bash_snippet + source $VENV_DIR/bin/activate else - inform "Found existing virtual Python environment in $VENV_DIR\n" + inform "Activating existing virtual Python environment in $VENV_DIR\n" + printf "source $VENV_DIR/bin/activate\n" + source $VENV_DIR/bin/activate fi - inform "Activating virtual Python environment in $VENV_DIR..." - inform "source $VENV_DIR/bin/activate\n" - source $VENV_DIR/bin/activate - else - exit 1 + printf "\n" + fatal "Please create and/or activate a virtual Python environment and try again!\n" fi fi + printf "\n" +} + +check_for_error() { + if [ $? -ne 0 ]; then + CMD_ERRORS=true + warning "^^^ 😬" + fi } function do_config_backup { @@ -132,6 +146,7 @@ function do_config_backup { function apt_pkg_install { PACKAGES=() PACKAGES_IN=("$@") + # Check the list of packages and only run update/install if we need to for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi @@ -143,12 +158,14 @@ function apt_pkg_install { done PACKAGES="${PACKAGES[@]}" if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" + printf "\n" + inform "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then sudo apt update APT_HAS_UPDATED=true fi sudo apt install -y $PACKAGES + check_for_error if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER fi @@ -156,7 +173,9 @@ function apt_pkg_install { } function pip_pkg_install { + # A null Keyring prevents pip stalling in the background PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" + check_for_error } while [[ $# -gt 0 ]]; do @@ -186,30 +205,33 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing $LIBRARY_NAME...\n\n" + user_check venv_check if [ ! -f `which $PYTHON` ]; then - printf "Python path $PYTHON not found!\n" - exit 1 + fatal "Python path $PYTHON not found!\n" fi PYTHON_VER=`$PYTHON --version` -printf "$LIBRARY_NAME Python Library: Installer\n\n" - inform "Checking Dependencies. Please wait..." +# Install toml and try to read pyproject.toml into bash variables + pip_pkg_install toml CONFIG_VARS=`$PYTHON - < $UNINSTALLER printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" @@ -244,19 +270,23 @@ exit 1 source $VIRTUAL_ENV/bin/activate EOF -if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" -else - printf "Installing stable library from pypi.\n\n" -fi +printf "\n" inform "Installing for $PYTHON_VER...\n" + +# Install apt packages from pyproject.toml / tool.pimoroni.apt_packages apt_pkg_install "${APT_PACKAGES[@]}" + +printf "\n" + if $UNSTABLE; then + warning "Installing unstable library from source.\n" pip_pkg_install . else + inform "Installing stable library from pypi.\n" pip_pkg_install $LIBRARY_NAME fi + if [ $? -eq 0 ]; then success "Done!\n" echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER @@ -266,6 +296,8 @@ cd $WD find_config +# Run the setup commands from pyproject.toml / tool.pimoroni.commands + for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" # Attempt to catch anything that touches config.txt and trigger a backup @@ -273,13 +305,18 @@ for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do do_config_backup fi eval $CMD + check_for_error done +printf "\n" + +# Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt + for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do CONFIG_LINE="${CONFIG_TXT[$i]}" if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup - inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE\n" + inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE @@ -287,6 +324,10 @@ for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do fi done +printf "\n" + +# Just a straight copy of the examples/ dir into ~/Pimoroni/board/examples + if [ -d "examples" ]; then if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then inform "Copying examples to $RESOURCES_DIR" @@ -298,9 +339,12 @@ fi printf "\n" +# Use pdoc to generate basic documentation from the installed module + if confirm "Would you like to generate documentation?"; then + inform "Installing pdoc. Please wait..." pip_pkg_install pdoc - printf "Generating documentation.\n" + inform "Generating documentation.\n" $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null if [ $? -eq 0 ]; then inform "Documentation saved to $RESOURCES_DIR/docs" @@ -310,6 +354,22 @@ if confirm "Would you like to generate documentation?"; then fi fi -success "\nAll done!" -inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" -inform "Find uninstall steps in $UNINSTALLER\n" +printf "\n" + +if [ "$CMD_ERRORS" = true ]; then + warning "One or more setup commands appear to have failed." + printf "This might prevent things from working properly.\n" + printf "Make sure your OS is up to date and try re-running this installer.\n" + printf "If things still don't work, report this or find help at $GITHUB_URL.\n\n" +else + success "\nAll done!" +fi + +printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" +printf "Find uninstall steps in $UNINSTALLER\n\n" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi \ No newline at end of file From 8b1ab0afc6caa393004d494c927452b933752011 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 10 Jan 2024 14:29:50 +0000 Subject: [PATCH 103/122] Sync with latest boilerplate. * CI: Update GitHub Actions versions. * QA: Add shellcheck and fix/ignore all issues. * install.sh: slightly better feedback for setup commands. --- .github/workflows/build.yml | 6 +- .github/workflows/qa.yml | 9 ++- .github/workflows/test.yml | 2 +- Makefile | 5 +- check.sh | 20 +++--- install.sh | 119 ++++++++++++++++++------------------ uninstall.sh | 14 ++--- 7 files changed, 87 insertions(+), 88 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87200efb..07620e34 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -35,7 +35,7 @@ jobs: make build - name: Upload Packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.RELEASE_FILE }} path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 4f858832..ac672a52 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -10,16 +10,15 @@ jobs: test: name: linting & spelling runs-on: ubuntu-latest - env: TERM: xterm-256color steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python '3,11' - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.11' @@ -34,3 +33,7 @@ jobs: - name: Run Code Checks run: | make check + + - name: Run Bash Code Checks + run: | + make shellcheck diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 016a6780..6f8cff73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/Makefile b/Makefile index 9e0c15c5..34f4a7dd 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,14 @@ uninstall: dev-deps: python3 -m pip install -r requirements-dev.txt - sudo apt install dos2unix + sudo apt install dos2unix shellcheck check: @bash check.sh +shellcheck: + shellcheck *.sh + qa: tox -e qa diff --git a/check.sh b/check.sh index 4395d89c..38dfc3a1 100755 --- a/check.sh +++ b/check.sh @@ -3,9 +3,9 @@ # This script handles some basic QA checks on the source NOPOST=$1 -LIBRARY_NAME=`hatch project metadata name` -LIBRARY_VERSION=`hatch version | awk -F "." '{print $1"."$2"."$3}'` -POST_VERSION=`hatch version | awk -F "." '{print substr($4,0,length($4))}'` +LIBRARY_NAME=$(hatch project metadata name) +LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') +POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') TERM=${TERM:="xterm-256color"} success() { @@ -29,7 +29,7 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; + printf "Unrecognised option: %s\n" "$1"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -40,8 +40,7 @@ done inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" inform "Checking for trailing whitespace..." -grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO -if [[ $? -eq 0 ]]; then +if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then warning "Trailing whitespace found!" exit 1 else @@ -50,8 +49,7 @@ fi printf "\n" inform "Checking for DOS line-endings..." -grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile -if [[ $? -eq 0 ]]; then +if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then warning "DOS line-endings found!" exit 1 else @@ -60,8 +58,7 @@ fi printf "\n" inform "Checking CHANGELOG.md..." -cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 -if [[ $? -eq 1 ]]; then +if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." exit 1 else @@ -70,8 +67,7 @@ fi printf "\n" inform "Checking for git tag ${LIBRARY_VERSION}..." -git tag -l | grep -E "${LIBRARY_VERSION}$" -if [[ $? -eq 1 ]]; then +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then warning "Missing git tag for version ${LIBRARY_VERSION}" fi printf "\n" diff --git a/install.sh b/install.sh index d42c48d0..059d3c46 100755 --- a/install.sh +++ b/install.sh @@ -1,14 +1,13 @@ #!/bin/bash -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') CONFIG_FILE=config.txt CONFIG_DIR="/boot/firmware" -DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") CONFIG_BACKUP=false APT_HAS_UPDATED=false RESOURCES_TOP_DIR="$HOME/Pimoroni" VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" VENV_DIR="$HOME/.virtualenvs/pimoroni" -WD=`pwd` USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() FORCE=false @@ -18,7 +17,7 @@ CMD_ERRORS=false user_check() { - if [ $(id -u) -eq 0 ]; then + if [ "$(id -u)" -eq 0 ]; then fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -36,15 +35,6 @@ confirm() { fi } -prompt() { - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi -} - success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" } @@ -79,10 +69,10 @@ find_config() { venv_bash_snippet() { inform "Checking for $VENV_BASH_SNIPPET\n" - if [ ! -f $VENV_BASH_SNIPPET ]; then + if [ ! -f "$VENV_BASH_SNIPPET" ]; then inform "Creating $VENV_BASH_SNIPPET\n" - mkdir -p $RESOURCES_TOP_DIR - cat << EOF > $VENV_BASH_SNIPPET + mkdir -p "$RESOURCES_TOP_DIR" + cat << EOF > "$VENV_BASH_SNIPPET" # Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate # the Pimoroni virtual environment automagically! VENV_DIR="$VENV_DIR" @@ -98,21 +88,23 @@ EOF } venv_check() { - PYTHON_BIN=`which $PYTHON` + PYTHON_BIN=$(which "$PYTHON") if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then printf "This script should be run in a virtual Python environment.\n" if confirm "Would you like us to create and/or use a default one?"; then printf "\n" - if [ ! -f $VENV_DIR/bin/activate ]; then + if [ ! -f "$VENV_DIR/bin/activate" ]; then inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" - mkdir -p $VENV_DIR - /usr/bin/python3 -m venv $VENV_DIR --system-site-packages + mkdir -p "$VENV_DIR" + /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages venv_bash_snippet - source $VENV_DIR/bin/activate + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" else inform "Activating existing virtual Python environment in $VENV_DIR\n" - printf "source $VENV_DIR/bin/activate\n" - source $VENV_DIR/bin/activate + printf "source \"%s/bin/activate\"\n" "$VENV_DIR" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" fi else printf "\n" @@ -125,7 +117,7 @@ venv_check() { check_for_error() { if [ $? -ne 0 ]; then CMD_ERRORS=true - warning "^^^ 😬" + warning "^^^ 😬 previous command did not exit cleanly!" fi } @@ -134,29 +126,29 @@ function do_config_backup { CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" - sudo cp $CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG_DIR/$CONFIG_FILE $RESOURCES_TOP_DIR/config-backups/$FILENAME + sudo cp "$CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME" + mkdir -p "$RESOURCES_TOP_DIR/config-backups/" + cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> $UNINSTALLER + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" fi fi } function apt_pkg_install { - PACKAGES=() + PACKAGES_NEEDED=() PACKAGES_IN=("$@") # Check the list of packages and only run update/install if we need to for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi - printf "Checking for $PACKAGE\n" - dpkg -L $PACKAGE > /dev/null 2>&1 + printf "Checking for %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 if [ "$?" == "1" ]; then - PACKAGES+=("$PACKAGE") + PACKAGES_NEEDED+=("$PACKAGE") fi done - PACKAGES="${PACKAGES[@]}" + PACKAGES="${PACKAGES_NEEDED[*]}" if ! [ "$PACKAGES" == "" ]; then printf "\n" inform "Installing missing packages: $PACKAGES" @@ -164,10 +156,10 @@ function apt_pkg_install { sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y $PACKAGES + sudo apt install -y "$PACKAGES" check_for_error if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER + echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" fi fi } @@ -196,8 +188,8 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; - printf "Usage: $USAGE\n"; + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -205,16 +197,16 @@ while [[ $# -gt 0 ]]; do esac done -printf "Installing $LIBRARY_NAME...\n\n" +printf "Installing %s...\n\n" "$LIBRARY_NAME" user_check venv_check -if [ ! -f `which $PYTHON` ]; then - fatal "Python path $PYTHON not found!\n" +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" fi -PYTHON_VER=`$PYTHON --version` +PYTHON_VER=$($PYTHON --version) inform "Checking Dependencies. Please wait..." @@ -222,7 +214,8 @@ inform "Checking Dependencies. Please wait..." pip_pkg_install toml -CONFIG_VARS=`$PYTHON - < $UNINSTALLER +cat << EOF > "$UNINSTALLER" printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" @@ -284,27 +279,30 @@ if $UNSTABLE; then pip_pkg_install . else inform "Installing stable library from pypi.\n" - pip_pkg_install $LIBRARY_NAME + pip_pkg_install "$LIBRARY_NAME" fi +# shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag if [ $? -eq 0 ]; then success "Done!\n" - echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" fi -cd $WD - find_config +printf "\n" + # Run the setup commands from pyproject.toml / tool.pimoroni.commands +inform "Running setup commands...\n" for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" # Attempt to catch anything that touches config.txt and trigger a backup if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi - eval $CMD + printf "\"%s\"\n" "$CMD" + eval "$CMD" check_for_error done @@ -319,7 +317,7 @@ for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then - printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE + printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE fi fi done @@ -331,8 +329,8 @@ printf "\n" if [ -d "examples" ]; then if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then inform "Copying examples to $RESOURCES_DIR" - cp -r examples/ $RESOURCES_DIR - echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER + cp -r examples/ "$RESOURCES_DIR" + echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" success "Done!" fi fi @@ -345,8 +343,7 @@ if confirm "Would you like to generate documentation?"; then inform "Installing pdoc. Please wait..." pip_pkg_install pdoc inform "Generating documentation.\n" - $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null - if [ $? -eq 0 ]; then + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" else @@ -360,16 +357,16 @@ if [ "$CMD_ERRORS" = true ]; then warning "One or more setup commands appear to have failed." printf "This might prevent things from working properly.\n" printf "Make sure your OS is up to date and try re-running this installer.\n" - printf "If things still don't work, report this or find help at $GITHUB_URL.\n\n" + printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" else success "\nAll done!" fi printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" -printf "Find uninstall steps in $UNINSTALLER\n\n" +printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" if [ "$CMD_ERRORS" = true ]; then exit 1 else exit 0 -fi \ No newline at end of file +fi diff --git a/uninstall.sh b/uninstall.sh index f213fc52..3314b7fc 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,13 +1,13 @@ #!/bin/bash FORCE=false -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME PYTHON="python" venv_check() { - PYTHON_BIN=`which $PYTHON` + PYTHON_BIN=$(which $PYTHON) if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then printf "This script should be run in a virtual Python environment.\n" exit 1 @@ -15,7 +15,7 @@ venv_check() { } user_check() { - if [ $(id -u) -eq 0 ]; then + if [ "$(id -u)" -eq 0 ]; then printf "Script should not be run as root. Try './uninstall.sh'\n" exit 1 fi @@ -55,17 +55,17 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } -printf "$LIBRARY_NAME Python Library: Uninstaller\n\n" +printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" user_check venv_check printf "Uninstalling for Python 3...\n" -$PYTHON -m pip uninstall $LIBRARY_NAME +$PYTHON -m pip uninstall "$LIBRARY_NAME" -if [ -d $RESOURCES_DIR ]; then +if [ -d "$RESOURCES_DIR" ]; then if confirm "Would you like to delete $RESOURCES_DIR?"; then - rm -r $RESOURCES_DIR + rm -r "$RESOURCES_DIR" fi fi From 97c824a23e9dd1245b7a4f94f5e1a473129dba77 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 10 Jan 2024 14:52:23 +0000 Subject: [PATCH 104/122] Sync with latest boilerplate. * install.sh: fix quoting bug in do_config_backup. * install.sh: don't output printf commands. --- install.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 059d3c46..bb671f29 100755 --- a/install.sh +++ b/install.sh @@ -126,7 +126,7 @@ function do_config_backup { CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" - sudo cp "$CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME" + sudo cp "$CONFIG_DIR/$CONFIG_FILE" "$CONFIG_DIR/$FILENAME" mkdir -p "$RESOURCES_TOP_DIR/config-backups/" cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" if [ -f "$UNINSTALLER" ]; then @@ -301,7 +301,9 @@ for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi - printf "\"%s\"\n" "$CMD" + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + fi eval "$CMD" check_for_error done From 34c3efaa0e538e2e9d753474a0dfd28b94ef1df2 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 17 Apr 2024 09:21:59 +0100 Subject: [PATCH 105/122] Standardise on GPIOn pin labels. As of https://github.com/raspberrypi/linux/commit/bd9542b8271ccb251490c814e9237ed94d7ceb56 all downstream GPIO line names use the form GPIOn, and PIN_n is deprecated. Simplify code and examples to reflect this. --- enviroplus/gas.py | 8 ++------ examples/all-in-one-enviro-mini.py | 4 ++-- examples/all-in-one-no-pm.py | 4 ++-- examples/all-in-one.py | 4 ++-- examples/combined.py | 4 ++-- examples/lcd.py | 4 ++-- examples/mqtt-all.py | 4 ++-- examples/noise-amps-at-freqs.py | 4 ++-- examples/noise-profile.py | 4 ++-- examples/sensorcommunity.py | 4 ++-- examples/sensorcommunity_combined.py | 4 ++-- examples/weather-and-light.py | 4 ++-- tests/conftest.py | 1 + 13 files changed, 25 insertions(+), 28 deletions(-) diff --git a/enviroplus/gas.py b/enviroplus/gas.py index e1c4b76d..3583428a 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -11,11 +11,7 @@ MICS6814_GAIN = 6.144 OUTH = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE) -PLATFORMS = { - "Radxa ROCK 5B": {"heater": ("PIN_18", OUTH)}, - "Raspberry Pi 5": {"heater": ("PIN18", OUTH)}, - "Raspberry Pi 4": {"heater": ("GPIO24", OUTH)} -} + ads1015.I2C_ADDRESS_DEFAULT = ads1015.I2C_ADDRESS_ALTERNATE _is_setup = False @@ -68,7 +64,7 @@ def setup(): else: adc.set_sample_rate(1600) - _heater = gpiodevice.get_pins_for_platform(PLATFORMS)[0] + _heater = gpiodevice.get_pin("GPIO24") atexit.register(cleanup) diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py index 01491f37..4aea5c4d 100755 --- a/examples/all-in-one-enviro-mini.py +++ b/examples/all-in-one-enviro-mini.py @@ -37,8 +37,8 @@ st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index c2d9078d..c0172054 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -38,8 +38,8 @@ st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 9b7121ea..b558f486 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -44,8 +44,8 @@ st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/combined.py b/examples/combined.py index eaab10f3..79e5d621 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -47,8 +47,8 @@ st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/lcd.py b/examples/lcd.py index cb2af2f3..97c2be44 100755 --- a/examples/lcd.py +++ b/examples/lcd.py @@ -21,8 +21,8 @@ disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 7238d3b0..ecab4a35 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -234,8 +234,8 @@ def main(): disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index 825f53cf..957511b4 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -18,8 +18,8 @@ disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/noise-profile.py b/examples/noise-profile.py index 17ada829..be6a1855 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -16,8 +16,8 @@ disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/sensorcommunity.py b/examples/sensorcommunity.py index 7128ad72..cdd3a480 100755 --- a/examples/sensorcommunity.py +++ b/examples/sensorcommunity.py @@ -40,8 +40,8 @@ disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/sensorcommunity_combined.py b/examples/sensorcommunity_combined.py index 8dac7232..a7aeb7b8 100644 --- a/examples/sensorcommunity_combined.py +++ b/examples/sensorcommunity_combined.py @@ -153,8 +153,8 @@ def check_wifi(): st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index fcfb5338..04f1bd8f 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -291,8 +291,8 @@ def describe_light(light): disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/tests/conftest.py b/tests/conftest.py index 6ad5557c..20237430 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,6 +45,7 @@ def gpiod(): def gpiodevice(): gpiodevice = mock.Mock() gpiodevice.get_pins_for_platform.return_value = [(mock.Mock(), 0)] + gpiodevice.get_pin.return_value = (mock.Mock(), 0) sys.modules["gpiodevice"] = gpiodevice yield gpiodevice From b7fad2415edcd667b6efb190298334a4b8347251 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 17 Apr 2024 10:01:25 +0100 Subject: [PATCH 106/122] Prep for v1.0.0. --- CHANGELOG.md | 5 +++++ enviroplus/__init__.py | 2 +- pyproject.toml | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f175aa..67206be0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1.0.0 +----- + +* BREAKING: Port to gpiod/gpiodevice for Pi 5/Bookworm. + 0.0.6 ----- diff --git a/enviroplus/__init__.py b/enviroplus/__init__.py index 034f46c3..5becc17c 100644 --- a/enviroplus/__init__.py +++ b/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = "0.0.6" +__version__ = "1.0.0" diff --git a/pyproject.toml b/pyproject.toml index a6a4e6b1..d97b4171 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,8 @@ classifiers = [ "Topic :: System :: Hardware", ] dependencies = [ + "gpiod >= 2.1.3", + "gpiodevice >= 0.0.3", "pimoroni-bme280 >= 1.0.0", "pms5003 >= 1.0.0", "ltr559 >= 1.0.0", From 6391486dcadb32c0bdf4f7d9547bea32219cd20a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 17 Apr 2024 09:37:23 +0100 Subject: [PATCH 107/122] install.sh: drop quotes around apt packages. Quotes would cause a list of packages to be treated as a single package and lookup would fail. Reported-by: thirdr --- install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index bb671f29..495919b4 100755 --- a/install.sh +++ b/install.sh @@ -156,7 +156,8 @@ function apt_pkg_install { sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y "$PACKAGES" + # shellcheck disable=SC2086 + sudo apt install -y $PACKAGES check_for_error if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" From 65c7e9da80521ef4d42a9cc7ba0cd99f7a086635 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 17 Apr 2024 11:16:46 +0100 Subject: [PATCH 108/122] README.md: Absolute image URLs for PyPI. --- CHANGELOG.md | 5 +++++ README.md | 4 ++-- enviroplus/__init__.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67206be0..d28efc61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1.0.1 +----------- + +* README.md: Fix images + 1.0.0 ----- diff --git a/README.md b/README.md index 72e0597a..45436ff4 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ You are best using the "One-line" install method if you want all of the UART ser **Note** The code in this repository supports both the Enviro+ and Enviro Mini boards. _The Enviro Mini board does not have the Gas sensor or the breakout for the PM sensor._ -![Enviro Plus pHAT](./Enviro-Plus-pHAT.jpg) -![Enviro Mini pHAT](./Enviro-mini-pHAT.jpg) +![Enviro Plus pHAT](https://raw.githubusercontent.com/pimoroni/enviroplus-python/main/Enviro-Plus-pHAT.jpg) +![Enviro Mini pHAT](https://raw.githubusercontent.com/pimoroni/enviroplus-python/main/Enviro-mini-pHAT.jpg) :warning: This library now supports Python 3 only, Python 2 is EOL - https://www.python.org/doc/sunset-python-2/ diff --git a/enviroplus/__init__.py b/enviroplus/__init__.py index 5becc17c..5c4105cd 100644 --- a/enviroplus/__init__.py +++ b/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.0.1" From 288b64ca7c87a1127f5a757405a56497fe164f96 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 22 Apr 2024 11:31:20 +0100 Subject: [PATCH 109/122] Install: Enable serial. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d97b4171..25179f4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,5 +141,5 @@ commands = [ "sudo raspi-config nonint do_i2c 0", "printf \"Setting up serial for PMS5003..\\n\"", "sudo raspi-config nonint do_serial_cons 1", - "sudo raspi-config nonint do_serial_hw 1" + "sudo raspi-config nonint do_serial_hw 0" ] From 7ea2264a188042a180094ba14279bfd47315eb7a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 22 Apr 2024 11:31:31 +0100 Subject: [PATCH 110/122] README.md: Update install instructions. --- README.md | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 45436ff4..83098815 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ Designed for environmental monitoring, Enviro+ lets you measure air quality (pollutant gases and particulates), temperature, pressure, humidity, light, and noise level. Learn more - https://shop.pimoroni.com/products/enviro-plus -[![Build Status](https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master)](https://travis-ci.com/pimoroni/enviroplus-python) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/enviroplus-python?branch=master) +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/enviroplus-python/test.yml?branch=main)](https://github.com/pimoroni/enviroplus-python/actions/workflows/test.yml) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/enviroplus-python?branch=main) [![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) @@ -18,25 +18,24 @@ You are best using the "One-line" install method if you want all of the UART ser :warning: This library now supports Python 3 only, Python 2 is EOL - https://www.python.org/doc/sunset-python-2/ -## One-line (Installs from GitHub) - -```bash -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: +## Install and configure dependencies from GitHub: * `git clone https://github.com/pimoroni/enviroplus-python` * `cd enviroplus-python` -* `sudo ./install.sh` +* `./install.sh` **Note** Raspbian/Raspberry Pi OS Lite users may first need to install git: `sudo apt install git` ## Or... Install from PyPi and configure manually: -* Run `sudo python3 -m pip install enviroplus` +* `python3 -m venv --system-site-packages $HOME/.virtualenvs/pimoroni` +* Run `python3 -m pip install enviroplus` + +And install additional dependencies: + +```bash +sudo apt install python3-numpy python3-smbus python3-pil python3-setuptools +``` **Note** this will not perform any of the required configuration changes on your Pi, you may additionally need to: @@ -45,15 +44,17 @@ curl -sSL https://get.pimoroni.com/enviroplus | bash 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` +### Bookworm + +* Enable serial: `raspi-config nonint do_serial_hw 0` +* Disable serial terminal: `raspi-config nonint do_serial_cons 1` * Add `dtoverlay=pi3-miniuart-bt` to your `/boot/config.txt` -And install additional dependencies: +### Bullseye -```bash -sudo apt install python3-numpy python3-smbus python3-pil python3-setuptools -``` +* 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` ## Alternate Software & User Projects From 345d75da1be9ed450d2d390b7a9d93125aca4114 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 23 Apr 2024 10:24:39 +0100 Subject: [PATCH 111/122] Gas: Fix gpiodevice.get_pin. --- enviroplus/gas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enviroplus/gas.py b/enviroplus/gas.py index 3583428a..1e78010f 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -64,7 +64,7 @@ def setup(): else: adc.set_sample_rate(1600) - _heater = gpiodevice.get_pin("GPIO24") + _heater = gpiodevice.get_pin("GPIO24", "EnviroPlus", OUTH) atexit.register(cleanup) From b2a076a05d551fa7ea5e261224ea6da7f5ed85f3 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 23 Apr 2024 10:27:30 +0100 Subject: [PATCH 112/122] README.md: Add note about enabling venv. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 83098815..c3e492de 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,12 @@ You are best using the "One-line" install method if you want all of the UART ser * `cd enviroplus-python` * `./install.sh` +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: + +``` +source ~/.virtualenvs/pimoroni/bin/activate +``` + **Note** Raspbian/Raspberry Pi OS Lite users may first need to install git: `sudo apt install git` ## Or... Install from PyPi and configure manually: From cbfd4418836e03fce4a8711cd909863414340de0 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 23 Apr 2024 11:18:33 +0100 Subject: [PATCH 113/122] Prep for v1.0.2. --- CHANGELOG.md | 9 ++++++++- enviroplus/__init__.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d28efc61..47cb04d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ +1.0.2 +----- + +* README.md: Update install instructions +* Fix installer to enable serial +* Fix gas sensor heater pin + 1.0.1 ------------ +----- * README.md: Fix images diff --git a/enviroplus/__init__.py b/enviroplus/__init__.py index 5c4105cd..7863915f 100644 --- a/enviroplus/__init__.py +++ b/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = "1.0.1" +__version__ = "1.0.2" From 7972ca33859bd516a49393be2b50ae36505b2ac3 Mon Sep 17 00:00:00 2001 From: Hel Gibbons <50950368+helgibbons@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:35:11 +0100 Subject: [PATCH 114/122] Update README.md Remove outdated mention of one line installer --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c3e492de..d76de497 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ Designed for environmental monitoring, Enviro+ lets you measure air quality (pol # Installing -You are 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. - **Note** The code in this repository supports both the Enviro+ and Enviro Mini boards. _The Enviro Mini board does not have the Gas sensor or the breakout for the PM sensor._ ![Enviro Plus pHAT](https://raw.githubusercontent.com/pimoroni/enviroplus-python/main/Enviro-Plus-pHAT.jpg) From d283e14dd7608f1f92a43456c21214887f97b53f Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 24 Apr 2024 15:15:48 +0100 Subject: [PATCH 115/122] Bump PMS5003 to v1.0.1. Includes better support for Pi devices, defaults to Pi-compatible pins and includes support for custom GPIO pins. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 25179f4d..7c565bb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "gpiod >= 2.1.3", "gpiodevice >= 0.0.3", "pimoroni-bme280 >= 1.0.0", - "pms5003 >= 1.0.0", + "pms5003 >= 1.0.1", "ltr559 >= 1.0.0", "st7735 >= 1.0.0", "ads1015 >= 1.0.0", From 655f9f39b865b799e78afff5ebdbce172d1eaf0b Mon Sep 17 00:00:00 2001 From: idotj Date: Thu, 23 May 2024 10:49:34 +0200 Subject: [PATCH 116/122] Fix path for Bookworm Fix path for config.txt changes in Bookworm --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d76de497..b2033326 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ And if you're using a PMS5003 sensor you will need to: * Enable serial: `raspi-config nonint do_serial_hw 0` * Disable serial terminal: `raspi-config nonint do_serial_cons 1` -* Add `dtoverlay=pi3-miniuart-bt` to your `/boot/config.txt` +* Add `dtoverlay=pi3-miniuart-bt` to your `/boot/firmware/config.txt` ### Bullseye From 72267be957c4fe21cf46b420922bf06282dac9fe Mon Sep 17 00:00:00 2001 From: Alexandre Esse Date: Tue, 11 Jun 2024 01:21:41 +0200 Subject: [PATCH 117/122] examples: add shebang Signed-off-by: Alexandre Esse --- examples/noise-amps-at-freqs.py | 2 ++ examples/noise-profile.py | 2 ++ examples/sensorcommunity_combined.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index 957511b4..75497df2 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import st7735 from PIL import Image, ImageDraw diff --git a/examples/noise-profile.py b/examples/noise-profile.py index be6a1855..480815e6 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import st7735 from PIL import Image, ImageDraw diff --git a/examples/sensorcommunity_combined.py b/examples/sensorcommunity_combined.py index a7aeb7b8..bb9869d6 100644 --- a/examples/sensorcommunity_combined.py +++ b/examples/sensorcommunity_combined.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import colorsys import logging import time From b6fd390e03843d16d20970a331635c382f1176e5 Mon Sep 17 00:00:00 2001 From: Alexandre Esse Date: Tue, 11 Jun 2024 03:15:48 +0200 Subject: [PATCH 118/122] mqtt-all: fix display box The previous display box function wasn't properly used. This fix the information displayed on the enviro+ display. Signed-off-by: Alexandre Esse --- examples/mqtt-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index ecab4a35..db4cf875 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -136,7 +136,7 @@ def display_status(disp, mqtt_broker): message = f"{device_serial_number}\nWi-Fi: {wifi_status}\nmqtt-broker: {mqtt_broker}" img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) - x1, y1, x2, y2 = font.getbbox(message) + x1, y1, x2, y2 = draw.textbbox((0,0), message, font=font) size_x = x2 - x1 size_y = y2 - y1 x = (WIDTH - size_x) / 2 From f32e560580d32356858fddae2a3e18ef2eabb0ae Mon Sep 17 00:00:00 2001 From: Alexandre Esse Date: Tue, 11 Jun 2024 03:22:26 +0200 Subject: [PATCH 119/122] mqtt-all: update display message Signed-off-by: Alexandre Esse --- examples/mqtt-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index db4cf875..b4fd19ef 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -133,7 +133,7 @@ def display_status(disp, mqtt_broker): text_colour = (255, 255, 255) back_colour = (0, 170, 170) if check_wifi() else (85, 15, 15) device_serial_number = get_serial_number() - message = f"{device_serial_number}\nWi-Fi: {wifi_status}\nmqtt-broker: {mqtt_broker}" + message = f"Serial: {device_serial_number}\nWi-Fi: {wifi_status}\nmqtt-broker: {mqtt_broker}" img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) x1, y1, x2, y2 = draw.textbbox((0,0), message, font=font) From f2b11de49d0499d3b3d430208d61aba4f2d59a7f Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 18 Mar 2025 16:31:53 +0000 Subject: [PATCH 120/122] packaging: sync with boilerplate, move example requirements. --- Makefile | 5 ++++- install.sh | 20 +++++++++++++++----- pyproject.toml | 8 +------- requirements-examples.txt | 7 +++++++ tox.ini | 11 ++--------- 5 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 requirements-examples.txt diff --git a/Makefile b/Makefile index 34f4a7dd..56cf0dfe 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ endif @echo "deploy: build and upload to PyPi" @echo "tag: tag the repository with the current version\n" +version: + @hatch version + install: ./install.sh --unstable @@ -47,7 +50,7 @@ pytest: nopost: @bash check.sh --nopost -tag: +tag: version git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" build: check diff --git a/install.sh b/install.sh index 495919b4..61f1a4a6 100755 --- a/install.sh +++ b/install.sh @@ -58,11 +58,6 @@ find_config() { if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then fatal "Could not find $CONFIG_FILE!" fi - else - if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then - warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" - warning "You might want to fix this!" - fi fi inform "Using $CONFIG_FILE in $CONFIG_DIR" } @@ -171,6 +166,12 @@ function pip_pkg_install { check_for_error } +function pip_requirements_install { + # A null Keyring prevents pip stalling in the background + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install -r "$@" + check_for_error +} + while [[ $# -gt 0 ]]; do K="$1" case $K in @@ -340,6 +341,15 @@ fi printf "\n" +if [ -f "requirements-examples.txt" ]; then + if confirm "Would you like to install example dependencies?"; then + inform "Installing dependencies from requirements-examples.txt..." + pip_requirements_install requirements-examples.txt + fi +fi + +printf "\n" + # Use pdoc to generate basic documentation from the installed module if confirm "Would you like to generate documentation?"; then diff --git a/pyproject.toml b/pyproject.toml index 7c565bb5..057079f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,13 +41,7 @@ dependencies = [ "pms5003 >= 1.0.1", "ltr559 >= 1.0.0", "st7735 >= 1.0.0", - "ads1015 >= 1.0.0", - "fonts", - "font-roboto", - "astral", - "pytz", - "sounddevice", - "paho-mqtt" + "ads1015 >= 1.0.0" ] [project.urls] diff --git a/requirements-examples.txt b/requirements-examples.txt new file mode 100644 index 00000000..b61fd7f5 --- /dev/null +++ b/requirements-examples.txt @@ -0,0 +1,7 @@ +fonts +font-roboto +astral +pytz +sounddevice +paho-mqtt + diff --git a/tox.ini b/tox.ini index 44c86546..2b6d87b8 100644 --- a/tox.ini +++ b/tox.ini @@ -20,15 +20,8 @@ commands = python -m build --no-isolation python -m twine check dist/* isort --check . - ruff . + ruff check . codespell . deps = - check-manifest - ruff - codespell - isort - twine - build - hatch - hatch-fancy-pypi-readme + -r{toxinidir}/requirements-dev.txt From 70947aa09406f5cfebce4b78b6630a0b48ec9919 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 18 Mar 2025 16:38:08 +0000 Subject: [PATCH 121/122] Add pillow to example requirements. --- requirements-examples.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-examples.txt b/requirements-examples.txt index b61fd7f5..96def717 100644 --- a/requirements-examples.txt +++ b/requirements-examples.txt @@ -4,4 +4,4 @@ astral pytz sounddevice paho-mqtt - +pillow From e85fab90893f2ed34b4c9deeabfaecb83f1475d9 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 11 Jul 2025 14:55:38 +0100 Subject: [PATCH 122/122] Prefer pip binary installs for #154. --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 61f1a4a6..d2d973a9 100755 --- a/install.sh +++ b/install.sh @@ -162,13 +162,13 @@ function apt_pkg_install { function pip_pkg_install { # A null Keyring prevents pip stalling in the background - PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --prefer-binary --upgrade "$@" check_for_error } function pip_requirements_install { # A null Keyring prevents pip stalling in the background - PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install -r "$@" + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --prefer-binary -r "$@" check_for_error }