Compare commits

...

55 Commits

Author SHA1 Message Date
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
66818cd075 prepare 3.3.3 release 2025-04-04 11:31:19 +07:00
c1a6ddc68f Merge pull request #299 from airgradienthq/tmp/avg-max-period
Calculate measurement average max period use the same constant
2025-04-04 11:04:15 +07:00
20a32dd22c Measures average max period use the same constant
Cellular network options using wifi measurement interval as the constant reference to calculate max period
2025-04-04 10:54:03 +07:00
263dc9934e Merge pull request #298 from airgradienthq/fix/recover-cellular-connection
Restarting cellular module when cellular client is not ready
2025-04-04 10:43:59 +07:00
61b863b7f1 Fix esp_log logs not come out on O-PP 2025-04-04 10:21:50 +07:00
e01c1029fe Bump ag client
ensure client connection properly
2025-04-04 10:09:34 +07:00
ba5d817739 Merge pull request #297 from airgradienthq/feat/api-root
New local configuration to set HTTP domain name for monitor to post measures and fetch configuration from server
2025-04-03 16:51:04 +07:00
a91747e379 Update config sample 2025-04-02 16:30:13 +07:00
029457c3fa Add accepted value to http domain 2025-04-02 16:26:07 +07:00
55710dd4d9 Update docs for new configuration http domain 2025-04-02 16:18:55 +07:00
4886163cda Show on oled when httpDomain is set 2025-04-02 02:33:24 +07:00
7c57477238 Add local configuration to set http domain
change http domain by PUT from local server request
2025-04-02 02:12:13 +07:00
9ed58d1853 Prepare release 3.3.2 2025-03-31 17:12:15 +07:00
6c52b038e9 Merge pull request #295 from airgradienthq/feat/enable-at-debug
Enable cellular AT command debug when in network registration
2025-03-31 17:09:43 +07:00
2f69932ef7 add depth submodule update 2025-03-31 17:04:37 +07:00
1d96a274a6 Merge branch 'develop' into feat/enable-at-debug 2025-03-31 16:55:12 +07:00
df9f6dfc95 Fix bugs from 3.3.1 release 2025-03-31 16:52:09 +07:00
3fc02b3f54 Check signal when initialize cellular client 2025-03-31 16:51:29 +07:00
958ed0bd80 Fix TVOC and NOx payload position 2025-03-31 15:26:34 +07:00
e9be9dcc83 Fix mqtt host still exist on local when on server is disabled 2025-03-31 14:51:53 +07:00
7fbab82088 Change log level when correction not found 2025-03-31 14:07:30 +07:00
decdecdf22 Don't start mqtt when network option is cellular
Even when mqtt host is set
2025-03-31 14:01:49 +07:00
145c612867 Enable cellular at debug when registering network
On boot, airgradient-client change cellular init timeout to 5 mins
2025-03-31 13:53:56 +07:00
14 changed files with 292 additions and 70 deletions

View File

@ -38,6 +38,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.2.2
with: with:
fetch-depth: 0
submodules: 'true' submodules: 'true'
- uses: arduino/compile-sketches@v1.1.2 - uses: arduino/compile-sketches@v1.1.2
with: with:

2
.gitignore vendored
View File

@ -4,5 +4,7 @@ build
/.idea/ /.idea/
.pio .pio
.cache .cache
.clangd
logs logs
gen_compile_commands.py
compile_commands.json compile_commands.json

View File

@ -93,6 +93,7 @@ Compensated values apply correction algorithms to make the sensor values more ac
"tvocLearningOffset": 12, "tvocLearningOffset": 12,
"noxLearningOffset": 12, "noxLearningOffset": 12,
"mqttBrokerUrl": "", "mqttBrokerUrl": "",
"httpDomain": "",
"temperatureUnit": "c", "temperatureUnit": "c",
"configurationControl": "local", "configurationControl": "local",
"postDataToAirGradient": true, "postDataToAirGradient": true,
@ -146,7 +147,8 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `displayBrightness` | Brightness of the Display. | Number | 0-100 | `{"displayBrightness": 50}` | | `displayBrightness` | Brightness of the Display. | Number | 0-100 | `{"displayBrightness": 50}` |
| `ledBarBrightness` | Brightness of the LEDBar. | Number | 0-100 | `{"ledBarBrightness": 40}` | | `ledBarBrightness` | Brightness of the LEDBar. | Number | 0-100 | `{"ledBarBrightness": 40}` |
| `abcDays` | Number of days for CO2 automatic baseline calibration. | Number | Maximum 200 days. Default 8 days. | `{"abcDays": 8}` | | `abcDays` | Number of days for CO2 automatic baseline calibration. | Number | Maximum 200 days. Default 8 days. | `{"abcDays": 8}` |
| `mqttBrokerUrl` | MQTT broker URL. | String | | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` | | `mqttBrokerUrl` | MQTT broker URL. | String | Maximum 255 characters. Set value to empty string to disable mqtt connection. | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` |
| `httpDomain` | Domain name for http request. (version > 3.3.2) | String | Maximum 255 characters. Set value to empty string to set http domain to default airgradient | `{"httpDomain": "sub.domain.com"}` |
| `temperatureUnit` | Temperature unit shown on the display. | String | `c` or `C`: Degree Celsius °C <br>`f` or `F`: Degree Fahrenheit °F | `{"temperatureUnit": "c"}` | | `temperatureUnit` | Temperature unit shown on the display. | String | `c` or `C`: Degree Celsius °C <br>`f` or `F`: Degree Fahrenheit °F | `{"temperatureUnit": "c"}` |
| `configurationControl` | The configuration source of the device. | String | `both`: Accept local and cloud configuration <br>`local`: Accept only local configuration <br>`cloud`: Accept only cloud configuration | `{"configurationControl": "both"}` | | `configurationControl` | The configuration source of the device. | String | `both`: Accept local and cloud configuration <br>`local`: Accept only local configuration <br>`cloud`: Accept only cloud configuration | `{"configurationControl": "both"}` |
| `postDataToAirGradient` | Send data to AirGradient cloud. | Boolean | `true`: Enabled <br>`false`: Disabled | `{"postDataToAirGradient": true}` | | `postDataToAirGradient` | Send data to AirGradient cloud. | Boolean | `true`: Enabled <br>`false`: Disabled | `{"postDataToAirGradient": true}` |
@ -154,8 +156,8 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `ledBarTestRequested` | Can be set to trigger a test. | Boolean | `true` : LEDs will run test sequence | `{"ledBarTestRequested": true}` | | `ledBarTestRequested` | Can be set to trigger a test. | Boolean | `true` : LEDs will run test sequence | `{"ledBarTestRequested": true}` |
| `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 12}` | | `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 12}` |
| `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` | | `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on [3.1.9]()) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` | | `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on 3.1.9) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `corrections` | Sets correction options to display and measurement values on local server response. (version >= [3.1.11]()) | Object | _see corrections section_ | _see corrections section_ | | `corrections` | Sets correction options to display and measurement values on local server response. (version >= 3.1.11) | Object | _see corrections section_ | _see corrections section_ |
**Notes** **Notes**

View File

@ -26,7 +26,6 @@ https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/ */
#include "AgConfigure.h" #include "AgConfigure.h"
#include "AgSchedule.h" #include "AgSchedule.h"
#include "AgStateMachine.h" #include "AgStateMachine.h"
@ -37,6 +36,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include "Arduino.h" #include "Arduino.h"
#include "EEPROM.h" #include "EEPROM.h"
#include "ESPmDNS.h" #include "ESPmDNS.h"
#include "Libraries/airgradient-client/src/common.h"
#include "LocalServer.h" #include "LocalServer.h"
#include "MqttClient.h" #include "MqttClient.h"
#include "OpenMetrics.h" #include "OpenMetrics.h"
@ -45,6 +45,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <WebServer.h> #include <WebServer.h>
#include <WiFi.h> #include <WiFi.h>
#include <cstdint>
#include <string> #include <string>
#include "Libraries/airgradient-client/src/agSerial.h" #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 WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */
#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */ #define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */
#define CELLULAR_MEASUREMENT_INTERVAL 3 * 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 MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */ #define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */ #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 SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */ #define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** 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 MAXIMUM_MEASUREMENT_CYCLE_QUEUE 80
#define RESERVED_MEASUREMENT_CYCLE_CAPACITY 10 #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_EXPANSION_CARD_POWER 4
#define GPIO_IIC_RESET 3 #define GPIO_IIC_RESET 3
#define MINUTES() ((uint32_t)(esp_timer_get_time() / 1000 / 1000 / 60))
static MqttClient mqttClient(Serial); static MqttClient mqttClient(Serial);
static TaskHandle_t mqttTask = NULL; static TaskHandle_t mqttTask = NULL;
static Configuration configuration(Serial); static Configuration configuration(Serial);
@ -102,7 +108,7 @@ static OpenMetrics openMetrics(measurements, configuration, wifiConnector);
static LocalServer localServer(Serial, openMetrics, measurements, configuration, static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector); wifiConnector);
static AgSerial *agSerial; static AgSerial *agSerial;
static CellularModule *cell; static CellularModule *cellularCard;
static AirgradientClient *agClient; static AirgradientClient *agClient;
enum NetworkOption { enum NetworkOption {
@ -111,12 +117,17 @@ enum NetworkOption {
}; };
NetworkOption networkOption; NetworkOption networkOption;
TaskHandle_t handleNetworkTask = NULL; TaskHandle_t handleNetworkTask = NULL;
static bool otaInProgress = false; static bool firmwareUpdateInProgress = false;
static uint32_t factoryBtnPressTime = 0; static uint32_t factoryBtnPressTime = 0;
static AgFirmwareMode fwMode = FW_MODE_I_9PSL; static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
static bool ledBarButtonTest = false; static bool ledBarButtonTest = false;
static String fwNewVersion; 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; SemaphoreHandle_t mutexMeasurementCycleQueue;
static std::vector<Measurements::Measures> measurementCycleQueue; static std::vector<Measurements::Measures> measurementCycleQueue;
@ -145,6 +156,7 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
static int calculateMaxPeriod(int updateInterval); static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod(); static void setMeasurementMaxPeriod();
static void newMeasurementCycle(); static void newMeasurementCycle();
static void restartIfCeClientIssueOverTwoHours();
static void networkSignalCheck(); static void networkSignalCheck();
static void networkingTask(void *args); static void networkingTask(void *args);
@ -297,11 +309,17 @@ void setup() {
} }
void loop() { 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 // Schedule to feed external watchdog
watchdogFeedSchedule.run(); watchdogFeedSchedule.run();
if (otaInProgress) { if (firmwareUpdateInProgress) {
// OTA currently in progress, temporarily disable running sensor schedules // Firmare update currently in progress, temporarily disable running sensor schedules
delay(10000); delay(10000);
return; return;
} }
@ -334,7 +352,7 @@ void loop() {
static bool pmsConnected = false; static bool pmsConnected = false;
if (pmsConnected != ag->pms5003.connected()) { if (pmsConnected != ag->pms5003.connected()) {
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 { } else {
@ -423,6 +441,11 @@ static void initMqtt(void) {
return; return;
} }
if (networkOption == UseCellular) {
Serial.println("MQTT not available for cellular options");
return;
}
if (mqttClient.begin(mqttUri)) { if (mqttClient.begin(mqttUri)) {
Serial.println("Successfully connected to MQTT broker"); Serial.println("Successfully connected to MQTT broker");
createMqttTask(); createMqttTask();
@ -536,31 +559,28 @@ void checkForFirmwareUpdate(void) {
if (networkOption == UseWifi) { if (networkOption == UseWifi) {
agOta = new AirgradientOTAWifi; agOta = new AirgradientOTAWifi;
} else { } else {
agOta = new AirgradientOTACellular(cell); agOta = new AirgradientOTACellular(cellularCard);
} }
// Indicate main task that ota is performing // Indicate main task that firmware update is in progress
Serial.println("Check for firmware update, disabling main task"); firmwareUpdateInProgress = true;
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();
}
agOta->setHandlerCallback(otaHandlerCallback); 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 // Handled by otaHandlerCallback
otaInProgress = false; // Indicate main task that firmware update finish
if (configuration.hasSensorSGP && networkOption == UseCellular) { firmwareUpdateInProgress = false;
// Re-start SGP41 task
if (!sgp41Init()) {
Serial.println("Failed re-start SGP41 task");
}
}
delete agOta; delete agOta;
Serial.println(); Serial.println();
@ -568,14 +588,25 @@ void checkForFirmwareUpdate(void) {
void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) { void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) {
switch (result) { 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); displayExecuteOta(result, fwNewVersion, 0);
break; break;
}
case AirgradientOTA::InProgress: case AirgradientOTA::InProgress:
Serial.printf("OTA progress: %s\n", msg); Serial.printf("OTA progress: %s\n", msg);
displayExecuteOta(result, "", std::stoi(msg)); displayExecuteOta(result, "", std::stoi(msg));
break; break;
case AirgradientOTA::Failed: case AirgradientOTA::Failed:
displayExecuteOta(result, "", 0);
if (configuration.hasSensorSGP && networkOption == UseCellular) {
ag->sgp41.resume();
}
break;
case AirgradientOTA::Skipped: case AirgradientOTA::Skipped:
case AirgradientOTA::AlreadyUpToDate: case AirgradientOTA::AlreadyUpToDate:
displayExecuteOta(result, "", 0); displayExecuteOta(result, "", 0);
@ -642,7 +673,11 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
} }
delay(1000); delay(1000);
} }
oledDisplay.setAirGradient(0);
if (ag->isOne()) {
oledDisplay.setAirGradient(0);
oledDisplay.setBrightness(0);
}
break; break;
} }
default: default:
@ -808,8 +843,6 @@ static void openAirInit(void) {
Serial.println("CO2 S8 sensor not found"); Serial.println("CO2 S8 sensor not found");
Serial.println("Can not detect S8 run mode 'PPT'"); Serial.println("Can not detect S8 run mode 'PPT'");
fwMode = FW_MODE_O_1PPT; fwMode = FW_MODE_O_1PPT;
Serial0.end();
delay(200); delay(200);
} else { } else {
Serial.println("Found S8 on Serial0"); Serial.println("Found S8 on Serial0");
@ -922,8 +955,8 @@ void initializeNetwork() {
if (agSerial->open()) { if (agSerial->open()) {
Serial.println("Cellular module found"); Serial.println("Cellular module found");
// Initialize cellular module and use cellular as agClient // Initialize cellular module and use cellular as agClient
cell = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN); cellularCard = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN);
agClient = new AirgradientCellularClient(cell); agClient = new AirgradientCellularClient(cellularCard);
networkOption = UseCellular; networkOption = UseCellular;
} else { } else {
Serial.println("Cellular module not available, using wifi"); Serial.println("Cellular module not available, using wifi");
@ -934,6 +967,19 @@ void initializeNetwork() {
networkOption = UseWifi; networkOption = UseWifi;
} }
if (networkOption == UseCellular) {
// Enable serial stream debugging to check the AT command when doing registration
agSerial->setDebug(true);
}
String httpDomain = configuration.getHttpDomain();
if (httpDomain != "") {
agClient->setHttpDomain(httpDomain.c_str());
Serial.printf("HTTP domain name is set to: %s\n", httpDomain.c_str());
oledDisplay.setText("HTTP domain name", "using local", "configuration");
delay(2500);
}
if (!agClient->begin(ag->deviceId().c_str())) { if (!agClient->begin(ag->deviceId().c_str())) {
oledDisplay.setText("Client", "initialization", "failed"); oledDisplay.setText("Client", "initialization", "failed");
delay(5000); delay(5000);
@ -943,6 +989,11 @@ void initializeNetwork() {
ESP.restart(); ESP.restart();
} }
if (networkOption == UseCellular) {
// Disabling it again
agSerial->setDebug(false);
}
if (networkOption == UseWifi) { if (networkOption == UseWifi) {
if (!wifiConnector.connect()) { if (!wifiConnector.connect()) {
Serial.println("Cannot initiate wifi connection"); Serial.println("Cannot initiate wifi connection");
@ -1026,6 +1077,16 @@ static void configUpdateHandle() {
initMqtt(); initMqtt();
} }
String httpDomain = configuration.getHttpDomain();
if (httpDomain != "") {
Serial.printf("HTTP domain name set to: %s\n", httpDomain.c_str());
agClient->setHttpDomain(httpDomain.c_str());
} else {
// Its empty, set to default
Serial.println("HTTP domain name from configuration empty, set to default");
agClient->setHttpDomainDefault();
}
if (configuration.hasSensorSGP) { if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() || if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) { configuration.tvocLearnOffsetChanged()) {
@ -1309,7 +1370,10 @@ void postUsingWifi() {
Serial.printf("Free heap: %u\n", ESP.getFreeHeap()); 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 // Aquire queue mutex to get queue size
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY); xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
@ -1321,6 +1385,14 @@ void postUsingCellular() {
return; 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 // Build payload include all measurements from queue
std::string payload; std::string payload;
payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds
@ -1362,7 +1434,7 @@ void sendDataToServer(void) {
if (networkOption == UseWifi) { if (networkOption == UseWifi) {
postUsingWifi(); postUsingWifi();
} else if (networkOption == UseCellular) { } else if (networkOption == UseCellular) {
postUsingCellular(); postUsingCellular(false);
} }
} }
@ -1432,12 +1504,8 @@ void setMeasurementMaxPeriod() {
int calculateMaxPeriod(int updateInterval) { int calculateMaxPeriod(int updateInterval) {
// 0.8 is 80% reduced interval for max period // 0.8 is 80% reduced interval for max period
if (networkOption == UseWifi) { // NOTE: Both network option use the same measurement interval
return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval; return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval;
} else {
// Cellular
return (CELLULAR_MEASUREMENT_INTERVAL - (CELLULAR_MEASUREMENT_INTERVAL * 0.8)) / updateInterval;
}
} }
@ -1445,26 +1513,54 @@ void networkSignalCheck() {
if (networkOption == UseWifi) { if (networkOption == UseWifi) {
Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI()); Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI());
} else if (networkOption == UseCellular) { } else if (networkOption == UseCellular) {
auto result = cell->retrieveSignal(); auto result = cellularCard->retrieveSignal();
if (result.status != CellReturnStatus::Ok) { if (result.status != CellReturnStatus::Ok) {
agClient->setClientReady(false); agClient->setClientReady(false);
lastCellSignalQuality = 99;
return; return;
} }
// Save last signal quality
lastCellSignalQuality = result.data;
if (result.data == 99) { if (result.data == 99) {
// 99 indicate cellular not attached to network // 99 indicate cellular not attached to network
agClient->setClientReady(false); agClient->setClientReady(false);
return; 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) { void networkingTask(void *args) {
// OTA check on boot // OTA check on boot
#ifdef ESP8266 #ifndef ESP8266
// ota not supported
#else
// because cellular it takes too long, watchdog triggered
checkForFirmwareUpdate(); checkForFirmwareUpdate();
checkForUpdateSchedule.update(); checkForUpdateSchedule.update();
#endif #endif
@ -1474,8 +1570,9 @@ void networkingTask(void *args) {
if (networkOption == UseCellular) { if (networkOption == UseCellular) {
Serial.println("Prepare first measures cycle to send on boot for 20s"); Serial.println("Prepare first measures cycle to send on boot for 20s");
delay(20000); delay(20000);
networkSignalCheck();
newMeasurementCycle(); newMeasurementCycle();
sendDataToServer(); postUsingCellular(true);
measurementSchedule.update(); measurementSchedule.update();
} }
@ -1494,12 +1591,43 @@ void networkingTask(void *args) {
} }
else if (networkOption == UseCellular) { else if (networkOption == UseCellular) {
if (agClient->isClientReady() == false) { if (agClient->isClientReady() == false) {
// Start time if value still default
if (agCeClientProblemDetectedTime == 0) {
agCeClientProblemDetectedTime = MINUTES();
}
// Enable at command debug
agSerial->setDebug(true);
// 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..."); Serial.println("Cellular client not ready, ensuring connection...");
if (agClient->ensureClientConnection() == false) { if (agClient->ensureClientConnection(resetModule) == false) {
Serial.println("Cellular client connection not ready, retry in 5s..."); Serial.println("Cellular client connection not ready, retry in 30s...");
delay(5000); delay(30000); // before retry, wait for 30s
continue; continue;
} }
// Client is ready
agCeClientProblemDetectedTime = 0; // reset to default
agSerial->setDebug(false); // disable at command debug
} }
} }
@ -1529,7 +1657,10 @@ void newMeasurementCycle() {
measurementCycleQueue.erase(measurementCycleQueue.begin()); measurementCycleQueue.erase(measurementCycleQueue.begin());
} }
// Get current measures
auto mc = measurements.getMeasures(); auto mc = measurements.getMeasures();
mc.signal = cellularCard->csqToDbm(lastCellSignalQuality); // convert to RSSI
measurementCycleQueue.push_back(mc); measurementCycleQueue.push_back(mc);
Serial.println("New measurement cycle added to queue"); Serial.println("New measurement cycle added to queue");
// Release mutex // Release mutex

View File

@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor name=AirGradient Air Quality Sensor
version=3.3.1 version=3.3.6
author=AirGradient <support@airgradient.com> author=AirGradient <support@airgradient.com>
maintainer=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. 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 platform = espressif32
board = esp32-c3-devkitm-1 board = esp32-c3-devkitm-1
framework = arduino 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 board_build.partitions = partitions.csv
monitor_speed = 115200 monitor_speed = 115200
lib_deps = lib_deps =

View File

@ -46,6 +46,7 @@ JSON_PROP_DEF(abcDays);
JSON_PROP_DEF(tvocLearningOffset); JSON_PROP_DEF(tvocLearningOffset);
JSON_PROP_DEF(noxLearningOffset); JSON_PROP_DEF(noxLearningOffset);
JSON_PROP_DEF(mqttBrokerUrl); JSON_PROP_DEF(mqttBrokerUrl);
JSON_PROP_DEF(httpDomain);
JSON_PROP_DEF(temperatureUnit); JSON_PROP_DEF(temperatureUnit);
JSON_PROP_DEF(configurationControl); JSON_PROP_DEF(configurationControl);
JSON_PROP_DEF(postDataToAirGradient); JSON_PROP_DEF(postDataToAirGradient);
@ -68,6 +69,7 @@ JSON_PROP_DEF(rhum);
#define jprop_tvocLearningOffset_default 12 #define jprop_tvocLearningOffset_default 12
#define jprop_noxLearningOffset_default 12 #define jprop_noxLearningOffset_default 12
#define jprop_mqttBrokerUrl_default "" #define jprop_mqttBrokerUrl_default ""
#define jprop_httpDomain_default ""
#define jprop_temperatureUnit_default "c" #define jprop_temperatureUnit_default "c"
#define jprop_configurationControl_default String(CONFIGURATION_CONTROL_NAME[ConfigurationControl::ConfigurationControlBoth]) #define jprop_configurationControl_default String(CONFIGURATION_CONTROL_NAME[ConfigurationControl::ConfigurationControlBoth])
#define jprop_postDataToAirGradient_default true #define jprop_postDataToAirGradient_default true
@ -240,7 +242,7 @@ bool Configuration::updateTempHumCorrection(JSONVar &json, TempHumCorrection &ta
JSONVar corrections = json[jprop_corrections]; JSONVar corrections = json[jprop_corrections];
if (!corrections.hasOwnProperty(correctionName)) { if (!corrections.hasOwnProperty(correctionName)) {
logWarning(String(correctionName) + " correction field not found on configuration"); logInfo(String(correctionName) + " correction field not found on configuration");
return false; return false;
} }
@ -377,6 +379,7 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_country] = jprop_country_default; jconfig[jprop_country] = jprop_country_default;
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default; jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
jconfig[jprop_httpDomain] = jprop_httpDomain_default;
jconfig[jprop_configurationControl] = jprop_configurationControl_default; jconfig[jprop_configurationControl] = jprop_configurationControl_default;
jconfig[jprop_pmStandard] = jprop_pmStandard_default; jconfig[jprop_pmStandard] = jprop_pmStandard_default;
jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default; jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default;
@ -735,11 +738,17 @@ bool Configuration::parse(String data, bool isLocal) {
jconfig[jprop_mqttBrokerUrl] = broker; jconfig[jprop_mqttBrokerUrl] = broker;
} }
} else { } else {
failedMessage = "\"mqttBrokerUrl\" length should <= 255"; failedMessage = "\"mqttBrokerUrl\" length should less than 255 character";
jsonInvalid(); jsonInvalid();
return false; return false;
} }
} else { }
else if (JSON.typeof_(root[jprop_mqttBrokerUrl]) == "null" and !isLocal) {
// So if its not available on the json and json comes from aigradient server
// then set its value to default (empty)
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
}
else {
if (jsonTypeInvalid(root[jprop_mqttBrokerUrl], "string")) { if (jsonTypeInvalid(root[jprop_mqttBrokerUrl], "string")) {
failedMessage = failedMessage =
jsonTypeInvalidMessage(String(jprop_mqttBrokerUrl), "string"); jsonTypeInvalidMessage(String(jprop_mqttBrokerUrl), "string");
@ -748,6 +757,32 @@ bool Configuration::parse(String data, bool isLocal) {
} }
} }
if (isLocal) {
if (JSON.typeof_(root[jprop_httpDomain]) == "string") {
String httpDomain = root[jprop_httpDomain];
String oldHttpDomain = jconfig[jprop_httpDomain];
if (httpDomain.length() <= 255) {
if (httpDomain != oldHttpDomain) {
changed = true;
configLogInfo(String(jprop_httpDomain), oldHttpDomain, httpDomain);
jconfig[jprop_httpDomain] = httpDomain;
}
} else {
failedMessage = "\"httpDomain\" length should less than 255 character";
jsonInvalid();
return false;
}
}
else {
if (jsonTypeInvalid(root[jprop_httpDomain], "string")) {
failedMessage =
jsonTypeInvalidMessage(String(jprop_httpDomain), "string");
jsonInvalid();
return false;
}
}
}
if (JSON.typeof_(root[jprop_temperatureUnit]) == "string") { if (JSON.typeof_(root[jprop_temperatureUnit]) == "string") {
String unit = root[jprop_temperatureUnit]; String unit = root[jprop_temperatureUnit];
String oldUnit = jconfig[jprop_temperatureUnit]; String oldUnit = jconfig[jprop_temperatureUnit];
@ -1030,6 +1065,16 @@ String Configuration::getMqttBrokerUri(void) {
return broker; return broker;
} }
/**
* @brief Get HTTP domain for post measures and get configuration
*
* @return String http domain, might be empty string
*/
String Configuration::getHttpDomain(void) {
String httpDomain = jconfig[jprop_httpDomain];
return httpDomain;
}
/** /**
* @brief Get configuratoin post data to AirGradient cloud * @brief Get configuratoin post data to AirGradient cloud
* *
@ -1115,7 +1160,7 @@ bool Configuration::isUpdated(void) {
} }
String Configuration::jsonTypeInvalidMessage(String name, String type) { String Configuration::jsonTypeInvalidMessage(String name, String type) {
return "'" + name + "' type invalid, it's should '" + type + "'"; return "'" + name + "' type is invalid, expecting '" + type + "'";
} }
String Configuration::jsonValueInvalidMessage(String name, String value) { String Configuration::jsonValueInvalidMessage(String name, String value) {
@ -1269,6 +1314,18 @@ void Configuration::toConfig(const char *buf) {
logInfo("toConfig: mqttBroker changed"); logInfo("toConfig: mqttBroker changed");
} }
/** validate http domain */
if (JSON.typeof_(jconfig[jprop_httpDomain]) != "string") {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
}
if (isConfigFieldInvalid) {
changed = true;
jconfig[jprop_httpDomain] = jprop_httpDomain_default;
logInfo("toConfig: httpDomain changed");
}
/** Validate temperature unit */ /** Validate temperature unit */
if (JSON.typeof_(jconfig[jprop_temperatureUnit]) != "string") { if (JSON.typeof_(jconfig[jprop_temperatureUnit]) != "string") {
isConfigFieldInvalid = true; isConfigFieldInvalid = true;

View File

@ -82,6 +82,7 @@ public:
String getLedBarModeName(void); String getLedBarModeName(void);
bool getDisplayMode(void); bool getDisplayMode(void);
String getMqttBrokerUri(void); String getMqttBrokerUri(void);
String getHttpDomain(void);
bool isPostDataToAirGradient(void); bool isPostDataToAirGradient(void);
ConfigurationControl getConfigurationControl(void); ConfigurationControl getConfigurationControl(void);
bool isCo2CalibrationRequested(void); bool isCo2CalibrationRequested(void);

View File

@ -804,16 +804,16 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
oss << ","; oss << ",";
// NOx // TVOC
if (utils::isValidNOx(mc.nox)) { if (utils::isValidVOC(mc.tvoc)) {
oss << std::round(mc.nox); oss << std::round(mc.tvoc);
} }
oss << ","; oss << ",";
// TVOC // NOx
if (utils::isValidVOC(mc.tvoc)) { if (utils::isValidNOx(mc.nox)) {
oss << std::round(mc.tvoc); oss << std::round(mc.nox);
} }
oss << ","; oss << ",";
@ -827,9 +827,11 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
oss << std::round(mc.pm_03_pc[1]); oss << std::round(mc.pm_03_pc[1]);
} }
// char datapoint[128] = {0}; oss << ",";
// snprintf(datapoint, 128, "%d,%.0f,%.0f,%.0f,%.0f,%.0f,%d,%d,%d", co2, temp * 10,
// hum * 10, pm01 * 10, pm25 * 10, pm10 * 10, tvoc, nox, pm003Count); if (mc.signal < 0) {
oss << mc.signal;
}
return oss.str(); return oss.str();
} }

View File

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

View File

@ -131,6 +131,22 @@ void Sgp41::handle(void) {
} }
#else #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 * @brief Handle the sensor conditioning and run time udpate value, This method
* must not call, it's called on private task * must not call, it's called on private task
@ -152,6 +168,11 @@ void Sgp41::_handle(void) {
uint16_t srawVoc, srawNox; uint16_t srawVoc, srawNox;
for (;;) { for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
if (onPause) {
continue;
}
if (getRawSignal(srawVoc, srawNox)) { if (getRawSignal(srawVoc, srawNox)) {
tvocRaw = srawVoc; tvocRaw = srawVoc;
noxRaw = srawNox; noxRaw = srawNox;

View File

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