From 16c932962a60c72296af04c1b9642d304359cc9c Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 00:10:08 +0700 Subject: [PATCH 01/15] Handle pm correction algorithm from ag server config --- src/AgConfigure.cpp | 114 ++++++++++++++++++++++++++++++++++++++++++++ src/AgConfigure.h | 13 +++++ src/App/AppDef.h | 12 +++++ 3 files changed, 139 insertions(+) diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 1446d1a..5e438af 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -22,6 +22,18 @@ const char *LED_BAR_MODE_NAMES[] = { [LedBarModeCO2] = "co2", }; +const char *PM_CORRECTION_ALGORITHM_NAMES[] = { + [Unknown] = "-", // This is only to pass "non-trivial designated initializers" error + [None] = "none", + [EPA_2021] = "epa_2021", + [SLR_PMS5003_20220802] = "slr_PMS5003_20220802", + [SLR_PMS5003_20220803] = "slr_PMS5003_20220803", + [SLR_PMS5003_20220824] = "slr_PMS5003_20220824", + [SLR_PMS5003_20231030] = "slr_PMS5003_20231030", + [SLR_PMS5003_20231218] = "slr_PMS5003_20231218", + [SLR_PMS5003_20240104] = "slr_PMS5003_20240104", +}; + #define JSON_PROP_NAME(name) jprop_##name #define JSON_PROP_DEF(name) const char *JSON_PROP_NAME(name) = #name @@ -87,6 +99,96 @@ String Configuration::getLedBarModeName(LedBarMode mode) { return String("unknown"); } +PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) { + // Loop through all algorithm names in the PM_CORRECTION_ALGORITHM_NAMES array + // If the input string matches an algorithm name, return the corresponding enum value + // Else return Unknown + for (int idx = 0; + idx < sizeof(PM_CORRECTION_ALGORITHM_NAMES) / sizeof(PM_CORRECTION_ALGORITHM_NAMES[0]); + idx++) { + if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[idx]) { + return (PMCorrectionAlgorithm)idx; + } + } + + return Unknown; +} + +bool Configuration::parsePmCorrection(JSONVar &json) { + if (!json.hasOwnProperty("corrections")) { + // TODO: need to response message? + Serial.println("corrections not found"); + return false; + } + + JSONVar corrections = json["corrections"]; + if (!corrections.hasOwnProperty("pm02")) { + Serial.println("pm02 not found"); + return false; + } + + JSONVar pm02 = corrections["pm02"]; + if (!pm02.hasOwnProperty("correctionAlgorithm")) { + Serial.println("correctionAlgorithm not found"); + return false; + } + + // Check algorithm + const char *algorithm = (const char *)pm02["correctionAlgorithm"]; + PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm); + if (algo == Unknown) { + Serial.println("Unknown algorithm"); + return false; + } + Serial.printf("Correction algorithm: %s\n", algorithm); + + // If algo is None or EPA_2021, no need to check slr + // But first check if pmCorrection different from algo + if (algo == None || algo == EPA_2021) { + if (pmCorrection.algorithm != algo) { + pmCorrection.algorithm = algo; + pmCorrection.changed = true; + Serial.println("PM2.5 correction updated"); + return true; + } + + return false; + } + + // Check if pm02 has slr object + if (!pm02.hasOwnProperty("slr")) { + Serial.println("slr not found"); + return false; + } + + JSONVar slr = pm02["slr"]; + + // Validate required slr properties exist + if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor") || + !slr.hasOwnProperty("useEpa2021")) { + Serial.println("Missing required slr properties"); + return false; + } + + // Compare with current pmCorrection + if (pmCorrection.algorithm == algo && pmCorrection.intercept == (double)slr["intercept"] && + pmCorrection.scalingFactor == (double)slr["scalingFactor"] && + pmCorrection.useEPA == (bool)slr["useEpa2021"]) { + return false; // No changes needed + } + + // Update pmCorrection with new values + pmCorrection.algorithm = algo; + pmCorrection.intercept = (double)slr["intercept"]; + pmCorrection.scalingFactor = (double)slr["scalingFactor"]; + pmCorrection.useEPA = (bool)slr["useEpa2021"]; + pmCorrection.changed = true; + + // Correction values were updated + Serial.println("PM2.5 correction updated"); + return true; +} + /** * @brief Save configure to device storage (EEPROM) * @@ -171,6 +273,13 @@ void Configuration::defaultConfig(void) { jconfig[jprop_offlineMode] = jprop_offlineMode_default; jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default; + // PM2.5 correction + pmCorrection.algorithm = None; + pmCorrection.changed = false; + pmCorrection.intercept = -1; + pmCorrection.scalingFactor = -1; + pmCorrection.useEPA = false; + saveConfig(); } @@ -667,6 +776,11 @@ bool Configuration::parse(String data, bool isLocal) { } } + // Corrections + if (parsePmCorrection(root)) { + changed = true; + } + if (changed) { udpated = true; saveConfig(); diff --git a/src/AgConfigure.h b/src/AgConfigure.h index a899b64..3a7be13 100644 --- a/src/AgConfigure.h +++ b/src/AgConfigure.h @@ -5,8 +5,18 @@ #include "Main/PrintLog.h" #include "AirGradient.h" #include +#include "Libraries/Arduino_JSON/src/Arduino_JSON.h" class Configuration : public PrintLog { +public: + struct PMCorrection { + PMCorrectionAlgorithm algorithm; + int intercept; + int scalingFactor; + bool useEPA; // EPA 2021 + bool changed; + }; + private: bool co2CalibrationRequested; bool ledBarTestRequested; @@ -19,10 +29,13 @@ private: String otaNewFirmwareVersion; bool _offlineMode = false; bool _ledBarModeChanged = false; + PMCorrection pmCorrection; AirGradient* ag; String getLedBarModeName(LedBarMode mode); + PMCorrectionAlgorithm matchPmAlgorithm(String algorithm); + bool parsePmCorrection(JSONVar &json); void saveConfig(void); void loadConfig(void); void defaultConfig(void); diff --git a/src/App/AppDef.h b/src/App/AppDef.h index 3bb9320..1e114e7 100644 --- a/src/App/AppDef.h +++ b/src/App/AppDef.h @@ -94,6 +94,18 @@ enum ConfigurationControl { ConfigurationControlBoth }; +enum PMCorrectionAlgorithm { + Unknown, // Unknown algorithm + None, // No PM correction + EPA_2021, + SLR_PMS5003_20220802, + SLR_PMS5003_20220803, + SLR_PMS5003_20220824, + SLR_PMS5003_20231030, + SLR_PMS5003_20231218, + SLR_PMS5003_20240104, +}; + enum AgFirmwareMode { FW_MODE_I_9PSL, /** ONE_INDOOR */ FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */ From ea46b812c13403d7c6bfa47173deee718849ab27 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 00:53:33 +0700 Subject: [PATCH 02/15] Handle saving back to eeprom rename the function --- src/AgConfigure.cpp | 17 ++++++++++++++--- src/AgConfigure.h | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 5e438af..16f91b0 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -1,5 +1,4 @@ #include "AgConfigure.h" -#include "Libraries/Arduino_JSON/src/Arduino_JSON.h" #if ESP32 #include "FS.h" #include "SPIFFS.h" @@ -54,6 +53,7 @@ JSON_PROP_DEF(co2CalibrationRequested); JSON_PROP_DEF(ledBarTestRequested); JSON_PROP_DEF(offlineMode); JSON_PROP_DEF(monitorDisplayCompensatedValues); +JSON_PROP_DEF(corrections); #define jprop_model_default "" #define jprop_country_default "TH" @@ -114,7 +114,7 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) { return Unknown; } -bool Configuration::parsePmCorrection(JSONVar &json) { +bool Configuration::updatePmCorrection(JSONVar &json) { if (!json.hasOwnProperty("corrections")) { // TODO: need to response message? Serial.println("corrections not found"); @@ -777,7 +777,9 @@ bool Configuration::parse(String data, bool isLocal) { } // Corrections - if (parsePmCorrection(root)) { + if (updatePmCorrection(root)) { + // Deep copy corrections from root to jconfig, so it will be saved later + jconfig[jprop_corrections] = JSON.parse(JSON.stringify(root["corrections"])); changed = true; } @@ -1232,6 +1234,15 @@ void Configuration::toConfig(const char *buf) { jprop_monitorDisplayCompensatedValues_default; } + + // Set default first before parsing local config + pmCorrection.algorithm = PMCorrectionAlgorithm::None; + pmCorrection.intercept = 0; + pmCorrection.scalingFactor = 0; + pmCorrection.useEPA = false; + // Load correction from saved config + updatePmCorrection(jconfig); + if (changed) { saveConfig(); } diff --git a/src/AgConfigure.h b/src/AgConfigure.h index 3a7be13..58d0ee8 100644 --- a/src/AgConfigure.h +++ b/src/AgConfigure.h @@ -35,7 +35,7 @@ private: String getLedBarModeName(LedBarMode mode); PMCorrectionAlgorithm matchPmAlgorithm(String algorithm); - bool parsePmCorrection(JSONVar &json); + bool updatePmCorrection(JSONVar &json); void saveConfig(void); void loadConfig(void); void defaultConfig(void); From 0275aee370b69624398dc7005c334eee21b8035f Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 10:34:35 +0700 Subject: [PATCH 03/15] Copy correction object to jconfig --- src/AgConfigure.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 16f91b0..7d311e1 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -146,6 +146,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) { // But first check if pmCorrection different from algo if (algo == None || algo == EPA_2021) { if (pmCorrection.algorithm != algo) { + jconfig[jprop_corrections]["pm02"]["correctionAlgorithm"] = algorithm; pmCorrection.algorithm = algo; pmCorrection.changed = true; Serial.println("PM2.5 correction updated"); @@ -177,6 +178,10 @@ bool Configuration::updatePmCorrection(JSONVar &json) { return false; // No changes needed } + // Deep copy corrections from root to jconfig, so it will be saved later + jconfig[jprop_corrections] = corrections; + Serial.println("Correction copied"); + // Update pmCorrection with new values pmCorrection.algorithm = algo; pmCorrection.intercept = (double)slr["intercept"]; @@ -778,8 +783,6 @@ bool Configuration::parse(String data, bool isLocal) { // Corrections if (updatePmCorrection(root)) { - // Deep copy corrections from root to jconfig, so it will be saved later - jconfig[jprop_corrections] = JSON.parse(JSON.stringify(root["corrections"])); changed = true; } From 641003f9d148b94b9010d694403d9dec58e991df Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 10:41:01 +0700 Subject: [PATCH 04/15] Get pm config function --- src/AgConfigure.cpp | 10 ++++++++++ src/AgConfigure.h | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 7d311e1..0a6a2cf 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -1344,3 +1344,13 @@ String Configuration::newFirmwareVersion(void) { otaNewFirmwareVersion = String(""); return newFw; } + +bool Configuration::isPMCorrectionChanged(void) { + bool changed = pmCorrection.changed; + pmCorrection.changed = false; + return changed; +} + +PMCorrection Configuration::getPMCorrection(void) { + return pmCorrection; +} diff --git a/src/AgConfigure.h b/src/AgConfigure.h index 58d0ee8..94c2606 100644 --- a/src/AgConfigure.h +++ b/src/AgConfigure.h @@ -96,6 +96,8 @@ public: void setOfflineModeWithoutSave(bool offline); bool isLedBarModeChanged(void); bool isMonitorDisplayCompensatedValues(void); + bool isPMCorrectionChanged(void); + PMCorrection getPMCorrection(void); }; #endif /** _AG_CONFIG_H_ */ From a98d77e0c3694197bf8cfc57cd4dd28a56b353bc Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 11:02:36 +0700 Subject: [PATCH 05/15] slr pm2.5 correction implementation --- src/PMS/PMS.cpp | 30 ++++++++++++++++++++++++++++++ src/PMS/PMS.h | 1 + src/PMS/PMS5003.cpp | 4 ++++ src/PMS/PMS5003.h | 1 + src/PMS/PMS5003T.cpp | 4 ++++ src/PMS/PMS5003T.h | 1 + 6 files changed, 41 insertions(+) diff --git a/src/PMS/PMS.cpp b/src/PMS/PMS.cpp index 88f17bb..feeb4b4 100644 --- a/src/PMS/PMS.cpp +++ b/src/PMS/PMS.cpp @@ -314,6 +314,36 @@ int PMSBase::pm25ToAQI(int pm02) { return 500; } + +/** + * @brief SLR correction for PM2.5 + * + * Reference: https://www.airgradient.com/blog/low-readings-from-pms5003/ + * + * @param pm25 PM2.5 raw value + * @param pm003Count PM0.3 count + * @param scalingFactor Scaling factor + * @param intercept Intercept + * @return float Calibrated PM2.5 value + */ +float PMSBase::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) { + float calibrated; + + float lowCalibrated = (scalingFactor * pm003Count) + intercept; + if (lowCalibrated < 31) { + calibrated = lowCalibrated; + } else { + calibrated = pm25; + } + + // No negative value for pm2.5 + if (calibrated < 0) { + return 0.0; + } + + return calibrated; +} + /** * @brief Correction PM2.5 * diff --git a/src/PMS/PMS.h b/src/PMS/PMS.h index 460a87a..c363c69 100644 --- a/src/PMS/PMS.h +++ b/src/PMS/PMS.h @@ -39,6 +39,7 @@ public: uint8_t getErrorCode(void); int pm25ToAQI(int pm02); + float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept); float compensate(float pm25, float humidity); private: diff --git a/src/PMS/PMS5003.cpp b/src/PMS/PMS5003.cpp index 974bb18..d20c4e9 100644 --- a/src/PMS/PMS5003.cpp +++ b/src/PMS/PMS5003.cpp @@ -172,6 +172,10 @@ int PMS5003::getPm10ParticleCount(void) { return pms.getCount10(); } */ int PMS5003::convertPm25ToUsAqi(int pm25) { return pms.pm25ToAQI(pm25); } +float PMS5003::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) { + return pms.slrCorrection(pm25, pm003Count, scalingFactor, intercept); +} + /** * @brief Correct PM2.5 * diff --git a/src/PMS/PMS5003.h b/src/PMS/PMS5003.h index 594964e..bd2cf7b 100644 --- a/src/PMS/PMS5003.h +++ b/src/PMS/PMS5003.h @@ -42,6 +42,7 @@ public: int getPm10ParticleCount(void); int convertPm25ToUsAqi(int pm25); + float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept); float compensate(float pm25, float humidity); int getFirmwareVersion(void); uint8_t getErrorCode(void); diff --git a/src/PMS/PMS5003T.cpp b/src/PMS/PMS5003T.cpp index 27d6313..e50d58a 100644 --- a/src/PMS/PMS5003T.cpp +++ b/src/PMS/PMS5003T.cpp @@ -205,6 +205,10 @@ float PMS5003T::getRelativeHumidity(void) { return pms.getHum() / 10.0f; } +float PMS5003T::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) { + return pms.slrCorrection(pm25, pm003Count, scalingFactor, intercept); +} + /** * @brief Correct PM2.5 * diff --git a/src/PMS/PMS5003T.h b/src/PMS/PMS5003T.h index 73017e7..4f09e8b 100644 --- a/src/PMS/PMS5003T.h +++ b/src/PMS/PMS5003T.h @@ -45,6 +45,7 @@ public: int convertPm25ToUsAqi(int pm25); float getTemperature(void); float getRelativeHumidity(void); + float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept); float compensate(float pm25, float humidity); int getFirmwareVersion(void); uint8_t getErrorCode(void); From 5867d0f1d5498b4e46f380ef3e29b20d7b59c57d Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 14:40:35 +0700 Subject: [PATCH 06/15] Fix pmcorrection member datatype Log using printlog Function to check if correction is not none --- src/AgConfigure.cpp | 23 ++++++++++++++++------- src/AgConfigure.h | 5 +++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 0a6a2cf..68f62ea 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -134,13 +134,13 @@ bool Configuration::updatePmCorrection(JSONVar &json) { } // Check algorithm - const char *algorithm = (const char *)pm02["correctionAlgorithm"]; + String algorithm = pm02["correctionAlgorithm"]; PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm); if (algo == Unknown) { - Serial.println("Unknown algorithm"); + logInfo("Unknown algorithm"); return false; } - Serial.printf("Correction algorithm: %s\n", algorithm); + logInfo("Correction algorithm: " + algorithm); // If algo is None or EPA_2021, no need to check slr // But first check if pmCorrection different from algo @@ -149,7 +149,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) { jconfig[jprop_corrections]["pm02"]["correctionAlgorithm"] = algorithm; pmCorrection.algorithm = algo; pmCorrection.changed = true; - Serial.println("PM2.5 correction updated"); + logInfo("PM2.5 correction updated"); return true; } @@ -180,7 +180,6 @@ bool Configuration::updatePmCorrection(JSONVar &json) { // Deep copy corrections from root to jconfig, so it will be saved later jconfig[jprop_corrections] = corrections; - Serial.println("Correction copied"); // Update pmCorrection with new values pmCorrection.algorithm = algo; @@ -190,7 +189,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) { pmCorrection.changed = true; // Correction values were updated - Serial.println("PM2.5 correction updated"); + logInfo("PM2.5 correction updated"); return true; } @@ -1351,6 +1350,16 @@ bool Configuration::isPMCorrectionChanged(void) { return changed; } -PMCorrection Configuration::getPMCorrection(void) { +/** + * @brief Check if PM correction is enabled + * + * @return true if PM correction algorithm is not None, otherwise false + */ +bool Configuration::isPMCorrectionEnabled(void) { + PMCorrection pmCorrection = getPMCorrection(); + return pmCorrection.algorithm != PMCorrectionAlgorithm::None; +} + +Configuration::PMCorrection Configuration::getPMCorrection(void) { return pmCorrection; } diff --git a/src/AgConfigure.h b/src/AgConfigure.h index 94c2606..f351fab 100644 --- a/src/AgConfigure.h +++ b/src/AgConfigure.h @@ -11,8 +11,8 @@ class Configuration : public PrintLog { public: struct PMCorrection { PMCorrectionAlgorithm algorithm; - int intercept; - int scalingFactor; + float intercept; + float scalingFactor; bool useEPA; // EPA 2021 bool changed; }; @@ -97,6 +97,7 @@ public: bool isLedBarModeChanged(void); bool isMonitorDisplayCompensatedValues(void); bool isPMCorrectionChanged(void); + bool isPMCorrectionEnabled(void); PMCorrection getPMCorrection(void); }; From 7b0381dea315e9bdbfce45b203e6e3cbf37d92bf Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 14:44:32 +0700 Subject: [PATCH 07/15] Apply pm correction to display and led bar --- src/AgOledDisplay.cpp | 10 ++++------ src/AgStateMachine.cpp | 4 ++-- src/AgValue.cpp | 33 +++++++++++++++++++++++++++++++++ src/AgValue.h | 12 ++++++++++++ 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index 5661f21..7f11bb5 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -314,13 +314,11 @@ void OledDisplay::showDashboard(const char *status) { /** Draw PM2.5 value */ int pm25 = value.get(Measurements::PM25); - if (utils::isValidPm(pm25)) { - /** Compensate PM2.5 value. */ - if (config.hasSensorSHT && config.isMonitorDisplayCompensatedValues()) { - pm25 = ag->pms5003.compensate(pm25, value.getFloat(Measurements::Humidity)); - logInfo("PM2.5 compensate: " + String(pm25)); - } + if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { + pm25 = (int)value.getCorrectedPM25(*ag, config); + } + if (utils::isValidPm(pm25)) { if (config.isPmStandardInUSAQI()) { sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25)); } else { diff --git a/src/AgStateMachine.cpp b/src/AgStateMachine.cpp index 9cbe068..8e0be6a 100644 --- a/src/AgStateMachine.cpp +++ b/src/AgStateMachine.cpp @@ -142,8 +142,8 @@ void StateMachine::co2handleLeds(void) { */ void StateMachine::pm25handleLeds(void) { int pm25Value = value.get(Measurements::PM25); - if (config.isMonitorDisplayCompensatedValues() && config.hasSensorSHT) { - pm25Value = ag->pms5003.compensate(pm25Value, value.getFloat(Measurements::Humidity)); + if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { + pm25Value = (int)value.getCorrectedPM25(*ag, config); } if (pm25Value < 5) { diff --git a/src/AgValue.cpp b/src/AgValue.cpp index fb7135a..bcf617b 100644 --- a/src/AgValue.cpp +++ b/src/AgValue.cpp @@ -1,6 +1,7 @@ #include "AgValue.h" #include "AgConfigure.h" #include "AirGradient.h" +#include "App/AppDef.h" #define json_prop_pmFirmware "firmware" #define json_prop_pm01Ae "pm01" @@ -482,6 +483,38 @@ void Measurements::validateChannel(int ch) { } } +float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg, int ch) { + float pm25; + float humidity; + float pm003Count; + int channel = ch - 1; // Array index + if (useAvg) { + // Directly call from the index + pm25 = _pm_25[channel].update.avg; + humidity = _humidity[channel].update.avg; + pm003Count = _pm_03_pc[channel].update.avg; + } else { + pm25 = get(PM25, ch); + humidity = getFloat(Humidity, ch); + pm003Count = get(PM03_PC, ch); + } + + Configuration::PMCorrection pmCorrection = config.getPMCorrection(); + if (pmCorrection.algorithm == PMCorrectionAlgorithm::EPA_2021) { + // EPA correction directly applied + pm25 = ag.pms5003.compensate(pm25, humidity); + } else { + // SLR correction, this is assumes before calling this function, correction algorithm is not None + pm25 = ag.pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor, pmCorrection.intercept); + if (pmCorrection.useEPA) { + // Add EPA compensation on top of SLR + pm25 = ag.pms5003.compensate(pm25, humidity); + } + } + + return pm25; +} + String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag, Configuration &config) { JSONVar root; diff --git a/src/AgValue.h b/src/AgValue.h index 5efea18..3b129b3 100644 --- a/src/AgValue.h +++ b/src/AgValue.h @@ -114,6 +114,18 @@ public: */ float getFloat(MeasurementType type, int ch = 1); + + /** + * @brief Get the Corrected PM25 object based on the correction algorithm from configuration + * + * @param ag AirGradient instance + * @param config Configuration instance + * @param useAvg Use moving average value if true, otherwise use latest value + * @param ch MeasurementType channel + * @return float Corrected PM2.5 value + */ + float getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg = false, int ch = 1); + /** * build json payload for every measurements */ From 9fbbea22ffa5ff34497a8ae287aa0ca7d0194701 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 14:52:58 +0700 Subject: [PATCH 08/15] Fix typo --- src/AgConfigure.cpp | 10 +++++----- src/AgConfigure.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 68f62ea..43c97a4 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -773,7 +773,7 @@ bool Configuration::parse(String data, bool isLocal) { if (curVer != newVer) { logInfo("Detected new firmware version: " + newVer); otaNewFirmwareVersion = newVer; - udpated = true; + updated = true; } else { otaNewFirmwareVersion = String(""); } @@ -786,12 +786,12 @@ bool Configuration::parse(String data, bool isLocal) { } if (changed) { - udpated = true; + updated = true; saveConfig(); printConfig(); } else { if (ledBarTestRequested || co2CalibrationRequested) { - udpated = true; + updated = true; } } return true; @@ -978,8 +978,8 @@ String Configuration::getModel(void) { } bool Configuration::isUpdated(void) { - bool updated = this->udpated; - this->udpated = false; + bool updated = this->updated; + this->updated = false; return updated; } diff --git a/src/AgConfigure.h b/src/AgConfigure.h index f351fab..1cca028 100644 --- a/src/AgConfigure.h +++ b/src/AgConfigure.h @@ -20,7 +20,7 @@ public: private: bool co2CalibrationRequested; bool ledBarTestRequested; - bool udpated; + bool updated; String failedMessage; bool _noxLearnOffsetChanged; bool _tvocLearningOffsetChanged; From ade72ff3b8183d04aa7eefca34610ed93ed90ef9 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 16:11:00 +0700 Subject: [PATCH 09/15] Apply correction to transmission payload Only for indoor --- src/AgValue.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/AgValue.cpp b/src/AgValue.cpp index bcf617b..ca148ca 100644 --- a/src/AgValue.cpp +++ b/src/AgValue.cpp @@ -638,10 +638,9 @@ JSONVar Measurements::buildIndoor(bool localServer, AirGradient &ag, Configurati // Add pm25 compensated value only if PM2.5 and humidity value is valid if (config.hasSensorPMS1 && utils::isValidPm(_pm_25[0].update.avg)) { if (config.hasSensorSHT && utils::isValidHumidity(_humidity[0].update.avg)) { - float pm25 = ag.pms5003.compensate(_pm_25[0].update.avg, _humidity[0].update.avg); - if (utils::isValidPm(pm25)) { - indoor[json_prop_pm25Compensated] = ag.round2(pm25); - } + // Correction using moving average value + float tmp = getCorrectedPM25(ag, config, true); + indoor[json_prop_pm25Compensated] = ag.round2(tmp); } } From c6961b3ca8ea4da3e1fc0cd51b8c8997dd3f6202 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 16:11:47 +0700 Subject: [PATCH 10/15] Validate raw pm before correction --- src/AgOledDisplay.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index 7f11bb5..94dd5e6 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -314,11 +314,10 @@ void OledDisplay::showDashboard(const char *status) { /** Draw PM2.5 value */ int pm25 = value.get(Measurements::PM25); - if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { - pm25 = (int)value.getCorrectedPM25(*ag, config); - } - if (utils::isValidPm(pm25)) { + if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { + pm25 = (int)value.getCorrectedPM25(*ag, config); + } if (config.isPmStandardInUSAQI()) { sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25)); } else { From 75f88b00092ba20c70d77e69e48e5cc5730b97cf Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 16:15:46 +0700 Subject: [PATCH 11/15] Remove slr correction for pms5003t --- src/PMS/PMS5003T.cpp | 4 ---- src/PMS/PMS5003T.h | 1 - 2 files changed, 5 deletions(-) diff --git a/src/PMS/PMS5003T.cpp b/src/PMS/PMS5003T.cpp index e50d58a..27d6313 100644 --- a/src/PMS/PMS5003T.cpp +++ b/src/PMS/PMS5003T.cpp @@ -205,10 +205,6 @@ float PMS5003T::getRelativeHumidity(void) { return pms.getHum() / 10.0f; } -float PMS5003T::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) { - return pms.slrCorrection(pm25, pm003Count, scalingFactor, intercept); -} - /** * @brief Correct PM2.5 * diff --git a/src/PMS/PMS5003T.h b/src/PMS/PMS5003T.h index 4f09e8b..73017e7 100644 --- a/src/PMS/PMS5003T.h +++ b/src/PMS/PMS5003T.h @@ -45,7 +45,6 @@ public: int convertPm25ToUsAqi(int pm25); float getTemperature(void); float getRelativeHumidity(void); - float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept); float compensate(float pm25, float humidity); int getFirmwareVersion(void); uint8_t getErrorCode(void); From f49e4a4b370348c51d1d10e42aabe18c2132a13c Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 17:23:15 +0700 Subject: [PATCH 12/15] Fix casting enum issue Previously if algo is slr, it's always consider new update --- src/AgConfigure.cpp | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 43c97a4..04efa76 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -103,15 +103,18 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) { // Loop through all algorithm names in the PM_CORRECTION_ALGORITHM_NAMES array // If the input string matches an algorithm name, return the corresponding enum value // Else return Unknown - for (int idx = 0; - idx < sizeof(PM_CORRECTION_ALGORITHM_NAMES) / sizeof(PM_CORRECTION_ALGORITHM_NAMES[0]); - idx++) { - if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[idx]) { - return (PMCorrectionAlgorithm)idx; + + const size_t enumSize = SLR_PMS5003_20240104 + 1; // Get the actual size of the enum + PMCorrectionAlgorithm result = PMCorrectionAlgorithm::Unknown; + + // Loop through enum values + for (size_t enumVal = 0; enumVal < enumSize; enumVal++) { + if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[enumVal]) { + result = static_cast(enumVal); } } - return Unknown; + return result; } bool Configuration::updatePmCorrection(JSONVar &json) { @@ -171,9 +174,13 @@ bool Configuration::updatePmCorrection(JSONVar &json) { return false; } + // arduino_json doesn't support float type, need to cast to double first + float intercept = (float)((double)slr["intercept"]); + float scalingFactor = (float)((double)slr["scalingFactor"]); + // Compare with current pmCorrection - if (pmCorrection.algorithm == algo && pmCorrection.intercept == (double)slr["intercept"] && - pmCorrection.scalingFactor == (double)slr["scalingFactor"] && + if (pmCorrection.algorithm == algo && pmCorrection.intercept == intercept && + pmCorrection.scalingFactor == scalingFactor && pmCorrection.useEPA == (bool)slr["useEpa2021"]) { return false; // No changes needed } @@ -183,8 +190,8 @@ bool Configuration::updatePmCorrection(JSONVar &json) { // Update pmCorrection with new values pmCorrection.algorithm = algo; - pmCorrection.intercept = (double)slr["intercept"]; - pmCorrection.scalingFactor = (double)slr["scalingFactor"]; + pmCorrection.intercept = intercept; + pmCorrection.scalingFactor = scalingFactor; pmCorrection.useEPA = (bool)slr["useEpa2021"]; pmCorrection.changed = true; From d850d27dc15e75276eee1aa8cc365874d3ddbf5d Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 17:31:05 +0700 Subject: [PATCH 13/15] Clear slr when not avail --- src/AgConfigure.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 04efa76..266886e 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -136,6 +136,8 @@ bool Configuration::updatePmCorrection(JSONVar &json) { return false; } + // TODO: Need to have data type check, with error message response if invalid + // Check algorithm String algorithm = pm02["correctionAlgorithm"]; PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm); @@ -149,7 +151,10 @@ bool Configuration::updatePmCorrection(JSONVar &json) { // But first check if pmCorrection different from algo if (algo == None || algo == EPA_2021) { if (pmCorrection.algorithm != algo) { + // Deep copy corrections from root to jconfig, so it will be saved later jconfig[jprop_corrections]["pm02"]["correctionAlgorithm"] = algorithm; + jconfig[jprop_corrections]["pm02"]["slr"] = JSON.parse("{}"); // Clear slr + // Update pmCorrection with new values pmCorrection.algorithm = algo; pmCorrection.changed = true; logInfo("PM2.5 correction updated"); From 1db8fbefe98a3ab402c457df8e92731b1e046458 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 2 Nov 2024 18:44:44 +0700 Subject: [PATCH 14/15] Corrections from local server Tidy some things --- docs/local-server.md | 72 ++++++++++++++++++++++++++++++++++++++----- src/AgOledDisplay.cpp | 5 +-- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/docs/local-server.md b/docs/local-server.md index 247ec06..dfbbd07 100644 --- a/docs/local-server.md +++ b/docs/local-server.md @@ -73,16 +73,17 @@ You get the following response: | `noxIndex` | Number | Senisirion NOx Index | | `noxRaw` | Number | NOx raw value | | `boot` | Number | Counts every measurement cycle. Low boot counts indicate restarts. | -| `bootCount` | Number | Same as boot property. Required for Home Assistant compatability. Will be depreciated. | +| `bootCount` | Number | Same as boot property. Required for Home Assistant compatability. (deprecated soon!) | | `ledMode` | String | Current configuration of the LED mode | | `firmware` | String | Current firmware version | | `model` | String | Current model name | -| `monitorDisplayCompensatedValues` | Boolean | Switching Display of AirGradient ONE to Compensated / Non Compensated Values | Compensated values apply correction algorithms to make the sensor values more accurate. Temperature and relative humidity correction is only applied on the outdoor monitor Open Air but the properties _compensated will still be send also for the indoor monitor AirGradient ONE. #### Get Configuration Parameters (GET) -With the path "/config" you can get the current configuration. + +With the path "/config" you can get monitor current configurations. + ```json { "country": "TH", @@ -99,27 +100,39 @@ With the path "/config" you can get the current configuration. "displayBrightness": 100, "offlineMode": false, "model": "I-9PSL", - "monitorDisplayCompensatedValues": true + "monitorDisplayCompensatedValues": true, + "corrections": { + "pm02": { + "correctionAlgorithm": "epa_2021", + "slr": {} + } + } + } } ``` #### Set Configuration Parameters (PUT) -Configuration parameters can be changed with a put request to the monitor, e.g. +Configuration parameters can be changed with a PUT request to the monitor, e.g. Example to force CO2 calibration - ```curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config ``` + ```bash + curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config + ``` Example to set monitor to Celsius - ```curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config ``` + ```bash + curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config + ``` If you use command prompt on Windows, you need to escape the quotes: ``` -d "{\"param\":\"value\"}" ``` #### Avoiding Conflicts with Configuration on AirGradient Server + If the monitor is set up on the AirGradient dashboard, it will also receive configurations from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset. #### Configuration Parameters (GET/PUT) @@ -142,4 +155,47 @@ If the monitor is set up on the AirGradient dashboard, it will also receive conf | `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 12}` | | `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` | | `offlineMode` | Set monitor to run without WiFi. | Boolean | `false`: Disabled (default)
`true`: Enabled | `{"offlineMode": true}` | -| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (From [3.1.9]()) | Boolean | `false`: Without compensate (default)
`true`: with compensate | `{"monitorDisplayCompensatedValues": false }` | +| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on [3.1.9]()) | Boolean | `false`: Without compensate (default)
`true`: with compensate | `{"monitorDisplayCompensatedValues": false }` | +| `corrections` | Sets correction options to display and measurement values on local server response. | Object | _see corretions section_ | _see corretions section_ | + + + +#### Corrections + +The `corrections` object allows configuring PM2.5 correction algorithms and parameters. This affects both the display and local server response values. + +Example correction configuration: + +```json +{ + "corrections": { + "pm02": { + "correctionAlgorithm": "