forked from airgradienthq/arduino
Merge pull request #261 from airgradienthq/feat/allavg
Values on display and led bar using measurement average values
This commit is contained in:
@ -162,7 +162,7 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
|
|||||||
|
|
||||||
#### Corrections
|
#### Corrections
|
||||||
|
|
||||||
The `corrections` object allows configuring PM2.5 correction algorithms and parameters. This affects both the display and local server response values.
|
The `corrections` object allows configuring PM2.5 correction algorithms and parameters locally. This affects both the display and local server response values.
|
||||||
|
|
||||||
Example correction configuration:
|
Example correction configuration:
|
||||||
|
|
||||||
@ -189,13 +189,28 @@ Example correction configuration:
|
|||||||
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
|
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
|
||||||
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
|
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
|
||||||
|
|
||||||
**Notes**:
|
**NOTES**:
|
||||||
|
|
||||||
- Set `useEpa2021` to true if want to apply EPA 2021 correction factors on top of SLR correction value.
|
- Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false`
|
||||||
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)
|
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)
|
||||||
|
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
|
||||||
|
|
||||||
**Example**:
|
**Examples**:
|
||||||
|
|
||||||
|
- PMS5003_20231030
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":false}}}}'
|
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":true}}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
- PMS5003_20231218
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0,03525,"useEpa2021":true}}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
- PMS5003_20240104
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}'
|
||||||
```
|
```
|
@ -1216,6 +1216,6 @@ void setMeasurementMaxPeriod() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int calculateMaxPeriod(int updateInterval) {
|
int calculateMaxPeriod(int updateInterval) {
|
||||||
// 0.5 is 50% reduced interval for max period
|
// 0.8 is 80% reduced interval for max period
|
||||||
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval;
|
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.8)) / updateInterval;
|
||||||
}
|
}
|
||||||
|
@ -292,8 +292,8 @@ void Configuration::defaultConfig(void) {
|
|||||||
// PM2.5 correction
|
// PM2.5 correction
|
||||||
pmCorrection.algorithm = None;
|
pmCorrection.algorithm = None;
|
||||||
pmCorrection.changed = false;
|
pmCorrection.changed = false;
|
||||||
pmCorrection.intercept = -1;
|
pmCorrection.intercept = 0;
|
||||||
pmCorrection.scalingFactor = -1;
|
pmCorrection.scalingFactor = 1;
|
||||||
pmCorrection.useEPA = false;
|
pmCorrection.useEPA = false;
|
||||||
|
|
||||||
saveConfig();
|
saveConfig();
|
||||||
@ -1369,7 +1369,12 @@ bool Configuration::isPMCorrectionChanged(void) {
|
|||||||
*/
|
*/
|
||||||
bool Configuration::isPMCorrectionEnabled(void) {
|
bool Configuration::isPMCorrectionEnabled(void) {
|
||||||
PMCorrection pmCorrection = getPMCorrection();
|
PMCorrection pmCorrection = getPMCorrection();
|
||||||
return pmCorrection.algorithm != PMCorrectionAlgorithm::None;
|
if (pmCorrection.algorithm == PMCorrectionAlgorithm::None ||
|
||||||
|
pmCorrection.algorithm == PMCorrectionAlgorithm::Unknown) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Configuration::PMCorrection Configuration::getPMCorrection(void) {
|
Configuration::PMCorrection Configuration::getPMCorrection(void) {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
|
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
|
||||||
/** Temperature */
|
/** Temperature */
|
||||||
float temp = value.getFloat(Measurements::Temperature);
|
float temp = value.getAverage(Measurements::Temperature);
|
||||||
if (utils::isValidTemperature(temp)) {
|
if (utils::isValidTemperature(temp)) {
|
||||||
float t = 0.0f;
|
float t = 0.0f;
|
||||||
if (config.isTemperatureUnitInF()) {
|
if (config.isTemperatureUnitInF()) {
|
||||||
@ -44,7 +44,7 @@ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
|
|||||||
DISP()->drawUTF8(1, 10, buf);
|
DISP()->drawUTF8(1, 10, buf);
|
||||||
|
|
||||||
/** Show humidity */
|
/** Show humidity */
|
||||||
int rhum = (int)value.getFloat(Measurements::Humidity);
|
int rhum = round(value.getAverage(Measurements::Humidity));
|
||||||
if (utils::isValidHumidity(rhum)) {
|
if (utils::isValidHumidity(rhum)) {
|
||||||
snprintf(buf, buf_size, "%d%%", rhum);
|
snprintf(buf, buf_size, "%d%%", rhum);
|
||||||
} else {
|
} else {
|
||||||
@ -292,7 +292,7 @@ void OledDisplay::showDashboard(const char *status) {
|
|||||||
DISP()->drawUTF8(1, 27, "CO2");
|
DISP()->drawUTF8(1, 27, "CO2");
|
||||||
|
|
||||||
DISP()->setFont(u8g2_font_t0_22b_tf);
|
DISP()->setFont(u8g2_font_t0_22b_tf);
|
||||||
int co2 = value.get(Measurements::CO2);
|
int co2 = round(value.getAverage(Measurements::CO2));
|
||||||
if (utils::isValidCO2(co2)) {
|
if (utils::isValidCO2(co2)) {
|
||||||
sprintf(strBuf, "%d", co2);
|
sprintf(strBuf, "%d", co2);
|
||||||
} else {
|
} else {
|
||||||
@ -313,11 +313,10 @@ void OledDisplay::showDashboard(const char *status) {
|
|||||||
DISP()->drawStr(55, 27, "PM2.5");
|
DISP()->drawStr(55, 27, "PM2.5");
|
||||||
|
|
||||||
/** Draw PM2.5 value */
|
/** Draw PM2.5 value */
|
||||||
|
int pm25 = round(value.getAverage(Measurements::PM25));
|
||||||
int pm25 = value.get(Measurements::PM25);
|
|
||||||
if (utils::isValidPm(pm25)) {
|
if (utils::isValidPm(pm25)) {
|
||||||
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||||
pm25 = (int)value.getCorrectedPM25(*ag, config);
|
pm25 = round(value.getCorrectedPM25(*ag, config, true));
|
||||||
}
|
}
|
||||||
if (config.isPmStandardInUSAQI()) {
|
if (config.isPmStandardInUSAQI()) {
|
||||||
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
|
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
|
||||||
@ -343,7 +342,7 @@ void OledDisplay::showDashboard(const char *status) {
|
|||||||
DISP()->drawStr(100, 27, "VOC:");
|
DISP()->drawStr(100, 27, "VOC:");
|
||||||
|
|
||||||
/** Draw tvocIndexvalue */
|
/** Draw tvocIndexvalue */
|
||||||
int tvoc = value.get(Measurements::TVOC);
|
int tvoc = round(value.getAverage(Measurements::TVOC));
|
||||||
if (utils::isValidVOC(tvoc)) {
|
if (utils::isValidVOC(tvoc)) {
|
||||||
sprintf(strBuf, "%d", tvoc);
|
sprintf(strBuf, "%d", tvoc);
|
||||||
} else {
|
} else {
|
||||||
@ -352,7 +351,7 @@ void OledDisplay::showDashboard(const char *status) {
|
|||||||
DISP()->drawStr(100, 39, strBuf);
|
DISP()->drawStr(100, 39, strBuf);
|
||||||
|
|
||||||
/** Draw NOx label */
|
/** Draw NOx label */
|
||||||
int nox = value.get(Measurements::NOx);
|
int nox = round(value.getAverage(Measurements::NOx));
|
||||||
DISP()->drawStr(100, 53, "NOx:");
|
DISP()->drawStr(100, 53, "NOx:");
|
||||||
if (utils::isValidNOx(nox)) {
|
if (utils::isValidNOx(nox)) {
|
||||||
sprintf(strBuf, "%d", nox);
|
sprintf(strBuf, "%d", nox);
|
||||||
@ -365,7 +364,7 @@ void OledDisplay::showDashboard(const char *status) {
|
|||||||
ag->display.clear();
|
ag->display.clear();
|
||||||
|
|
||||||
/** Set CO2 */
|
/** Set CO2 */
|
||||||
int co2 = value.get(Measurements::CO2);
|
int co2 = round(value.getAverage(Measurements::CO2));
|
||||||
if (utils::isValidCO2(co2)) {
|
if (utils::isValidCO2(co2)) {
|
||||||
snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2);
|
snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2);
|
||||||
} else {
|
} else {
|
||||||
@ -376,9 +375,9 @@ void OledDisplay::showDashboard(const char *status) {
|
|||||||
ag->display.setText(strBuf);
|
ag->display.setText(strBuf);
|
||||||
|
|
||||||
/** Set PM */
|
/** Set PM */
|
||||||
int pm25 = value.get(Measurements::PM25);
|
int pm25 = round(value.getAverage(Measurements::PM25));
|
||||||
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||||
pm25 = (int)value.getCorrectedPM25(*ag, config);
|
pm25 = round(value.getCorrectedPM25(*ag, config, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
ag->display.setCursor(0, 12);
|
ag->display.setCursor(0, 12);
|
||||||
@ -390,7 +389,7 @@ void OledDisplay::showDashboard(const char *status) {
|
|||||||
ag->display.setText(strBuf);
|
ag->display.setText(strBuf);
|
||||||
|
|
||||||
/** Set temperature and humidity */
|
/** Set temperature and humidity */
|
||||||
float temp = value.getFloat(Measurements::Temperature);
|
float temp = value.getAverage(Measurements::Temperature);
|
||||||
if (utils::isValidTemperature(temp)) {
|
if (utils::isValidTemperature(temp)) {
|
||||||
if (config.isTemperatureUnitInF()) {
|
if (config.isTemperatureUnitInF()) {
|
||||||
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
|
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
|
||||||
@ -408,7 +407,7 @@ void OledDisplay::showDashboard(const char *status) {
|
|||||||
ag->display.setCursor(0, 24);
|
ag->display.setCursor(0, 24);
|
||||||
ag->display.setText(strBuf);
|
ag->display.setText(strBuf);
|
||||||
|
|
||||||
int rhum = (int)value.getFloat(Measurements::Humidity);
|
int rhum = round(value.getAverage(Measurements::Humidity));
|
||||||
if (utils::isValidHumidity(rhum)) {
|
if (utils::isValidHumidity(rhum)) {
|
||||||
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
|
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
|
||||||
} else {
|
} else {
|
||||||
|
@ -86,7 +86,7 @@ bool StateMachine::sensorhandleLeds(void) {
|
|||||||
*/
|
*/
|
||||||
int StateMachine::co2handleLeds(void) {
|
int StateMachine::co2handleLeds(void) {
|
||||||
int totalUsed = ag->ledBar.getNumberOfLeds();
|
int totalUsed = ag->ledBar.getNumberOfLeds();
|
||||||
int co2Value = value.get(Measurements::CO2);
|
int co2Value = round(value.getAverage(Measurements::CO2));
|
||||||
if (co2Value <= 700) {
|
if (co2Value <= 700) {
|
||||||
/** G; 1 */
|
/** G; 1 */
|
||||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||||
@ -171,9 +171,10 @@ int StateMachine::co2handleLeds(void) {
|
|||||||
*/
|
*/
|
||||||
int StateMachine::pm25handleLeds(void) {
|
int StateMachine::pm25handleLeds(void) {
|
||||||
int totalUsed = ag->ledBar.getNumberOfLeds();
|
int totalUsed = ag->ledBar.getNumberOfLeds();
|
||||||
int pm25Value = value.get(Measurements::PM25);
|
|
||||||
|
int pm25Value = round(value.getAverage(Measurements::PM25));
|
||||||
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||||
pm25Value = (int)value.getCorrectedPM25(*ag, config);
|
pm25Value = round(value.getCorrectedPM25(*ag, config, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pm25Value <= 5) {
|
if (pm25Value <= 5) {
|
||||||
|
@ -396,6 +396,52 @@ float Measurements::getFloat(MeasurementType type, int ch) {
|
|||||||
return temporary->listValues.back();
|
return temporary->listValues.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float Measurements::getAverage(MeasurementType type, int ch) {
|
||||||
|
// Sanity check to validate channel, assert if invalid
|
||||||
|
validateChannel(ch);
|
||||||
|
|
||||||
|
// Follow array indexing just for get address of the value type
|
||||||
|
ch = ch - 1;
|
||||||
|
|
||||||
|
// Define data point source. Data type doesn't matter because only to get the average value
|
||||||
|
FloatValue *temporary = nullptr;
|
||||||
|
Update update;
|
||||||
|
float measurementAverage;
|
||||||
|
switch (type) {
|
||||||
|
case CO2:
|
||||||
|
measurementAverage = _co2.update.avg;
|
||||||
|
break;
|
||||||
|
case TVOC:
|
||||||
|
measurementAverage = _tvoc.update.avg;
|
||||||
|
break;
|
||||||
|
case NOx:
|
||||||
|
measurementAverage = _nox.update.avg;
|
||||||
|
break;
|
||||||
|
case PM25:
|
||||||
|
measurementAverage = _pm_25[ch].update.avg;
|
||||||
|
break;
|
||||||
|
case Temperature:
|
||||||
|
measurementAverage = _temperature[ch].update.avg;
|
||||||
|
break;
|
||||||
|
case Humidity:
|
||||||
|
measurementAverage = _humidity[ch].update.avg;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Invalidate, measurements type not handled
|
||||||
|
measurementAverage = -1000;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanity check if measurement type is not defined
|
||||||
|
if (measurementAverage == -1000) {
|
||||||
|
Serial.printf("ERROR! %s is not defined on get average value function\n", measurementTypeStr(type));
|
||||||
|
delay(1000);
|
||||||
|
assert(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return measurementAverage;
|
||||||
|
}
|
||||||
|
|
||||||
String Measurements::pms5003FirmwareVersion(int fwCode) {
|
String Measurements::pms5003FirmwareVersion(int fwCode) {
|
||||||
return pms5003FirmwareVersionBase("PMS5003x", fwCode);
|
return pms5003FirmwareVersionBase("PMS5003x", fwCode);
|
||||||
}
|
}
|
||||||
@ -485,11 +531,12 @@ void Measurements::validateChannel(int ch) {
|
|||||||
|
|
||||||
float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg, int ch) {
|
float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg, int ch) {
|
||||||
float pm25;
|
float pm25;
|
||||||
|
float corrected;
|
||||||
float humidity;
|
float humidity;
|
||||||
float pm003Count;
|
float pm003Count;
|
||||||
int channel = ch - 1; // Array index
|
|
||||||
if (useAvg) {
|
if (useAvg) {
|
||||||
// Directly call from the index
|
// Directly call from the index
|
||||||
|
int channel = ch - 1; // Array index
|
||||||
pm25 = _pm_25[channel].update.avg;
|
pm25 = _pm_25[channel].update.avg;
|
||||||
humidity = _humidity[channel].update.avg;
|
humidity = _humidity[channel].update.avg;
|
||||||
pm003Count = _pm_03_pc[channel].update.avg;
|
pm003Count = _pm_03_pc[channel].update.avg;
|
||||||
@ -500,19 +547,27 @@ float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
Configuration::PMCorrection pmCorrection = config.getPMCorrection();
|
Configuration::PMCorrection pmCorrection = config.getPMCorrection();
|
||||||
if (pmCorrection.algorithm == PMCorrectionAlgorithm::EPA_2021) {
|
switch (pmCorrection.algorithm) {
|
||||||
// EPA correction directly applied
|
case PMCorrectionAlgorithm::Unknown:
|
||||||
pm25 = ag.pms5003.compensate(pm25, humidity);
|
case PMCorrectionAlgorithm::None:
|
||||||
} else {
|
// If correction is Unknown, then default is None
|
||||||
// SLR correction, this is assumes before calling this function, correction algorithm is not None
|
corrected = pm25;
|
||||||
pm25 = ag.pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor, pmCorrection.intercept);
|
break;
|
||||||
|
case PMCorrectionAlgorithm::EPA_2021:
|
||||||
|
corrected = ag.pms5003.compensate(pm25, humidity);
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
// All SLR correction using the same flow, hence default condition
|
||||||
|
corrected = ag.pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor,
|
||||||
|
pmCorrection.intercept);
|
||||||
if (pmCorrection.useEPA) {
|
if (pmCorrection.useEPA) {
|
||||||
// Add EPA compensation on top of SLR
|
// Add EPA compensation on top of SLR
|
||||||
pm25 = ag.pms5003.compensate(pm25, humidity);
|
corrected = ag.pms5003.compensate(pm25, humidity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return pm25;
|
return corrected;
|
||||||
}
|
}
|
||||||
|
|
||||||
String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
|
String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
|
||||||
|
@ -114,9 +114,19 @@ public:
|
|||||||
*/
|
*/
|
||||||
float getFloat(MeasurementType type, int ch = 1);
|
float getFloat(MeasurementType type, int ch = 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the target measurement average value
|
||||||
|
*
|
||||||
|
* @param type measurement type that will be retrieve
|
||||||
|
* @param ch target type value channel
|
||||||
|
* @return moving average value of target measurements type
|
||||||
|
*/
|
||||||
|
float getAverage(MeasurementType type, int ch = 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the Corrected PM25 object based on the correction algorithm from configuration
|
* @brief Get the Corrected PM25 object based on the correction algorithm from configuration
|
||||||
|
*
|
||||||
|
* If correction is not enabled, then will return the raw value (either average or last value)
|
||||||
*
|
*
|
||||||
* @param ag AirGradient instance
|
* @param ag AirGradient instance
|
||||||
* @param config Configuration instance
|
* @param config Configuration instance
|
||||||
|
Reference in New Issue
Block a user