Compare commits

...

6 Commits

Author SHA1 Message Date
Samuel Siburian
72bf812235 Merge pull request #350 from airgradienthq/feat/pm-ext-measures
Post extra PM values to server based on remote configuration
2025-11-24 11:01:38 +07:00
samuelbles07
2c37ab9895 Post measures through cellular payload based on extendedPmMeasures config 2025-11-23 19:08:40 +07:00
samuelbles07
565a7fa9fd Build CE payload format for extended measures 2025-11-23 18:16:26 +07:00
samuelbles07
9e07b67951 New configuration extendedPmMeasures 2025-11-22 12:53:24 +07:00
Samuel Siburian
23f8c383fd Merge pull request #345 from airgradienthq/feat/print-s8-info
Print S8 sensor information
2025-10-02 19:22:22 +07:00
samuelbles07
c0ad1dbfad Print S8 sensor information 2025-10-02 19:18:59 +07:00
8 changed files with 130 additions and 5 deletions

View File

@@ -952,6 +952,8 @@ static void boardInit(void) {
} else {
Serial.println("Set S8 AbcDays failure");
}
ag->s8.printInformation();
}
localServer.setFwMode(fwMode);
@@ -1414,18 +1416,19 @@ void postUsingCellular(bool forcePost) {
// Build payload include all measurements from queue
std::string payload;
bool extendPmMeasures = configuration.isExtendedPmMeasuresEnabled();
payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds
for (int i = 0; i < queueSize; i++) {
auto mc = measurementCycleQueue.at(i);
payload += ",";
payload += measurements.buildMeasuresPayload(mc);
payload += measurements.buildMeasuresPayload(mc, extendPmMeasures);
}
// Release before actually post measures that might takes too long
xSemaphoreGive(mutexMeasurementCycleQueue);
// Attempt to send
if (agClient->httpPostMeasures(payload) == false) {
if (agClient->httpPostMeasures(payload, extendPmMeasures) == false) {
// Consider network has a problem, retry in next schedule
Serial.println("Post measures failed, retry in next schedule");
return;

View File

@@ -60,6 +60,7 @@ JSON_PROP_DEF(monitorDisplayCompensatedValues);
JSON_PROP_DEF(corrections);
JSON_PROP_DEF(atmp);
JSON_PROP_DEF(rhum);
JSON_PROP_DEF(extendedPmMeasures);
#define jprop_model_default ""
#define jprop_country_default "TH"
@@ -78,6 +79,7 @@ JSON_PROP_DEF(rhum);
#define jprop_displayBrightness_default 100
#define jprop_offlineMode_default false
#define jprop_monitorDisplayCompensatedValues_default false
#define jprop_extendedPmMeasures_default false
JSONVar jconfig;
@@ -400,6 +402,7 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_model] = jprop_model_default;
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default;
jconfig[jprop_extendedPmMeasures] = jprop_extendedPmMeasures_default;
// PM2.5 default correction
pmCorrection.algorithm = COR_ALGO_PM_NONE;
@@ -940,6 +943,26 @@ bool Configuration::parse(String data, bool isLocal) {
}
}
if (JSON.typeof_(root[jprop_extendedPmMeasures]) == "boolean") {
bool value = root[jprop_extendedPmMeasures];
bool oldValue = jconfig[jprop_extendedPmMeasures];
if (value != oldValue) {
changed = true;
configLogInfo(String(jprop_extendedPmMeasures),
String(oldValue ? "true" : "false"),
String(value ? "true" : "false"));
jconfig[jprop_extendedPmMeasures] = value;
}
} else {
if (jsonTypeInvalid(root[jprop_extendedPmMeasures], "boolean")) {
failedMessage = jsonTypeInvalidMessage(
String(jprop_extendedPmMeasures), "boolean");
jsonInvalid();
return false;
}
}
// PM2.5 Corrections
if (updatePmCorrection(root)) {
changed = true;
@@ -1002,6 +1025,11 @@ bool Configuration::isTemperatureUnitInF(void) {
return (unit == "f");
}
bool Configuration::isExtendedPmMeasuresEnabled(void) {
return jconfig[jprop_extendedPmMeasures];
}
/**
* @brief Country name, it's short name ex: TH = Thailand
*
@@ -1368,6 +1396,18 @@ void Configuration::toConfig(const char *buf) {
logInfo("toConfig: disableCloudConnection changed");
}
/** validate extendedPmMeasures configuration */
if (JSON.typeof_(jconfig[jprop_extendedPmMeasures]) != "boolean") {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
}
if (isConfigFieldInvalid) {
jconfig[jprop_extendedPmMeasures] = jprop_extendedPmMeasures_default;
changed = true;
logInfo("toConfig: extendedPmMeasures changed");
}
/** validate configuration control */
if (JSON.typeof_(jprop_configurationControl) != "string") {
isConfigFieldInvalid = true;

View File

@@ -79,6 +79,7 @@ public:
String toString(void);
String toString(AgFirmwareMode fwMode);
bool isTemperatureUnitInF(void);
bool isExtendedPmMeasuresEnabled(void);
String getCountry(void);
bool isPmStandardInUSAQI(void);
int getCO2CalibrationAbcDays(void);

View File

@@ -885,7 +885,7 @@ Measurements::Measures Measurements::getMeasures() {
return mc;
}
std::string Measurements::buildMeasuresPayload(Measures &mc) {
std::string Measurements::buildMeasuresPayload(Measures &mc, bool extendedPmMeasures) {
std::ostringstream oss;
// CO2
@@ -984,6 +984,76 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
oss << mc.signal;
}
if (extendedPmMeasures) {
oss << ",,,,,,,,"; // Add placeholder for MAX payload (BMS & O3/NO2)
/// PM 0.5 particle count
if (utils::isValidPm03Count(mc.pm_05_pc[0]) && utils::isValidPm03Count(mc.pm_05_pc[1])) {
oss << std::round((mc.pm_05_pc[0] + mc.pm_05_pc[1]) / 2.0f);
} else if (utils::isValidPm03Count(mc.pm_05_pc[0])) {
oss << std::round(mc.pm_05_pc[0]);
} else if (utils::isValidPm03Count(mc.pm_05_pc[1])) {
oss << std::round(mc.pm_05_pc[1]);
}
oss << ",";
/// PM 1.0 particle count
if (utils::isValidPm03Count(mc.pm_01_pc[0]) && utils::isValidPm03Count(mc.pm_01_pc[1])) {
oss << std::round((mc.pm_01_pc[0] + mc.pm_01_pc[1]) / 2.0f);
} else if (utils::isValidPm03Count(mc.pm_01_pc[0])) {
oss << std::round(mc.pm_01_pc[0]);
} else if (utils::isValidPm03Count(mc.pm_01_pc[1])) {
oss << std::round(mc.pm_01_pc[1]);
}
oss << ",";
/// PM 2.5 particle count
if (utils::isValidPm03Count(mc.pm_25_pc[0]) && utils::isValidPm03Count(mc.pm_25_pc[1])) {
oss << std::round((mc.pm_25_pc[0] + mc.pm_25_pc[1]) / 2.0f);
} else if (utils::isValidPm03Count(mc.pm_25_pc[0])) {
oss << std::round(mc.pm_25_pc[0]);
} else if (utils::isValidPm03Count(mc.pm_25_pc[1])) {
oss << std::round(mc.pm_25_pc[1]);
}
oss << ",";
/// PM 5.0 particle count
if (utils::isValidPm03Count(mc.pm_5_pc[0]) && utils::isValidPm03Count(mc.pm_5_pc[1])) {
oss << std::round((mc.pm_5_pc[0] + mc.pm_5_pc[1]) / 2.0f);
} else if (utils::isValidPm03Count(mc.pm_5_pc[0])) {
oss << std::round(mc.pm_5_pc[0]);
} else if (utils::isValidPm03Count(mc.pm_5_pc[1])) {
oss << std::round(mc.pm_5_pc[1]);
}
oss << ",";
/// PM 10 particle count
if (utils::isValidPm03Count(mc.pm_10_pc[0]) && utils::isValidPm03Count(mc.pm_10_pc[1])) {
oss << std::round((mc.pm_10_pc[0] + mc.pm_10_pc[1]) / 2.0f);
} else if (utils::isValidPm03Count(mc.pm_10_pc[0])) {
oss << std::round(mc.pm_10_pc[0]);
} else if (utils::isValidPm03Count(mc.pm_10_pc[1])) {
oss << std::round(mc.pm_10_pc[1]);
}
oss << ",";
/// PM2.5 standard particle
if (utils::isValidPm(mc.pm_25_sp[0]) && utils::isValidPm(mc.pm_25_sp[1])) {
float pm10 = (mc.pm_25_sp[0] + mc.pm_25_sp[1]) / 2.0f;
oss << std::round(pm10 * 10);
} else if (utils::isValidPm(mc.pm_25_sp[0])) {
oss << std::round(mc.pm_25_sp[0] * 10);
} else if (utils::isValidPm(mc.pm_25_sp[1])) {
oss << std::round(mc.pm_25_sp[1] * 10);
}
}
return oss.str();
}

View File

@@ -184,7 +184,7 @@ public:
Measures getMeasures();
std::string buildMeasuresPayload(Measures &measures);
std::string buildMeasuresPayload(Measures &mc, bool extendedPmMeasures);
/**
* Set to true if want to debug every update value

View File

@@ -835,3 +835,13 @@ bool S8::setAbcPeriod(int hours) {
* @return int Hour
*/
int S8::getAbcPeriod(void) { return getCalibPeriodABC(); }
void S8::printInformation(void) {
Serial.print("S8 type ID: 0x");
Serial.println(getSensorTypeId(), HEX);
Serial.print("S8 serial number: 0x");
Serial.println(getSensorId(), HEX);
Serial.print("S8 memory map version: 0x");
Serial.println(getMemoryMapVersion(), HEX);
}

View File

@@ -80,6 +80,7 @@ public:
bool isBaseLineCalibrationDone(void);
bool setAbcPeriod(int hours);
int getAbcPeriod(void);
void printInformation(void);
private:
/** Variables */