diff --git a/doc/node_help/commands.rst b/doc/node_help/commands.rst index d2a93bcc..3d217f6d 100644 --- a/doc/node_help/commands.rst +++ b/doc/node_help/commands.rst @@ -21,6 +21,7 @@ Alphabetic list - `display44780 `_ - `display `_ - `do_later `_ +- `dsm501a `_ - `ds18b20 `_ - `edge_counter `_ - `ens16x `_ diff --git a/doc/node_help/dsm501a.rst b/doc/node_help/dsm501a.rst new file mode 100644 index 00000000..daf1aeee --- /dev/null +++ b/doc/node_help/dsm501a.rst @@ -0,0 +1,43 @@ +dsm501a +======== + +.. code-block:: cpp + + dsm501a(name, pm1_pin, pm25_pin); + +Create a new DSM501A dust sensor device. + +The sensor exposes two pulse outputs. The driver measures low-pulse occupancy +for a 30 second sample window and publishes estimated concentrations for both +channels as separate subtopics. + +Parameters +---------- + +- ``name``: the name it can be addressed via MQTT in the network. + Inside the code it can be addressed via IN(name). + +- ``pm1_pin``: where the DSM501A PM1 output pin is connected to + +- ``pm25_pin``: where the DSM501A PM2.5 output pin is connected to + +Outputs +------- + +- ``pm1``: estimated PM1 concentration from the PM1 output. +- ``pm25``: estimated PM2.5 concentration from the PM2.5 output. + +The driver uses a fixed 30 second measurement window, matching the common +DSM501A low-pulse occupancy calculation pattern. + +Example +------- + +.. code-block:: cpp + + dsm501a(dust, D6, D5); + +In this example: + +- ``D6`` reads the DSM501A ``PM1`` output +- ``D5`` reads the DSM501A ``PM2.5`` output diff --git a/lib/node_types/esp/devices.ini b/lib/node_types/esp/devices.ini index fb21ea75..5aaa2e0c 100644 --- a/lib/node_types/esp/devices.ini +++ b/lib/node_types/esp/devices.ini @@ -102,6 +102,10 @@ lib = milesburton/DallasTemperature@^4.0.5,https://github.com/sivar2311/OneWire ; stoffregen's latest 2.3.8 does not compile due to a wrong #endif - fixed in sivar2311/OneWire fork depends = dht11 +[dsm501a] +filename = dsm501a +class = DSM501A + [bmp180] aliases = bmp085 filename = barometer_bmp180 diff --git a/lib/node_types/esp/src/dev_dsm501a.cpp b/lib/node_types/esp/src/dev_dsm501a.cpp new file mode 100644 index 00000000..8b05c8eb --- /dev/null +++ b/lib/node_types/esp/src/dev_dsm501a.cpp @@ -0,0 +1,95 @@ +// dsm501a.cpp + +// Important to include Functional Interrupt as else it does not allow to +// define a class-based (object specific) interrupt. +#include +#include "dev_dsm501a.h" + +DSM501A::DSM501A(const char* name, uint8_t pm1_pin, uint8_t pm25_pin) : + Device(name, 500000) { + _pm1_pin = pm1_pin; + _pm25_pin = pm25_pin; + add_subdevice(new Subdevice(F("pm1"))); + add_subdevice(new Subdevice(F("pm25"))); +} + +void DSM501A::pm1_changed() { + uint32_t now_us = micros(); + if(digitalRead(_pm1_pin) == LOW) { + if(!_pm1_low_active) { + _pm1_low_active = true; + _pm1_low_started_us = now_us; + } + } else if(_pm1_low_active) { + _pm1_lowpulse_us += now_us - _pm1_low_started_us; + _pm1_low_active = false; + } +} + +void DSM501A::pm25_changed() { + uint32_t now_us = micros(); + if(digitalRead(_pm25_pin) == LOW) { + if(!_pm25_low_active) { + _pm25_low_active = true; + _pm25_low_started_us = now_us; + } + } else if(_pm25_low_active) { + _pm25_lowpulse_us += now_us - _pm25_low_started_us; + _pm25_low_active = false; + } +} + +float DSM501A::calculate_concentration(uint32_t lowpulse_us) const { + float ratio = (float)lowpulse_us / ((float)_sampletime_ms * 10.0f); + float concentration = 0.001915f * ratio * ratio + 0.09522f * ratio - 0.04884f; + if(concentration < 0.0f) concentration = 0.0f; + return concentration; +} + +void DSM501A::start() { + pinMode(_pm1_pin, INPUT); + pinMode(_pm25_pin, INPUT); + _sample_started_ms = millis(); + + attachInterrupt(digitalPinToInterrupt(_pm1_pin), [&] () { pm1_changed(); }, CHANGE); + attachInterrupt(digitalPinToInterrupt(_pm25_pin), [&] () { pm25_changed(); }, CHANGE); + + _started = true; +} + +bool DSM501A::measure() { + uint32_t now_ms = millis(); + if((uint32_t)(now_ms - _sample_started_ms) < _sampletime_ms) { + return false; + } + + // Snapshot and reset both low-pulse accumulators at the sample boundary. + // If a pulse is still active, split it at "now" so the next window keeps + // measuring seamlessly without losing time. + uint32_t pm1_lowpulse_us = 0; + uint32_t pm25_lowpulse_us = 0; + uint32_t now_us = micros(); + + noInterrupts(); + pm1_lowpulse_us = _pm1_lowpulse_us; + pm25_lowpulse_us = _pm25_lowpulse_us; + + if(_pm1_low_active) { + pm1_lowpulse_us += now_us - _pm1_low_started_us; + _pm1_low_started_us = now_us; + } + if(_pm25_low_active) { + pm25_lowpulse_us += now_us - _pm25_low_started_us; + _pm25_low_started_us = now_us; + } + + _pm1_lowpulse_us = 0; + _pm25_lowpulse_us = 0; + interrupts(); + + _sample_started_ms = now_ms; + + value(0).printf("%.3f", calculate_concentration(pm1_lowpulse_us)); + value(1).printf("%.3f", calculate_concentration(pm25_lowpulse_us)); + return true; +} diff --git a/lib/node_types/esp/src/dev_dsm501a.h b/lib/node_types/esp/src/dev_dsm501a.h new file mode 100644 index 00000000..7faf81d6 --- /dev/null +++ b/lib/node_types/esp/src/dev_dsm501a.h @@ -0,0 +1,36 @@ +// dsm501a.h +// DSM501A dust sensor using low-pulse occupancy on two output pins + +#ifndef _IOTEMPOWER_DSM501A_H_ +#define _IOTEMPOWER_DSM501A_H_ + +#include +#include + +#define DSM501A_DEFAULT_SAMPLETIME_MS 30000 + +class DSM501A : public Device { + private: + uint8_t _pm1_pin; + uint8_t _pm25_pin; + uint32_t _sampletime_ms = DSM501A_DEFAULT_SAMPLETIME_MS; + uint32_t _sample_started_ms = 0; + + volatile uint32_t _pm1_lowpulse_us = 0; + volatile uint32_t _pm25_lowpulse_us = 0; + volatile uint32_t _pm1_low_started_us = 0; + volatile uint32_t _pm25_low_started_us = 0; + volatile bool _pm1_low_active = false; + volatile bool _pm25_low_active = false; + + void pm1_changed(); + void pm25_changed(); + float calculate_concentration(uint32_t lowpulse_us) const; + + public: + DSM501A(const char* name, uint8_t pm1_pin, uint8_t pm25_pin); + void start() override; + bool measure() override; +}; + +#endif // _IOTEMPOWER_DSM501A_H_ diff --git a/tests/conf_data.py b/tests/conf_data.py index 62dfa69b..3cc963e0 100644 --- a/tests/conf_data.py +++ b/tests/conf_data.py @@ -70,6 +70,7 @@ {"device_name": "serial_hex", "example_syntax": "serial_hex(example_name, IOT_TEST_DIGITAL, IOT_TEST_DIGITAL_2);"}, {"device_name": "soil_7in1", "example_syntax": "soil_7in1(example_name, IOT_TEST_DIGITAL, IOT_TEST_DIGITAL_2);"}, {"device_name": "gmp343", "example_syntax": "gmp343(example_name, IOT_TEST_DIGITAL, IOT_TEST_DIGITAL_2);"}, + {"device_name": "dsm501a", "example_syntax": "dsm501a(example_name, IOT_TEST_DIGITAL, IOT_TEST_DIGITAL_2);"}, ] isolated_combinations_to_test = [