adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.08k stars 1.2k forks source link

Support Sensirion SPS30 Particulate Matter Sensor (Sparkfun SEN-15103) #4323

Closed mew-cx closed 2 years ago

mew-cx commented 3 years ago

I'm looking for a circuitpython library for the Sensirion SPS30, sold by Sparkfun:

I haven't found one, so planning to start development myself:

These seem good places to borrow code:

tannewt commented 3 years ago

Great! Thanks for working on it. Another good reference for standard sensor property names and units is: https://circuitpython.readthedocs.io/en/latest/docs/design_guide.html#sensor-properties-and-units

tannewt commented 3 years ago

Library creation is covered here: https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library

kevinjwalters commented 3 years ago

@mew-cx How far did you get with this? Were you thinking UART or i2c or both?

>>> ["{:02x}".format(x) for x in i2c.scan()]
['08', '23', '69', '76']
kevinjwalters commented 3 years ago

https://github.com/Sensirion/embedded-sps Might be useful too, for (i2c) comparison.

mew-cx commented 3 years ago

Hi @kevinjwalters, thanks for reaching out. It seems that creating this issue basically caused my time to work on it to evaporate. However, wildfire season is fast approaching, so I'll prob be reprioritizing.

My interest and work to date have been on the UART side. I think I saw on the SparkFun site the UART interface provided more complete data than I2C, tho I haven't confirmed.

I'm a C++ guy, haven't done much w Python, this was a learning project. I have the Sensirion code tweaked a bit, the firmware in my device is newer than their example code.

I'd certainly welcome any collaboration.

kevinjwalters commented 3 years ago

I'm certainly familiar with the finite amount of time issue! I saw the SparkFun comment too on https://www.sparkfun.com/products/15103 too (retrieved 25-May-2021):

Sensirion has written drivers for both the UART protocol and I2C. Unfortunately we've found the I2C is limited to only mass concentrations (not number concentrations) using the Arduino platform. So if you plan to use this sensor with an Arduino, use the UART interface. Both interfaces are described in their datasheet.

Re-reading that I now wonder if it's specifically about Sensirion's drivers and not the physical device? Hopefully, that's the case.

I have a few more days of code tidying and documentating for my current project then I'll have a look at the SPS-30. I'll keep this ticket updated with any work.

kevinjwalters commented 3 years ago

FYI, I've got about half of an i2c library now:

Adafruit CircuitPython 6.2.0 on 2021-04-05; Adafruit Feather nRF52840 Express with nRF52840
>>>
>>> import board
>>> import adafruit_sps30.sps30_i2c
>>> i2c = board.I2C()
>>> spsi2c = adafruit_sps30.sps30_i2c.SPS30_I2C(i2c)
>>> spsi2c.fw_version
(2, 2)
>>> spsi2c.read()
{'particles 40um': 51, 'particles 10um': 49, 'pm10 standard': 6, 'pm100 standard': 9, 'pm25 standard': 8, 'particles 25um': 50, 'particles 100um': 51, 'particles 05um': 41, 'tps': 576, 'pm40 standard': 9}
>>> spsi2c.read()
{'particles 40um': 41, 'particles 10um': 41, 'pm10 standard': 5, 'pm100 standard': 6, 'pm25 standard': 6, 'particles 25um': 41, 'particles 100um': 41, 'particles 05um': 35, 'tps': 632, 'pm40 standard': 6}

I don't have an immediate need for the uart side of things but there's some degree of overlap.

Are there any existing Adafruit implementations of Sensirion's SHDLC protocol?

ladyada commented 3 years ago

not that we know of! we've only used i2c? good luck!

kevinjwalters commented 3 years ago

I've just noticed @Jacksonbaker323 has an i2c library https://github.com/Jacksonbaker323/CircuitPython_SPS30/ - that one doesn't follow the style of https://github.com/adafruit/Adafruit_CircuitPython_PM25

I'm tidying the code for my library in the mold of the Adafruit_CircuitPython_PM25 library, I've not implemented the UART version yet...

kevinjwalters commented 3 years ago

Has the basic functionality for i2c: https://github.com/kevinjwalters/Adafruit_CircuitPython_SPS30

kevinjwalters commented 3 years ago

The odd comment on Sparkfun site

Sensirion has written drivers for both the UART protocol and I2C. Unfortunately we've found the I2C is limited to only mass concentrations (not number concentrations) using the Arduino platform. So if you plan to use this sensor with an Arduino, use the UART interface. Both interfaces are described in their datasheet.

might be explained by this

https://github.com/Sensirion/arduino-sps/blob/df6346ab6485410963adf24164e955456383dd81/examples/sps30/sps30.ino#L44-L51 which cites https://github.com/Sensirion/arduino-sps#esp8266-partial-legacy-support

mew-cx commented 3 years ago

Hi @kevinjwalters I have some time for the UART implementation. I see you have a repo going, I'll have a look. Just curious: do you have SHDLC encapsulated?

kevinjwalters commented 3 years ago

No, I've focussed on i2c and I've not done any SHDLC but I'd imagine there's a lot of code out there for inspiration. Might be worth sticking to a small code / lowish memory approach to make the library useful on the low end boards like SAMD21.

On i2c front my SPS30 works well for returing 16 bit integers but I can't get it (running V2.2 firmware) to go into floating point mode. I'm probably doing something daft...

kevinjwalters commented 3 years ago

After too many hours of fiddling I have discovered the issue with integer and floating-point data selection. The argument for data type in the SPS30 start command is only used by the sensor after power-up - the value is retained regardless of subsequent start commands while the device is powered. A stop then a start works to change data type, I'm going to change my code to do that.

kevinjwalters commented 3 years ago

I've also found it's easy to knock the (Sparkfun) cable loose or out of the SPS30.

kevinjwalters commented 3 years ago

FYI, I put a few observations about datasheet, behaviour and Sensirion libs in https://github.com/Sensirion/embedded-sps/issues/15#issuecomment-877713715

I also need add support for some of the other functions like cleaning and review the style of interface as lots of the Adafruit libraries use properties (rather than methods) for values fetched over i2c/similar.

Jacksonbaker323 commented 3 years ago

Sorry, just now seeing this. Happy to try to remember what I learned about this sensor. At one point last year I basically had the datasheet memorized 😄

Looking forward to seeing your library completed!

mew-cx commented 3 years ago

Hi @kevinjwalters From my reading of your linked comments above, what I'm getting is the data limitations of the I2C implementation were to accommodate the ESP's memory constraints, rather than limits of the SPS itself. If that's true, are you able to query the full data from the SPS over I2C? If so, then I have no real need for a UART implementation, and I can use your I2C directly. Thanks much for your efforts!

kevinjwalters commented 3 years ago

Yes, it all comes back and looks plausible over i2c. Here's a run I did last week, I think it's documented as taking a while for numbers to stabilise on first start.

>>> import sps30_simpletest
Found SPS30 sensor, reading data...

Concentration Units (standard)
---------------------------------------
PM 1.0: 3       PM2.5: 11       PM10: 21
Concentration Units (number count)
---------------------------------------
Particles 0.3-0.5um / cm3: 0
Particles 0.3-1.0um / cm3: 14
Particles 0.3-2.5um / cm3: 23
Particles 0.3-4.0um / cm3: 25
Particles 0.3-10.0um / cm3: 25
---------------------------------------

Concentration Units (standard)
---------------------------------------
PM 1.0: 5       PM2.5: 8        PM10: 12
Concentration Units (number count)
---------------------------------------
Particles 0.3-0.5um / cm3: 29
Particles 0.3-1.0um / cm3: 39
Particles 0.3-2.5um / cm3: 42
Particles 0.3-4.0um / cm3: 43
Particles 0.3-10.0um / cm3: 43
---------------------------------------

The ESP8266/Uno thing appears to be down to the fixed size of 32 bytes on many of the "small" microcontrollers for the standard i2c receive buffer size. The size for reading data is 40 bytes (16 bit integer) or 60 bytes (32 bit floating point) for SPS30.

kevinjwalters commented 3 years ago

I'm finding the (default for device) floating point mode a bit flaky with my setup of a Feather nRF52840 attached to an Pimoroni Enviro+ doing nothing and that's on a breadboard with 15cm wires over to SPS30 and 10k pull-ups and there's some dangling 25cm unconnected wires (from previous probing). It'll give CRC errors after maybe 10-30 reads. The integer mode (current default for library which I'm pondering changing) is more robust but just gave an error at the 386th read.

The data length is different (40 -> 60) but I can't see any reason for the gross difference here between quick failure on floating-point and rock solid integers. I think I'm going to struggle to capture this as the buffer size on my IkaLogic SQ25 isn't vast.

I'll try removing some things and maybe lower 2.2k pull ups but there must be some on the Pimoroni board...

I noticed this while running the new https://github.com/kevinjwalters/Adafruit_CircuitPython_SPS30/blob/master/examples/sps30_test.py

kevinjwalters commented 3 years ago

I'm using just nRF52840 2k pull-ups and SPS30 and it's working fine for over an hour reading fp values about once a second at i2c 100kHz.

kevinjwalters commented 3 years ago

Turns out the SparkFun cable is fine, I hadn't realised it needed a much firmer push to go in. My i2c reliability has increased since then.

I've put all the functionality now in the code (for i2c) so you can put it to sleep and set/use the fan cleaning mode.

@mew-cx I believe the data sheet's statement of PM4 and PM10 output values are calculated based on distribution profile of all measured particles. can be translated as those values are guessed based on PM1 & PM2.5 values. The testing in https://amt.copernicus.org/articles/13/2413/2020/amt-13-2413-2020.pdf (page 2420) shows the Sensirion SPS30 can't sense anything above about 2um. Also shows the Plantower PMS5003 is even worse.

I've just got an Omron B5W LD0101 which appears to be the only cheap sensor that can reliably sense larger particles. It's marked as being discontinued in 2022, not clear if there's a replacement planned. A curious difference is it uses a heater (low value resistor!) for convection driven airflow.

kevinjwalters commented 3 years ago
Adafruit CircuitPython 6.3.0 on 2021-06-01; Adafruit Feather nRF52840 Express with nRF52840
>>>
soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.

code.py output:

Reminder: tps units are different between integer and floating-point modes

Sleeping for 20 seconds
BEGIN TEST sps30_test version 1.2
Creating SPS30_I2C defaults
Firmware version: 2.2
Six reads in integer mode
PM1     PM2.5   PM4     PM10
4       12      18      21
1       1       2       2
1       1       2       2
1       2       2       2
1       2       2       2
1       2       2       2
ALL for last read
pm10 standard: 1
pm25 standard: 2
pm40 standard: 2
pm100 standard: 2
particles 05um: 5
particles 10um: 7
particles 25um: 8
particles 40um: 8
particles 100um: 8
tps: 873
Creating SPS30_I2C fp_mode=True
Six reads in default floating-point mode
PM1     PM2.5   PM4     PM10
1.01833 3.71968 5.9602  7.06157
1.01722 3.50825 5.57051 6.58427
0.977043        2.73538 4.17846 4.88783
1.00557 2.70296 4.09298 4.77627
1.00557 2.70296 4.09298 4.77627
1.00557 2.70296 4.09298 4.77627
ALL for last read
pm10 standard: 1.00557
pm25 standard: 2.70296
pm40 standard: 4.09298
pm100 standard: 4.77627
particles 05um: 2.33434
particles 10um: 5.93574
particles 25um: 7.71367
particles 40um: 8.08145
particles 100um: 8.14025
tps: 1.33523
Stop and wait 10 seconds
Start and wait for data to become available
Time since start:  0.992188
Data available: True
Six more reads
PM1     PM2.5   PM4     PM10
1.63452 3.51789 5.03494 5.78068
0.796107        1.31297 1.71236 1.90869
0.807277        1.29327 1.66595 1.84915
0.866612        1.64402 2.26086 2.56408
0.866612        1.64402 2.26086 2.56408
0.866612        1.64402 2.26086 2.56408
ALL for last read
pm10 standard: 0.866612
pm25 standard: 1.64402
pm40 standard: 2.26086
pm100 standard: 2.56408
particles 05um: 3.97353
particles 10um: 5.98317
particles 25um: 6.77801
particles 40um: 6.94238
particles 100um: 6.96909
tps: 1.08334
Reset (goes to idle mode)
Start
Six reads after reset+start
PM1     PM2.5   PM4     PM10
0.0     0.0     0.0     0.0
4.92805 11.9677 17.6956 20.5113
4.47865 8.88499 12.4024 14.1314
4.39735 8.92435 12.548  14.3293
4.39735 8.92435 12.548  14.3293
4.39735 8.92435 12.548  14.3293
ALL for last read
pm10 standard: 4.39735
pm25 standard: 8.92435
pm40 standard: 12.548
pm100 standard: 14.3293
particles 05um: 18.4958
particles 10um: 29.6226
particles 25um: 34.2822
particles 40um: 35.2459
particles 100um: 35.4018
tps: 1.03518
Stop / Sleep / 10 second pause / Wake-up / Start
Six reads after wakeup and start
PM1     PM2.5   PM4     PM10
4.39735 8.92435 12.548  14.3293
3.80971 6.09764 7.85169 8.71394
3.8881  7.03594 9.51517 10.7339
3.88332 7.01609 9.48278 10.6953
3.88332 7.01609 9.48278 10.6953
3.88332 7.01609 9.48278 10.6953
ALL for last read
pm10 standard: 3.88332
pm25 standard: 7.01609
pm40 standard: 9.48278
pm100 standard: 10.6953
particles 05um: 18.8096
particles 10um: 27.2549
particles 25um: 30.4392
particles 40um: 31.0976
particles 100um: 31.2051
tps: 1.06479
Six more reads after wakeup and start
PM1     PM2.5   PM4     PM10
3.88332 7.01609 9.48278 10.6953
3.51544 5.03977 6.16079 6.71185
3.32966 4.39655 5.13882 5.5037
3.26342 4.19544 4.8266  5.13686
3.26342 4.19544 4.8266  5.13686
3.26342 4.19544 4.8266  5.13686
ALL for last read
pm10 standard: 3.26342
pm25 standard: 4.19544
pm40 standard: 4.8266
pm100 standard: 5.13686
particles 05um: 20.6744
particles 10um: 25.057
particles 25um: 25.9034
particles 40um: 26.0781
particles 100um: 26.109
tps: 0.705884
Fan clean (the speed up is audible)
ccccccccccccccccccc.
Six reads after clean
PM1     PM2.5   PM4     PM10
3.26342 4.19544 4.8266  5.13686
2.92918 3.56165 3.95514 4.14857
2.82149 3.45845 3.86099 4.05887
2.7585  3.29595 3.61721 3.77513
2.7585  3.29595 3.61721 3.77513
2.7585  3.29595 3.61721 3.77513
ALL for last read
pm10 standard: 2.7585
pm25 standard: 3.29595
pm40 standard: 3.61721
pm100 standard: 3.77513
particles 05um: 18.1922
particles 10um: 21.497
particles 25um: 21.9431
particles 40um: 22.0351
particles 100um: 22.0524
tps: 0.681176
END TEST

Code done running.
kevinjwalters commented 2 years ago

Lost my Adafruit IO virginity over the weekend. We have fireworks in the UK and that was a good excluse to get the three particulate matter sensors outside to measure air quality: https://io.adafruit.com/kevinjwalters/dashboards/maker-pi-pico-particulate-matter-sensors

mew-cx commented 2 years ago

As the original creator of this issue, I'm looping back to close this issue and happily use the work of @kevinjwalters at https://github.com/kevinjwalters/Adafruit_CircuitPython_SPS30