Skip to content

Commit db40c1e

Browse files
author
Kevin J Walters
committed
Improving exception handling for connectivity errors.
Cleaning up debug printing.
1 parent e933001 commit db40c1e

File tree

1 file changed

+371
-0
lines changed

1 file changed

+371
-0
lines changed

pmsensors-adafruitio.py

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
### pmsensors-adafruitio v1.1
2+
### Send values from Plantower PMS5003, Sensirion SPS-30 and Omron B5W LD0101 to Adafruit IO
3+
4+
### Tested with Maker Pi PICO using CircuitPython 7.0.0
5+
### and ESP-01S using Cytron's firmware 2.2.0.0
6+
7+
### copy this file to Maker Pi Pico as code.py
8+
9+
### MIT License
10+
11+
### Copyright (c) 2021 Kevin J. Walters
12+
13+
### Permission is hereby granted, free of charge, to any person obtaining a copy
14+
### of this software and associated documentation files (the "Software"), to deal
15+
### in the Software without restriction, including without limitation the rights
16+
### to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
### copies of the Software, and to permit persons to whom the Software is
18+
### furnished to do so, subject to the following conditions:
19+
20+
### The above copyright notice and this permission notice shall be included in all
21+
### copies or substantial portions of the Software.
22+
23+
### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
### AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
### LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
### OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
### SOFTWARE.
30+
31+
32+
import random
33+
import time
34+
from collections import OrderedDict
35+
36+
from secrets import secrets
37+
38+
import board
39+
import busio
40+
import analogio
41+
import digitalio
42+
import pwmio
43+
##import ulab
44+
from neopixel import NeoPixel
45+
46+
47+
### ESP-01S
48+
##import adafruit_requests as requests
49+
import adafruit_espatcontrol.adafruit_espatcontrol_socket as socket
50+
from adafruit_espatcontrol import adafruit_espatcontrol
51+
from adafruit_espatcontrol import adafruit_espatcontrol_wifimanager
52+
import adafruit_minimqtt.adafruit_minimqtt as MQTT
53+
from adafruit_io.adafruit_io import IO_MQTT
54+
from adafruit_io.adafruit_io_errors import AdafruitIO_MQTTError
55+
from microcontroller import cpu
56+
57+
### Particulate Matter sensors
58+
from adafruit_pm25.uart import PM25_UART
59+
from adafruit_b5wld0101 import B5WLD0101
60+
from adafruit_sps30.i2c import SPS30_I2C
61+
62+
debug = 5
63+
mu_output = 2
64+
65+
### Instructables video was shot with this set to 25 seconds
66+
UPLOAD_PERIOD = 25 ### TODO - DEFAULT VALUE??? 60? NOTE
67+
ADAFRUIT_IO_GROUP_NAME = "mpp-pm"
68+
VCC_DIVIDER = 2.0
69+
SENSORS = ("pms5003", "sps30", "b5wld0101")
70+
71+
### Pins
72+
SPS30_SDA = board.GP0
73+
SPS30_SCL = board.GP1
74+
75+
PMS5003_EN = board.GP2
76+
PMS5003_RST = board.GP3
77+
PMS5003_TX = board.GP4
78+
PMS5003_RX = board.GP5
79+
80+
B5WLD0101_OUT1 = board.GP10
81+
B5WLD0101_OUT2 = board.GP11
82+
B5WLD0101_VTH = board.GP12
83+
84+
ESP01_TX = board.GP16
85+
ESP01_RX = board.GP17
86+
87+
### Pi Pico only has three analogue capable inputs GP26 - GP28
88+
B5WLD0101_VTH_MON = board.GP26
89+
B5WLD0101_VCC_DIV2 = board.GP27
90+
91+
### Maker Pi Pico has a WS2812 RGB pixel on GP28
92+
### In general, GP28 can be used for analogue input but
93+
### should be last choice given its dual role on this board)
94+
MPP_NEOPIXEL = board.GP28
95+
96+
### RGB pixel indications
97+
GOOD = (0, 8, 0)
98+
UPLOADING = (0, 0, 12)
99+
ERROR = (12, 0, 0)
100+
BLACK = (0, 0, 0)
101+
102+
### Voltage of GPIO PWM
103+
PWM_V = 3.3
104+
MS_TO_NS = 1000 * 1000 * 1000
105+
PMS5003_READ_ATTEMPTS = 10
106+
ADC_SAMPLES = 100
107+
RECONNECT_SLEEP = 1.25
108+
109+
### Data fields to publish to Adafruit IO
110+
UPLOAD_PMS5003 = ("pm10 standard", "pm25 standard")
111+
UPLOAD_SPS30 = ("pm10 standard", "pm25 standard")
112+
UPLOAD_B5WLD0101 = ("raw out1", "raw out2")
113+
114+
UPLOAD_PM_FIELDS = ("pms5003-pm10-standard", "pms5003-pm25-standard",
115+
"sps30-pm10-standard", "sps30-pm25-standard",
116+
"b5wld0101-raw-out1", "b5wld0101-raw-out2")
117+
UPLOAD_V_FIELDS = ("b5wld0101-vth", "b5wld0101-vcc")
118+
UPLOAD_CPU_FIELDS = ("cpu-temperature",)
119+
UPLOAD_FIELDS = UPLOAD_PM_FIELDS + UPLOAD_V_FIELDS + UPLOAD_CPU_FIELDS
120+
121+
122+
def d_print(level, *args, **kwargs):
123+
"""A simple conditional print for debugging based on global debug level."""
124+
if not isinstance(level, int):
125+
print(level, *args, **kwargs)
126+
elif debug >= level:
127+
print(*args, **kwargs)
128+
129+
130+
pixel = NeoPixel(MPP_NEOPIXEL, 1)
131+
pixel.fill(BLACK)
132+
133+
### Initialise the trio of sensors
134+
### Plantower PMS5003 - serial connected
135+
pms5003_en = digitalio.DigitalInOut(PMS5003_EN)
136+
pms5003_en.direction = digitalio.Direction.OUTPUT
137+
pms5003_en.value = True
138+
pms5003_rst = digitalio.DigitalInOut(PMS5003_RST)
139+
pms5003_rst.direction = digitalio.Direction.OUTPUT
140+
pms5003_rst.value = True
141+
serial = busio.UART(PMS5003_TX,
142+
PMS5003_RX,
143+
baudrate=9600,
144+
timeout=15.0)
145+
146+
### Sensirion SPS30 - i2c connected
147+
i2c = busio.I2C(SPS30_SCL, SPS30_SDA)
148+
pms5003 = PM25_UART(serial)
149+
sps30 = SPS30_I2C(i2c, fp_mode=True)
150+
b5wld0101 = B5WLD0101(B5WLD0101_OUT1, B5WLD0101_OUT2)
151+
152+
### Omron B5W LD0101 - pulsed outputs
153+
### create Vth with smoothed PWM
154+
b5wld0101_vth_pwm = pwmio.PWMOut(B5WLD0101_VTH, frequency=125 * 1000)
155+
### R=10k (to pin) C=0.1uF - looks flat on 0.1 AC on scope
156+
### 0.5 shows as 515mV (496mV on Astro AI at pin GP26, 491mV on resistor on breadboard)
157+
b5wld0101_vth_pwm.duty_cycle = round(0.5 / PWM_V * 65535)
158+
b5wld0101_vth_mon = analogio.AnalogIn(B5WLD0101_VTH_MON)
159+
b5wld0101_vcc_div2 = analogio.AnalogIn(B5WLD0101_VCC_DIV2)
160+
161+
162+
def read_voltages(samples=ADC_SAMPLES):
163+
v_data = OrderedDict()
164+
conv = b5wld0101_vth_mon.reference_voltage / (samples * 65535)
165+
v_data["b5wld0101-vcc"] = (sum([b5wld0101_vcc_div2.value
166+
for _ in range(samples)]) * conv * VCC_DIVIDER)
167+
v_data["b5wld0101-vth"] = (sum([b5wld0101_vth_mon.value
168+
for _ in range(samples)]) * conv)
169+
return v_data
170+
171+
172+
def get_pm(sensors):
173+
all_data = OrderedDict()
174+
175+
for sensor in sensors:
176+
s_data = {}
177+
if sensor == "pms5003":
178+
for _ in range(PMS5003_READ_ATTEMPTS):
179+
try:
180+
s_data = pms5003.read()
181+
except RuntimeError:
182+
pass
183+
if s_data:
184+
break
185+
elif sensor == "sps30":
186+
s_data = sps30.read()
187+
elif sensor == "b5wld0101":
188+
s_data = b5wld0101.read()
189+
else:
190+
print("Whatcha talkin' bout Willis?")
191+
192+
for key in s_data.keys():
193+
new_key = sensor + "-" + key.replace(" ","-")
194+
all_data[new_key] = s_data[key]
195+
196+
return all_data
197+
198+
199+
class DataWarehouse():
200+
SECRETS_REQUIRED = ("ssid", "password", "aio_username", "aio_key")
201+
202+
def __init__(self, secrets_, *,
203+
esp01_pins=[],
204+
esp01_uart=None,
205+
esp01_baud=115200,
206+
pub_prefix="",
207+
debug=False ### pylint: disable=redefined-outer-name
208+
):
209+
210+
if esp01_uart:
211+
self.esp01_uart = esp01_uart
212+
else:
213+
self.esp01_uart = busio.UART(*esp01_pins, receiver_buffer_size=2048)
214+
215+
self.debug = debug
216+
self.esp = adafruit_espatcontrol.ESP_ATcontrol(self.esp01_uart,
217+
esp01_baud,
218+
debug=debug)
219+
self.esp_version = ""
220+
self.wifi = None
221+
try:
222+
_ = [secrets_[key] for key in self.SECRETS_REQUIRED]
223+
except KeyError:
224+
raise RuntimeError("secrets.py must contain: "
225+
+ " ".join(self.SECRETS_REQUIRED))
226+
self.secrets = secrets_
227+
self.io = None
228+
self.pub_prefix = pub_prefix
229+
self.pub_name = {}
230+
self.init_connect()
231+
232+
233+
def init_connect(self):
234+
self.esp.soft_reset()
235+
236+
self.wifi = adafruit_espatcontrol_wifimanager.ESPAT_WiFiManager(self.esp,
237+
self.secrets)
238+
### A few retries here seems to greatly improve reliability
239+
for _ in range(4):
240+
if self.debug:
241+
print("Connecting to WiFi...")
242+
try:
243+
self.wifi.connect()
244+
self.esp_version = self.esp.get_version()
245+
if self.debug:
246+
print("Connected!")
247+
break
248+
except (RuntimeError,
249+
TypeError,
250+
adafruit_espatcontrol.OKError) as ex:
251+
if self.debug:
252+
print("EXCEPTION: Failed to publish()", repr(ex))
253+
time.sleep(RECONNECT_SLEEP)
254+
255+
### This uses global variables
256+
socket.set_interface(self.esp)
257+
258+
### MQTT Client
259+
### pylint: disable=protected-access
260+
self.mqtt_client = MQTT.MQTT(
261+
broker="io.adafruit.com",
262+
username=self.secrets["aio_username"],
263+
password=self.secrets["aio_key"],
264+
socket_pool=socket,
265+
ssl_context=MQTT._FakeSSLContext(self.esp)
266+
)
267+
self.io = IO_MQTT(self.mqtt_client)
268+
### Callbacks of interest on io are
269+
### on_connect on_disconnect on_subscribe
270+
self.io.connect()
271+
272+
273+
def reset_and_reconnect(self):
274+
self.wifi.reset()
275+
self.io.reconnect()
276+
277+
278+
def update_pub_name(self, field_name):
279+
pub_name = self.pub_prefix + field_name
280+
return pub_name
281+
282+
283+
def poll(self):
284+
dw_poll_ok = False
285+
try_reconnect = False
286+
for _ in range(2):
287+
try:
288+
### Process any incoming messages
289+
if try_reconnect:
290+
self.reset_and_reconnect()
291+
try_reconnect = False
292+
self.io.loop()
293+
dw_poll_ok = True
294+
except (ValueError, RuntimeError, AttributeError,
295+
MQTT.MMQTTException,
296+
adafruit_espatcontrol.OKError,
297+
AdafruitIO_MQTTError) as ex:
298+
if self.debug:
299+
print("EXCEPTION: Failed to get data in loop()", repr(ex))
300+
try_reconnect = True
301+
302+
return dw_poll_ok
303+
304+
305+
def publish(self, p_data, p_fields):
306+
ok = [False] * len(p_fields)
307+
try_reconnect = False
308+
if self.debug:
309+
print("publish()")
310+
311+
for idx, field_name in enumerate(p_fields):
312+
try:
313+
pub_name = self.pub_name[field_name]
314+
except KeyError:
315+
pub_name = self.update_pub_name(field_name)
316+
for _ in range(2):
317+
try:
318+
if try_reconnect:
319+
self.reset_and_reconnect()
320+
try_reconnect = False
321+
self.io.publish(pub_name, p_data[field_name])
322+
ok[idx] = True
323+
break
324+
except (ValueError, RuntimeError, AttributeError,
325+
MQTT.MMQTTException,
326+
adafruit_espatcontrol.OKError,
327+
AdafruitIO_MQTTError) as ex:
328+
if self.debug:
329+
print("EXCEPTION: Failed to publish()", repr(ex))
330+
try_reconnect = True
331+
time.sleep(RECONNECT_SLEEP)
332+
333+
return all(ok)
334+
335+
336+
dw = DataWarehouse(secrets,
337+
esp01_pins=(ESP01_TX, ESP01_RX),
338+
pub_prefix=ADAFRUIT_IO_GROUP_NAME + ".",
339+
debug=(debug >= 5))
340+
341+
last_upload_ns = 0
342+
343+
while True:
344+
poll_ok = dw.poll()
345+
pixel.fill(GOOD if poll_ok else ERROR)
346+
347+
cpu_temp = cpu.temperature
348+
voltages = read_voltages()
349+
time_ns = time.monotonic_ns()
350+
351+
data = get_pm(SENSORS)
352+
data.update(voltages)
353+
data.update({"cpu-temperature": cpu_temp})
354+
355+
if debug >= 2:
356+
print(data)
357+
elif mu_output:
358+
output = ("("
359+
+ ",".join(str(item)
360+
for item in [data[key] for key in UPLOAD_PM_FIELDS])
361+
+ ")")
362+
print(output)
363+
364+
if time_ns - last_upload_ns >= UPLOAD_PERIOD * MS_TO_NS:
365+
pixel.fill(UPLOADING)
366+
pub_ok = dw.publish(data, UPLOAD_FIELDS)
367+
pixel.fill(GOOD if pub_ok else ERROR)
368+
if pub_ok:
369+
last_upload_ns = time_ns
370+
371+
time.sleep(1.5 + random.random())

0 commit comments

Comments
 (0)