Merge branch 'develop' into feat/ledbar-sensor-indicator

This commit is contained in:
Samuel Siburian authored and GitHub committed 2024-11-03 14:12:57 +07:00
commit ae0b4038d4
27 files changed
+2248 -1069

No files matched your search

+1 -1
View File
@@ -130,7 +130,7 @@ bool AgApiClient::postToServer(String data) {
client.end();
logInfo(String("POST: ") + uri);
logInfo(String("DATA: ") + data);
// logInfo(String("DATA: ") + data);
logInfo(String("Return code: ") + String(retCode));
if ((retCode == 200) || (retCode == 429)) {
+166 -7
View File
@@ -1,5 +1,4 @@
#include "AgConfigure.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#if ESP32
#include "FS.h"
#include "SPIFFS.h"
@@ -22,6 +21,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
@@ -42,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"
@@ -87,6 +99,112 @@ 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
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<PMCorrectionAlgorithm>(enumVal);
}
}
return result;
}
bool Configuration::updatePmCorrection(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;
}
// TODO: Need to have data type check, with error message response if invalid
// Check algorithm
String algorithm = pm02["correctionAlgorithm"];
PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm);
if (algo == Unknown) {
logInfo("Unknown algorithm");
return false;
}
logInfo("Correction algorithm: " + 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) {
// 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");
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;
}
// 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 == intercept &&
pmCorrection.scalingFactor == scalingFactor &&
pmCorrection.useEPA == (bool)slr["useEpa2021"]) {
return false; // No changes needed
}
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections] = corrections;
// Update pmCorrection with new values
pmCorrection.algorithm = algo;
pmCorrection.intercept = intercept;
pmCorrection.scalingFactor = scalingFactor;
pmCorrection.useEPA = (bool)slr["useEpa2021"];
pmCorrection.changed = true;
// Correction values were updated
logInfo("PM2.5 correction updated");
return true;
}
/**
* @brief Save configure to device storage (EEPROM)
*
@@ -162,7 +280,7 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_displayBrightness] = jprop_displayBrightness_default;
}
if (ag->isOne()) {
jconfig[jprop_ledBarMode] = jprop_ledBarBrightness_default;
jconfig[jprop_ledBarMode] = jprop_ledBarMode_default;
}
jconfig[jprop_tvocLearningOffset] = jprop_tvocLearningOffset_default;
jconfig[jprop_noxLearningOffset] = jprop_noxLearningOffset_default;
@@ -171,6 +289,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();
}
@@ -660,20 +785,25 @@ 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("");
}
}
}
// Corrections
if (updatePmCorrection(root)) {
changed = true;
}
if (changed) {
udpated = true;
updated = true;
saveConfig();
printConfig();
} else {
if (ledBarTestRequested || co2CalibrationRequested) {
udpated = true;
updated = true;
}
}
return true;
@@ -860,8 +990,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;
}
@@ -1118,6 +1248,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();
}
@@ -1216,3 +1355,23 @@ String Configuration::newFirmwareVersion(void) {
otaNewFirmwareVersion = String("");
return newFw;
}
bool Configuration::isPMCorrectionChanged(void) {
bool changed = pmCorrection.changed;
pmCorrection.changed = false;
return changed;
}
/**
* @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;
}
+17 -1
View File
@@ -5,12 +5,22 @@
#include "Main/PrintLog.h"
#include "AirGradient.h"
#include <Arduino.h>
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
class Configuration : public PrintLog {
public:
struct PMCorrection {
PMCorrectionAlgorithm algorithm;
float intercept;
float scalingFactor;
bool useEPA; // EPA 2021
bool changed;
};
private:
bool co2CalibrationRequested;
bool ledBarTestRequested;
bool udpated;
bool updated;
String failedMessage;
bool _noxLearnOffsetChanged;
bool _tvocLearningOffsetChanged;
@@ -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 updatePmCorrection(JSONVar &json);
void saveConfig(void);
void loadConfig(void);
void defaultConfig(void);
@@ -83,6 +96,9 @@ public:
void setOfflineModeWithoutSave(bool offline);
bool isLedBarModeChanged(void);
bool isMonitorDisplayCompensatedValues(void);
bool isPMCorrectionChanged(void);
bool isPMCorrectionEnabled(void);
PMCorrection getPMCorrection(void);
};
#endif /** _AG_CONFIG_H_ */
+34 -30
View File
@@ -12,12 +12,13 @@
*/
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
/** Temperature */
if (utils::isValidTemperature(value.Temperature)) {
float temp = value.getFloat(Measurements::Temperature);
if (utils::isValidTemperature(temp)) {
float t = 0.0f;
if (config.isTemperatureUnitInF()) {
t = utils::degreeC_To_F(value.Temperature);
t = utils::degreeC_To_F(temp);
} else {
t = value.Temperature;
t = temp;
}
if (config.isTemperatureUnitInF()) {
@@ -43,13 +44,14 @@ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
DISP()->drawUTF8(1, 10, buf);
/** Show humidity */
if (utils::isValidHumidity(value.Humidity)) {
snprintf(buf, buf_size, "%d%%", value.Humidity);
int rhum = (int)value.getFloat(Measurements::Humidity);
if (utils::isValidHumidity(rhum)) {
snprintf(buf, buf_size, "%d%%", rhum);
} else {
snprintf(buf, buf_size, "-%%");
}
if (value.Humidity > 99) {
if (rhum > 99.0) {
DISP()->drawStr(97, 10, buf);
} else {
DISP()->drawStr(105, 10, buf);
@@ -290,8 +292,9 @@ void OledDisplay::showDashboard(const char *status) {
DISP()->drawUTF8(1, 27, "CO2");
DISP()->setFont(u8g2_font_t0_22b_tf);
if (utils::isValidCO2(value.CO2)) {
sprintf(strBuf, "%d", value.CO2);
int co2 = value.get(Measurements::CO2);
if (utils::isValidCO2(co2)) {
sprintf(strBuf, "%d", co2);
} else {
sprintf(strBuf, "%s", "-");
}
@@ -310,15 +313,12 @@ void OledDisplay::showDashboard(const char *status) {
DISP()->drawStr(55, 27, "PM2.5");
/** Draw PM2.5 value */
if (utils::isValidPm(value.pm25_1)) {
int pm25 = value.pm25_1;
/** Compensate PM2.5 value. */
if (config.hasSensorSHT && config.isMonitorDisplayCompensatedValues()) {
pm25 = ag->pms5003.compensate(pm25, value.Humidity);
logInfo("PM2.5 compensate: " + String(pm25));
int pm25 = value.get(Measurements::PM25);
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 {
@@ -343,17 +343,19 @@ void OledDisplay::showDashboard(const char *status) {
DISP()->drawStr(100, 27, "VOC:");
/** Draw tvocIndexvalue */
if (utils::isValidVOC(value.TVOC)) {
sprintf(strBuf, "%d", value.TVOC);
int tvoc = value.get(Measurements::TVOC);
if (utils::isValidVOC(tvoc)) {
sprintf(strBuf, "%d", tvoc);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(100, 39, strBuf);
/** Draw NOx label */
int nox = value.get(Measurements::NOx);
DISP()->drawStr(100, 53, "NOx:");
if (utils::isValidNOx(value.NOx)) {
sprintf(strBuf, "%d", value.NOx);
if (utils::isValidNOx(nox)) {
sprintf(strBuf, "%d", nox);
} else {
sprintf(strBuf, "%s", "-");
}
@@ -363,8 +365,9 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.clear();
/** Set CO2 */
if (utils::isValidCO2(value.CO2)) {
snprintf(strBuf, sizeof(strBuf), "CO2:%d", value.CO2);
int co2 = value.get(Measurements::CO2);
if (utils::isValidCO2(co2)) {
snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2);
} else {
snprintf(strBuf, sizeof(strBuf), "CO2:-");
}
@@ -373,9 +376,9 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setText(strBuf);
/** Set PM */
int pm25 = value.pm25_1;
if (config.hasSensorSHT && config.isMonitorDisplayCompensatedValues()) {
pm25 = (int)ag->pms5003.compensate(pm25, value.Humidity);
int pm25 = value.get(Measurements::PM25);
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = (int)value.getCorrectedPM25(*ag, config);
}
ag->display.setCursor(0, 12);
@@ -387,12 +390,12 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setText(strBuf);
/** Set temperature and humidity */
if (utils::isValidTemperature(value.Temperature)) {
float temp = value.getFloat(Measurements::Temperature);
if (utils::isValidTemperature(temp)) {
if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F",
utils::degreeC_To_F(value.Temperature));
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
} else {
snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", value.Temperature);
snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", temp);
}
} else {
if (config.isTemperatureUnitInF()) {
@@ -405,8 +408,9 @@ void OledDisplay::showDashboard(const char *status) {
ag->display.setCursor(0, 24);
ag->display.setText(strBuf);
if (utils::isValidHumidity(value.Humidity)) {
snprintf(strBuf, sizeof(strBuf), "H:%d %%", (int)value.Humidity);
int rhum = (int)value.getFloat(Measurements::Humidity);
if (utils::isValidHumidity(rhum)) {
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
} else {
snprintf(strBuf, sizeof(strBuf), "H:- %%");
}
+4 -4
View File
@@ -69,7 +69,7 @@ void StateMachine::sensorhandleLeds(void) {
*
*/
void StateMachine::co2handleLeds(void) {
int co2Value = value.CO2;
int co2Value = value.get(Measurements::CO2);
if (co2Value <= 700) {
/** G; 1 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
@@ -141,9 +141,9 @@ void StateMachine::co2handleLeds(void) {
*
*/
void StateMachine::pm25handleLeds(void) {
int pm25Value = value.pm25_1;
if (config.isMonitorDisplayCompensatedValues() && config.hasSensorSHT) {
pm25Value = ag->pms5003.compensate(value.pm25_1, value.Humidity);
int pm25Value = value.get(Measurements::PM25);
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25Value = (int)value.getCorrectedPM25(*ag, config);
}
if (pm25Value <= 5) {
+973 -374
View File
File diff suppressed because it is too large. Load diff
+189 -61
View File
@@ -1,79 +1,207 @@
#ifndef _AG_VALUE_H_
#define _AG_VALUE_H_
#include <Arduino.h>
#include "AgConfigure.h"
#include "AirGradient.h"
#include "App/AppDef.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#include "Main/utils.h"
#include <Arduino.h>
#include <vector>
class Measurements {
private:
String pms5003FirmwareVersion(int fwCode);
String pms5003TFirmwareVersion(int fwCode);
String pms5003FirmwareVersionBase(String prefix, int fwCode);
// Generic struct for update indication for respective value
struct Update {
int invalidCounter; // Counting on how many invalid value that are passed to update function
int max; // Maximum length of the period of the moving average
float avg; // Moving average value, updated every update function called
};
// Reading type for sensor value that outputs float
struct FloatValue {
float sumValues; // Total value from each update
std::vector<float> listValues; // List of update value that are kept
Update update;
};
// Reading type for sensor value that outputs integer
struct IntegerValue {
unsigned long sumValues; // Total value from each update; unsigned long to accomodate TVOx and
// NOx raw data
std::vector<int> listValues; // List of update value that are kept
Update update;
};
public:
Measurements() {
pm25_1 = -1;
pm01_1 = -1;
pm10_1 = -1;
pm03PCount_1 = -1;
temp_1 = -1001;
hum_1 = -1;
pm25_2 = -1;
pm01_2 = -1;
pm10_2 = -1;
pm03PCount_2 = -1;
temp_2 = -1001;
hum_2 = -1;
Temperature = -1001;
Humidity = -1;
CO2 = -1;
TVOC = -1;
TVOCRaw = -1;
NOx = -1;
NOxRaw = -1;
}
Measurements() {}
~Measurements() {}
float Temperature;
int Humidity;
int CO2;
int TVOC;
int TVOCRaw;
int NOx;
int NOxRaw;
// Enumeration for every AG measurements
enum MeasurementType {
Temperature,
Humidity,
CO2,
TVOC, // index value
TVOCRaw,
NOx, // index value
NOxRaw,
PM01, // PM1.0 under atmospheric environment
PM25, // PM2.5 under athompheric environment
PM10, // PM10 under atmospheric environment
PM01_SP, // PM1.0 standard particle
PM25_SP, // PM2.5 standard particle
PM10_SP, // PM10 standard particle
PM03_PC, // Particle 0.3 count
PM05_PC, // Particle 0.5 count
PM01_PC, // Particle 1.0 count
PM25_PC, // Particle 2.5 count
PM5_PC, // Particle 5.0 count
PM10_PC, // Particle 10 count
};
int pm25_1;
int pm01_1;
int pm10_1;
int pm03PCount_1;
float temp_1;
int hum_1;
/**
* @brief Set each MeasurementType maximum period length for moving average
*
* @param type the target measurement type to set
* @param max the maximum period length
*/
void maxPeriod(MeasurementType, int max);
int pm25_2;
int pm01_2;
int pm10_2;
int pm03PCount_2;
float temp_2;
int hum_2;
/**
* @brief update target measurement type with new value.
* Each MeasurementType has last raw value and moving average value based on max period
* This function is for MeasurementType that use INT as the data type
*
* @param type measurement type that will be updated
* @param val (int) the new value
* @param ch (int) the MeasurementType channel, not every MeasurementType has more than 1 channel.
* Currently maximum channel is 2. Default: 1 (channel 1)
* @return false if new value invalid consecutively reach threshold (max period)
* @return true otherwise
*/
bool update(MeasurementType type, int val, int ch = 1);
int pm1Value01;
int pm1Value25;
int pm1Value10;
int pm1PCount;
int pm1temp;
int pm1hum;
int pm2Value01;
int pm2Value25;
int pm2Value10;
int pm2PCount;
int pm2temp;
int pm2hum;
int countPosition;
const int targetCount = 20;
/**
* @brief update target measurement type with new value.
* Each MeasurementType has last raw value and moving average value based on max period
* This function is for MeasurementType that use FLOAT as the data type
*
* @param type measurement type that will be updated
* @param val (float) the new value
* @param ch (int) the MeasurementType channel, not every MeasurementType has more than 1 channel.
* Currently maximum channel is 2. Default: 1 (channel 1)
* @return false if new value invalid consecutively reach threshold (max period)
* @return true otherwise
*/
bool update(MeasurementType type, float val, int ch = 1);
/**
* @brief Get the target measurement latest value
*
* @param type measurement type that will be retrieve
* @param ch target type value channel
* @return int measurement type value
*/
int get(MeasurementType type, int ch = 1);
/**
* @brief Get the target measurement latest value
*
* @param type measurement type that will be retrieve
* @param ch target type value channel
* @return float measurement type value
*/
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
*/
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
Configuration &config);
/**
* Set to true if want to debug every update value
*/
void setDebug(bool debug);
// TODO: update this to use setter
int bootCount;
String toString(bool isLocal, AgFirmwareMode fwMode, int rssi, void* _ag, void* _config);
private:
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
FloatValue _temperature[2];
FloatValue _humidity[2];
IntegerValue _co2;
IntegerValue _tvoc; // Index value
IntegerValue _tvoc_raw;
IntegerValue _nox; // Index value
IntegerValue _nox_raw;
IntegerValue _pm_01[2]; // pm 1.0 atmospheric environment
IntegerValue _pm_25[2]; // pm 2.5 atmospheric environment
IntegerValue _pm_10[2]; // pm 10 atmospheric environment
IntegerValue _pm_01_sp[2]; // pm 1.0 standard particle
IntegerValue _pm_25_sp[2]; // pm 2.5 standard particle
IntegerValue _pm_10_sp[2]; // pm 10 standard particle
IntegerValue _pm_03_pc[2]; // particle count 0.3
IntegerValue _pm_05_pc[2]; // particle count 0.5
IntegerValue _pm_01_pc[2]; // particle count 1.0
IntegerValue _pm_25_pc[2]; // particle count 2.5
IntegerValue _pm_5_pc[2]; // particle count 5.0
IntegerValue _pm_10_pc[2]; // particle count 10
bool _debug = false;
/**
* @brief Get PMS5003 firmware version string
*
* @param fwCode
* @return String
*/
String pms5003FirmwareVersion(int fwCode);
/**
* @brief Get PMS5003T firmware version string
*
* @param fwCode
* @return String
*/
String pms5003TFirmwareVersion(int fwCode);
/**
* @brief Get firmware version string
*
* @param prefix Prefix firmware string
* @param fwCode Version code
* @return string
*/
String pms5003FirmwareVersionBase(String prefix, int fwCode);
/**
* Convert AgValue Type to string representation of the value
*/
String measurementTypeStr(MeasurementType type);
/**
* @brief check if provided channel is a valid channel or not
* abort program if invalid
*/
void validateChannel(int ch);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode, AirGradient &ag,
Configuration &config);
JSONVar buildIndoor(bool localServer, AirGradient &ag, Configuration &config);
JSONVar buildPMS(AirGradient &ag, int ch, bool allCh, bool withTempHum, bool compensate);
};
#endif /** _AG_VALUE_H_ */
+12
View File
@@ -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 */
+76 -30
View File
@@ -151,6 +151,7 @@ void PMSBase::readPackage(Stream *serial) {
if (ms >= READ_PACKGE_TIMEOUT) {
lastPackage = 0;
_connected = false;
Serial.println("PMS disconnected");
}
}
}
@@ -297,24 +298,52 @@ uint8_t PMSBase::getErrorCode(void) { return pms_errorCode; }
* @return int
*/
int PMSBase::pm25ToAQI(int pm02) {
if (pm02 <= 12.0)
return ((50 - 0) / (12.0 - .0) * (pm02 - .0) + 0);
if (pm02 <= 9.0)
return ((50 - 0) / (9.0 - .0) * (pm02 - .0) + 0);
else if (pm02 <= 35.4)
return ((100 - 50) / (35.4 - 12.0) * (pm02 - 12.0) + 50);
return ((100 - 51) / (35.4 - 9.1) * (pm02 - 9.0) + 51);
else if (pm02 <= 55.4)
return ((150 - 100) / (55.4 - 35.4) * (pm02 - 35.4) + 100);
else if (pm02 <= 150.4)
return ((200 - 150) / (150.4 - 55.4) * (pm02 - 55.4) + 150);
else if (pm02 <= 250.4)
return ((300 - 200) / (250.4 - 150.4) * (pm02 - 150.4) + 200);
else if (pm02 <= 350.4)
return ((400 - 300) / (350.4 - 250.4) * (pm02 - 250.4) + 300);
else if (pm02 <= 500.4)
return ((500 - 400) / (500.4 - 350.4) * (pm02 - 350.4) + 400);
return ((150 - 101) / (55.4 - 35.5) * (pm02 - 35.5) + 101);
else if (pm02 <= 125.4)
return ((200 - 151) / (125.4 - 55.5) * (pm02 - 55.5) + 151);
else if (pm02 <= 225.4)
return ((300 - 201) / (225.4 - 125.5) * (pm02 - 125.5) + 201);
else if (pm02 <= 325.4)
return ((500 - 301) / (325.4 - 225.5) * (pm02 - 225.5) + 301);
else
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
*
@@ -322,11 +351,12 @@ int PMSBase::pm25ToAQI(int pm02) {
*
* @param pm25 Raw PM2.5 value
* @param humidity Humidity value (%)
* @return int
* @return compensated pm25 value
*/
int PMSBase::compensate(int pm25, float humidity) {
float PMSBase::compensate(float pm25, float humidity) {
float value;
float fpm25 = pm25;
// Correct invalid humidity value
if (humidity < 0) {
humidity = 0;
}
@@ -334,23 +364,33 @@ int PMSBase::compensate(int pm25, float humidity) {
humidity = 100.0f;
}
if(pm25 < 30) { /** pm2.5 < 30 */
value = (fpm25 * 0.524f) - (humidity * 0.0862f) + 5.75f;
} else if(pm25 < 50) { /** 30 <= pm2.5 < 50 */
value = (0.786f * (fpm25 * 0.05f - 1.5f) + 0.524f * (1.0f - (fpm25 * 0.05f - 1.5f))) * fpm25 - (0.0862f * humidity) + 5.75f;
} else if(pm25 < 210) { /** 50 <= pm2.5 < 210 */
value = (0.786f * fpm25) - (0.0862f * humidity) + 5.75f;
} else if(pm25 < 260) { /** 210 <= pm2.5 < 260 */
value = (0.69f * (fpm25 * 0.02f - 4.2f) + 0.786f * (1.0f - (fpm25 * 0.02f - 4.2f))) * fpm25 - (0.0862f * humidity * (1.0f - (fpm25 * 0.02f - 4.2f))) + (2.966f * (fpm25 * 0.02f - 4.2f)) + (5.75f * (1.0f - (fpm25 * 0.02f - 4.2f))) + (8.84f * (1.e-4) * fpm25 * fpm25 * (fpm25 * 0.02f - 4.2f));
// If its already 0, do not proceed
if (pm25 == 0) {
return 0.0;
}
if (pm25 < 30) { /** pm2.5 < 30 */
value = (pm25 * 0.524f) - (humidity * 0.0862f) + 5.75f;
} else if (pm25 < 50) { /** 30 <= pm2.5 < 50 */
value = (0.786f * (pm25 * 0.05f - 1.5f) + 0.524f * (1.0f - (pm25 * 0.05f - 1.5f))) * pm25 -
(0.0862f * humidity) + 5.75f;
} else if (pm25 < 210) { /** 50 <= pm2.5 < 210 */
value = (0.786f * pm25) - (0.0862f * humidity) + 5.75f;
} else if (pm25 < 260) { /** 210 <= pm2.5 < 260 */
value = (0.69f * (pm25 * 0.02f - 4.2f) + 0.786f * (1.0f - (pm25 * 0.02f - 4.2f))) * pm25 -
(0.0862f * humidity * (1.0f - (pm25 * 0.02f - 4.2f))) +
(2.966f * (pm25 * 0.02f - 4.2f)) + (5.75f * (1.0f - (pm25 * 0.02f - 4.2f))) +
(8.84f * (1.e-4) * pm25 * pm25 * (pm25 * 0.02f - 4.2f));
} else { /** 260 <= pm2.5 */
value = 2.966f + (0.69f * fpm25) + (8.84f * (1.e-4) * fpm25 * fpm25);
value = 2.966f + (0.69f * pm25) + (8.84f * (1.e-4) * pm25 * pm25);
}
// No negative value for pm2.5
if (value < 0) {
value = 0;
return 0.0;
}
return (int)value;
return value;
}
/**
@@ -390,20 +430,26 @@ bool PMSBase::validate(const uint8_t *buf) {
}
void PMSBase::parse(const uint8_t *buf) {
// Standard particle
pms_raw0_1 = toU16(&buf[4]);
pms_raw2_5 = toU16(&buf[6]);
pms_raw10 = toU16(&buf[8]);
// atmospheric
pms_pm0_1 = toU16(&buf[10]);
pms_pm2_5 = toU16(&buf[12]);
pms_pm10 = toU16(&buf[14]);
// particle count
pms_count0_3 = toU16(&buf[16]);
pms_count0_5 = toU16(&buf[18]);
pms_count1_0 = toU16(&buf[20]);
pms_count2_5 = toU16(&buf[22]);
pms_count5_0 = toU16(&buf[24]);
pms_count10 = toU16(&buf[26]);
pms_temp = toU16(&buf[24]);
pms_hum = toU16(&buf[26]);
pms_count5_0 = toU16(&buf[24]); // PMS5003 only
pms_count10 = toU16(&buf[26]); // PMS5003 only
// Others
pms_temp = toU16(&buf[24]); // PMS5003T only
pms_hum = toU16(&buf[26]); // PMS5003T only
pms_firmwareVersion = buf[28];
pms_errorCode = buf[29];
}
+2 -1
View File
@@ -39,7 +39,8 @@ public:
uint8_t getErrorCode(void);
int pm25ToAQI(int pm02);
int compensate(int pm25, float humidity);
float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept);
float compensate(float pm25, float humidity);
private:
static const uint8_t package_size = 32;
+71 -13
View File
@@ -79,28 +79,49 @@ bool PMS5003::begin(void) {
}
/**
* @brief Read PM1.0 must call this function after @ref readData success
* @brief Read PM1.0
*
* @return int PM1.0 index
* @return int PM1.0 index (atmospheric environment)
*/
int PMS5003::getPm01Ae(void) { return pms.getPM0_1(); }
/**
* @brief Read PM2.5 must call this function after @ref readData success
* @brief Read PM2.5
*
* @return int PM2.5 index
* @return int PM2.5 index (atmospheric environment)
*/
int PMS5003::getPm25Ae(void) { return pms.getPM2_5(); }
/**
* @brief Read PM10.0 must call this function after @ref readData success
* @brief Read PM10.0
*
* @return int PM10.0 index
* @return int PM10.0 index (atmospheric environment)
*/
int PMS5003::getPm10Ae(void) { return pms.getPM10(); }
/**
* @brief Read PM0.3 must call this function after @ref readData success
* @brief Read PM1.0
*
* @return int PM1.0 index (standard particle)
*/
int PMS5003::getPm01Sp(void) { return pms.getRaw0_1(); }
/**
* @brief Read PM2.5
*
* @return int PM2.5 index (standard particle)
*/
int PMS5003::getPm25Sp(void) { return pms.getRaw2_5(); }
/**
* @brief Read PM10
*
* @return int PM10 index (standard particle)
*/
int PMS5003::getPm10Sp(void) { return pms.getRaw10(); }
/**
* @brief Read particle 0.3 count
*
* @return int PM0.3 index
*/
@@ -108,6 +129,41 @@ int PMS5003::getPm03ParticleCount(void) {
return pms.getCount0_3();
}
/**
* @brief Read particle 1.0 count
*
* @return int particle 1.0 count index
*/
int PMS5003::getPm01ParticleCount(void) { return pms.getCount1_0(); }
/**
* @brief Read particle 0.5 count
*
* @return int particle 0.5 count index
*/
int PMS5003::getPm05ParticleCount(void) { return pms.getCount0_5(); }
/**
* @brief Read particle 2.5 count
*
* @return int particle 2.5 count index
*/
int PMS5003::getPm25ParticleCount(void) { return pms.getCount2_5(); }
/**
* @brief Read particle 5.0 count
*
* @return int particle 5.0 count index
*/
int PMS5003::getPm5ParticleCount(void) { return pms.getCount5_0(); }
/**
* @brief Read particle 10 count
*
* @return int particle 10 count index
*/
int PMS5003::getPm10ParticleCount(void) { return pms.getCount10(); }
/**
* @brief Convert PM2.5 to US AQI
*
@@ -116,18 +172,20 @@ int PMS5003::getPm03ParticleCount(void) {
*/
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
*
*
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
*
*
* @param pm25 PM2.5 raw value
* @param humidity Humidity value
* @return int
* @return compensated value in float
*/
int PMS5003::compensate(int pm25, float humidity) {
return pms.compensate(pm25, humidity);
}
float PMS5003::compensate(float pm25, float humidity) { return pms.compensate(pm25, humidity); }
/**
* @brief Get sensor firmware version
+14 -1
View File
@@ -25,12 +25,25 @@ public:
void resetFailCount(void);
int getFailCount(void);
int getFailCountMax(void);
// Atmospheric environment
int getPm01Ae(void);
int getPm25Ae(void);
int getPm10Ae(void);
// Standard particle
int getPm01Sp(void);
int getPm25Sp(void);
int getPm10Sp(void);
// Particle count
int getPm03ParticleCount(void);
int getPm05ParticleCount(void);
int getPm01ParticleCount(void);
int getPm25ParticleCount(void);
int getPm5ParticleCount(void);
int getPm10ParticleCount(void);
int convertPm25ToUsAqi(int pm25);
int compensate(int pm25, float humidity);
float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept);
float compensate(float pm25, float humidity);
int getFirmwareVersion(void);
uint8_t getErrorCode(void);
bool connected(void);
+54 -14
View File
@@ -108,35 +108,77 @@ bool PMS5003T::begin(void) {
}
/**
* @brief Read PM1.0 must call this function after @ref readData success
* @brief Read PM1.0
*
* @return int PM1.0 index
* @return int PM1.0 index (atmospheric environment)
*/
int PMS5003T::getPm01Ae(void) { return pms.getPM0_1(); }
/**
* @brief Read PM2.5 must call this function after @ref readData success
* @brief Read PM2.5
*
* @return int PM2.5 index
* @return int PM2.5 index (atmospheric environment)
*/
int PMS5003T::getPm25Ae(void) { return pms.getPM2_5(); }
/**
* @brief Read PM10.0 must call this function after @ref readData success
* @brief Read PM10.0
*
* @return int PM10.0 index
* @return int PM10.0 index (atmospheric environment)
*/
int PMS5003T::getPm10Ae(void) { return pms.getPM10(); }
/**
* @brief Read PM 0.3 Count must call this function after @ref readData success
* @brief Read PM1.0
*
* @return int PM 0.3 Count index
* @return int PM1.0 index (standard particle)
*/
int PMS5003T::getPm01Sp(void) { return pms.getRaw0_1(); }
/**
* @brief Read PM2.5
*
* @return int PM2.5 index (standard particle)
*/
int PMS5003T::getPm25Sp(void) { return pms.getRaw2_5(); }
/**
* @brief Read PM10
*
* @return int PM10 index (standard particle)
*/
int PMS5003T::getPm10Sp(void) { return pms.getRaw10(); }
/**
* @brief Read particle 0.3 count
*
* @return int particle 0.3 count index
*/
int PMS5003T::getPm03ParticleCount(void) {
return pms.getCount0_3();
}
/**
* @brief Read particle 0.5 count
*
* @return int particle 0.5 count index
*/
int PMS5003T::getPm05ParticleCount(void) { return pms.getCount0_5(); }
/**
* @brief Read particle 1.0 count
*
* @return int particle 1.0 count index
*/
int PMS5003T::getPm01ParticleCount(void) { return pms.getCount1_0(); }
/**
* @brief Read particle 2.5 count
*
* @return int particle 2.5 count index
*/
int PMS5003T::getPm25ParticleCount(void) { return pms.getCount2_5(); }
/**
* @brief Convert PM2.5 to US AQI
*
@@ -165,16 +207,14 @@ float PMS5003T::getRelativeHumidity(void) {
/**
* @brief Correct PM2.5
*
*
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
*
*
* @param pm25 PM2.5 raw value
* @param humidity Humidity value
* @return int
* @return compensated value
*/
int PMS5003T::compensate(int pm25, float humidity) {
return pms.compensate(pm25, humidity);
}
float PMS5003T::compensate(float pm25, float humidity) { return pms.compensate(pm25, humidity); }
/**
* @brief Get module(s) firmware version
+11 -1
View File
@@ -28,14 +28,24 @@ public:
void resetFailCount(void);
int getFailCount(void);
int getFailCountMax(void);
// Atmospheric environment
int getPm01Ae(void);
int getPm25Ae(void);
int getPm10Ae(void);
// Standard particle
int getPm01Sp(void);
int getPm25Sp(void);
int getPm10Sp(void);
// Particle count
int getPm03ParticleCount(void);
int getPm05ParticleCount(void);
int getPm01ParticleCount(void);
int getPm25ParticleCount(void);
int convertPm25ToUsAqi(int pm25);
float getTemperature(void);
float getRelativeHumidity(void);
int compensate(int pm25, float humidity);
float compensate(float pm25, float humidity);
int getFirmwareVersion(void);
uint8_t getErrorCode(void);
bool connected(void);