├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── boot.py ├── lib ├── adc.py ├── datapoint.py ├── ds3231.py ├── helpers.py ├── influxdb.py ├── mqtt.py ├── persistence.py ├── pms5003.py ├── sht1x.py ├── thingspeak.py └── urequests.py ├── main.py └── pymakr.conf /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *.cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | .static_storage/ 54 | .media/ 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # Environments 83 | .env 84 | .venv 85 | env/ 86 | venv/ 87 | ENV/ 88 | env.bak/ 89 | venv.bak/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | *~ 105 | .*~ 106 | lib/keychain.py 107 | datapoints.txt 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.7.0 - 2018/01/20: 2 | * Move to a dedicated PCB 3 | * Battery voltage measurement not included yet 4 | 5 | 0.6.0 - 2018/01/18: 6 | * Add external RTC module and bring back 10-minute granularity for influx data 7 | * Reorganize pin connections, almost all pins are now used... 8 | 9 | 0.5.1 - 2018/01/16: 10 | * More LoPy4 issues: RTC state not kept when running on battery 11 | * Report data to influx the same way as to thingspeak, i.e. mean data from 6 consecutive 12 | measurements, with no timestamp 13 | * Temporarily removed RTC code that is not needed as we don't send timestamps 14 | 15 | 0.5.0 - 2018/01/16: 16 | * Update for LoPy4: change PMS5003 pins because old ones are partially used by LoRa chip 17 | * Remove LED PWM code (temporarily?) beacuse LoPy4 crashed there... 18 | * Sleep for 598 seconds because 595 was a bit too short 19 | 20 | 0.4.1 - 2017/12/07: 21 | * Fix reading datapoints from file 22 | * Fix computing mean datapoint 23 | * Use wake by button to enable debug mode (enable WiFi and exit) 24 | * Sleep for 595 seconds (5s to compensate for boot-up time) 25 | 26 | 0.4.0: Send data to Thingspeak using MQTT and measure outside T/RH 27 | * send hourly mean data (average of last 6 measurements) - this is mainly to learn how 28 | MQTT works, it's suboptimal and it should use REST API and send batched updates with 29 | multiple measurements 30 | * Add DataPoint class that gathers all measurements and has adapters for influx and 31 | thingspeak formats 32 | * Move keys to keychain.py that's outside the repository 33 | 34 | 0.3.0: Button to trigger sending all available data 35 | 36 | 0.2.2: Keep data file locally until it has been successfully posted 37 | * also add LED flashing for storing data, sending data and failing to send data 38 | * disable expansion board LED during deep sleep 39 | * move Watchdog to boot.py 40 | 41 | 0.2.1: Resync RTC every time when connecting to WLAN 42 | 43 | 0.2.0: Gather data every 10 minutes, but send every hour 44 | * Store measurements data in a text file on flash 45 | * Once 5 measurements are stored, after taking sixth one 46 | send all the data to influx and remove the file 47 | * WiFi is required before first ever measurement to set up RTC 48 | and every hour to send data to influx 49 | * Some helper data is stored in NVRAM - adjust boot.py to reset 50 | NVRAM and delete cached measurements on hard resets 51 | 52 | 0.1.7: 53 | * clean up project file, move PMS code to PMS5003 class 54 | 55 | 0.1.6: 56 | * install 700mAh battery and step-up circuit inside pycase 57 | 58 | 0.1.5: 59 | * fix battery voltage calculation 60 | 61 | 0.1.4: 62 | * read 5 AQI samples instead of 10 (no. 2, 4, 6, 8 and 10) 63 | 64 | 0.1.3: 65 | * disable printing with timestamp 66 | 67 | 0.1.2: 68 | * connect to WLAN and setup RTC in a thread after temperature/humidity measurement 69 | 70 | 0.1.1: 71 | * measure temperature, humidity and voltage in a separate thread 72 | 73 | 0.1.0: 74 | * AQI 75 | * temperature 76 | * relative humidity 77 | * battery voltage 78 | * measurement duration 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Dominik Kapusta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # upython-aq-monitor 2 | Air Quality monitor using PMS5003 sensor and WiPy development board implemented in MicroPython 3 | 4 | Read more here: http://kapusta.cc/2017/12/02/home-made-air-quality-monitoring-using-wipy/ 5 | -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | import machine 2 | import pycom 3 | import persistence 4 | from network import Bluetooth, WLAN 5 | from machine import WDT 6 | from helpers import * 7 | 8 | bluetooth = Bluetooth() 9 | bluetooth.deinit() 10 | wlan = WLAN(mode=WLAN.STA) 11 | wlan.deinit() 12 | 13 | ######################################## 14 | ## Handle wake by button to enter debug mode 15 | ######################################## 16 | 17 | if machine.wake_reason()[0] is machine.PIN_WAKE: 18 | print('PIN WAKE') 19 | connect_to_WLAN() 20 | else: 21 | watchdog_timer = None 22 | 23 | if machine.reset_cause() not in [machine.DEEPSLEEP_RESET, machine.SOFT_RESET, machine.WDT_RESET]: 24 | print('hard reset, erasing NVS') 25 | pycom.nvs_erase_all() 26 | persistence.cleanup() 27 | else: 28 | print('soft/deepsleep reset, enabling watchdog timer') 29 | watchdog_timer = WDT(timeout=30000) 30 | 31 | machine.main('main.py') 32 | -------------------------------------------------------------------------------- /lib/adc.py: -------------------------------------------------------------------------------- 1 | from machine import ADC 2 | numADCreadings = const(1000) 3 | Vmax = const(4240) 4 | 5 | def vbatt(): 6 | adc = ADC(0) 7 | apin = adc.channel(attn=ADC.ATTN_2_5DB, pin='P16') 8 | samplesADC = [0.0]*numADCreadings 9 | meanADC = 0.0 10 | i = 0 11 | while (i < numADCreadings): 12 | sample = apin() 13 | samplesADC[i] = sample 14 | meanADC += sample 15 | i += 1 16 | meanADC /= numADCreadings 17 | varianceADC = 0.0 18 | for sample in samplesADC: 19 | varianceADC += (sample - meanADC)**2 20 | varianceADC /= (numADCreadings - 1) 21 | # print("%u ADC readings :\n%s" %(numADCreadings, str(samplesADC))) 22 | print("Mean of ADC readings (0-4095) = %15.13f" % meanADC) 23 | print("Mean of ADC voltage readings (0-%dmV) = %15.13f" % (apin.value_to_voltage(4095), apin.value_to_voltage(int(meanADC)))) 24 | print("Variance of ADC readings = %15.13f" % varianceADC) 25 | print("10**6*Variance/(Mean**2) of ADC readings = %15.13f" % ((varianceADC*10**6)//(meanADC**2))) 26 | return meanADC/4095*Vmax 27 | -------------------------------------------------------------------------------- /lib/datapoint.py: -------------------------------------------------------------------------------- 1 | class DataPoint: 2 | 3 | __slots__ = ['pm10', 'pm25', 'temperature', 'humidity', 'voltage', 'duration', 'version', 'timestamp'] 4 | 5 | def __init__(self, **kwargs): 6 | required = list(self.__slots__) 7 | for key, value in kwargs.items(): 8 | setattr(self, key, value) 9 | required.remove(key) 10 | if len(required) > 0: 11 | raise ValueError 12 | 13 | def to_influx(self, include_timestamp=True): 14 | data = 'aqi,indoor=1,version={} pm25={},pm10={},temperature={},humidity={},voltage={},duration={}' \ 15 | .format(self.version, self.pm25, self.pm10, self.temperature, self.humidity, \ 16 | self.voltage, self.duration) 17 | if include_timestamp is True: 18 | data += ' {}000000000'.format(self.timestamp) 19 | return data 20 | 21 | def to_thingspeak(self): 22 | return 'field1={}&field2={}&field3={}&field4={}&field5={}&field6={}' \ 23 | .format(self.pm10, self.pm25, self.temperature, self.humidity, self.voltage, \ 24 | self.duration) 25 | 26 | @classmethod 27 | def mean(cls, datapoints): 28 | mean_pm10 = 0 29 | mean_pm25 = 0 30 | mean_temperature = 0 31 | mean_humidity = 0 32 | mean_duration = 0 33 | valid_temp_datapoints = 0 34 | 35 | for d in datapoints: 36 | mean_pm10 += d.pm10 37 | mean_pm25 += d.pm25 38 | mean_duration += d.duration 39 | if d.temperature is not -1: 40 | mean_temperature += d.temperature 41 | mean_humidity += d.humidity 42 | valid_temp_datapoints += 1 43 | 44 | return cls( 45 | pm10 = mean_pm10/len(datapoints), 46 | pm25 = mean_pm25/len(datapoints), 47 | temperature = -1 if valid_temp_datapoints == 0 else mean_temperature/valid_temp_datapoints, 48 | humidity = -1 if valid_temp_datapoints == 0 else mean_humidity/valid_temp_datapoints, 49 | duration = mean_duration/len(datapoints), 50 | voltage = datapoints[-1].voltage, 51 | version = datapoints[-1].version, 52 | timestamp = datapoints[-1].timestamp 53 | ) 54 | -------------------------------------------------------------------------------- /lib/ds3231.py: -------------------------------------------------------------------------------- 1 | # MicroPython DS3231 precison real time clock driver for Pycom devices. 2 | # Adapted from Pyboard driver at https://github.com/peterhinch/micropython-samples 3 | # Adapted by Dominik Kapusta, Jan 2018 4 | # 5 | # The MIT License (MIT) 6 | # 7 | # Copyright (c) 2014 Peter Hinch 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | 26 | import utime 27 | from machine import RTC, I2C 28 | 29 | DS3231_I2C_ADDR = 0x68 30 | 31 | class DS3231Exception(OSError): 32 | pass 33 | 34 | rtc = RTC() 35 | 36 | def now(): # Return the current time from the RTC in millisecs from year 2000 37 | secs = utime.time() 38 | ms = int(rtc.now()[6]/1000) 39 | if ms < 50: # Might have just rolled over 40 | secs = utime.time() 41 | return 1000 * secs + ms 42 | 43 | def bcd2dec(bcd): 44 | return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f)) 45 | 46 | def dec2bcd(dec): 47 | tens, units = divmod(dec, 10) 48 | return (tens << 4) + units 49 | 50 | class DS3231: 51 | def __init__(self, bus, pins, baudrate=400000): 52 | self.ds3231 = I2C(bus, pins=pins, mode=I2C.MASTER, baudrate=baudrate) 53 | self.timebuf = bytearray(7) 54 | if DS3231_I2C_ADDR not in self.ds3231.scan(): 55 | raise DS3231Exception("DS3231 not found on I2C bus at %s" % hex(DS3231_I2C_ADDR)) 56 | 57 | def deinit(self): 58 | self.ds3231.deinit() 59 | 60 | def get_time(self, set_rtc = False): 61 | if set_rtc: 62 | data = self.await_transition() # For accuracy set RTC immediately after a seconds transition 63 | else: 64 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) 65 | data = self.timebuf 66 | ss = bcd2dec(data[0]) 67 | mm = bcd2dec(data[1]) 68 | if data[2] & 0x40: 69 | hh = bcd2dec(data[2] & 0x1f) 70 | if data[2] & 0x20: 71 | hh += 12 72 | else: 73 | hh = bcd2dec(data[2]) 74 | wday = data[3] 75 | DD = bcd2dec(data[4]) 76 | MM = bcd2dec(data[5] & 0x1f) 77 | YY = bcd2dec(data[6]) 78 | if data[5] & 0x80: 79 | YY += 2000 80 | else: 81 | YY += 1900 82 | if set_rtc: 83 | rtc.init((YY, MM, DD, hh, mm, ss, 0)) 84 | return (YY, MM, DD, hh, mm, ss, 0, 0) # Time from DS3231 in time.time() format (less yday) 85 | 86 | def save_time(self): 87 | (YY, MM, DD, hh, mm, ss, wday, yday) = utime.gmtime() 88 | wday += 1 # needs to be 1 == Monday, 7 == Sunday 89 | 90 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 0, dec2bcd(ss)) 91 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 1, dec2bcd(mm)) 92 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 2, dec2bcd(hh)) # Sets to 24hr mode 93 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 3, dec2bcd(wday)) # 1 == Monday, 7 == Sunday 94 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 4, dec2bcd(DD)) 95 | if YY >= 2000: 96 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, dec2bcd(MM) | 0b10000000) 97 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, dec2bcd(YY-2000)) 98 | else: 99 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, dec2bcd(MM)) 100 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, dec2bcd(YY-1900)) 101 | 102 | def delta(self): # Return no. of mS RTC leads DS3231 103 | self.await_transition() 104 | rtc_ms = now() 105 | t_ds3231 = utime.mktime(self.get_time()) # To second precision, still in same sec as transition 106 | return rtc_ms - 1000 * t_ds3231 107 | 108 | def await_transition(self): # Wait until DS3231 seconds value changes 109 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) 110 | ss = self.timebuf[0] 111 | while ss == self.timebuf[0]: 112 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) 113 | return self.timebuf 114 | -------------------------------------------------------------------------------- /lib/helpers.py: -------------------------------------------------------------------------------- 1 | import utime 2 | import machine 3 | import pycom 4 | from network import WLAN 5 | from keychain import * 6 | from ds3231 import DS3231 7 | 8 | def connect_to_WLAN(): 9 | wlan = WLAN(mode=WLAN.STA) 10 | if not wlan.isconnected(): 11 | wlan = __connect_to_WLAN(wlan, WLAN_SSID, WLAN_PASSKEY) 12 | return wlan 13 | 14 | 15 | def __connect_to_WLAN(wlan, ssid, passkey): 16 | wlan.connect(ssid, auth=(WLAN.WPA2, passkey), timeout=10000) 17 | while not wlan.isconnected(): 18 | utime.sleep_ms(500) 19 | print('WLAN connection succeeded!') 20 | return wlan 21 | 22 | 23 | def setup_rtc(): 24 | rtc = machine.RTC() 25 | rtc.ntp_sync("pool.ntp.org") 26 | while not rtc.synced(): 27 | utime.sleep_ms(100) 28 | utime.timezone(3600) 29 | 30 | 31 | def flash_led(color, n=1): 32 | for _ in range(n): 33 | pycom.rgbled(color) 34 | utime.sleep_ms(20) 35 | pycom.rgbled(0x000000) 36 | if n != 1: 37 | utime.sleep_ms(200) 38 | -------------------------------------------------------------------------------- /lib/influxdb.py: -------------------------------------------------------------------------------- 1 | import urequests 2 | from datapoint import DataPoint 3 | from helpers import * 4 | 5 | def send_to_influx(datapoints): 6 | data = '\n'.join(d.to_influx() for d in datapoints) 7 | # mean_data = DataPoint.mean(datapoints) 8 | # data = mean_data.to_influx(include_timestamp=False) 9 | 10 | print('sending data\n{}'.format(data)) 11 | influx_url = 'http://rpi.local:8086/write?db=mydb' 12 | success = False 13 | number_of_retries = 3 14 | 15 | while not success and number_of_retries > 0: 16 | try: 17 | urequests.post(influx_url, data=data) 18 | success = True 19 | except OSError as e: 20 | print('network error: {}'.format(e.errno)) 21 | number_of_retries -= 1 22 | pass 23 | 24 | return success 25 | -------------------------------------------------------------------------------- /lib/mqtt.py: -------------------------------------------------------------------------------- 1 | import usocket as socket 2 | import ustruct as struct 3 | from ubinascii import hexlify 4 | 5 | class MQTTException(Exception): 6 | pass 7 | 8 | class MQTTClient: 9 | 10 | def __init__(self, client_id, server, port=0, user=None, password=None, keepalive=0, 11 | ssl=False, ssl_params={}): 12 | if port == 0: 13 | port = 8883 if ssl else 1883 14 | self.client_id = client_id 15 | self.sock = None 16 | self.addr = socket.getaddrinfo(server, port)[0][-1] 17 | self.ssl = ssl 18 | self.ssl_params = ssl_params 19 | self.pid = 0 20 | self.cb = None 21 | self.user = user 22 | self.pswd = password 23 | self.keepalive = keepalive 24 | self.lw_topic = None 25 | self.lw_msg = None 26 | self.lw_qos = 0 27 | self.lw_retain = False 28 | 29 | def _send_str(self, s): 30 | self.sock.write(struct.pack("!H", len(s))) 31 | self.sock.write(s) 32 | 33 | def _recv_len(self): 34 | n = 0 35 | sh = 0 36 | while 1: 37 | b = self.sock.read(1)[0] 38 | n |= (b & 0x7f) << sh 39 | if not b & 0x80: 40 | return n 41 | sh += 7 42 | 43 | def set_callback(self, f): 44 | self.cb = f 45 | 46 | def set_last_will(self, topic, msg, retain=False, qos=0): 47 | assert 0 <= qos <= 2 48 | assert topic 49 | self.lw_topic = topic 50 | self.lw_msg = msg 51 | self.lw_qos = qos 52 | self.lw_retain = retain 53 | 54 | def connect(self, clean_session=True): 55 | self.sock = socket.socket() 56 | self.sock.connect(self.addr) 57 | if self.ssl: 58 | import ussl 59 | self.sock = ussl.wrap_socket(self.sock, **self.ssl_params) 60 | msg = bytearray(b"\x10\0\0\x04MQTT\x04\x02\0\0") 61 | msg[1] = 10 + 2 + len(self.client_id) 62 | msg[9] = clean_session << 1 63 | if self.user is not None: 64 | msg[1] += 2 + len(self.user) + 2 + len(self.pswd) 65 | msg[9] |= 0xC0 66 | if self.keepalive: 67 | assert self.keepalive < 65536 68 | msg[10] |= self.keepalive >> 8 69 | msg[11] |= self.keepalive & 0x00FF 70 | if self.lw_topic: 71 | msg[1] += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) 72 | msg[9] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 73 | msg[9] |= self.lw_retain << 5 74 | self.sock.write(msg) 75 | #print(hex(len(msg)), hexlify(msg, ":")) 76 | self._send_str(self.client_id) 77 | if self.lw_topic: 78 | self._send_str(self.lw_topic) 79 | self._send_str(self.lw_msg) 80 | if self.user is not None: 81 | self._send_str(self.user) 82 | self._send_str(self.pswd) 83 | resp = self.sock.read(4) 84 | assert resp[0] == 0x20 and resp[1] == 0x02 85 | if resp[3] != 0: 86 | raise MQTTException(resp[3]) 87 | return resp[2] & 1 88 | 89 | def disconnect(self): 90 | self.sock.write(b"\xe0\0") 91 | self.sock.close() 92 | 93 | def ping(self): 94 | self.sock.write(b"\xc0\0") 95 | 96 | def publish(self, topic, msg, retain=False, qos=0): 97 | pkt = bytearray(b"\x30\0\0\0") 98 | pkt[0] |= qos << 1 | retain 99 | sz = 2 + len(topic) + len(msg) 100 | if qos > 0: 101 | sz += 2 102 | assert sz < 2097152 103 | i = 1 104 | while sz > 0x7f: 105 | pkt[i] = (sz & 0x7f) | 0x80 106 | sz >>= 7 107 | i += 1 108 | pkt[i] = sz 109 | #print(hex(len(pkt)), hexlify(pkt, ":")) 110 | self.sock.write(pkt, i + 1) 111 | self._send_str(topic) 112 | if qos > 0: 113 | self.pid += 1 114 | pid = self.pid 115 | struct.pack_into("!H", pkt, 0, pid) 116 | self.sock.write(pkt, 2) 117 | self.sock.write(msg) 118 | if qos == 1: 119 | while 1: 120 | op = self.wait_msg() 121 | if op == 0x40: 122 | sz = self.sock.read(1) 123 | assert sz == b"\x02" 124 | rcv_pid = self.sock.read(2) 125 | rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] 126 | if pid == rcv_pid: 127 | return 128 | elif qos == 2: 129 | assert 0 130 | 131 | def subscribe(self, topic, qos=0): 132 | assert self.cb is not None, "Subscribe callback is not set" 133 | pkt = bytearray(b"\x82\0\0\0") 134 | self.pid += 1 135 | struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) 136 | #print(hex(len(pkt)), hexlify(pkt, ":")) 137 | self.sock.write(pkt) 138 | self._send_str(topic) 139 | self.sock.write(qos.to_bytes(1)) 140 | while 1: 141 | op = self.wait_msg() 142 | if op == 0x90: 143 | resp = self.sock.read(4) 144 | #print(resp) 145 | assert resp[1] == pkt[2] and resp[2] == pkt[3] 146 | if resp[3] == 0x80: 147 | raise MQTTException(resp[3]) 148 | return 149 | 150 | # Wait for a single incoming MQTT message and process it. 151 | # Subscribed messages are delivered to a callback previously 152 | # set by .set_callback() method. Other (internal) MQTT 153 | # messages processed internally. 154 | def wait_msg(self): 155 | res = self.sock.read(1) 156 | self.sock.setblocking(True) 157 | if res is None: 158 | return None 159 | if res == b"": 160 | raise OSError(-1) 161 | if res == b"\xd0": # PINGRESP 162 | sz = self.sock.read(1)[0] 163 | assert sz == 0 164 | return None 165 | op = res[0] 166 | if op & 0xf0 != 0x30: 167 | return op 168 | sz = self._recv_len() 169 | topic_len = self.sock.read(2) 170 | topic_len = (topic_len[0] << 8) | topic_len[1] 171 | topic = self.sock.read(topic_len) 172 | sz -= topic_len + 2 173 | if op & 6: 174 | pid = self.sock.read(2) 175 | pid = pid[0] << 8 | pid[1] 176 | sz -= 2 177 | msg = self.sock.read(sz) 178 | self.cb(topic, msg) 179 | if op & 6 == 2: 180 | pkt = bytearray(b"\x40\x02\0\0") 181 | struct.pack_into("!H", pkt, 2, pid) 182 | self.sock.write(pkt) 183 | elif op & 6 == 4: 184 | assert 0 185 | 186 | # Checks whether a pending message from server is available. 187 | # If not, returns immediately with None. Otherwise, does 188 | # the same processing as wait_msg. 189 | def check_msg(self): 190 | self.sock.setblocking(False) 191 | return self.wait_msg() 192 | -------------------------------------------------------------------------------- /lib/persistence.py: -------------------------------------------------------------------------------- 1 | import uos 2 | import pycom 3 | import urequests 4 | import utime 5 | import ujson 6 | from datapoint import DataPoint 7 | from helpers import * 8 | from influxdb import send_to_influx 9 | from thingspeak import send_to_thingspeak 10 | 11 | __max_queue_size = const(5) 12 | 13 | def cleanup(): 14 | try: 15 | uos.unlink(__filename()) 16 | pycom.nvs_set('queue_size', 0) 17 | except OSError as e: 18 | print('error while removing data file: {}'.format(e.errno)) 19 | pass 20 | 21 | 22 | def __queue_size(): 23 | return pycom.nvs_get('queue_size') or 0 24 | 25 | 26 | def __filename(): 27 | return '{}{}datapoints.txt'.format(uos.getcwd(), uos.sep) 28 | 29 | 30 | def send_datapoints_adhoc(): 31 | sent = False 32 | queue_size = __queue_size() 33 | if queue_size > 0: 34 | try: 35 | datapoints = [] 36 | with open(__filename(), 'r') as data_file: 37 | try: 38 | json_dict = ujson.loads(data_file.readline().rstrip()) 39 | datapoints.append(DataPoint(**json_dict)) 40 | except ValueError: 41 | pass 42 | 43 | 44 | if __send_data(datapoints): 45 | cleanup() 46 | flash_led(0x008800, 10) 47 | sent = True 48 | else: 49 | flash_led(0x880000, 10) 50 | 51 | except OSError as e: 52 | print('file access error: {}'.format(e.errno)) 53 | flash_led(0x880000, 5) 54 | pass 55 | else: 56 | # no data to send 57 | flash_led(0x000088, 1) 58 | 59 | return sent 60 | 61 | 62 | def store_datapoint(datapoint): 63 | sent = False 64 | queue_size = __queue_size() 65 | print('queue size: {}'.format(queue_size)) 66 | if queue_size >= __max_queue_size: 67 | datapoints = [] 68 | try: 69 | with open(__filename(), 'r') as data_file: 70 | for line in data_file: 71 | try: 72 | json_dict = ujson.loads(line.rstrip()) 73 | datapoints.append(DataPoint(**json_dict)) 74 | except ValueError as e: 75 | print('error while reading data from flash: {}'.format(e.errno)) 76 | pass 77 | 78 | datapoints.append(datapoint) 79 | 80 | if __send_datapoints(datapoints): 81 | flash_led(0x008800, 3) 82 | cleanup() 83 | sent = True 84 | else: 85 | flash_led(0x880000, 3) 86 | with open(__filename(), 'w') as data_file: 87 | __save_datapoints_to_file([datapoints]) 88 | pycom.nvs_set('queue_size', queue_size+1) 89 | 90 | except OSError as e: 91 | print('file access error: {}'.format(e.errno)) 92 | cleanup() 93 | __save_datapoints_to_file([datapoint]) 94 | flash_led(0x888888) 95 | pycom.nvs_set('queue_size', 1) 96 | pass 97 | else: 98 | __save_datapoints_to_file([datapoint]) 99 | flash_led(0x888888) 100 | pycom.nvs_set('queue_size', queue_size+1) 101 | 102 | return sent 103 | 104 | 105 | def __save_datapoints_to_file(datapoints): 106 | with open(__filename(), 'a') as data_file: 107 | for datapoint in datapoints: 108 | data_file.write(ujson.dumps(datapoint.__dict__)) 109 | data_file.write('\n') 110 | 111 | 112 | def __send_datapoints(datapoints): 113 | wlan = connect_to_WLAN() 114 | setup_rtc() 115 | success = send_to_thingspeak(datapoints) and send_to_influx(datapoints) 116 | wlan.deinit() 117 | return success 118 | -------------------------------------------------------------------------------- /lib/pms5003.py: -------------------------------------------------------------------------------- 1 | import machine 2 | from machine import UART, Pin 3 | from helpers import * 4 | import utime 5 | 6 | class PMS5003: 7 | def __init__(self, en, tx, rx, rst): 8 | self.en = en 9 | self.tx = tx 10 | self.rx = rx 11 | self.rst = rst 12 | self.en.mode(Pin.OUT) 13 | self.rst.mode(Pin.OUT) 14 | 15 | self.uart = UART(1, baudrate=9600, pins=(self.tx, self.rx)) 16 | self.uart.deinit() 17 | 18 | 19 | def wake_up(self): 20 | self.en(True) 21 | self.rst(True) 22 | self.uart.init(pins=(self.tx, self.rx)) 23 | # sleep for 7 seconds to initialize the sensor properly 24 | utime.sleep_ms(7000) 25 | 26 | 27 | def idle(self): 28 | self.en(False) 29 | self.uart.deinit() 30 | 31 | 32 | def read_frames(self, count): 33 | frames = [] 34 | # flush the buffer to read fresh data 35 | self.uart.readall() 36 | while len(frames) < count: 37 | self.__wait_for_data(32) 38 | 39 | while self.uart.read(1) != b'\x42': 40 | machine.idle() 41 | 42 | if self.uart.read(1) == b'\x4D': 43 | self.__wait_for_data(30) 44 | 45 | try: 46 | data = PMSData.from_bytes(b'\x42\x4D' + self.uart.read(30)) 47 | print('cPM25: {}, cPM10: {}, PM25: {}, PM10: {}' \ 48 | .format(data.cpm25, data.cpm10, data.pm25, data.pm10)) 49 | frames.append(data) 50 | except ValueError as e: 51 | print('error reading frame: {}'.format(e.message)) 52 | pass 53 | 54 | return frames 55 | 56 | 57 | def __wait_for_data(self, byte_count): 58 | def idle_timer(alarm): 59 | pycom.rgbled(0x550000) 60 | 61 | alarm = None 62 | u = self.uart.any() 63 | if u < byte_count: 64 | alarm = machine.Timer.Alarm(idle_timer, 3) 65 | # print('waiting for data {}'.format(str(u))) 66 | while u < byte_count: 67 | u = self.uart.any() 68 | # 32*8*1000/9600 (32 bytes @9600kbps) 69 | # but let's assume byte is 10 bits to skip complex math 70 | machine.Timer.sleep_us(byte_count) 71 | try: 72 | alarm.cancel() 73 | except AttributeError: 74 | pass 75 | pycom.rgbled(0x000000) 76 | # print('data ready {}'.format(str(self.uart.any()))) 77 | 78 | 79 | class PMSData: 80 | def __init__(self, cpm25, cpm10, pm25, pm10): 81 | self.pm25 = pm25 82 | self.pm10 = pm10 83 | self.cpm25 = cpm25 84 | self.cpm10 = cpm10 85 | 86 | 87 | @classmethod 88 | def from_bytes(cls, frame): 89 | 90 | def __sum_of_bytes(bytes): 91 | s = 0 92 | for b in bytes: 93 | s += b 94 | return s 95 | 96 | cpm25 = 0 97 | cpm10 = 0 98 | pm25 = 0 99 | pm10 = 0 100 | control_sum = 0x42 + 0x4d 101 | o = 2 102 | frame_length = int.from_bytes(frame[o:o+2], 'big') 103 | control_sum += __sum_of_bytes(frame[o:o+2]) 104 | o += 2 105 | 106 | if frame_length == 28: 107 | control_sum += __sum_of_bytes(frame[o:o+2]) # cpm1.0 108 | o += 2 109 | 110 | cpm25 = int.from_bytes(frame[o:o+2], 'big') 111 | control_sum += __sum_of_bytes(frame[o:o+2]) 112 | o += 2 113 | 114 | cpm10 = int.from_bytes(frame[o:o+2], 'big') 115 | control_sum += __sum_of_bytes(frame[o:o+2]) 116 | o += 2 117 | 118 | control_sum += __sum_of_bytes(frame[o:o+2]) # pm1.0 119 | o += 2 120 | 121 | pm25 = int.from_bytes(frame[o:o+2], 'big') 122 | control_sum += __sum_of_bytes(frame[o:o+2]) 123 | o += 2 124 | 125 | pm10 = int.from_bytes(frame[o:o+2], 'big') 126 | control_sum += __sum_of_bytes(frame[o:o+2]) 127 | o += 2 128 | 129 | control_sum += __sum_of_bytes(frame[o:o+14]) 130 | o += 14 131 | 132 | control_sum_data = int.from_bytes(frame[o:o+2], 'big') 133 | if control_sum == control_sum_data: 134 | # print('control sum is {}'.format(str(control_sum))) 135 | return cls(cpm25, cpm10, pm25, pm10) 136 | 137 | raise ValueError('control sum is {}, expected {}'.format(str(control_sum_data), str(control_sum))) 138 | -------------------------------------------------------------------------------- /lib/sht1x.py: -------------------------------------------------------------------------------- 1 | from machine import Pin 2 | import utime 3 | import pycom 4 | 5 | class SHT1X: 6 | 7 | class AckException(Exception): 8 | pass 9 | 10 | class CRCException(Exception): 11 | pass 12 | 13 | MEASURE_T = 0b00000011 14 | MEASURE_RH = 0b00000101 15 | SOFT_RESET = 0b00011110 16 | READ_STATUS_REGISTER = 0b000000111 17 | WRITE_STATUS_REGISTER = 0b000000110 18 | 19 | CLOCK_TIME_US = 10 20 | 21 | CRC_TABLE = [ 22 | 0, 49, 98, 83, 196, 245, 166, 151, 185, 136, 219, 234, 125, 76, 31, 46, 23 | 67, 114, 33, 16, 135, 182, 229, 212, 250, 203, 152, 169, 62, 15, 92, 109, 24 | 134, 183, 228, 213, 66, 115, 32, 17, 63, 14, 93, 108, 251, 202, 153, 168, 25 | 197, 244, 167, 150, 1, 48, 99, 82, 124, 77, 30, 47, 184, 137, 218, 235, 26 | 61, 12, 95, 110, 249, 200, 155, 170, 132, 181, 230, 215, 64, 113, 34, 19, 27 | 126, 79, 28, 45, 186, 139, 216, 233, 199, 246, 165, 148, 3, 50, 97, 80, 28 | 187, 138, 217, 232, 127, 78, 29, 44, 2, 51, 96, 81, 198, 247, 164, 149, 29 | 248, 201, 154, 171, 60, 13, 94, 111, 65, 112, 35, 18, 133, 180, 231, 214, 30 | 122, 75, 24, 41, 190, 143, 220, 237, 195, 242, 161, 144, 7, 54, 101, 84, 31 | 57, 8, 91, 106, 253, 204, 159, 174, 128, 177, 226, 211, 68, 117, 38, 23, 32 | 252, 205, 158, 175, 56, 9, 90, 107, 69, 116, 39, 22, 129, 176, 227, 210, 33 | 191, 142, 221, 236, 123, 74, 25, 40, 6, 55, 100, 85, 194, 243, 160, 145, 34 | 71, 118, 37, 20, 131, 178, 225, 208, 254, 207, 156, 173, 58, 11, 88, 105, 35 | 4, 53, 102, 87, 192, 241, 162, 147, 189, 140, 223, 238, 121, 72, 27, 42, 36 | 193, 240, 163, 146, 5, 52, 103, 86, 120, 73, 26, 43, 188, 141, 222, 239, 37 | 130, 179, 224, 209, 70, 119, 36, 21, 59, 10, 89, 104, 255, 206, 157, 172 38 | ] 39 | 40 | def __init__(self, gnd, sck, data, vcc): 41 | self.gnd = gnd 42 | self.sck = sck 43 | self.data = data 44 | self.vcc = vcc 45 | 46 | self.gnd.mode(Pin.OUT) 47 | self.vcc.mode(Pin.OUT) 48 | self.sck.mode(Pin.OUT) 49 | self.data.mode(Pin.OPEN_DRAIN) 50 | 51 | self.gnd.pull(Pin.PULL_DOWN) 52 | self.vcc.pull(Pin.PULL_UP) 53 | self.sck.pull(Pin.PULL_DOWN) 54 | self.data.pull(None) 55 | 56 | self.sleep() 57 | 58 | 59 | def sleep(self): 60 | self.gnd(False) 61 | self.vcc(False) 62 | 63 | 64 | def wake_up(self): 65 | self.gnd(False) 66 | self.vcc(True) 67 | utime.sleep_ms(11) 68 | 69 | 70 | def temperature(self): 71 | readout = self.__send_command(self.MEASURE_T) 72 | return readout/2**14 * 163.8 - 40 73 | 74 | 75 | def humidity(self, temperature=25): 76 | readout = self.__send_command(self.MEASURE_RH) 77 | humidity = -2.0468 + 0.0367*readout - 1.5955e-6*readout**2 78 | if temperature != 25: 79 | humidity += (temperature - 25) * (0.01 + 8e-5 * readout) 80 | return min(humidity, 100) 81 | 82 | 83 | def __send_command(self, command): 84 | self.__command_start() 85 | self.__write_byte(command) 86 | self.__ack_bit() 87 | utime.sleep_ms(330) 88 | 89 | # data should be low when ready to read 90 | if self.data() == True: 91 | raise self.AckException 92 | 93 | msb = self.__read_byte() 94 | lsb = self.__read_byte() 95 | readout = (msb << 8) + lsb 96 | 97 | crc = self.__read_byte() 98 | utime.sleep_ms(11) 99 | computed_crc = self.__crc(command, msb, lsb) 100 | if crc != computed_crc: 101 | print('crc: {}, computed: {}'.format(crc, computed_crc)) 102 | raise self.CRCException 103 | 104 | return readout 105 | 106 | def __crc(self, command, msb, lsb): 107 | crc = self.CRC_TABLE[command] 108 | crc ^= msb 109 | crc = self.CRC_TABLE[crc] 110 | crc ^= lsb 111 | crc = self.CRC_TABLE[crc] 112 | reversed_crc = 0 113 | for pos in range(8): 114 | bit = crc & 1< 0: 18 | try: 19 | client_id = binascii.hexlify(machine.unique_id()) 20 | client = MQTTClient(client_id, 'mqtt.thingspeak.com', user='wipy#1', password=MQTT_API_KEY, port=8883, ssl=True) 21 | client.connect() 22 | client.publish(topic='channels/379710/publish/{}'.format(MQTT_WRITE_API_KEY), msg=thingspeak_data) 23 | client.disconnect() 24 | success = True 25 | except OSError as e: 26 | print('network error: {}'.format(e.errno)) 27 | number_of_retries -= 1 28 | pass 29 | 30 | return success 31 | -------------------------------------------------------------------------------- /lib/urequests.py: -------------------------------------------------------------------------------- 1 | import usocket 2 | 3 | class Response: 4 | 5 | def __init__(self, f): 6 | self.raw = f 7 | self.encoding = "utf-8" 8 | self._cached = None 9 | 10 | def close(self): 11 | if self.raw: 12 | self.raw.close() 13 | self.raw = None 14 | self._cached = None 15 | 16 | @property 17 | def content(self): 18 | if self._cached is None: 19 | try: 20 | self._cached = self.raw.read() 21 | finally: 22 | self.raw.close() 23 | self.raw = None 24 | return self._cached 25 | 26 | @property 27 | def text(self): 28 | return str(self.content, self.encoding) 29 | 30 | def json(self): 31 | import ujson 32 | return ujson.loads(self.content) 33 | 34 | 35 | def request(method, url, data=None, json=None, headers={}, stream=None): 36 | try: 37 | proto, dummy, host, path = url.split("/", 3) 38 | except ValueError: 39 | proto, dummy, host = url.split("/", 2) 40 | path = "" 41 | if proto == "http:": 42 | port = 80 43 | elif proto == "https:": 44 | import ussl 45 | port = 443 46 | else: 47 | raise ValueError("Unsupported protocol: " + proto) 48 | 49 | if ":" in host: 50 | host, port = host.split(":", 1) 51 | port = int(port) 52 | 53 | ai = usocket.getaddrinfo(host, port) 54 | addr = ai[0][-1] 55 | 56 | s = usocket.socket() 57 | try: 58 | s.connect(addr) 59 | if proto == "https:": 60 | s = ussl.wrap_socket(s, server_hostname=host) 61 | s.write(b"%s /%s HTTP/1.0\r\n" % (method, path)) 62 | if not "Host" in headers: 63 | s.write(b"Host: %s\r\n" % host) 64 | # Iterate over keys to avoid tuple alloc 65 | for k in headers: 66 | s.write(k) 67 | s.write(b": ") 68 | s.write(headers[k]) 69 | s.write(b"\r\n") 70 | if json is not None: 71 | assert data is None 72 | import ujson 73 | data = ujson.dumps(json) 74 | if data: 75 | s.write(b"Content-Length: %d\r\n" % len(data)) 76 | s.write(b"\r\n") 77 | if data: 78 | s.write(data) 79 | 80 | l = s.readline() 81 | protover, status, msg = l.split(None, 2) 82 | status = int(status) 83 | #print(protover, status, msg) 84 | while True: 85 | l = s.readline() 86 | if not l or l == b"\r\n": 87 | break 88 | #print(l) 89 | if l.startswith(b"Transfer-Encoding:"): 90 | if b"chunked" in l: 91 | raise ValueError("Unsupported " + l) 92 | elif l.startswith(b"Location:") and not 200 <= status <= 299: 93 | raise NotImplementedError("Redirects not yet supported") 94 | except OSError: 95 | s.close() 96 | raise 97 | finally: 98 | s.close() 99 | 100 | resp = Response(s) 101 | resp.status_code = status 102 | resp.reason = msg.rstrip() 103 | return resp 104 | 105 | 106 | def head(url, **kw): 107 | return request("HEAD", url, **kw) 108 | 109 | def get(url, **kw): 110 | return request("GET", url, **kw) 111 | 112 | def post(url, **kw): 113 | return request("POST", url, **kw) 114 | 115 | def put(url, **kw): 116 | return request("PUT", url, **kw) 117 | 118 | def patch(url, **kw): 119 | return request("PATCH", url, **kw) 120 | 121 | def delete(url, **kw): 122 | return request("DELETE", url, **kw) 123 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pycom 2 | import machine 3 | from machine import Timer, Pin, PWM 4 | from helpers import * 5 | import _thread 6 | import utime 7 | import adc 8 | from sht1x import SHT1X 9 | from pms5003 import PMS5003, PMSData 10 | import persistence 11 | from datapoint import DataPoint 12 | from ds3231 import DS3231 13 | 14 | pycom.heartbeat(False) 15 | 16 | VERSION = '0.7.0' 17 | 18 | 19 | alive_timer = Timer.Chrono() 20 | alive_timer.start() 21 | 22 | def tear_down(timer, initial_time_remaining): 23 | timer.stop() 24 | elapsed_ms = int(timer.read()*1000) 25 | timer.reset() 26 | time_remaining = initial_time_remaining - elapsed_ms 27 | print('sleeping for {}ms ({})'.format(time_remaining, ertc.get_time())) 28 | 29 | # deepsleep_pin = Pin('P10', mode=Pin.IN, pull=Pin.PULL_UP) 30 | # machine.pin_deepsleep_wakeup(pins=[deepsleep_pin], mode=machine.WAKEUP_ALL_LOW, enable_pull=True) 31 | machine.deepsleep(time_remaining) 32 | 33 | 34 | ###################### 35 | # External RTC 36 | ###################### 37 | ertc = DS3231(0, (Pin.module.P21, Pin.module.P20)) 38 | 39 | 40 | ###################### 41 | # T/RH Measurement 42 | ###################### 43 | class AsyncMeasurements: 44 | def __init__(self, voltage=None, temperature=-1, rel_humidity=-1): 45 | self.voltage = voltage 46 | self.temperature = temperature 47 | self.rel_humidity = rel_humidity 48 | 49 | measurements = AsyncMeasurements() 50 | 51 | lock = _thread.allocate_lock() 52 | 53 | def th_func(data): 54 | global lock 55 | global ertc 56 | 57 | lock.acquire() 58 | ertc.get_time(True) 59 | 60 | data.voltage = adc.vbatt() 61 | 62 | # gnd not used 63 | humid = SHT1X(gnd=Pin.module.P3, sck=Pin.module.P23, data=Pin.module.P22, vcc=Pin.module.P19) 64 | humid.wake_up() 65 | try: 66 | data.temperature = humid.temperature() 67 | data.rel_humidity = humid.humidity(data.temperature) 68 | print('temperature: {}, humidity: {}'.format(data.temperature, data.rel_humidity)) 69 | except SHT1X.AckException: 70 | print('ACK exception in temperature meter') 71 | pycom.rgbled(0x443300) 72 | pass 73 | finally: 74 | humid.sleep() 75 | 76 | rtc_synced = pycom.nvs_get('rtc_synced') 77 | if rtc_synced is None: 78 | print ('RTC not synced, syncing now') 79 | wlan = connect_to_WLAN() 80 | setup_rtc() 81 | ertc.save_time() 82 | wlan.deinit() 83 | pycom.nvs_set('rtc_synced', 1) 84 | else: 85 | print('RTC synced: {}'.format(ertc.get_time())) 86 | 87 | lock.release() 88 | 89 | 90 | _thread.start_new_thread(th_func, (measurements,)) 91 | ###################### 92 | 93 | en = Pin('P4', mode=Pin.OUT, pull=Pin.PULL_DOWN) # MOSFET gate 94 | en(True) 95 | 96 | aq_sensor = PMS5003(Pin.module.P8, Pin.module.P10, Pin.module.P11, Pin.module.P9) 97 | aq_sensor.wake_up() 98 | frames = aq_sensor.read_frames(5) 99 | aq_sensor.idle() 100 | 101 | en.value(0) 102 | 103 | cpm25 = 0 104 | cpm10 = 0 105 | pm25 = 0 106 | pm10 = 0 107 | 108 | for data in frames: 109 | cpm25 += data.cpm25 110 | cpm10 += data.cpm10 111 | pm25 += data.pm25 112 | pm10 += data.pm10 113 | 114 | 115 | mean_data = PMSData(cpm25/len(frames), cpm10/len(frames), \ 116 | pm25/len(frames), pm10/len(frames)) 117 | 118 | if lock.locked(): 119 | print('waiting for humidity/temp/voltage reading') 120 | while lock.locked(): 121 | machine.idle() 122 | 123 | time_alive = alive_timer.read_ms() 124 | timestamp = utime.time() 125 | 126 | print('cPM25: {}, cPM10: {}, PM25: {}, PM10: {}, temp: {}, rh: {}, Vbat: {}, time: {}' \ 127 | .format(mean_data.cpm25, mean_data.cpm10, mean_data.pm25, mean_data.pm10, \ 128 | measurements.temperature, measurements.rel_humidity, measurements.voltage, time_alive)) 129 | 130 | datapoint = DataPoint(timestamp=timestamp, pm10=mean_data.pm10, pm25=mean_data.pm25, temperature=measurements.temperature, 131 | humidity=measurements.rel_humidity, voltage=measurements.voltage, duration=time_alive, version=VERSION) 132 | 133 | # store datapoints, and if sent, the RTC was synced so update the external RTC 134 | if persistence.store_datapoint(datapoint) is True: 135 | ertc.save_time() 136 | 137 | # sleep for 10 minutes - 2 seconds :) 138 | tear_down(alive_timer,598*1000) 139 | -------------------------------------------------------------------------------- /pymakr.conf: -------------------------------------------------------------------------------- 1 | { 2 | "address": "/dev/cu.usbserial-A9U1TFV3", 3 | "username": "micro", 4 | "password": "python", 5 | "sync_folder": "", 6 | "open_on_start": true, 7 | "sync_file_types": "py,log,json,xml", 8 | "ctrl_c_on_connect": true 9 | } 10 | --------------------------------------------------------------------------------