Skip to content

Apply PM corrections to all models #283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion examples/OneOpenAir/OneOpenAir.ino
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ static void sendDataToAg() {
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);

stateMachine.handleLeds(AgStateMachineNormal);
}

Expand Down Expand Up @@ -908,6 +908,11 @@ static void configurationUpdateSchedule(void) {
return;
}

if (wifiConnector.isConnected() == false) {
Serial.println(" WiFi not connected, skipping fetch configuration from AG server");
return;
}

if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
Expand Down Expand Up @@ -1008,6 +1013,7 @@ static void updateDisplayAndLedBar(void) {
if (wifiConnector.isConnected() == false) {
stateMachine.displayHandle(AgStateMachineWiFiLost);
stateMachine.handleLeds(AgStateMachineWiFiLost);
return;
}

if (configuration.isCloudConnectionDisabled()) {
Expand Down
55 changes: 28 additions & 27 deletions src/AgConfigure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,10 @@ const char *LED_BAR_MODE_NAMES[] = {
};

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",
[COR_ALGO_PM_UNKNOWN] = "-", // This is only to pass "non-trivial designated initializers" error
[COR_ALGO_PM_NONE] = "none",
[COR_ALGO_PM_EPA_2021] = "epa_2021",
[COR_ALGO_PM_SLR_CUSTOM] = "custom",
};

const char *TEMP_HUM_CORRECTION_ALGORITHM_NAMES[] = {
Expand Down Expand Up @@ -115,8 +110,8 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
// 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;
const size_t enumSize = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;;

// Loop through enum values
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
Expand All @@ -125,6 +120,15 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
}
}

// If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections
if (result == COR_ALGO_PM_UNKNOWN) {
// Check the substring "slr_PMS5003_xxxxxxxx"
if (algorithm.substring(0, 11) == "slr_PMS5003") {
// If it is, then its a custom correction
result = COR_ALGO_PM_SLR_CUSTOM;
}
}

return result;
}

Expand All @@ -145,36 +149,34 @@ TempHumCorrectionAlgorithm Configuration::matchTempHumAlgorithm(String algorithm

bool Configuration::updatePmCorrection(JSONVar &json) {
if (!json.hasOwnProperty("corrections")) {
Serial.println("corrections not found");
logInfo("corrections not found");
return false;
}

JSONVar corrections = json["corrections"];
if (!corrections.hasOwnProperty("pm02")) {
Serial.println("pm02 not found");
logWarning("pm02 not found");
return false;
}

JSONVar pm02 = corrections["pm02"];
if (!pm02.hasOwnProperty("correctionAlgorithm")) {
Serial.println("correctionAlgorithm not found");
logWarning("pm02 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");
if (algo == COR_ALGO_PM_UNKNOWN) {
logWarning("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 (algo == COR_ALGO_PM_NONE || algo == COR_ALGO_PM_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;
Expand All @@ -191,7 +193,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {

// Check if pm02 has slr object
if (!pm02.hasOwnProperty("slr")) {
Serial.println("slr not found");
logWarning("slr not found");
return false;
}

Expand All @@ -200,7 +202,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor") ||
!slr.hasOwnProperty("useEpa2021")) {
Serial.println("Missing required slr properties");
logWarning("Missing required slr properties");
return false;
}

Expand Down Expand Up @@ -238,7 +240,6 @@ bool Configuration::updateTempHumCorrection(JSONVar &json, TempHumCorrection &ta

JSONVar corrections = json[jprop_corrections];
if (!corrections.hasOwnProperty(correctionName)) {
Serial.println("pm02 not found");
logWarning(String(correctionName) + " correction field not found on configuration");
return false;
}
Expand Down Expand Up @@ -397,8 +398,8 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default;

// PM2.5 correction
pmCorrection.algorithm = None;
// PM2.5 default correction
pmCorrection.algorithm = COR_ALGO_PM_NONE;
pmCorrection.changed = false;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 1;
Expand Down Expand Up @@ -1380,7 +1381,7 @@ void Configuration::toConfig(const char *buf) {

// PM2.5 correction
/// Set default first before parsing local config
pmCorrection.algorithm = PMCorrectionAlgorithm::None;
pmCorrection.algorithm = COR_ALGO_PM_NONE;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 0;
pmCorrection.useEPA = false;
Expand Down Expand Up @@ -1526,8 +1527,8 @@ bool Configuration::isPMCorrectionChanged(void) {
*/
bool Configuration::isPMCorrectionEnabled(void) {
PMCorrection pmCorrection = getPMCorrection();
if (pmCorrection.algorithm == PMCorrectionAlgorithm::None ||
pmCorrection.algorithm == PMCorrectionAlgorithm::Unknown) {
if (pmCorrection.algorithm == COR_ALGO_PM_NONE ||
pmCorrection.algorithm == COR_ALGO_PM_UNKNOWN) {
return false;
}

Expand Down
116 changes: 55 additions & 61 deletions src/AgValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ float Measurements::getCorrectedTempHum(MeasurementType type, int ch, bool force
return corrected;
}

float Measurements::getCorrectedPM25(bool useAvg, int ch) {
float Measurements::getCorrectedPM25(bool useAvg, int ch, bool forceCorrection) {
float pm25;
float corrected;
float humidity;
Expand All @@ -658,12 +658,18 @@ float Measurements::getCorrectedPM25(bool useAvg, int ch) {

Configuration::PMCorrection pmCorrection = config.getPMCorrection();
switch (pmCorrection.algorithm) {
case PMCorrectionAlgorithm::Unknown:
case PMCorrectionAlgorithm::None:
// If correction is Unknown, then default is None
corrected = pm25;
case PMCorrectionAlgorithm::COR_ALGO_PM_UNKNOWN:
case PMCorrectionAlgorithm::COR_ALGO_PM_NONE: {
// If correction is Unknown or None, then default is None
// Unless forceCorrection enabled
if (forceCorrection) {
corrected = ag->pms5003.compensate(pm25, humidity);
} else {
corrected = pm25;
}
break;
case PMCorrectionAlgorithm::EPA_2021:
}
case PMCorrectionAlgorithm::COR_ALGO_PM_EPA_2021:
corrected = ag->pms5003.compensate(pm25, humidity);
break;
default: {
Expand Down Expand Up @@ -780,8 +786,8 @@ JSONVar Measurements::buildIndoor(bool localServer) {
// buildPMS params:
/// PMS channel 1 (indoor only have 1 PMS; hence allCh false)
/// Not include temperature and humidity from PMS sensor
/// Not include compensated calculation
indoor = buildPMS(1, false, false, false);
/// Include compensated calculation
indoor = buildPMS(1, false, false, true);
if (!localServer) {
// Indoor is using PMS5003
indoor[json_prop_pmFirmware] = this->pms5003FirmwareVersion(ag->pms5003.getFirmwareVersion());
Expand All @@ -805,15 +811,6 @@ JSONVar Measurements::buildIndoor(bool localServer) {
}
}

// 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)) {
// Correction using moving average value
float tmp = getCorrectedPM25(true);
indoor[json_prop_pm25Compensated] = ag->round2(tmp);
}
}

return indoor;
}

Expand All @@ -826,58 +823,58 @@ JSONVar Measurements::buildPMS(int ch, bool allCh, bool withTempHum, bool compen
validateChannel(ch);

// Follow array indexing just for get address of the value type
ch = ch - 1;
int chIndex = ch - 1;

if (utils::isValidPm(_pm_01[ch].update.avg)) {
pms[json_prop_pm01Ae] = ag->round2(_pm_01[ch].update.avg);
if (utils::isValidPm(_pm_01[chIndex].update.avg)) {
pms[json_prop_pm01Ae] = ag->round2(_pm_01[chIndex].update.avg);
}
if (utils::isValidPm(_pm_25[ch].update.avg)) {
pms[json_prop_pm25Ae] = ag->round2(_pm_25[ch].update.avg);
if (utils::isValidPm(_pm_25[chIndex].update.avg)) {
pms[json_prop_pm25Ae] = ag->round2(_pm_25[chIndex].update.avg);
}
if (utils::isValidPm(_pm_10[ch].update.avg)) {
pms[json_prop_pm10Ae] = ag->round2(_pm_10[ch].update.avg);
if (utils::isValidPm(_pm_10[chIndex].update.avg)) {
pms[json_prop_pm10Ae] = ag->round2(_pm_10[chIndex].update.avg);
}
if (utils::isValidPm(_pm_01_sp[ch].update.avg)) {
pms[json_prop_pm01Sp] = ag->round2(_pm_01_sp[ch].update.avg);
if (utils::isValidPm(_pm_01_sp[chIndex].update.avg)) {
pms[json_prop_pm01Sp] = ag->round2(_pm_01_sp[chIndex].update.avg);
}
if (utils::isValidPm(_pm_25_sp[ch].update.avg)) {
pms[json_prop_pm25Sp] = ag->round2(_pm_25_sp[ch].update.avg);
if (utils::isValidPm(_pm_25_sp[chIndex].update.avg)) {
pms[json_prop_pm25Sp] = ag->round2(_pm_25_sp[chIndex].update.avg);
}
if (utils::isValidPm(_pm_10_sp[ch].update.avg)) {
pms[json_prop_pm10Sp] = ag->round2(_pm_10_sp[ch].update.avg);
if (utils::isValidPm(_pm_10_sp[chIndex].update.avg)) {
pms[json_prop_pm10Sp] = ag->round2(_pm_10_sp[chIndex].update.avg);
}
if (utils::isValidPm03Count(_pm_03_pc[ch].update.avg)) {
pms[json_prop_pm03Count] = ag->round2(_pm_03_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_03_pc[chIndex].update.avg)) {
pms[json_prop_pm03Count] = ag->round2(_pm_03_pc[chIndex].update.avg);
}
if (utils::isValidPm03Count(_pm_05_pc[ch].update.avg)) {
pms[json_prop_pm05Count] = ag->round2(_pm_05_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_05_pc[chIndex].update.avg)) {
pms[json_prop_pm05Count] = ag->round2(_pm_05_pc[chIndex].update.avg);
}
if (utils::isValidPm03Count(_pm_01_pc[ch].update.avg)) {
pms[json_prop_pm1Count] = ag->round2(_pm_01_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_01_pc[chIndex].update.avg)) {
pms[json_prop_pm1Count] = ag->round2(_pm_01_pc[chIndex].update.avg);
}
if (utils::isValidPm03Count(_pm_25_pc[ch].update.avg)) {
pms[json_prop_pm25Count] = ag->round2(_pm_25_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_25_pc[chIndex].update.avg)) {
pms[json_prop_pm25Count] = ag->round2(_pm_25_pc[chIndex].update.avg);
}
if (_pm_5_pc[ch].listValues.empty() == false) {
if (_pm_5_pc[chIndex].listValues.empty() == false) {
// Only include pm5.0 count when values available on its list
// If not, means no pm5_pc available from the sensor
if (utils::isValidPm03Count(_pm_5_pc[ch].update.avg)) {
pms[json_prop_pm5Count] = ag->round2(_pm_5_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_5_pc[chIndex].update.avg)) {
pms[json_prop_pm5Count] = ag->round2(_pm_5_pc[chIndex].update.avg);
}
}
if (_pm_10_pc[ch].listValues.empty() == false) {
if (_pm_10_pc[chIndex].listValues.empty() == false) {
// Only include pm10 count when values available on its list
// If not, means no pm10_pc available from the sensor
if (utils::isValidPm03Count(_pm_10_pc[ch].update.avg)) {
pms[json_prop_pm10Count] = ag->round2(_pm_10_pc[ch].update.avg);
if (utils::isValidPm03Count(_pm_10_pc[chIndex].update.avg)) {
pms[json_prop_pm10Count] = ag->round2(_pm_10_pc[chIndex].update.avg);
}
}

if (withTempHum) {
float _vc;
// Set temperature if valid
if (utils::isValidTemperature(_temperature[ch].update.avg)) {
pms[json_prop_temp] = ag->round2(_temperature[ch].update.avg);
if (utils::isValidTemperature(_temperature[chIndex].update.avg)) {
pms[json_prop_temp] = ag->round2(_temperature[chIndex].update.avg);
// Compensate temperature when flag is set
if (compensate) {
_vc = getCorrectedTempHum(Temperature, ch, true);
Expand All @@ -887,8 +884,8 @@ JSONVar Measurements::buildPMS(int ch, bool allCh, bool withTempHum, bool compen
}
}
// Set humidity if valid
if (utils::isValidHumidity(_humidity[ch].update.avg)) {
pms[json_prop_rhum] = ag->round2(_humidity[ch].update.avg);
if (utils::isValidHumidity(_humidity[chIndex].update.avg)) {
pms[json_prop_rhum] = ag->round2(_humidity[chIndex].update.avg);
// Compensate relative humidity when flag is set
if (compensate) {
_vc = getCorrectedTempHum(Humidity, ch, true);
Expand All @@ -898,17 +895,14 @@ JSONVar Measurements::buildPMS(int ch, bool allCh, bool withTempHum, bool compen
}
}

// Add pm25 compensated value only if PM2.5 and humidity value is valid
if (compensate) {
if (utils::isValidPm(_pm_25[ch].update.avg) &&
utils::isValidHumidity(_humidity[ch].update.avg)) {
// Note: the pms5003t object is not matter either for channel 1 or 2, compensate points to
// the same base function
float pm25 = ag->pms5003t_1.compensate(_pm_25[ch].update.avg, _humidity[ch].update.avg);
if (utils::isValidPm(pm25)) {
pms[json_prop_pm25Compensated] = ag->round2(pm25);
}
}
}

// Add pm25 compensated value only if PM2.5 and humidity value is valid
if (compensate) {
if (utils::isValidPm(_pm_25[chIndex].update.avg) &&
utils::isValidHumidity(_humidity[chIndex].update.avg)) {
float pm25 = getCorrectedPM25(true, ch, true);
pms[json_prop_pm25Compensated] = ag->round2(pm25);
}
}

Expand Down Expand Up @@ -1156,12 +1150,12 @@ JSONVar Measurements::buildPMS(int ch, bool allCh, bool withTempHum, bool compen
float pm25_comp2 = utils::getInvalidPmValue();
if (utils::isValidPm(_pm_25[0].update.avg) &&
utils::isValidHumidity(_humidity[0].update.avg)) {
pm25_comp1 = ag->pms5003t_1.compensate(_pm_25[0].update.avg, _humidity[0].update.avg);
pm25_comp1 = getCorrectedPM25(true, 1, true);
pms["channels"]["1"][json_prop_pm25Compensated] = ag->round2(pm25_comp1);
}
if (utils::isValidPm(_pm_25[1].update.avg) &&
utils::isValidHumidity(_humidity[1].update.avg)) {
pm25_comp2 = ag->pms5003t_2.compensate(_pm_25[1].update.avg, _humidity[1].update.avg);
pm25_comp2 = getCorrectedPM25(true, 2, true);
pms["channels"]["2"][json_prop_pm25Compensated] = ag->round2(pm25_comp2);
}

Expand Down
3 changes: 2 additions & 1 deletion src/AgValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ class Measurements {
*
* @param useAvg Use moving average value if true, otherwise use latest value
* @param ch MeasurementType channel
* @param forceCorrection force using correction even though config correction is not applied, default to EPA
* @return float Corrected PM2.5 value
*/
float getCorrectedPM25(bool useAvg = false, int ch = 1);
float getCorrectedPM25(bool useAvg = false, int ch = 1, bool forceCorrection = false);

/**
* build json payload for every measurements
Expand Down
Loading