Skip to content

Commit e933001

Browse files
author
Kevin J Walters
committed
New program to send data from trio of particulate matter sensors to Adafruit IO using Maker Pi Pico and ESP-01S.
1 parent 2db14c7 commit e933001

File tree

1 file changed

+349
-0
lines changed

1 file changed

+349
-0
lines changed

pico/pmsensors-adafruitio.py

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
### pmsensors-adafruitio v1.0
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 microcontroller import cpu
55+
56+
### Particulate Matter sensors
57+
from adafruit_pm25.uart import PM25_UART
58+
from adafruit_b5wld0101 import B5WLD0101
59+
from adafruit_sps30.i2c import SPS30_I2C
60+
61+
62+
63+
debug = 5
64+
mu_output = 2
65+
66+
### Instructables video was shot with this set to 25 seconds
67+
UPLOAD_PERIOD = 25 ### TODO - DEFAULT VALUE??? 60? NOTE
68+
ADAFRUIT_IO_GROUP_NAME = "mpp-pm"
69+
VCC_DIVIDER = 2.0
70+
SENSORS = ("pms5003", "sps30", "b5wld0101")
71+
72+
### Pins
73+
SPS30_SDA = board.GP0
74+
SPS30_SCL = board.GP1
75+
76+
PMS5003_EN = board.GP2
77+
PMS5003_RST = board.GP3
78+
PMS5003_TX = board.GP4
79+
PMS5003_RX = board.GP5
80+
81+
B5WLD0101_OUT1 = board.GP10
82+
B5WLD0101_OUT2 = board.GP11
83+
B5WLD0101_VTH = board.GP12
84+
85+
ESP01_TX = board.GP16
86+
ESP01_RX = board.GP17
87+
88+
### Pi Pico only has three analogue capable inputs GP26 - GP28
89+
B5WLD0101_VTH_MON = board.GP26
90+
B5WLD0101_VCC_DIV2 = board.GP27
91+
92+
### Maker Pi Pico has a WS2812 RGB pixel on GP28
93+
### In general, GP28 can be used for analogue input but
94+
### should be last choice given its dual role on this board)
95+
MPP_NEOPIXEL = board.GP28
96+
97+
### RGB pixel indications
98+
GOOD = (0, 8, 0)
99+
UPLOADING = (0, 0, 12)
100+
ERROR = (12, 0, 0)
101+
BLACK = (0, 0, 0)
102+
103+
### Voltage of GPIO PWM
104+
PWM_V = 3.3
105+
MS_TO_NS = 1000 * 1000 * 1000
106+
PMS5003_READ_ATTEMPTS = 10
107+
ADC_SAMPLES = 100
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 = self.esp.get_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+
print("Connecting to WiFi...")
241+
try:
242+
self.wifi.connect()
243+
print("Connected!")
244+
break
245+
except (RuntimeError, TypeError) as ex:
246+
print("wifi.connect exception", repr(ex))
247+
248+
### This uses global variables
249+
socket.set_interface(self.esp)
250+
251+
### MQTT Client
252+
### pylint: disable=protected-access
253+
self.mqtt_client = MQTT.MQTT(
254+
broker="io.adafruit.com",
255+
username=self.secrets["aio_username"],
256+
password=self.secrets["aio_key"],
257+
socket_pool=socket,
258+
ssl_context=MQTT._FakeSSLContext(self.esp)
259+
)
260+
self.io = IO_MQTT(self.mqtt_client)
261+
### Callbacks of interest on io are
262+
### on_connect on_disconnect on_subscribe
263+
self.io.connect()
264+
265+
266+
def reset_and_reconnect(self):
267+
self.wifi.reset()
268+
self.io.reconnect()
269+
270+
271+
def update_pub_name(self, field_name):
272+
pub_name = self.pub_prefix + field_name
273+
return pub_name
274+
275+
276+
def poll(self):
277+
poll_ok = True
278+
try:
279+
### Process any incoming messages
280+
self.io.loop()
281+
except (ValueError, RuntimeError, MQTT.MMQTTException) as ex:
282+
print("Failed to get data", repr(ex))
283+
284+
poll_ok = False
285+
286+
return poll_ok
287+
288+
289+
def publish(self, p_data, p_fields):
290+
all_ok = True
291+
print("UPLOAD") ### TODO
292+
293+
for field_name in p_fields:
294+
try:
295+
pub_name = self.pub_name[field_name]
296+
except KeyError:
297+
pub_name = self.update_pub_name(field_name)
298+
try:
299+
self.io.publish(pub_name, p_data[field_name])
300+
except (ValueError, RuntimeError, MQTT.MMQTTException):
301+
all_ok = False
302+
self.reset_and_reconnect()
303+
304+
return all_ok
305+
306+
307+
dw = DataWarehouse(secrets,
308+
esp01_pins=(ESP01_TX, ESP01_RX),
309+
pub_prefix=ADAFRUIT_IO_GROUP_NAME + ".",
310+
debug=(debug >= 5))
311+
312+
last_upload_ns = 0
313+
314+
while True:
315+
pixel.fill(GOOD)
316+
if not dw.poll():
317+
pixel.fill(ERROR)
318+
print("ESP_ATcontrol OKError exception")
319+
for _ in range(20):
320+
print("EXCEPTION")
321+
322+
cpu_temp = cpu.temperature
323+
voltages = read_voltages()
324+
time_ns = time.monotonic_ns()
325+
326+
data = get_pm(SENSORS)
327+
data.update(voltages)
328+
data.update({"cpu-temperature": cpu_temp})
329+
330+
if debug >= 2:
331+
print(data)
332+
elif mu_output:
333+
output = ("("
334+
+ ",".join(str(item)
335+
for item in [data[key] for key in UPLOAD_PM_FIELDS])
336+
+ ")")
337+
print(output)
338+
339+
if time_ns - last_upload_ns >= UPLOAD_PERIOD * MS_TO_NS:
340+
pixel.fill(UPLOADING)
341+
pub_ok = dw.publish(data, UPLOAD_FIELDS)
342+
pixel.fill(GOOD if pub_ok else ERROR)
343+
if pub_ok:
344+
last_upload_ns = time_ns
345+
else:
346+
for _ in range(20):
347+
print("UPLOAD ERROR")
348+
349+
time.sleep(1.5 + random.random())

0 commit comments

Comments
 (0)