Compare commits

...

56 Commits
3.3.3 ... 3.3.9

Author SHA1 Message Date
d8eb6b3c1a Prepare release 3.3.9 2025-06-18 14:25:14 +07:00
969858b5cb Merge pull request #324 from airgradienthq/fix/comms-ag-server
Post measures and fetch configuration on boot only if respective configuration is set
2025-06-10 01:29:58 +07:00
09b5805686 Apply brightness 2025-06-10 01:26:29 +07:00
b09b753339 Only send first measures on boot if postDataToAirgradient is enabled 2025-06-10 01:10:14 +07:00
ddb3dba131 Skip fetch configuration on boot when configuration control is local 2025-06-10 01:09:53 +07:00
e780b0ace6 Merge pull request #323 from airgradienthq/fix/local-config-update
Update configuration changes by callback
2025-06-09 02:21:07 +07:00
e82da5401e Add new flag for command request
Such as led bar test and co2 calibration test
2025-06-09 02:13:32 +07:00
50a98acde4 Update configuration changes to main by callback 2025-06-06 04:10:53 +07:00
7049d21a41 Prepare release 3.3.8 2025-05-14 13:09:12 +07:00
d5cdeaa9f3 Fix print average function schedule
if pms value invalid show the channel
2025-05-14 13:01:23 +07:00
09207c6923 Merge pull request #319 from airgradienthq/fix/resizing-queue
Fix resizing measurement queue after post by cellular post
2025-05-14 12:53:07 +07:00
0a64424196 add show content delay for display brightness 2025-05-14 12:39:42 +07:00
5b38ca222b Prepare release 3.3.7 2025-05-12 10:54:38 +07:00
9ee35341a5 Merge pull request #318 from airgradienthq/feat/improve-measure-logs
Improve measurements logging
2025-05-11 14:22:08 +07:00
cec0514444 print measurements on schedule 2025-05-11 14:10:51 +07:00
626a2240fa Fix resizing queue after success post
This fix should be make it more consistent
2025-05-09 15:45:53 +07:00
174ec6568f Merge pull request #313 from airgradienthq/fix/mode-cloud-disable
Fix bootloop when cloud connection is disabled
2025-05-08 01:28:07 +07:00
6b55719399 Fix cloud connection mode
Use continue instead of return to ignore the rest of transmission code
2025-05-05 17:37:28 +07:00
e2084f0738 Fix OTA request on boot when cloud Connection disabled 2025-05-05 17:36:14 +07:00
5e07923690 Merge pull request #311 from ccoley/fix/submodule-url
Fix failing to clone submodules recursively
2025-05-04 14:13:47 +07:00
04049439b1 Merge pull request #310 from airgradienthq/fix/openmetrics
Fix calling airgradient client before initialization on open metrics
2025-05-04 13:29:24 +07:00
c148d256d7 Use a relative path for submodule URL
We keep the username in the path so that forks don't need to also fork
the submodules
2025-04-30 23:25:30 -07:00
02849a1938 Fix pass agclient to openmetrics
previously, agclient initialized after setAirgradient
2025-05-01 13:38:42 +08:00
074337a96d Merge pull request #304 from airgradienthq/fix/api-root
FIX: HTTP domain configuration changes applied for OTA too
2025-04-21 13:42:26 +07:00
4daa817a0b Change airgradient-ota commit to main branch 2025-04-21 13:41:15 +07:00
81a4502952 Fix: http domain applied for OTA 2025-04-21 13:27:05 +07:00
764e2eae38 Prepare release 3.3.6 2025-04-16 12:34:17 +07:00
79bf9811be Merge pull request #303 from airgradienthq/fix/ce-tvoc
Fix incorrect TVOC / NOx values when when network option is cellular
2025-04-15 12:25:39 +07:00
9475724d0c Remove comment 2025-04-15 12:20:26 +07:00
e7603a7659 Update feedback
Change airgradient-ota submodule to latest main instead of branch
2025-04-14 15:24:53 +07:00
9bba89722e Fix sgp unreliable value by only pause task when performing ota 2025-04-12 02:25:04 +07:00
81945a358e SGP41 add method to pause and resume task handle 2025-04-12 02:22:55 +07:00
3d26a54d69 Prepare release 3.3.5 2025-04-11 15:56:05 +07:00
b70ee75d50 Merge pull request #302 from airgradienthq/improve-ce-reconnection
Improve cellular client reconnection
2025-04-11 15:49:25 +07:00
c6846c818a Rename MICROS_TO_MINUTES() to follow convention 2025-04-11 15:46:21 +07:00
0b1c901a76 Rename cellularModule object name to cellularCard
Rename checkCellularClientNotReady to restartIfCeClientIssueOverTwoHours
2025-04-11 13:41:07 +07:00
83504c8628 Bump libs to latest 2025-04-10 19:05:28 +07:00
4487992748 Remove unnecessary code 2025-04-10 14:58:51 +07:00
3c8a65a329 Use esp_timer_get_time for timer of ce client not ready 2025-04-10 14:58:11 +07:00
673d564ddb Fix based on feedback 2025-04-10 12:45:18 +07:00
423eb4808f Change airgradient-client to latest main 2025-04-10 02:14:34 +07:00
18a710ffc2 Make sure transmit cycle not too long to wait divisible by 3 2025-04-10 02:06:11 +07:00
040cb79a4d Transmit measures only if queue size is 1 or divisible by 3 2025-04-10 00:27:44 +07:00
52d3dc03f1 Redundant check if cellular client not ready for 2 hours
Check calls happen in both task
2025-04-09 23:46:03 +07:00
1c6bc3ec55 Bump airgradient-client fix esp8266 compile 2025-04-09 22:48:21 +07:00
34d7c93e14 Improve reconnection of CE network option
Restart system if it already too long
2025-04-09 15:51:54 +07:00
fee1dc25d6 Improve reconnection of CE network option
Restart system if it already too long
Bump airgradient-client: Improve ensureClientConnection
2025-04-09 15:49:34 +07:00
9fb01d42f4 Prepare release 3.3.4 2025-04-07 16:56:54 +07:00
7bb013939c Merge pull request #301 from airgradienthq/feat/signal
Include cellular signal in RSSI (dbm) when post measures
2025-04-07 16:55:42 +07:00
0da21155e7 bump submodule to post measures with new endpoint
that include signal in rssi
2025-04-07 16:29:54 +07:00
7a153cc0ea add cellular signal quality to post measures payload
If value invalid 0, then do include it to payload
2025-04-07 16:29:15 +07:00
b079c35e6b Include cellular signal in rssi to measurement cycle 2025-04-07 16:28:37 +07:00
6051e183b8 Merge pull request #300 from airgradienthq/fix/pms-error
Remove CORE_DEBUG_LEVEL that affected PM sensor reading
2025-04-07 15:33:37 +07:00
c95379b957 Update submodule to the latest main branch 2025-04-07 15:30:52 +07:00
0cae8bc185 Change ag log level to info 2025-04-05 23:56:14 +07:00
5902a4c8e4 Remove arduino-esp32 core debug level from build_flags
And change it to airgradient log level that take effect to airgradient submodules
Temporary bump submodule to WIP branch
2025-04-05 23:45:46 +07:00
15 changed files with 432 additions and 94 deletions

4
.gitmodules vendored
View File

@ -1,6 +1,6 @@
[submodule "src/Libraries/airgradient-client"]
path = src/Libraries/airgradient-client
url = git@github.com:airgradienthq/airgradient-client.git
url = ../../airgradienthq/airgradient-client.git
[submodule "src/Libraries/airgradient-ota"]
path = src/Libraries/airgradient-ota
url = git@github.com:airgradienthq/airgradient-ota.git
url = ../../airgradienthq/airgradient-ota.git

View File

@ -26,7 +26,6 @@ https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgStateMachine.h"
@ -37,6 +36,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include "Arduino.h"
#include "EEPROM.h"
#include "ESPmDNS.h"
#include "Libraries/airgradient-client/src/common.h"
#include "LocalServer.h"
#include "MqttClient.h"
#include "OpenMetrics.h"
@ -45,6 +45,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include <HardwareSerial.h>
#include <WebServer.h>
#include <WiFi.h>
#include <cstdint>
#include <string>
#include "Libraries/airgradient-client/src/agSerial.h"
@ -65,7 +66,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */
#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */
#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */
#define CELLULAR_TRANSMISSION_INTERVAL 9 * 60000 /** ms */
#define CELLULAR_TRANSMISSION_INTERVAL 3 * 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
@ -74,7 +75,10 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
#define TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE (1 * 60) /** minutes */
#define TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY (2 * 60) /** minutes */
#define MEASUREMENT_TRANSMIT_CYCLE 3
#define MAXIMUM_MEASUREMENT_CYCLE_QUEUE 80
#define RESERVED_MEASUREMENT_CYCLE_CAPACITY 10
@ -88,6 +92,8 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#define GPIO_EXPANSION_CARD_POWER 4
#define GPIO_IIC_RESET 3
#define MINUTES() ((uint32_t)(esp_timer_get_time() / 1000 / 1000 / 60))
static MqttClient mqttClient(Serial);
static TaskHandle_t mqttTask = NULL;
static Configuration configuration(Serial);
@ -102,7 +108,7 @@ static OpenMetrics openMetrics(measurements, configuration, wifiConnector);
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static AgSerial *agSerial;
static CellularModule *cell;
static CellularModule *cellularCard;
static AirgradientClient *agClient;
enum NetworkOption {
@ -111,12 +117,17 @@ enum NetworkOption {
};
NetworkOption networkOption;
TaskHandle_t handleNetworkTask = NULL;
static bool otaInProgress = false;
static bool firmwareUpdateInProgress = false;
static uint32_t factoryBtnPressTime = 0;
static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
static bool ledBarButtonTest = false;
static String fwNewVersion;
static int lastCellSignalQuality = 99; // CSQ
// Default value is 0, indicate its not started yet
// In minutes
uint32_t agCeClientProblemDetectedTime = 0;
SemaphoreHandle_t mutexMeasurementCycleQueue;
static std::vector<Measurements::Measures> measurementCycleQueue;
@ -132,6 +143,7 @@ static void updatePm(void);
static void sendDataToServer(void);
static void tempHumUpdate(void);
static void co2Update(void);
static void printMeasurements();
static void mdnsInit(void);
static void createMqttTask(void);
static void initMqtt(void);
@ -145,6 +157,7 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod();
static void newMeasurementCycle();
static void restartIfCeClientIssueOverTwoHours();
static void networkSignalCheck();
static void networkingTask(void *args);
@ -160,6 +173,7 @@ AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, checkForFirmwareUpdate);
AgSchedule networkSignalCheckSchedule(10000, networkSignalCheck);
AgSchedule printMeasurementsSchedule(6000, printMeasurements);
void setup() {
/** Serial for print debug message */
@ -179,6 +193,7 @@ void setup() {
/** Initialize local configure */
configuration.begin();
configuration.setConfigurationUpdatedCallback(configUpdateHandle);
/** Init I2C */
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
@ -198,7 +213,7 @@ void setup() {
oledDisplay.setAirGradient(ag);
stateMachine.setAirGradient(ag);
wifiConnector.setAirGradient(ag);
openMetrics.setAirGradient(ag, agClient);
openMetrics.setAirGradient(ag);
localServer.setAirGraident(ag);
measurements.setAirGradient(ag);
@ -206,9 +221,6 @@ void setup() {
boardInit();
setMeasurementMaxPeriod();
// Comment below line to disable debug measurement readings
measurements.setDebug(true);
bool connectToNetwork = true;
if (ag->isOne()) { // Offline mode only available for indoor monitor
/** Show message confirm offline mode, should me perform if LED bar button
@ -259,6 +271,7 @@ void setup() {
Serial.println("Display brightness: " + String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
@ -297,11 +310,17 @@ void setup() {
}
void loop() {
if (networkOption == UseCellular) {
// Check if cellular client not ready until certain time
// Redundant check in both task to make sure its executed
restartIfCeClientIssueOverTwoHours();
}
// Schedule to feed external watchdog
watchdogFeedSchedule.run();
if (otaInProgress) {
// OTA currently in progress, temporarily disable running sensor schedules
if (firmwareUpdateInProgress) {
// Firmare update currently in progress, temporarily disable running sensor schedules
delay(10000);
return;
}
@ -334,7 +353,7 @@ void loop() {
static bool pmsConnected = false;
if (pmsConnected != ag->pms5003.connected()) {
pmsConnected = ag->pms5003.connected();
Serial.printf("PMS sensor %s ", pmsConnected?"connected":"removed");
Serial.printf("PMS sensor %s \n", pmsConnected?"connected":"removed");
}
}
} else {
@ -346,11 +365,17 @@ void loop() {
}
}
/* Run measurement schedule */
printMeasurementsSchedule.run();
/** factory reset handle */
factoryConfigReset();
/** check that local configuration changed then do some action */
configUpdateHandle();
if (configuration.isCommandRequested()) {
// Each state machine already has an independent request command check
stateMachine.executeCo2Calibration();
stateMachine.executeLedBarTest();
}
}
static void co2Update(void) {
@ -367,6 +392,10 @@ static void co2Update(void) {
}
}
void printMeasurements() {
measurements.printCurrentAverage();
}
static void mdnsInit(void) {
if (!MDNS.begin(localServer.getHostname().c_str())) {
Serial.println("Init mDNS failed");
@ -537,35 +566,37 @@ static bool sgp41Init(void) {
}
void checkForFirmwareUpdate(void) {
if (configuration.isCloudConnectionDisabled()) {
Serial.println("Cloud connection is disabled, skip firmware update");
return;
}
AirgradientOTA *agOta;
if (networkOption == UseWifi) {
agOta = new AirgradientOTAWifi;
} else {
agOta = new AirgradientOTACellular(cell);
agOta = new AirgradientOTACellular(cellularCard);
}
// Indicate main task that ota is performing
Serial.println("Check for firmware update, disabling main task");
otaInProgress = true;
if (configuration.hasSensorSGP && networkOption == UseCellular) {
// Only for cellular because it can disturb i2c line
Serial.println("Disable SGP41 task for cellular OTA");
ag->sgp41.end();
}
// Indicate main task that firmware update is in progress
firmwareUpdateInProgress = true;
agOta->setHandlerCallback(otaHandlerCallback);
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION);
// Only goes to this line if OTA is not success
String httpDomain = configuration.getHttpDomain();
if (httpDomain != "") {
Serial.printf("httpDomain configuration available, start OTA with custom domain\n",
httpDomain.c_str());
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION, httpDomain.c_str());
} else {
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION);
}
// Only goes to this line if firmware update is not success
// Handled by otaHandlerCallback
otaInProgress = false;
if (configuration.hasSensorSGP && networkOption == UseCellular) {
// Re-start SGP41 task
if (!sgp41Init()) {
Serial.println("Failed re-start SGP41 task");
}
}
// Indicate main task that firmware update finish
firmwareUpdateInProgress = false;
delete agOta;
Serial.println();
@ -573,14 +604,25 @@ void checkForFirmwareUpdate(void) {
void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) {
switch (result) {
case AirgradientOTA::Starting:
case AirgradientOTA::Starting: {
Serial.println("Firmware update starting...");
if (configuration.hasSensorSGP && networkOption == UseCellular) {
// Temporary pause SGP41 task while cellular firmware update is in progress
ag->sgp41.pause();
}
displayExecuteOta(result, fwNewVersion, 0);
break;
}
case AirgradientOTA::InProgress:
Serial.printf("OTA progress: %s\n", msg);
displayExecuteOta(result, "", std::stoi(msg));
break;
case AirgradientOTA::Failed:
displayExecuteOta(result, "", 0);
if (configuration.hasSensorSGP && networkOption == UseCellular) {
ag->sgp41.resume();
}
break;
case AirgradientOTA::Skipped:
case AirgradientOTA::AlreadyUpToDate:
displayExecuteOta(result, "", 0);
@ -647,7 +689,11 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
}
delay(1000);
}
if (ag->isOne()) {
oledDisplay.setAirGradient(0);
oledDisplay.setBrightness(0);
}
break;
}
default:
@ -925,8 +971,8 @@ void initializeNetwork() {
if (agSerial->open()) {
Serial.println("Cellular module found");
// Initialize cellular module and use cellular as agClient
cell = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN);
agClient = new AirgradientCellularClient(cell);
cellularCard = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN);
agClient = new AirgradientCellularClient(cellularCard);
networkOption = UseCellular;
} else {
Serial.println("Cellular module not available, using wifi");
@ -959,6 +1005,9 @@ void initializeNetwork() {
ESP.restart();
}
// Provide openmetrics to have access to last transmission result
openMetrics.setAirgradientClient(agClient);
if (networkOption == UseCellular) {
// Disabling it again
agSerial->setDebug(false);
@ -994,10 +1043,17 @@ void initializeNetwork() {
return;
}
// Send data for the first time to AG server at boot
// Send data for the first time to AG server at boot only if postDataToAirgradient is enabled
if (configuration.isPostDataToAirGradient()) {
sendDataToAg();
}
}
// Skip fetch configuration if configuration control is set to "local" only
if (configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
ledBarEnabledUpdate();
return;
}
std::string config = agClient->httpFetchConfig();
configSchedule.update();
@ -1029,8 +1085,8 @@ static void configurationUpdateSchedule(void) {
}
std::string config = agClient->httpFetchConfig();
if (agClient->isLastFetchConfigSucceed() && configuration.parse(config.c_str(), false)) {
configUpdateHandle();
if (agClient->isLastFetchConfigSucceed()) {
configuration.parse(config.c_str(), false);
}
}
@ -1039,8 +1095,6 @@ static void configUpdateHandle() {
return;
}
stateMachine.executeCo2Calibration();
String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end();
@ -1112,11 +1166,6 @@ static void configUpdateHandle() {
if (configuration.isDisplayBrightnessChanged()) {
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
stateMachine.executeLedBarTest();
}
else if(ag->isOpenAir()) {
stateMachine.executeLedBarTest();
}
// Update display and led bar notification based on updated configuration
@ -1340,7 +1389,10 @@ void postUsingWifi() {
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
}
void postUsingCellular() {
/**
* forcePost to force post without checking transmit cycle
*/
void postUsingCellular(bool forcePost) {
// Aquire queue mutex to get queue size
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
@ -1352,6 +1404,14 @@ void postUsingCellular() {
return;
}
// Check queue size if its ready to transmit
// It is ready if size is divisible by 3
if (!forcePost && (queueSize % MEASUREMENT_TRANSMIT_CYCLE) > 0) {
Serial.printf("Not ready to transmit, queue size are %d\n", queueSize);
xSemaphoreGive(mutexMeasurementCycleQueue);
return;
}
// Build payload include all measurements from queue
std::string payload;
payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds
@ -1373,13 +1433,17 @@ void postUsingCellular() {
// Post success, remove the data that previously sent from queue
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
measurementCycleQueue.erase(measurementCycleQueue.begin(),
measurementCycleQueue.begin() + queueSize);
if (measurementCycleQueue.capacity() > RESERVED_MEASUREMENT_CYCLE_CAPACITY) {
Serial.println("measurementCycleQueue capacity more than reserved space, resizing..");
measurementCycleQueue.resize(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
std::vector<Measurements::Measures> tmp;
tmp.reserve(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
measurementCycleQueue.swap(tmp);
} else {
// If not more than the capacity, then just clear all the values
measurementCycleQueue.clear();
}
xSemaphoreGive(mutexMeasurementCycleQueue);
}
@ -1393,7 +1457,7 @@ void sendDataToServer(void) {
if (networkOption == UseWifi) {
postUsingWifi();
} else if (networkOption == UseCellular) {
postUsingCellular();
postUsingCellular(false);
}
}
@ -1472,26 +1536,56 @@ void networkSignalCheck() {
if (networkOption == UseWifi) {
Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI());
} else if (networkOption == UseCellular) {
auto result = cell->retrieveSignal();
auto result = cellularCard->retrieveSignal();
if (result.status != CellReturnStatus::Ok) {
agClient->setClientReady(false);
lastCellSignalQuality = 99;
return;
}
// Save last signal quality
lastCellSignalQuality = result.data;
if (result.data == 99) {
// 99 indicate cellular not attached to network
agClient->setClientReady(false);
return;
}
Serial.printf("Cellular signal strength %d\n", result.data);
Serial.printf("Cellular signal quality %d\n", result.data);
}
}
/**
* If in 2 hours cellular client still not ready, then restart system
*/
void restartIfCeClientIssueOverTwoHours() {
if (agCeClientProblemDetectedTime > 0 &&
(MINUTES() - agCeClientProblemDetectedTime) >
TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY) {
// Give up wait
Serial.println("Rebooting because CE client issues for 2 hours detected");
int i = 3;
while (i != 0) {
if (ag->isOne()) {
String tmp = "Rebooting in " + String(i);
oledDisplay.setText("CE error", "since 2h", tmp.c_str());
} else {
Serial.println("Rebooting... " + String(i));
}
i = i - 1;
delay(1000);
}
oledDisplay.setBrightness(0);
esp_restart();
}
}
void networkingTask(void *args) {
// If cloud connection enabled, run first transmission to server at boot
if (configuration.isCloudConnectionDisabled() == false) {
// OTA check on boot
#ifdef ESP8266
// ota not supported
#else
// because cellular it takes too long, watchdog triggered
#ifndef ESP8266
checkForFirmwareUpdate();
checkForUpdateSchedule.update();
#endif
@ -1501,14 +1595,15 @@ void networkingTask(void *args) {
if (networkOption == UseCellular) {
Serial.println("Prepare first measures cycle to send on boot for 20s");
delay(20000);
networkSignalCheck();
newMeasurementCycle();
sendDataToServer();
postUsingCellular(true);
measurementSchedule.update();
}
// Reset scheduler
configSchedule.update();
transmissionSchedule.update();
}
while (1) {
// Handle reconnection based on mode
@ -1521,21 +1616,50 @@ void networkingTask(void *args) {
}
else if (networkOption == UseCellular) {
if (agClient->isClientReady() == false) {
Serial.println("Cellular client not ready, ensuring connection...");
// Start time if value still default
if (agCeClientProblemDetectedTime == 0) {
agCeClientProblemDetectedTime = MINUTES();
}
// Enable at command debug
agSerial->setDebug(true);
if (agClient->ensureClientConnection() == false) {
Serial.println("Cellular client connection not ready, retry in 5s...");
delay(5000);
// Check if cellular client not ready until certain time
// Redundant check in both task to make sure its executed
restartIfCeClientIssueOverTwoHours();
// Power cycling cellular module due to network issues for more than 1 hour
bool resetModule = true;
if ((MINUTES() - agCeClientProblemDetectedTime) >
TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE) {
Serial.println("The CE client hasn't recovered in more than 1 hour, "
"performing a power cycle");
cellularCard->powerOff();
delay(2000);
cellularCard->powerOn();
delay(10000);
// no need to reset module when calling ensureClientConnection()
resetModule = false;
}
// Attempt to reconnect
Serial.println("Cellular client not ready, ensuring connection...");
if (agClient->ensureClientConnection(resetModule) == false) {
Serial.println("Cellular client connection not ready, retry in 30s...");
delay(30000); // before retry, wait for 30s
continue;
}
agSerial->setDebug(false);
// Client is ready
agCeClientProblemDetectedTime = 0; // reset to default
agSerial->setDebug(false); // disable at command debug
}
}
// If connection to AirGradient server disable don't run config and transmission schedule
if (configuration.isCloudConnectionDisabled()) {
delay(1000);
return;
continue;
}
// Run scheduler
@ -1558,7 +1682,10 @@ void newMeasurementCycle() {
measurementCycleQueue.erase(measurementCycleQueue.begin());
}
// Get current measures
auto mc = measurements.getMeasures();
mc.signal = cellularCard->csqToDbm(lastCellSignalQuality); // convert to RSSI
measurementCycleQueue.push_back(mc);
Serial.println("New measurement cycle added to queue");
// Release mutex

View File

@ -6,8 +6,11 @@ OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag, AirgradientClient *client) {
void OpenMetrics::setAirGradient(AirGradient *ag) {
this->ag = ag;
}
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
this->agClient = client;
}

View File

@ -19,7 +19,8 @@ public:
OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector);
~OpenMetrics();
void setAirGradient(AirGradient *ag, AirgradientClient *client);
void setAirGradient(AirGradient *ag);
void setAirgradientClient(AirgradientClient *client);
const char *getApiContentType(void);
const char* getApi(void);
String getPayload(void);

View File

@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor
version=3.3.3
version=3.3.9
author=AirGradient <support@airgradient.com>
maintainer=AirGradient <support@airgradient.com>
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.

View File

@ -12,7 +12,7 @@
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D CORE_DEBUG_LEVEL=3 -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D AG_LOG_LEVEL=AG_LOG_LEVEL_INFO -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
board_build.partitions = partitions.csv
monitor_speed = 115200
lib_deps =

View File

@ -456,6 +456,10 @@ bool Configuration::begin(void) {
return true;
}
void Configuration::setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback) {
_callback = callback;
}
/**
* @brief Parse JSON configura string to local configure
*
@ -951,15 +955,18 @@ bool Configuration::parse(String data, bool isLocal) {
changed = true;
}
if (ledBarTestRequested || co2CalibrationRequested) {
commandRequested = true;
updated = true;
}
if (changed) {
updated = true;
saveConfig();
printConfig();
} else {
if (ledBarTestRequested || co2CalibrationRequested) {
updated = true;
}
_callback();
}
return true;
}
@ -1159,6 +1166,12 @@ bool Configuration::isUpdated(void) {
return updated;
}
bool Configuration::isCommandRequested(void) {
bool oldState = this->commandRequested;
this->commandRequested = false;
return oldState;
}
String Configuration::jsonTypeInvalidMessage(String name, String type) {
return "'" + name + "' type is invalid, expecting '" + type + "'";
}

View File

@ -28,6 +28,7 @@ private:
bool co2CalibrationRequested;
bool ledBarTestRequested;
bool updated;
bool commandRequested = false;
String failedMessage;
bool _noxLearnOffsetChanged;
bool _tvocLearningOffsetChanged;
@ -70,6 +71,9 @@ public:
bool hasSensorSGP = true;
bool hasSensorSHT = true;
typedef void (*ConfigurationUpdatedCallback_t)();
void setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback);
bool begin(void);
bool parse(String data, bool isLocal);
String toString(void);
@ -90,6 +94,7 @@ public:
void reset(void);
String getModel(void);
bool isUpdated(void);
bool isCommandRequested(void);
String getFailedMesage(void);
void setPostToAirGradient(bool enable);
bool noxLearnOffsetChanged(void);
@ -116,6 +121,8 @@ public:
PMCorrection getPMCorrection(void);
TempHumCorrection getTempCorrection(void);
TempHumCorrection getHumCorrection(void);
private:
ConfigurationUpdatedCallback_t _callback;
};
#endif /** _AG_CONFIG_H_ */

View File

@ -76,6 +76,86 @@ Measurements::Measurements(Configuration &config) : config(config) {
void Measurements::setAirGradient(AirGradient *ag) { this->ag = ag; }
void Measurements::printCurrentAverage() {
Serial.println();
if (config.hasSensorS8) {
if (utils::isValidCO2(_co2.update.avg)) {
Serial.printf("CO2 = %.2f ppm\n", _co2.update.avg);
} else {
Serial.printf("CO2 = -\n");
}
}
if (config.hasSensorSHT) {
if (utils::isValidTemperature(_temperature[0].update.avg)) {
Serial.printf("Temperature = %.2f C\n", _temperature[0].update.avg);
} else {
Serial.printf("Temperature = -\n");
}
if (utils::isValidHumidity(_humidity[0].update.avg)) {
Serial.printf("Relative Humidity = %.2f\n", _humidity[0].update.avg);
} else {
Serial.printf("Relative Humidity = -\n");
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(_tvoc.update.avg)) {
Serial.printf("TVOC Index = %.1f\n", _tvoc.update.avg);
} else {
Serial.printf("TVOC Index = -\n");
}
if (utils::isValidVOC(_tvoc_raw.update.avg)) {
Serial.printf("TVOC Raw = %.1f\n", _tvoc_raw.update.avg);
} else {
Serial.printf("TVOC Raw = -\n");
}
if (utils::isValidNOx(_nox.update.avg)) {
Serial.printf("NOx Index = %.1f\n", _nox.update.avg);
} else {
Serial.printf("NOx Index = -\n");
}
if (utils::isValidNOx(_nox_raw.update.avg)) {
Serial.printf("NOx Raw = %.1f\n", _nox_raw.update.avg);
} else {
Serial.printf("NOx Raw = -\n");
}
}
if (config.hasSensorPMS1) {
printCurrentPMAverage(1);
if (!config.hasSensorSHT) {
if (utils::isValidTemperature(_temperature[0].update.avg)) {
Serial.printf("[1] Temperature = %.2f C\n", _temperature[0].update.avg);
} else {
Serial.printf("[1] Temperature = -\n");
}
if (utils::isValidHumidity(_humidity[0].update.avg)) {
Serial.printf("[1] Relative Humidity = %.2f\n", _humidity[0].update.avg);
} else {
Serial.printf("[1] Relative Humidity = -\n");
}
}
}
if (config.hasSensorPMS2) {
printCurrentPMAverage(2);
if (!config.hasSensorSHT) {
if (utils::isValidTemperature(_temperature[1].update.avg)) {
Serial.printf("[2] Temperature = %.2f C\n", _temperature[1].update.avg);
} else {
Serial.printf("[2] Temperature = -\n");
}
if (utils::isValidHumidity(_humidity[1].update.avg)) {
Serial.printf("[2] Relative Humidity = %.2f\n", _humidity[1].update.avg);
} else {
Serial.printf("[2] Relative Humidity = -\n");
}
}
}
Serial.println();
}
void Measurements::maxPeriod(MeasurementType type, int max) {
switch (type) {
case Temperature:
@ -570,6 +650,77 @@ String Measurements::measurementTypeStr(MeasurementType type) {
return str;
}
void Measurements::printCurrentPMAverage(int ch) {
int idx = ch - 1;
if (utils::isValidPm(_pm_01[idx].update.avg)) {
Serial.printf("[%d] Atmospheric PM 1.0 = %.2f ug/m3\n", ch, _pm_01[idx].update.avg);
} else {
Serial.printf("[%d] Atmospheric PM 1.0 = -\n", ch);
}
if (utils::isValidPm(_pm_25[idx].update.avg)) {
Serial.printf("[%d] Atmospheric PM 2.5 = %.2f ug/m3\n", ch, _pm_25[idx].update.avg);
} else {
Serial.printf("[%d] Atmospheric PM 2.5 = -\n", ch);
}
if (utils::isValidPm(_pm_10[idx].update.avg)) {
Serial.printf("[%d] Atmospheric PM 10 = %.2f ug/m3\n", ch, _pm_10[idx].update.avg);
} else {
Serial.printf("[%d] Atmospheric PM 10 = -\n", ch);
}
if (utils::isValidPm(_pm_01_sp[idx].update.avg)) {
Serial.printf("[%d] Standard Particle PM 1.0 = %.2f ug/m3\n", ch, _pm_01_sp[idx].update.avg);
} else {
Serial.printf("[%d] Standard Particle PM 1.0 = -\n", ch);
}
if (utils::isValidPm(_pm_25_sp[idx].update.avg)) {
Serial.printf("[%d] Standard Particle PM 2.5 = %.2f ug/m3\n", ch, _pm_25_sp[idx].update.avg);
} else {
Serial.printf("[%d] Standard Particle PM 2.5 = -\n", ch);
}
if (utils::isValidPm(_pm_10_sp[idx].update.avg)) {
Serial.printf("[%d] Standard Particle PM 10 = %.2f ug/m3\n", ch, _pm_10_sp[idx].update.avg);
} else {
Serial.printf("[%d] Standard Particle PM 10 = -\n", ch);
}
if (utils::isValidPm03Count(_pm_03_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 0.3 = %.1f\n", ch, _pm_03_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 0.3 = -\n", ch);
}
if (utils::isValidPm03Count(_pm_05_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 0.5 = %.1f\n", ch, _pm_05_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 0.5 = -\n", ch);
}
if (utils::isValidPm03Count(_pm_01_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 1.0 = %.1f\n", ch, _pm_01_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 1.0 = -\n", ch);
}
if (utils::isValidPm03Count(_pm_25_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 2.5 = %.1f\n", ch, _pm_25_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 2.5 = -\n", ch);
}
if (_pm_5_pc[idx].listValues.empty() == false) {
if (utils::isValidPm03Count(_pm_5_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 5.0 = %.1f\n", ch, _pm_5_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 5.0 = -\n", ch);
}
}
if (_pm_10_pc[idx].listValues.empty() == false) {
if (utils::isValidPm03Count(_pm_10_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 10 = %.1f\n", ch, _pm_10_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 10 = -\n", ch);
}
}
}
void Measurements::validateChannel(int ch) {
if (ch != 1 && ch != 2) {
Serial.printf("ERROR! Channel %d is undefined. Only channel 1 or 2 is the optional value!", ch);
@ -827,6 +978,12 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
oss << std::round(mc.pm_03_pc[1]);
}
oss << ",";
if (mc.signal < 0) {
oss << mc.signal;
}
return oss.str();
}

View File

@ -88,6 +88,8 @@ public:
PM10_PC, // Particle 10 count
};
void printCurrentAverage();
/**
* @brief Set each MeasurementType maximum period length for moving average
*
@ -258,6 +260,8 @@ private:
*/
void validateChannel(int ch);
void printCurrentPMAverage(int ch);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode);
JSONVar buildIndoor(bool localServer);
JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate);

View File

@ -15,7 +15,7 @@
#include "Main/utils.h"
#ifndef GIT_VERSION
#define GIT_VERSION "3.3.3-snap"
#define GIT_VERSION "3.3.9-snap"
#endif

View File

@ -131,6 +131,22 @@ void Sgp41::handle(void) {
}
#else
void Sgp41::pause() {
onPause = true;
Serial.println("Pausing SGP41 handler task");
// Set latest value to invalid
tvocRaw = utils::getInvalidVOC();
tvoc = utils::getInvalidVOC();
noxRaw = utils::getInvalidNOx();
nox = utils::getInvalidNOx();
}
void Sgp41::resume() {
onPause = false;
Serial.println("Resuming SGP41 handler task");
}
/**
* @brief Handle the sensor conditioning and run time udpate value, This method
* must not call, it's called on private task
@ -152,6 +168,11 @@ void Sgp41::_handle(void) {
uint16_t srawVoc, srawNox;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
if (onPause) {
continue;
}
if (getRawSignal(srawVoc, srawNox)) {
tvocRaw = srawVoc;
noxRaw = srawNox;

View File

@ -18,6 +18,10 @@ public:
bool begin(TwoWire &wire, Stream &stream);
void handle(void);
#else
/* pause _handle task to read sensor */
void pause();
/* resume _handle task to read sensor */
void resume();
void _handle(void);
#endif
void end(void);
@ -32,6 +36,7 @@ public:
int getTvocLearningOffset(void);
private:
bool onPause = false;
bool onConditioning = true;
bool ready = false;
bool _isBegin = false;