2024-03-23 16:58:17 +07:00
|
|
|
/*
|
2024-03-24 08:51:14 +07:00
|
|
|
This is the combined firmware code for AirGradient ONE and AirGradient Open Air
|
|
|
|
open-source hardware Air Quality Monitor with ESP32-C3 Microcontroller.
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
It is an air quality monitor for PM2.5, CO2, TVOCs, NOx, Temperature and
|
|
|
|
Humidity with a small display, an RGB led bar and can send data over Wifi.
|
|
|
|
|
|
|
|
Open source air quality monitors and kits are available:
|
|
|
|
Indoor Monitor: https://www.airgradient.com/indoor/
|
|
|
|
Outdoor Monitor: https://www.airgradient.com/outdoor/
|
|
|
|
|
2024-03-24 08:51:14 +07:00
|
|
|
Build Instructions: AirGradient ONE:
|
|
|
|
https://www.airgradient.com/documentation/one-v9/ Build Instructions:
|
|
|
|
AirGradient Open Air:
|
|
|
|
https://www.airgradient.com/documentation/open-air-pst-kit-1-3/
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2025-02-24 14:59:12 +07:00
|
|
|
Compile Instructions:
|
|
|
|
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
|
|
|
can be set through the AirGradient dashboard.
|
|
|
|
|
|
|
|
If you have any questions please visit our forum at
|
|
|
|
https://forum.airgradient.com/
|
|
|
|
|
|
|
|
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
2024-04-03 07:04:28 +07:00
|
|
|
#include "AgConfigure.h"
|
|
|
|
#include "AgSchedule.h"
|
|
|
|
#include "AgStateMachine.h"
|
2025-03-16 02:22:38 +07:00
|
|
|
#include "AgValue.h"
|
2024-04-04 10:36:59 +07:00
|
|
|
#include "AgWiFiConnector.h"
|
2024-11-29 18:01:14 +07:00
|
|
|
#include "AirGradient.h"
|
2025-03-24 03:52:46 +07:00
|
|
|
#include "App/AppDef.h"
|
2025-03-16 02:22:38 +07:00
|
|
|
#include "Arduino.h"
|
2024-03-23 16:58:17 +07:00
|
|
|
#include "EEPROM.h"
|
2024-04-07 16:39:01 +07:00
|
|
|
#include "ESPmDNS.h"
|
2025-04-09 15:51:54 +07:00
|
|
|
#include "Libraries/airgradient-client/src/common.h"
|
2024-04-07 16:39:01 +07:00
|
|
|
#include "LocalServer.h"
|
2024-04-03 07:04:28 +07:00
|
|
|
#include "MqttClient.h"
|
2024-04-07 16:39:01 +07:00
|
|
|
#include "OpenMetrics.h"
|
|
|
|
#include "WebServer.h"
|
2024-11-29 18:01:14 +07:00
|
|
|
#include "esp32c3/rom/rtc.h"
|
|
|
|
#include <HardwareSerial.h>
|
2024-03-23 16:58:17 +07:00
|
|
|
#include <WebServer.h>
|
2024-05-07 15:00:32 +07:00
|
|
|
#include <WiFi.h>
|
2025-03-17 15:12:11 +07:00
|
|
|
#include <string>
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2025-03-14 01:41:23 +07:00
|
|
|
#include "Libraries/airgradient-client/src/agSerial.h"
|
2025-03-17 15:12:11 +07:00
|
|
|
#include "Libraries/airgradient-client/src/cellularModule.h"
|
2025-03-14 01:41:23 +07:00
|
|
|
#include "Libraries/airgradient-client/src/cellularModuleA7672xx.h"
|
|
|
|
#include "Libraries/airgradient-client/src/airgradientCellularClient.h"
|
|
|
|
#include "Libraries/airgradient-client/src/airgradientWifiClient.h"
|
2025-03-26 16:18:48 +07:00
|
|
|
#include "Libraries/airgradient-ota/src/airgradientOta.h"
|
2025-03-17 15:12:11 +07:00
|
|
|
#include "Libraries/airgradient-ota/src/airgradientOtaWifi.h"
|
|
|
|
#include "Libraries/airgradient-ota/src/airgradientOtaCellular.h"
|
|
|
|
#include "esp_system.h"
|
2025-03-16 02:22:38 +07:00
|
|
|
#include "freertos/projdefs.h"
|
2025-03-16 23:15:01 +07:00
|
|
|
|
|
|
|
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
|
|
|
|
#define DISP_UPDATE_INTERVAL 2500 /** ms */
|
|
|
|
#define WIFI_SERVER_CONFIG_SYNC_INTERVAL 1 * 60000 /** ms */
|
|
|
|
#define WIFI_MEASUREMENT_INTERVAL 1 * 60000 /** ms */
|
|
|
|
#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */
|
2025-03-21 04:40:27 +07:00
|
|
|
#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */
|
2025-03-16 23:15:01 +07:00
|
|
|
#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */
|
2025-03-21 04:40:27 +07:00
|
|
|
#define CELLULAR_TRANSMISSION_INTERVAL 9 * 60000 /** ms */
|
2025-03-16 23:15:01 +07:00
|
|
|
#define MQTT_SYNC_INTERVAL 60000 /** ms */
|
|
|
|
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
|
|
|
|
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
|
|
|
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
|
|
|
|
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
|
|
|
|
#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 */
|
2024-04-03 07:04:28 +07:00
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
#define MAXIMUM_MEASUREMENT_CYCLE_QUEUE 80
|
2025-03-27 14:34:26 +07:00
|
|
|
#define RESERVED_MEASUREMENT_CYCLE_CAPACITY 10
|
2025-03-21 04:40:27 +07:00
|
|
|
|
2024-03-23 17:44:11 +07:00
|
|
|
/** I2C define */
|
2024-03-23 16:58:17 +07:00
|
|
|
#define I2C_SDA_PIN 7
|
|
|
|
#define I2C_SCL_PIN 6
|
|
|
|
#define OLED_I2C_ADDR 0x3C
|
|
|
|
|
2025-03-14 01:41:23 +07:00
|
|
|
/** Power pin */
|
|
|
|
#define GPIO_POWER_MODULE_PIN 5
|
|
|
|
#define GPIO_EXPANSION_CARD_POWER 4
|
|
|
|
#define GPIO_IIC_RESET 3
|
|
|
|
|
2024-04-03 07:04:28 +07:00
|
|
|
static MqttClient mqttClient(Serial);
|
2024-03-23 17:44:11 +07:00
|
|
|
static TaskHandle_t mqttTask = NULL;
|
2024-04-07 16:39:01 +07:00
|
|
|
static Configuration configuration(Serial);
|
2025-01-23 03:33:47 +07:00
|
|
|
static Measurements measurements(configuration);
|
2024-03-23 16:58:17 +07:00
|
|
|
static AirGradient *ag;
|
2024-04-07 16:39:01 +07:00
|
|
|
static OledDisplay oledDisplay(configuration, measurements, Serial);
|
|
|
|
static StateMachine stateMachine(oledDisplay, Serial, measurements,
|
|
|
|
configuration);
|
2024-04-22 16:31:40 +07:00
|
|
|
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
|
|
|
|
configuration);
|
2025-03-26 17:43:39 +07:00
|
|
|
static OpenMetrics openMetrics(measurements, configuration, wifiConnector);
|
2024-04-07 16:39:01 +07:00
|
|
|
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
|
|
|
|
wifiConnector);
|
2025-03-14 01:41:23 +07:00
|
|
|
static AgSerial *agSerial;
|
|
|
|
static CellularModule *cell;
|
|
|
|
static AirgradientClient *agClient;
|
|
|
|
|
2025-03-16 16:13:14 +07:00
|
|
|
enum NetworkOption {
|
|
|
|
UseWifi,
|
|
|
|
UseCellular
|
|
|
|
};
|
|
|
|
NetworkOption networkOption;
|
2025-03-16 02:22:38 +07:00
|
|
|
TaskHandle_t handleNetworkTask = NULL;
|
2025-03-17 22:17:59 +07:00
|
|
|
static bool otaInProgress = false;
|
2025-03-16 16:13:14 +07:00
|
|
|
|
2024-03-23 17:44:11 +07:00
|
|
|
static uint32_t factoryBtnPressTime = 0;
|
2024-04-03 07:04:28 +07:00
|
|
|
static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
|
2024-03-24 08:51:14 +07:00
|
|
|
static bool ledBarButtonTest = false;
|
2024-05-02 18:22:26 +07:00
|
|
|
static String fwNewVersion;
|
2025-04-07 16:28:37 +07:00
|
|
|
static int lastCellSignalQuality = 99; // CSQ
|
2024-04-05 11:45:02 +07:00
|
|
|
|
2025-03-16 02:22:38 +07:00
|
|
|
SemaphoreHandle_t mutexMeasurementCycleQueue;
|
2025-03-28 13:45:07 +07:00
|
|
|
static std::vector<Measurements::Measures> measurementCycleQueue;
|
2025-03-16 02:22:38 +07:00
|
|
|
|
2024-03-23 16:58:17 +07:00
|
|
|
static void boardInit(void);
|
2025-03-16 16:13:14 +07:00
|
|
|
static void initializeNetwork();
|
2024-03-23 16:58:17 +07:00
|
|
|
static void failedHandler(String msg);
|
2024-04-07 16:39:01 +07:00
|
|
|
static void configurationUpdateSchedule(void);
|
2025-03-16 02:22:38 +07:00
|
|
|
static void configUpdateHandle(void);
|
2024-09-22 13:18:15 +07:00
|
|
|
static void updateDisplayAndLedBar(void);
|
2024-04-07 16:39:01 +07:00
|
|
|
static void updateTvoc(void);
|
|
|
|
static void updatePm(void);
|
2024-03-23 16:58:17 +07:00
|
|
|
static void sendDataToServer(void);
|
|
|
|
static void tempHumUpdate(void);
|
|
|
|
static void co2Update(void);
|
2024-04-07 16:39:01 +07:00
|
|
|
static void mdnsInit(void);
|
2024-03-23 16:58:17 +07:00
|
|
|
static void createMqttTask(void);
|
2024-04-07 16:39:01 +07:00
|
|
|
static void initMqtt(void);
|
2024-03-23 16:58:17 +07:00
|
|
|
static void factoryConfigReset(void);
|
|
|
|
static void wdgFeedUpdate(void);
|
2024-04-07 16:39:01 +07:00
|
|
|
static void ledBarEnabledUpdate(void);
|
2024-04-22 16:31:40 +07:00
|
|
|
static bool sgp41Init(void);
|
2025-02-05 13:46:10 +07:00
|
|
|
static void checkForFirmwareUpdate(void);
|
2025-03-17 15:12:11 +07:00
|
|
|
static void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg);
|
|
|
|
static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int processing);
|
2024-10-20 23:20:16 +07:00
|
|
|
static int calculateMaxPeriod(int updateInterval);
|
2024-10-20 23:27:27 +07:00
|
|
|
static void setMeasurementMaxPeriod();
|
2025-03-16 02:22:38 +07:00
|
|
|
static void newMeasurementCycle();
|
2025-03-26 16:18:48 +07:00
|
|
|
static void networkSignalCheck();
|
2025-03-16 02:22:38 +07:00
|
|
|
static void networkingTask(void *args);
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-09-22 13:18:15 +07:00
|
|
|
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar);
|
2025-03-16 23:15:01 +07:00
|
|
|
AgSchedule configSchedule(WIFI_SERVER_CONFIG_SYNC_INTERVAL,
|
2024-04-07 16:39:01 +07:00
|
|
|
configurationUpdateSchedule);
|
2025-03-16 23:15:01 +07:00
|
|
|
AgSchedule transmissionSchedule(WIFI_TRANSMISSION_INTERVAL, sendDataToServer);
|
|
|
|
AgSchedule measurementSchedule(WIFI_MEASUREMENT_INTERVAL, newMeasurementCycle);
|
2024-03-23 16:58:17 +07:00
|
|
|
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
|
2024-04-07 16:39:01 +07:00
|
|
|
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
|
2024-03-23 16:58:17 +07:00
|
|
|
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
|
2024-04-07 16:39:01 +07:00
|
|
|
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
|
|
|
|
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
|
2025-02-05 13:46:10 +07:00
|
|
|
AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, checkForFirmwareUpdate);
|
2025-03-26 16:18:48 +07:00
|
|
|
AgSchedule networkSignalCheckSchedule(10000, networkSignalCheck);
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
void setup() {
|
|
|
|
/** Serial for print debug message */
|
|
|
|
Serial.begin(115200);
|
|
|
|
delay(100); /** For bester show log */
|
2024-04-07 16:39:01 +07:00
|
|
|
|
2025-03-16 02:22:38 +07:00
|
|
|
// Enable cullular module power board
|
2025-03-14 11:04:32 +07:00
|
|
|
pinMode(GPIO_EXPANSION_CARD_POWER, OUTPUT);
|
2025-03-16 02:22:38 +07:00
|
|
|
digitalWrite(GPIO_EXPANSION_CARD_POWER, HIGH);
|
2025-03-14 11:04:32 +07:00
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
/** Print device ID into log */
|
|
|
|
Serial.println("Serial nr: " + ag->deviceId());
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-11-29 18:01:14 +07:00
|
|
|
// Set reason why esp is reset
|
|
|
|
esp_reset_reason_t reason = esp_reset_reason();
|
|
|
|
measurements.setResetReason(reason);
|
|
|
|
|
2024-03-30 19:33:03 +07:00
|
|
|
/** Initialize local configure */
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.begin();
|
2024-03-30 19:33:03 +07:00
|
|
|
|
2024-03-23 16:58:17 +07:00
|
|
|
/** Init I2C */
|
|
|
|
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
|
|
|
|
delay(1000);
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
/** Detect board type: ONE_INDOOR has OLED display, Scan the I2C address to
|
|
|
|
* identify board type */
|
2024-03-23 16:58:17 +07:00
|
|
|
Wire.beginTransmission(OLED_I2C_ADDR);
|
|
|
|
if (Wire.endTransmission() == 0x00) {
|
|
|
|
ag = new AirGradient(BoardType::ONE_INDOOR);
|
|
|
|
} else {
|
|
|
|
ag = new AirGradient(BoardType::OPEN_AIR_OUTDOOR);
|
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
Serial.println("Detected " + ag->getBoardName());
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-04-25 06:49:14 +07:00
|
|
|
configuration.setAirGradient(ag);
|
2024-04-07 16:39:01 +07:00
|
|
|
oledDisplay.setAirGradient(ag);
|
|
|
|
stateMachine.setAirGradient(ag);
|
|
|
|
wifiConnector.setAirGradient(ag);
|
2025-03-26 17:43:39 +07:00
|
|
|
openMetrics.setAirGradient(ag, agClient);
|
2024-04-07 16:39:01 +07:00
|
|
|
localServer.setAirGraident(ag);
|
2025-01-23 03:33:47 +07:00
|
|
|
measurements.setAirGradient(ag);
|
2024-04-04 18:35:15 +07:00
|
|
|
|
2024-05-29 07:55:27 +07:00
|
|
|
/** Init sensor */
|
|
|
|
boardInit();
|
2024-10-20 23:27:27 +07:00
|
|
|
setMeasurementMaxPeriod();
|
2024-10-20 23:20:16 +07:00
|
|
|
|
2024-11-07 22:08:36 +07:00
|
|
|
// Comment below line to disable debug measurement readings
|
2025-03-18 00:01:04 +07:00
|
|
|
measurements.setDebug(true);
|
2025-03-14 01:41:23 +07:00
|
|
|
|
|
|
|
bool connectToNetwork = true;
|
|
|
|
if (ag->isOne()) { // Offline mode only available for indoor monitor
|
2024-05-13 14:43:53 +07:00
|
|
|
/** Show message confirm offline mode, should me perform if LED bar button
|
|
|
|
* test pressed */
|
|
|
|
if (ledBarButtonTest == false) {
|
|
|
|
oledDisplay.setText(
|
|
|
|
"Press now for",
|
|
|
|
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
|
|
|
|
uint32_t startTime = millis();
|
|
|
|
while (true) {
|
|
|
|
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
|
|
|
|
configuration.setOfflineMode(!configuration.isOfflineMode());
|
|
|
|
|
|
|
|
oledDisplay.setText(
|
|
|
|
"Offline Mode",
|
|
|
|
configuration.isOfflineMode() ? " = True" : " = False", "");
|
|
|
|
delay(1000);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
uint32_t periodMs = (uint32_t)(millis() - startTime);
|
|
|
|
if (periodMs >= 3000) {
|
|
|
|
break;
|
|
|
|
}
|
2024-05-09 14:32:42 +07:00
|
|
|
}
|
2025-03-14 01:41:23 +07:00
|
|
|
connectToNetwork = !configuration.isOfflineMode();
|
2024-05-13 14:43:53 +07:00
|
|
|
} else {
|
|
|
|
configuration.setOfflineModeWithoutSave(true);
|
2025-03-16 16:13:14 +07:00
|
|
|
connectToNetwork = false;
|
2024-05-09 14:32:42 +07:00
|
|
|
}
|
2024-03-24 08:51:14 +07:00
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2025-01-30 10:01:15 +07:00
|
|
|
// Initialize networking configuration
|
2025-03-14 01:41:23 +07:00
|
|
|
if (connectToNetwork) {
|
2025-03-18 00:01:04 +07:00
|
|
|
oledDisplay.setText("Initialize", "network...", "");
|
2025-03-16 16:13:14 +07:00
|
|
|
initializeNetwork();
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2025-01-25 03:01:32 +07:00
|
|
|
|
2024-05-13 15:07:10 +07:00
|
|
|
/** Set offline mode without saving, cause wifi is not configured */
|
2025-03-16 16:13:14 +07:00
|
|
|
if (wifiConnector.hasConfigurated() == false && networkOption == UseWifi) {
|
2024-05-13 15:07:10 +07:00
|
|
|
Serial.println("Set offline mode cause wifi is not configurated");
|
|
|
|
configuration.setOfflineModeWithoutSave(true);
|
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
/** Show display Warning up */
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str());
|
2024-03-23 16:58:17 +07:00
|
|
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
2024-05-13 10:34:06 +07:00
|
|
|
|
|
|
|
Serial.println("Display brightness: " + String(configuration.getDisplayBrightness()));
|
|
|
|
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
|
|
|
|
if (networkOption == UseCellular) {
|
|
|
|
// If using cellular re-set scheduler interval
|
|
|
|
configSchedule.setPeriod(CELLULAR_SERVER_CONFIG_SYNC_INTERVAL);
|
|
|
|
transmissionSchedule.setPeriod(CELLULAR_TRANSMISSION_INTERVAL);
|
|
|
|
measurementSchedule.setPeriod(CELLULAR_MEASUREMENT_INTERVAL);
|
|
|
|
measurementSchedule.update();
|
|
|
|
// Queue now only applied for cellular
|
|
|
|
// Allocate queue memory to avoid always reallocation
|
2025-03-27 14:34:26 +07:00
|
|
|
measurementCycleQueue.reserve(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
|
2025-03-21 04:40:27 +07:00
|
|
|
// Initialize mutex to access mesurementCycleQueue
|
|
|
|
mutexMeasurementCycleQueue = xSemaphoreCreateMutex();
|
|
|
|
}
|
2025-03-16 02:22:38 +07:00
|
|
|
|
2025-03-18 00:01:04 +07:00
|
|
|
// Only run network task if monitor is not in offline mode
|
|
|
|
if (configuration.isOfflineMode() == false) {
|
|
|
|
BaseType_t xReturned =
|
2025-03-17 02:20:43 +07:00
|
|
|
xTaskCreate(networkingTask, "NetworkingTask", 4096, null, 5, &handleNetworkTask);
|
2025-03-18 00:01:04 +07:00
|
|
|
if (xReturned == pdPASS) {
|
|
|
|
Serial.println("Success create networking task");
|
|
|
|
} else {
|
|
|
|
assert("Failed to create networking task");
|
|
|
|
}
|
2025-03-16 02:22:38 +07:00
|
|
|
}
|
|
|
|
|
2025-03-16 16:13:14 +07:00
|
|
|
// Log monitor mode for debugging purpose
|
|
|
|
if (configuration.isOfflineMode()) {
|
|
|
|
Serial.println("Running monitor in offline mode");
|
|
|
|
}
|
|
|
|
else if (configuration.isCloudConnectionDisabled()) {
|
|
|
|
Serial.println("Running monitor without connection to AirGradient server");
|
|
|
|
}
|
2025-03-16 23:15:01 +07:00
|
|
|
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
void loop() {
|
2025-03-16 02:22:38 +07:00
|
|
|
// Schedule to feed external watchdog
|
2025-02-01 13:52:12 +07:00
|
|
|
watchdogFeedSchedule.run();
|
2025-03-16 16:13:14 +07:00
|
|
|
|
2025-03-17 22:17:59 +07:00
|
|
|
if (otaInProgress) {
|
2025-03-18 00:01:04 +07:00
|
|
|
// OTA currently in progress, temporarily disable running sensor schedules
|
|
|
|
delay(10000);
|
2025-03-17 22:17:59 +07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Schedule to update display and led
|
|
|
|
dispLedSchedule.run();
|
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
if (networkOption == UseCellular) {
|
|
|
|
// Queue now only applied for cellular
|
2025-03-16 16:13:14 +07:00
|
|
|
measurementSchedule.run();
|
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.hasSensorS8) {
|
2024-03-23 16:58:17 +07:00
|
|
|
co2Schedule.run();
|
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.hasSensorPMS1 || configuration.hasSensorPMS2) {
|
2024-03-23 16:58:17 +07:00
|
|
|
pmsSchedule.run();
|
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
if (configuration.hasSensorSHT) {
|
2024-03-23 16:58:17 +07:00
|
|
|
tempHumSchedule.run();
|
|
|
|
}
|
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.hasSensorSGP) {
|
2024-03-23 16:58:17 +07:00
|
|
|
tvocSchedule.run();
|
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
if (configuration.hasSensorPMS1) {
|
2024-03-23 16:58:17 +07:00
|
|
|
ag->pms5003.handle();
|
2024-09-14 14:05:35 +07:00
|
|
|
static bool pmsConnected = false;
|
|
|
|
if (pmsConnected != ag->pms5003.connected()) {
|
|
|
|
pmsConnected = ag->pms5003.connected();
|
|
|
|
Serial.printf("PMS sensor %s ", pmsConnected?"connected":"removed");
|
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
} else {
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.hasSensorPMS1) {
|
2024-03-23 16:58:17 +07:00
|
|
|
ag->pms5003t_1.handle();
|
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.hasSensorPMS2) {
|
2024-03-23 16:58:17 +07:00
|
|
|
ag->pms5003t_2.handle();
|
|
|
|
}
|
|
|
|
}
|
2024-03-30 19:33:03 +07:00
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
/** factory reset handle */
|
|
|
|
factoryConfigReset();
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2025-02-01 13:52:12 +07:00
|
|
|
/** check that local configuration changed then do some action */
|
2024-04-07 16:39:01 +07:00
|
|
|
configUpdateHandle();
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
static void co2Update(void) {
|
2024-10-14 02:05:30 +07:00
|
|
|
if (!configuration.hasSensorS8) {
|
2024-10-22 00:11:58 +07:00
|
|
|
// Device don't have S8 sensor
|
2024-10-14 02:05:30 +07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-23 16:58:17 +07:00
|
|
|
int value = ag->s8.getCo2();
|
2024-07-24 09:05:57 +07:00
|
|
|
if (utils::isValidCO2(value)) {
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::CO2, value);
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::CO2, utils::getInvalidCO2());
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
static void mdnsInit(void) {
|
|
|
|
if (!MDNS.begin(localServer.getHostname().c_str())) {
|
2024-03-23 16:58:17 +07:00
|
|
|
Serial.println("Init mDNS failed");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
MDNS.addService("_airgradient", "_tcp", 80);
|
2024-04-03 07:04:28 +07:00
|
|
|
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
|
|
|
|
AgFirmwareModeName(fwMode));
|
2024-04-04 10:36:59 +07:00
|
|
|
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag->deviceId());
|
2024-03-23 16:58:17 +07:00
|
|
|
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag->getVersion());
|
|
|
|
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
|
|
|
|
}
|
|
|
|
|
|
|
|
static void createMqttTask(void) {
|
|
|
|
if (mqttTask) {
|
|
|
|
vTaskDelete(mqttTask);
|
|
|
|
mqttTask = NULL;
|
2024-04-04 18:35:15 +07:00
|
|
|
Serial.println("Delete old MQTT task");
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
2024-04-04 18:35:15 +07:00
|
|
|
Serial.println("Create new MQTT task");
|
2024-03-23 16:58:17 +07:00
|
|
|
xTaskCreate(
|
|
|
|
[](void *param) {
|
|
|
|
for (;;) {
|
|
|
|
delay(MQTT_SYNC_INTERVAL);
|
|
|
|
|
|
|
|
/** Send data */
|
2024-04-03 07:04:28 +07:00
|
|
|
if (mqttClient.isConnected()) {
|
2025-01-23 03:33:47 +07:00
|
|
|
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
|
2024-04-04 10:36:59 +07:00
|
|
|
String topic = "airgradient/readings/" + ag->deviceId();
|
2024-04-04 18:35:15 +07:00
|
|
|
|
|
|
|
if (mqttClient.publish(topic.c_str(), payload.c_str(),
|
|
|
|
payload.length())) {
|
2024-03-23 16:58:17 +07:00
|
|
|
Serial.println("MQTT sync success");
|
|
|
|
} else {
|
|
|
|
Serial.println("MQTT sync failure");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2024-04-04 18:35:15 +07:00
|
|
|
"mqtt-task", 1024 * 4, NULL, 6, &mqttTask);
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
if (mqttTask == NULL) {
|
|
|
|
Serial.println("Creat mqttTask failed");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
static void initMqtt(void) {
|
2024-09-21 14:57:05 +07:00
|
|
|
String mqttUri = configuration.getMqttBrokerUri();
|
|
|
|
if (mqttUri.isEmpty()) {
|
|
|
|
Serial.println(
|
|
|
|
"MQTT is not configured, skipping initialization of MQTT client");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-03-31 14:01:49 +07:00
|
|
|
if (networkOption == UseCellular) {
|
|
|
|
Serial.println("MQTT not available for cellular options");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-09-21 14:57:05 +07:00
|
|
|
if (mqttClient.begin(mqttUri)) {
|
|
|
|
Serial.println("Successfully connected to MQTT broker");
|
2024-04-07 16:39:01 +07:00
|
|
|
createMqttTask();
|
|
|
|
} else {
|
2024-09-21 14:57:05 +07:00
|
|
|
Serial.println("Connection to MQTT broker failed");
|
2024-04-07 16:39:01 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-23 16:58:17 +07:00
|
|
|
static void factoryConfigReset(void) {
|
|
|
|
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
|
|
|
|
if (factoryBtnPressTime == 0) {
|
|
|
|
factoryBtnPressTime = millis();
|
|
|
|
} else {
|
|
|
|
uint32_t ms = (uint32_t)(millis() - factoryBtnPressTime);
|
|
|
|
if (ms >= 2000) {
|
|
|
|
// Show display message: For factory keep for x seconds
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
oledDisplay.setText("Factory reset", "keep pressed", "for 8 sec");
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
|
|
|
Serial.println("Factory reset, keep pressed for 8 sec");
|
|
|
|
}
|
|
|
|
|
|
|
|
int count = 7;
|
|
|
|
while (ag->button.getState() == ag->button.BUTTON_PRESSED) {
|
|
|
|
delay(1000);
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-04-04 10:36:59 +07:00
|
|
|
String str = "for " + String(count) + " sec";
|
2024-04-07 16:39:01 +07:00
|
|
|
oledDisplay.setText("Factory reset", "keep pressed", str.c_str());
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
|
|
|
Serial.printf("Factory reset, keep pressed for %d sec\r\n", count);
|
|
|
|
}
|
|
|
|
count--;
|
|
|
|
if (count == 0) {
|
|
|
|
/** Stop MQTT task first */
|
|
|
|
if (mqttTask) {
|
|
|
|
vTaskDelete(mqttTask);
|
|
|
|
mqttTask = NULL;
|
|
|
|
}
|
|
|
|
|
2024-05-13 11:47:37 +07:00
|
|
|
/** Reset WIFI */
|
2024-08-26 15:47:49 +07:00
|
|
|
WiFi.disconnect(true, true);
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
/** Reset local config */
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.reset();
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
oledDisplay.setText("Factory reset", "successful", "");
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
|
|
|
Serial.println("Factory reset successful");
|
|
|
|
}
|
|
|
|
delay(3000);
|
2024-06-05 09:58:08 +07:00
|
|
|
oledDisplay.setText("","","");
|
2024-03-23 16:58:17 +07:00
|
|
|
ESP.restart();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Show current content cause reset ignore */
|
|
|
|
factoryBtnPressTime = 0;
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
2024-09-22 13:18:15 +07:00
|
|
|
updateDisplayAndLedBar();
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (factoryBtnPressTime != 0) {
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
2024-03-23 16:58:17 +07:00
|
|
|
/** Restore last display content */
|
2024-09-22 13:18:15 +07:00
|
|
|
updateDisplayAndLedBar();
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
factoryBtnPressTime = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void wdgFeedUpdate(void) {
|
|
|
|
ag->watchdog.reset();
|
2024-09-21 08:46:05 +07:00
|
|
|
Serial.println("External watchdog feed!");
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
static void ledBarEnabledUpdate(void) {
|
|
|
|
if (ag->isOne()) {
|
2024-05-13 10:34:06 +07:00
|
|
|
int brightness = configuration.getLedBarBrightness();
|
|
|
|
Serial.println("LED bar brightness: " + String(brightness));
|
|
|
|
if ((brightness == 0) || (configuration.getLedBarMode() == LedBarModeOff)) {
|
|
|
|
ag->ledBar.setEnable(false);
|
|
|
|
} else {
|
2024-05-22 11:17:11 +07:00
|
|
|
ag->ledBar.setBrightness(brightness);
|
2024-05-13 10:34:06 +07:00
|
|
|
ag->ledBar.setEnable(configuration.getLedBarMode() != LedBarModeOff);
|
|
|
|
}
|
2024-05-17 11:52:22 +07:00
|
|
|
ag->ledBar.show();
|
2024-04-07 16:39:01 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-22 16:31:40 +07:00
|
|
|
static bool sgp41Init(void) {
|
|
|
|
ag->sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
|
|
|
|
ag->sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
|
|
|
|
if (ag->sgp41.begin(Wire)) {
|
|
|
|
Serial.println("Init SGP41 success");
|
|
|
|
configuration.hasSensorSGP = true;
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
Serial.println("Init SGP41 failuire");
|
|
|
|
configuration.hasSensorSGP = false;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2025-03-17 15:12:11 +07:00
|
|
|
void checkForFirmwareUpdate(void) {
|
|
|
|
AirgradientOTA *agOta;
|
|
|
|
if (networkOption == UseWifi) {
|
|
|
|
agOta = new AirgradientOTAWifi;
|
|
|
|
} else {
|
|
|
|
agOta = new AirgradientOTACellular(cell);
|
2025-01-25 03:01:32 +07:00
|
|
|
}
|
|
|
|
|
2025-03-17 22:17:59 +07:00
|
|
|
// Indicate main task that ota is performing
|
2025-03-27 17:46:08 +07:00
|
|
|
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();
|
2025-03-17 22:17:59 +07:00
|
|
|
}
|
|
|
|
|
2025-03-17 15:12:11 +07:00
|
|
|
agOta->setHandlerCallback(otaHandlerCallback);
|
2025-03-26 21:38:21 +07:00
|
|
|
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION);
|
2025-03-17 22:17:59 +07:00
|
|
|
|
|
|
|
// Only goes to this line if OTA is not success
|
|
|
|
// Handled by otaHandlerCallback
|
2025-03-27 17:46:08 +07:00
|
|
|
|
|
|
|
otaInProgress = false;
|
|
|
|
if (configuration.hasSensorSGP && networkOption == UseCellular) {
|
|
|
|
// Re-start SGP41 task
|
|
|
|
if (!sgp41Init()) {
|
|
|
|
Serial.println("Failed re-start SGP41 task");
|
2025-03-17 22:17:59 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-17 15:12:11 +07:00
|
|
|
delete agOta;
|
2024-06-05 09:29:30 +07:00
|
|
|
Serial.println();
|
|
|
|
}
|
|
|
|
|
2025-03-17 15:12:11 +07:00
|
|
|
void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) {
|
|
|
|
switch (result) {
|
|
|
|
case AirgradientOTA::Starting:
|
|
|
|
displayExecuteOta(result, fwNewVersion, 0);
|
|
|
|
break;
|
|
|
|
case AirgradientOTA::InProgress:
|
2025-03-26 17:43:39 +07:00
|
|
|
Serial.printf("OTA progress: %s\n", msg);
|
2025-03-17 15:12:11 +07:00
|
|
|
displayExecuteOta(result, "", std::stoi(msg));
|
2024-05-02 18:22:26 +07:00
|
|
|
break;
|
2025-03-17 15:12:11 +07:00
|
|
|
case AirgradientOTA::Failed:
|
|
|
|
case AirgradientOTA::Skipped:
|
|
|
|
case AirgradientOTA::AlreadyUpToDate:
|
|
|
|
displayExecuteOta(result, "", 0);
|
2024-05-02 18:22:26 +07:00
|
|
|
break;
|
2025-03-17 15:12:11 +07:00
|
|
|
case AirgradientOTA::Success:
|
|
|
|
displayExecuteOta(result, "", 0);
|
|
|
|
esp_restart();
|
2024-05-02 18:22:26 +07:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-17 15:12:11 +07:00
|
|
|
static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int processing) {
|
|
|
|
switch (result) {
|
|
|
|
case AirgradientOTA::Starting:
|
2024-05-03 17:27:05 +07:00
|
|
|
if (ag->isOne()) {
|
2024-05-17 20:17:49 +07:00
|
|
|
oledDisplay.showFirmwareUpdateVersion(msg);
|
2024-05-03 17:27:05 +07:00
|
|
|
} else {
|
|
|
|
Serial.println("New firmware: " + msg);
|
|
|
|
}
|
|
|
|
delay(2500);
|
|
|
|
break;
|
2025-03-17 15:12:11 +07:00
|
|
|
case AirgradientOTA::Failed:
|
2024-05-03 17:27:05 +07:00
|
|
|
if (ag->isOne()) {
|
2024-05-17 20:17:49 +07:00
|
|
|
oledDisplay.showFirmwareUpdateFailed();
|
2024-05-03 17:27:05 +07:00
|
|
|
} else {
|
|
|
|
Serial.println("Error: Firmware update: failed");
|
|
|
|
}
|
|
|
|
delay(2500);
|
|
|
|
break;
|
2025-03-17 15:12:11 +07:00
|
|
|
case AirgradientOTA::Skipped:
|
2024-05-16 21:12:02 +07:00
|
|
|
if (ag->isOne()) {
|
2024-05-17 20:17:49 +07:00
|
|
|
oledDisplay.showFirmwareUpdateSkipped();
|
2024-05-16 21:12:02 +07:00
|
|
|
} else {
|
|
|
|
Serial.println("Firmware update: Skipped");
|
|
|
|
}
|
|
|
|
delay(2500);
|
|
|
|
break;
|
2025-03-17 15:12:11 +07:00
|
|
|
case AirgradientOTA::AlreadyUpToDate:
|
2024-05-16 21:12:02 +07:00
|
|
|
if (ag->isOne()) {
|
2024-05-17 20:17:49 +07:00
|
|
|
oledDisplay.showFirmwareUpdateUpToDate();
|
2024-05-16 21:12:02 +07:00
|
|
|
} else {
|
|
|
|
Serial.println("Firmware update: up to date");
|
|
|
|
}
|
|
|
|
delay(2500);
|
|
|
|
break;
|
2025-03-17 15:12:11 +07:00
|
|
|
case AirgradientOTA::InProgress:
|
2024-05-03 17:27:05 +07:00
|
|
|
if (ag->isOne()) {
|
2024-05-17 20:17:49 +07:00
|
|
|
oledDisplay.showFirmwareUpdateProgress(processing);
|
2024-05-03 17:27:05 +07:00
|
|
|
} else {
|
|
|
|
Serial.println("Firmware update: " + String(processing) + String("%"));
|
|
|
|
}
|
|
|
|
break;
|
2025-03-17 15:12:11 +07:00
|
|
|
case AirgradientOTA::Success: {
|
2025-03-27 17:46:08 +07:00
|
|
|
Serial.println("OTA update performed, restarting ...");
|
|
|
|
int i = 3;
|
2025-03-17 15:12:11 +07:00
|
|
|
while (i != 0) {
|
2024-05-08 12:31:54 +07:00
|
|
|
i = i - 1;
|
2025-03-27 17:46:08 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
oledDisplay.showFirmwareUpdateSuccess(i);
|
|
|
|
} else {
|
|
|
|
Serial.println("Rebooting... " + String(i));
|
2024-05-08 12:31:54 +07:00
|
|
|
}
|
2025-03-27 17:46:08 +07:00
|
|
|
delay(1000);
|
2024-05-03 17:27:05 +07:00
|
|
|
}
|
2025-03-27 17:46:08 +07:00
|
|
|
oledDisplay.setAirGradient(0);
|
2024-05-03 17:27:05 +07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
static void sendDataToAg() {
|
|
|
|
/** Change oledDisplay and led state */
|
|
|
|
if (ag->isOne()) {
|
|
|
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnecting);
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
/** Task handle led connecting animation */
|
|
|
|
xTaskCreate(
|
|
|
|
[](void *obj) {
|
|
|
|
for (;;) {
|
2024-04-04 10:36:59 +07:00
|
|
|
// ledSmHandler();
|
2024-04-07 16:39:01 +07:00
|
|
|
stateMachine.handleLeds();
|
|
|
|
if (stateMachine.getLedState() !=
|
|
|
|
AgStateMachineWiFiOkServerConnecting) {
|
2024-03-23 16:58:17 +07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
delay(LED_BAR_ANIMATION_PERIOD);
|
|
|
|
}
|
|
|
|
vTaskDelete(NULL);
|
|
|
|
},
|
|
|
|
"task_led", 2048, NULL, 5, NULL);
|
|
|
|
|
|
|
|
delay(1500);
|
2025-03-14 01:41:23 +07:00
|
|
|
|
|
|
|
// Build payload to check connection to airgradient server
|
|
|
|
JSONVar root;
|
|
|
|
root["wifi"] = wifiConnector.RSSI();
|
|
|
|
root["boot"] = measurements.bootCount();
|
|
|
|
std::string payload = JSON.stringify(root).c_str();
|
2025-03-23 21:48:46 +07:00
|
|
|
if (agClient->httpPostMeasures(payload)) {
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnected);
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2025-02-06 15:38:36 +07:00
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
stateMachine.handleLeds(AgStateMachineNormal);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
void dispSensorNotFound(String ss) {
|
2024-04-04 10:36:59 +07:00
|
|
|
ss = ss + " not found";
|
2024-04-07 16:39:01 +07:00
|
|
|
oledDisplay.setText("Sensor init", "Error:", ss.c_str());
|
2024-03-23 16:58:17 +07:00
|
|
|
delay(2000);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void oneIndoorInit(void) {
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorPMS2 = false;
|
2024-03-23 17:44:11 +07:00
|
|
|
|
2024-03-23 16:58:17 +07:00
|
|
|
/** Display init */
|
2024-04-07 16:39:01 +07:00
|
|
|
oledDisplay.begin();
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
/** Show boot display */
|
|
|
|
Serial.println("Firmware Version: " + ag->getVersion());
|
2024-04-22 19:52:13 +07:00
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
oledDisplay.setText("AirGradient ONE",
|
|
|
|
"FW Version: ", ag->getVersion().c_str());
|
2024-03-23 16:58:17 +07:00
|
|
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
|
|
|
|
|
|
|
ag->ledBar.begin();
|
|
|
|
ag->button.begin();
|
|
|
|
ag->watchdog.begin();
|
|
|
|
|
2024-05-29 16:34:00 +07:00
|
|
|
/** Run LED test on start up if button pressed */
|
2024-05-29 07:55:27 +07:00
|
|
|
oledDisplay.setText("Press now for", "LED test", "");
|
|
|
|
ledBarButtonTest = false;
|
|
|
|
uint32_t stime = millis();
|
|
|
|
while (true) {
|
|
|
|
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
|
|
|
|
ledBarButtonTest = true;
|
|
|
|
stateMachine.executeLedBarPowerUpTest();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
delay(1);
|
|
|
|
uint32_t ms = (uint32_t)(millis() - stime);
|
|
|
|
if (ms >= 3000) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-05 09:58:08 +07:00
|
|
|
/** Check for button to reset WiFi connecto to "airgraident" after test LED
|
|
|
|
* bar */
|
|
|
|
if (ledBarButtonTest) {
|
|
|
|
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
|
|
|
|
WiFi.begin("airgradient", "cleanair");
|
|
|
|
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
|
|
|
|
delay(2500);
|
|
|
|
oledDisplay.setText("Rebooting...", "","");
|
|
|
|
delay(2500);
|
|
|
|
oledDisplay.setText("","","");
|
|
|
|
ESP.restart();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ledBarEnabledUpdate();
|
|
|
|
|
|
|
|
/** Show message init sensor */
|
2024-09-15 08:26:38 +07:00
|
|
|
oledDisplay.setText("Monitor", "initializing...", "");
|
2024-06-05 09:58:08 +07:00
|
|
|
|
2024-03-23 16:58:17 +07:00
|
|
|
/** Init sensor SGP41 */
|
2024-04-22 16:31:40 +07:00
|
|
|
if (sgp41Init() == false) {
|
2024-03-23 16:58:17 +07:00
|
|
|
dispSensorNotFound("SGP41");
|
|
|
|
}
|
|
|
|
|
|
|
|
/** INit SHT */
|
|
|
|
if (ag->sht.begin(Wire) == false) {
|
|
|
|
Serial.println("SHTx sensor not found");
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorSHT = false;
|
2024-03-23 16:58:17 +07:00
|
|
|
dispSensorNotFound("SHT");
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Init S8 CO2 sensor */
|
|
|
|
if (ag->s8.begin(Serial1) == false) {
|
|
|
|
Serial.println("CO2 S8 sensor not found");
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorS8 = false;
|
2024-03-23 16:58:17 +07:00
|
|
|
dispSensorNotFound("S8");
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Init PMS5003 */
|
|
|
|
if (ag->pms5003.begin(Serial0) == false) {
|
|
|
|
Serial.println("PMS sensor not found");
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorPMS1 = false;
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
dispSensorNotFound("PMS");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
static void openAirInit(void) {
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorSHT = false;
|
2024-03-23 17:44:11 +07:00
|
|
|
|
2024-03-23 16:58:17 +07:00
|
|
|
fwMode = FW_MODE_O_1PST;
|
|
|
|
Serial.println("Firmware Version: " + ag->getVersion());
|
|
|
|
|
|
|
|
ag->watchdog.begin();
|
|
|
|
ag->button.begin();
|
|
|
|
ag->statusLed.begin();
|
|
|
|
|
|
|
|
/** detect sensor: PMS5003, PMS5003T, SGP41 and S8 */
|
|
|
|
/**
|
|
|
|
* Serial1 and Serial0 is use for connect S8 and PM sensor or both PM
|
|
|
|
*/
|
|
|
|
bool serial1Available = true;
|
|
|
|
bool serial0Available = true;
|
|
|
|
|
|
|
|
if (ag->s8.begin(Serial1) == false) {
|
|
|
|
Serial1.end();
|
|
|
|
delay(200);
|
|
|
|
Serial.println("Can not detect S8 on Serial1, try on Serial0");
|
|
|
|
/** Check on other port */
|
|
|
|
if (ag->s8.begin(Serial0) == false) {
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorS8 = false;
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
Serial.println("CO2 S8 sensor not found");
|
|
|
|
Serial.println("Can not detect S8 run mode 'PPT'");
|
|
|
|
fwMode = FW_MODE_O_1PPT;
|
|
|
|
delay(200);
|
|
|
|
} else {
|
|
|
|
Serial.println("Found S8 on Serial0");
|
|
|
|
serial0Available = false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Serial.println("Found S8 on Serial1");
|
|
|
|
serial1Available = false;
|
|
|
|
}
|
|
|
|
|
2024-04-22 16:31:40 +07:00
|
|
|
if (sgp41Init() == false) {
|
2024-03-23 16:58:17 +07:00
|
|
|
Serial.println("SGP sensor not found");
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.hasSensorS8 == false) {
|
2024-03-23 16:58:17 +07:00
|
|
|
Serial.println("Can not detect SGP run mode 'O-1PP'");
|
2024-03-23 17:44:11 +07:00
|
|
|
fwMode = FW_MODE_O_1PP;
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
|
|
|
Serial.println("Can not detect SGP run mode 'O-1PS'");
|
2024-04-13 20:50:11 +07:00
|
|
|
fwMode = FW_MODE_O_1PS;
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-15 08:26:38 +07:00
|
|
|
/** Attempt to detect PM sensors */
|
2024-03-23 16:58:17 +07:00
|
|
|
if (fwMode == FW_MODE_O_1PST) {
|
|
|
|
bool pmInitSuccess = false;
|
|
|
|
if (serial0Available) {
|
|
|
|
if (ag->pms5003t_1.begin(Serial0) == false) {
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorPMS1 = false;
|
2024-09-15 08:26:38 +07:00
|
|
|
Serial.println("No PM sensor detected on Serial0");
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
|
|
|
serial0Available = false;
|
|
|
|
pmInitSuccess = true;
|
2024-09-15 08:26:38 +07:00
|
|
|
Serial.println("Detected PM 1 on Serial0");
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (pmInitSuccess == false) {
|
|
|
|
if (serial1Available) {
|
|
|
|
if (ag->pms5003t_1.begin(Serial1) == false) {
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorPMS1 = false;
|
2024-09-15 08:26:38 +07:00
|
|
|
Serial.println("No PM sensor detected on Serial1");
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
|
|
|
serial1Available = false;
|
2024-09-15 08:26:38 +07:00
|
|
|
Serial.println("Detected PM 1 on Serial1");
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorPMS2 = false; // Disable PM2
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
|
|
|
if (ag->pms5003t_1.begin(Serial0) == false) {
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorPMS1 = false;
|
2024-09-15 08:26:38 +07:00
|
|
|
Serial.println("No PM sensor detected on Serial0");
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
2024-09-15 08:26:38 +07:00
|
|
|
Serial.println("Detected PM 1 on Serial0");
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
if (ag->pms5003t_2.begin(Serial1) == false) {
|
2024-04-07 16:39:01 +07:00
|
|
|
configuration.hasSensorPMS2 = false;
|
2024-09-15 08:26:38 +07:00
|
|
|
Serial.println("No PM sensor detected on Serial1");
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
2024-09-15 08:26:38 +07:00
|
|
|
Serial.println("Detected PM 2 on Serial1");
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2024-04-13 21:07:20 +07:00
|
|
|
|
|
|
|
if (fwMode == FW_MODE_O_1PP) {
|
|
|
|
int count = (configuration.hasSensorPMS1 ? 1 : 0) +
|
|
|
|
(configuration.hasSensorPMS2 ? 1 : 0);
|
|
|
|
if (count == 1) {
|
|
|
|
fwMode = FW_MODE_O_1P;
|
|
|
|
}
|
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
/** update the PMS poll period base on fw mode and sensor available */
|
|
|
|
if (fwMode != FW_MODE_O_1PST) {
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.hasSensorPMS1 && configuration.hasSensorPMS2) {
|
2024-03-23 16:58:17 +07:00
|
|
|
pmsSchedule.setPeriod(2000);
|
|
|
|
}
|
|
|
|
}
|
2024-04-03 07:04:28 +07:00
|
|
|
Serial.printf("Firmware Mode: %s\r\n", AgFirmwareModeName(fwMode));
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
static void boardInit(void) {
|
2024-04-07 16:39:01 +07:00
|
|
|
if (ag->isOne()) {
|
2024-03-23 16:58:17 +07:00
|
|
|
oneIndoorInit();
|
|
|
|
} else {
|
|
|
|
openAirInit();
|
|
|
|
}
|
2024-04-22 16:31:40 +07:00
|
|
|
|
|
|
|
/** Set S8 CO2 abc days period */
|
|
|
|
if (configuration.hasSensorS8) {
|
|
|
|
if (ag->s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
|
|
|
|
Serial.println("Set S8 AbcDays successful");
|
|
|
|
} else {
|
|
|
|
Serial.println("Set S8 AbcDays failure");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-13 20:39:05 +07:00
|
|
|
localServer.setFwMode(fwMode);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
static void failedHandler(String msg) {
|
|
|
|
while (true) {
|
|
|
|
Serial.println(msg);
|
|
|
|
vTaskDelay(1000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-16 16:13:14 +07:00
|
|
|
void initializeNetwork() {
|
|
|
|
// Check if cellular module available
|
|
|
|
agSerial = new AgSerial(Wire);
|
|
|
|
agSerial->init(GPIO_IIC_RESET);
|
|
|
|
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);
|
|
|
|
networkOption = UseCellular;
|
|
|
|
} else {
|
|
|
|
Serial.println("Cellular module not available, using wifi");
|
|
|
|
delete agSerial;
|
|
|
|
agSerial = nullptr;
|
|
|
|
// Use wifi as agClient
|
|
|
|
agClient = new AirgradientWifiClient;
|
|
|
|
networkOption = UseWifi;
|
|
|
|
}
|
|
|
|
|
2025-03-31 13:53:56 +07:00
|
|
|
if (networkOption == UseCellular) {
|
|
|
|
// Enable serial stream debugging to check the AT command when doing registration
|
|
|
|
agSerial->setDebug(true);
|
|
|
|
}
|
|
|
|
|
2025-04-02 02:12:13 +07:00
|
|
|
String httpDomain = configuration.getHttpDomain();
|
|
|
|
if (httpDomain != "") {
|
|
|
|
agClient->setHttpDomain(httpDomain.c_str());
|
2025-04-02 02:33:24 +07:00
|
|
|
Serial.printf("HTTP domain name is set to: %s\n", httpDomain.c_str());
|
|
|
|
oledDisplay.setText("HTTP domain name", "using local", "configuration");
|
|
|
|
delay(2500);
|
2025-04-02 02:12:13 +07:00
|
|
|
}
|
|
|
|
|
2025-03-26 21:38:21 +07:00
|
|
|
if (!agClient->begin(ag->deviceId().c_str())) {
|
2025-03-18 01:02:10 +07:00
|
|
|
oledDisplay.setText("Client", "initialization", "failed");
|
|
|
|
delay(5000);
|
|
|
|
oledDisplay.showRebooting();
|
|
|
|
delay(2500);
|
|
|
|
oledDisplay.setText("", "", "");
|
|
|
|
ESP.restart();
|
2025-03-16 16:13:14 +07:00
|
|
|
}
|
|
|
|
|
2025-03-31 13:53:56 +07:00
|
|
|
if (networkOption == UseCellular) {
|
|
|
|
// Disabling it again
|
|
|
|
agSerial->setDebug(false);
|
|
|
|
}
|
|
|
|
|
2025-03-16 16:13:14 +07:00
|
|
|
if (networkOption == UseWifi) {
|
2025-03-14 01:41:23 +07:00
|
|
|
if (!wifiConnector.connect()) {
|
|
|
|
Serial.println("Cannot initiate wifi connection");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!wifiConnector.isConnected()) {
|
|
|
|
Serial.println("Failed connect to WiFi");
|
|
|
|
if (wifiConnector.isConfigurePorttalTimeout()) {
|
|
|
|
oledDisplay.showRebooting();
|
|
|
|
delay(2500);
|
|
|
|
oledDisplay.setText("", "", "");
|
|
|
|
ESP.restart();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Directly return because the rest of the function applied if wifi is connect only
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initiate local network configuration
|
|
|
|
mdnsInit();
|
|
|
|
localServer.begin();
|
|
|
|
// Apply mqtt connection if configured
|
|
|
|
initMqtt();
|
|
|
|
|
|
|
|
// Ignore the rest if cloud connection to AirGradient is disabled
|
|
|
|
if (configuration.isCloudConnectionDisabled()) {
|
|
|
|
return;
|
2025-01-25 03:01:32 +07:00
|
|
|
}
|
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
// Send data for the first time to AG server at boot
|
|
|
|
sendDataToAg();
|
|
|
|
}
|
2025-01-25 03:01:32 +07:00
|
|
|
|
|
|
|
|
2025-03-23 21:48:46 +07:00
|
|
|
std::string config = agClient->httpFetchConfig();
|
2025-01-25 03:01:32 +07:00
|
|
|
configSchedule.update();
|
2025-03-14 01:41:23 +07:00
|
|
|
// Check if fetch configuration failed or fetch succes but parsing failed
|
|
|
|
if (agClient->isLastFetchConfigSucceed() == false ||
|
|
|
|
configuration.parse(config.c_str(), false) == false) {
|
2025-01-25 03:01:32 +07:00
|
|
|
if (ag->isOne()) {
|
2025-03-14 01:41:23 +07:00
|
|
|
if (agClient->isRegisteredOnAgServer() == false) {
|
2025-01-25 03:01:32 +07:00
|
|
|
stateMachine.displaySetAddToDashBoard();
|
|
|
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerOkSensorConfigFailed);
|
|
|
|
} else {
|
|
|
|
stateMachine.displayClearAddToDashBoard();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
stateMachine.handleLeds(AgStateMachineWiFiOkServerOkSensorConfigFailed);
|
|
|
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
2025-03-14 01:41:23 +07:00
|
|
|
}
|
|
|
|
else {
|
2025-01-25 03:01:32 +07:00
|
|
|
ledBarEnabledUpdate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
static void configurationUpdateSchedule(void) {
|
2025-03-16 16:13:14 +07:00
|
|
|
if (configuration.getConfigurationControl() ==
|
|
|
|
ConfigurationControl::ConfigurationControlLocal) {
|
|
|
|
Serial.println("Ignore fetch server configuration, configurationControl set to local");
|
2025-03-14 01:41:23 +07:00
|
|
|
agClient->resetFetchConfigurationStatus();
|
2025-01-25 03:01:32 +07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-03-23 21:48:46 +07:00
|
|
|
std::string config = agClient->httpFetchConfig();
|
2025-03-14 01:41:23 +07:00
|
|
|
if (agClient->isLastFetchConfigSucceed() && configuration.parse(config.c_str(), false)) {
|
2024-03-30 19:33:03 +07:00
|
|
|
configUpdateHandle();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void configUpdateHandle() {
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.isUpdated() == false) {
|
|
|
|
return;
|
2024-03-30 19:33:03 +07:00
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
stateMachine.executeCo2Calibration();
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
String mqttUri = configuration.getMqttBrokerUri();
|
2024-04-04 18:35:15 +07:00
|
|
|
if (mqttClient.isCurrentUri(mqttUri) == false) {
|
2024-04-03 07:04:28 +07:00
|
|
|
mqttClient.end();
|
2024-04-07 16:39:01 +07:00
|
|
|
initMqtt();
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
2025-04-02 02:12:13 +07:00
|
|
|
String httpDomain = configuration.getHttpDomain();
|
|
|
|
if (httpDomain != "") {
|
2025-04-02 02:33:24 +07:00
|
|
|
Serial.printf("HTTP domain name set to: %s\n", httpDomain.c_str());
|
2025-04-02 02:12:13 +07:00
|
|
|
agClient->setHttpDomain(httpDomain.c_str());
|
|
|
|
} else {
|
|
|
|
// Its empty, set to default
|
2025-04-02 02:33:24 +07:00
|
|
|
Serial.println("HTTP domain name from configuration empty, set to default");
|
2025-04-02 02:12:13 +07:00
|
|
|
agClient->setHttpDomainDefault();
|
|
|
|
}
|
|
|
|
|
2024-05-13 19:00:34 +07:00
|
|
|
if (configuration.hasSensorSGP) {
|
|
|
|
if (configuration.noxLearnOffsetChanged() ||
|
|
|
|
configuration.tvocLearnOffsetChanged()) {
|
|
|
|
ag->sgp41.end();
|
|
|
|
|
|
|
|
int oldTvocOffset = ag->sgp41.getTvocLearningOffset();
|
|
|
|
int oldNoxOffset = ag->sgp41.getNoxLearningOffset();
|
|
|
|
bool result = sgp41Init();
|
|
|
|
const char *resultStr = "successful";
|
|
|
|
if (!result) {
|
|
|
|
resultStr = "failure";
|
|
|
|
}
|
|
|
|
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
|
|
|
|
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
|
|
|
|
oldTvocOffset, configuration.getTvocLearningOffset(),
|
|
|
|
resultStr);
|
|
|
|
}
|
|
|
|
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
|
|
|
|
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
|
|
|
|
oldNoxOffset, configuration.getNoxLearningOffset(),
|
|
|
|
resultStr);
|
|
|
|
}
|
2024-04-14 21:30:56 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-13 19:00:34 +07:00
|
|
|
if (ag->isOne()) {
|
|
|
|
if (configuration.isLedBarBrightnessChanged()) {
|
2024-05-17 11:52:22 +07:00
|
|
|
if (configuration.getLedBarBrightness() == 0) {
|
|
|
|
ag->ledBar.setEnable(false);
|
|
|
|
} else {
|
|
|
|
if (configuration.getLedBarMode() != LedBarMode::LedBarModeOff) {
|
|
|
|
ag->ledBar.setEnable(true);
|
|
|
|
}
|
2024-05-22 11:17:11 +07:00
|
|
|
ag->ledBar.setBrightness(configuration.getLedBarBrightness());
|
2024-05-17 11:52:22 +07:00
|
|
|
}
|
|
|
|
ag->ledBar.show();
|
2024-05-13 19:00:34 +07:00
|
|
|
}
|
2024-05-17 11:52:22 +07:00
|
|
|
|
|
|
|
if (configuration.isLedBarModeChanged()) {
|
|
|
|
if (configuration.getLedBarBrightness() == 0) {
|
|
|
|
ag->ledBar.setEnable(false);
|
|
|
|
} else {
|
|
|
|
if(configuration.getLedBarMode() == LedBarMode::LedBarModeOff) {
|
|
|
|
ag->ledBar.setEnable(false);
|
|
|
|
} else {
|
|
|
|
ag->ledBar.setEnable(true);
|
2024-05-22 11:17:11 +07:00
|
|
|
ag->ledBar.setBrightness(configuration.getLedBarBrightness());
|
2024-05-17 11:52:22 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
ag->ledBar.show();
|
|
|
|
}
|
|
|
|
|
2024-05-13 19:00:34 +07:00
|
|
|
if (configuration.isDisplayBrightnessChanged()) {
|
|
|
|
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
|
|
|
}
|
2024-05-17 11:52:22 +07:00
|
|
|
|
|
|
|
stateMachine.executeLedBarTest();
|
2024-05-01 21:25:35 +07:00
|
|
|
}
|
2024-09-01 20:19:18 +07:00
|
|
|
else if(ag->isOpenAir()) {
|
|
|
|
stateMachine.executeLedBarTest();
|
|
|
|
}
|
2024-05-01 21:25:35 +07:00
|
|
|
|
2024-09-22 13:18:15 +07:00
|
|
|
// Update display and led bar notification based on updated configuration
|
|
|
|
updateDisplayAndLedBar();
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
2024-09-22 13:18:15 +07:00
|
|
|
static void updateDisplayAndLedBar(void) {
|
|
|
|
if (factoryBtnPressTime != 0) {
|
|
|
|
// Do not distrub factory reset sequence countdown
|
|
|
|
return;
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2024-04-04 18:35:15 +07:00
|
|
|
|
2024-09-22 13:18:15 +07:00
|
|
|
if (configuration.isOfflineMode()) {
|
|
|
|
// Ignore network related status when in offline mode
|
|
|
|
stateMachine.displayHandle(AgStateMachineNormal);
|
|
|
|
stateMachine.handleLeds(AgStateMachineNormal);
|
|
|
|
return;
|
2024-04-07 16:39:01 +07:00
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2025-03-24 03:52:46 +07:00
|
|
|
if (networkOption == UseWifi) {
|
|
|
|
if (wifiConnector.isConnected() == false) {
|
|
|
|
stateMachine.displayHandle(AgStateMachineWiFiLost);
|
|
|
|
stateMachine.handleLeds(AgStateMachineWiFiLost);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (networkOption == UseCellular) {
|
|
|
|
if (agClient->isClientReady() == false) {
|
|
|
|
// Same action as wifi
|
|
|
|
stateMachine.displayHandle(AgStateMachineWiFiLost);
|
|
|
|
stateMachine.handleLeds(AgStateMachineWiFiLost);
|
|
|
|
return;
|
|
|
|
}
|
2025-03-18 00:01:04 +07:00
|
|
|
}
|
2025-01-25 03:01:32 +07:00
|
|
|
|
|
|
|
if (configuration.isCloudConnectionDisabled()) {
|
|
|
|
// Ignore API related check since cloud is disabled
|
|
|
|
stateMachine.displayHandle(AgStateMachineNormal);
|
|
|
|
stateMachine.handleLeds(AgStateMachineNormal);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AgStateMachineState state = AgStateMachineNormal;
|
2025-03-14 01:41:23 +07:00
|
|
|
if (agClient->isLastFetchConfigSucceed() == false) {
|
2024-09-22 13:18:15 +07:00
|
|
|
state = AgStateMachineSensorConfigFailed;
|
2025-03-14 01:41:23 +07:00
|
|
|
if (agClient->isRegisteredOnAgServer() == false) {
|
2024-09-22 13:18:15 +07:00
|
|
|
stateMachine.displaySetAddToDashBoard();
|
|
|
|
} else {
|
|
|
|
stateMachine.displayClearAddToDashBoard();
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2025-03-14 01:41:23 +07:00
|
|
|
} else if (agClient->isLastPostMeasureSucceed() == false &&
|
|
|
|
configuration.isPostDataToAirGradient()) {
|
2024-09-22 13:18:15 +07:00
|
|
|
state = AgStateMachineServerLost;
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2024-09-22 13:18:15 +07:00
|
|
|
|
|
|
|
stateMachine.displayHandle(state);
|
|
|
|
stateMachine.handleLeds(state);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
static void updateTvoc(void) {
|
2024-10-14 02:05:30 +07:00
|
|
|
if (!configuration.hasSensorSGP) {
|
|
|
|
return;
|
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::TVOC, ag->sgp41.getTvocIndex());
|
|
|
|
measurements.update(Measurements::TVOCRaw, ag->sgp41.getTvocRaw());
|
|
|
|
measurements.update(Measurements::NOx, ag->sgp41.getNoxIndex());
|
|
|
|
measurements.update(Measurements::NOxRaw, ag->sgp41.getNoxRaw());
|
2024-10-14 02:05:30 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
static void updatePMS5003() {
|
|
|
|
if (ag->pms5003.connected()) {
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM01, ag->pms5003.getPm01Ae());
|
|
|
|
measurements.update(Measurements::PM25, ag->pms5003.getPm25Ae());
|
|
|
|
measurements.update(Measurements::PM10, ag->pms5003.getPm10Ae());
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_SP, ag->pms5003.getPm01Sp());
|
|
|
|
measurements.update(Measurements::PM25_SP, ag->pms5003.getPm25Sp());
|
|
|
|
measurements.update(Measurements::PM10_SP, ag->pms5003.getPm10Sp());
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM03_PC, ag->pms5003.getPm03ParticleCount());
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.update(Measurements::PM05_PC, ag->pms5003.getPm05ParticleCount());
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_PC, ag->pms5003.getPm01ParticleCount());
|
|
|
|
measurements.update(Measurements::PM25_PC, ag->pms5003.getPm25ParticleCount());
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.update(Measurements::PM5_PC, ag->pms5003.getPm5ParticleCount());
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM10_PC, ag->pms5003.getPm10ParticleCount());
|
2024-10-14 02:05:30 +07:00
|
|
|
} else {
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM01, utils::getInvalidPmValue());
|
|
|
|
measurements.update(Measurements::PM25, utils::getInvalidPmValue());
|
|
|
|
measurements.update(Measurements::PM10, utils::getInvalidPmValue());
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_SP, utils::getInvalidPmValue());
|
|
|
|
measurements.update(Measurements::PM25_SP, utils::getInvalidPmValue());
|
|
|
|
measurements.update(Measurements::PM10_SP, utils::getInvalidPmValue());
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue());
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.update(Measurements::PM05_PC, utils::getInvalidPmValue());
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_PC, utils::getInvalidPmValue());
|
|
|
|
measurements.update(Measurements::PM25_PC, utils::getInvalidPmValue());
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.update(Measurements::PM5_PC, utils::getInvalidPmValue());
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM10_PC, utils::getInvalidPmValue());
|
2024-10-14 02:05:30 +07:00
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
2024-04-07 16:39:01 +07:00
|
|
|
static void updatePm(void) {
|
|
|
|
if (ag->isOne()) {
|
2024-10-14 02:05:30 +07:00
|
|
|
updatePMS5003();
|
|
|
|
return;
|
|
|
|
}
|
2024-08-25 20:21:26 +07:00
|
|
|
|
2024-10-14 02:05:30 +07:00
|
|
|
// Open Air Monitor series, can have two PMS5003T sensor
|
|
|
|
bool newPMS1Value = false;
|
|
|
|
bool newPMS2Value = false;
|
|
|
|
|
|
|
|
// Read PMS channel 1 if available
|
|
|
|
int channel = 1;
|
|
|
|
if (configuration.hasSensorPMS1) {
|
|
|
|
if (ag->pms5003t_1.connected()) {
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM01, ag->pms5003t_1.getPm01Ae(), channel);
|
|
|
|
measurements.update(Measurements::PM25, ag->pms5003t_1.getPm25Ae(), channel);
|
|
|
|
measurements.update(Measurements::PM10, ag->pms5003t_1.getPm10Ae(), channel);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_SP, ag->pms5003t_1.getPm01Sp(), channel);
|
|
|
|
measurements.update(Measurements::PM25_SP, ag->pms5003t_1.getPm25Sp(), channel);
|
|
|
|
measurements.update(Measurements::PM10_SP, ag->pms5003t_1.getPm10Sp(), channel);
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM03_PC, ag->pms5003t_1.getPm03ParticleCount(), channel);
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.update(Measurements::PM05_PC, ag->pms5003t_1.getPm05ParticleCount(), channel);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_PC, ag->pms5003t_1.getPm01ParticleCount(), channel);
|
|
|
|
measurements.update(Measurements::PM25_PC, ag->pms5003t_1.getPm25ParticleCount(), channel);
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::Temperature, ag->pms5003t_1.getTemperature(), channel);
|
|
|
|
measurements.update(Measurements::Humidity, ag->pms5003t_1.getRelativeHumidity(), channel);
|
2024-10-14 02:05:30 +07:00
|
|
|
|
|
|
|
// flag that new valid PMS value exists
|
2024-10-22 17:13:15 +07:00
|
|
|
newPMS1Value = true;
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
2024-10-14 02:05:30 +07:00
|
|
|
// PMS channel 1 now is not connected, update using invalid value
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM01, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM25, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM10, utils::getInvalidPmValue(), channel);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_SP, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM25_SP, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM10_SP, utils::getInvalidPmValue(), channel);
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue(), channel);
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.update(Measurements::PM05_PC, utils::getInvalidPmValue(), channel);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_PC, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM25_PC, utils::getInvalidPmValue(), channel);
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::Temperature, utils::getInvalidTemperature(), channel);
|
|
|
|
measurements.update(Measurements::Humidity, utils::getInvalidHumidity(), channel);
|
2024-04-07 16:39:01 +07:00
|
|
|
}
|
2024-10-14 02:05:30 +07:00
|
|
|
}
|
2024-04-07 16:39:01 +07:00
|
|
|
|
2024-10-14 02:05:30 +07:00
|
|
|
// Read PMS channel 2 if available
|
|
|
|
channel = 2;
|
|
|
|
if (configuration.hasSensorPMS2) {
|
|
|
|
if (ag->pms5003t_2.connected()) {
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM01, ag->pms5003t_2.getPm01Ae(), channel);
|
|
|
|
measurements.update(Measurements::PM25, ag->pms5003t_2.getPm25Ae(), channel);
|
|
|
|
measurements.update(Measurements::PM10, ag->pms5003t_2.getPm10Ae(), channel);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_SP, ag->pms5003t_2.getPm01Sp(), channel);
|
|
|
|
measurements.update(Measurements::PM25_SP, ag->pms5003t_2.getPm25Sp(), channel);
|
|
|
|
measurements.update(Measurements::PM10_SP, ag->pms5003t_2.getPm10Sp(), channel);
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM03_PC, ag->pms5003t_2.getPm03ParticleCount(), channel);
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.update(Measurements::PM05_PC, ag->pms5003t_2.getPm05ParticleCount(), channel);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_PC, ag->pms5003t_2.getPm01ParticleCount(), channel);
|
|
|
|
measurements.update(Measurements::PM25_PC, ag->pms5003t_2.getPm25ParticleCount(), channel);
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::Temperature, ag->pms5003t_2.getTemperature(), channel);
|
|
|
|
measurements.update(Measurements::Humidity, ag->pms5003t_2.getRelativeHumidity(), channel);
|
2024-10-14 02:05:30 +07:00
|
|
|
|
|
|
|
// flag that new valid PMS value exists
|
|
|
|
newPMS2Value = true;
|
2024-03-23 16:58:17 +07:00
|
|
|
} else {
|
2024-10-22 17:13:15 +07:00
|
|
|
// PMS channel 2 now is not connected, update using invalid value
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM01, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM25, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM10, utils::getInvalidPmValue(), channel);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_SP, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM25_SP, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM10_SP, utils::getInvalidPmValue(), channel);
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue(), channel);
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.update(Measurements::PM05_PC, utils::getInvalidPmValue(), channel);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.update(Measurements::PM01_PC, utils::getInvalidPmValue(), channel);
|
|
|
|
measurements.update(Measurements::PM25_PC, utils::getInvalidPmValue(), channel);
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::Temperature, utils::getInvalidTemperature(), channel);
|
|
|
|
measurements.update(Measurements::Humidity, utils::getInvalidHumidity(), channel);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
2024-10-14 02:05:30 +07:00
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-10-14 02:05:30 +07:00
|
|
|
if (configuration.hasSensorSGP) {
|
|
|
|
float temp, hum;
|
|
|
|
if (newPMS1Value && newPMS2Value) {
|
|
|
|
// Both PMS has new valid value
|
2024-10-20 22:30:49 +07:00
|
|
|
temp = (measurements.getFloat(Measurements::Temperature, 1) +
|
|
|
|
measurements.getFloat(Measurements::Temperature, 2)) /
|
2024-10-14 02:05:30 +07:00
|
|
|
2.0f;
|
2024-10-20 22:30:49 +07:00
|
|
|
hum = (measurements.getFloat(Measurements::Humidity, 1) +
|
|
|
|
measurements.getFloat(Measurements::Humidity, 2)) /
|
2024-10-14 02:05:30 +07:00
|
|
|
2.0f;
|
|
|
|
} else if (newPMS1Value) {
|
|
|
|
// Only PMS1 has new valid value
|
2024-10-20 22:30:49 +07:00
|
|
|
temp = measurements.getFloat(Measurements::Temperature, 1);
|
|
|
|
hum = measurements.getFloat(Measurements::Humidity, 1);
|
2024-04-04 18:35:15 +07:00
|
|
|
} else {
|
2024-10-14 02:05:30 +07:00
|
|
|
// Only PMS2 has new valid value
|
2024-10-20 22:30:49 +07:00
|
|
|
temp = measurements.getFloat(Measurements::Temperature, 2);
|
|
|
|
hum = measurements.getFloat(Measurements::Humidity, 2);
|
2024-04-04 18:35:15 +07:00
|
|
|
}
|
|
|
|
|
2024-10-14 02:05:30 +07:00
|
|
|
// Update compensation temperature and humidity for SGP41
|
|
|
|
ag->sgp41.setCompensationTemperatureHumidity(temp, hum);
|
2024-08-25 20:21:26 +07:00
|
|
|
}
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
void postUsingWifi() {
|
|
|
|
// Increment bootcount when send measurements data is scheduled
|
|
|
|
int bootCount = measurements.bootCount() + 1;
|
|
|
|
measurements.setBootCount(bootCount);
|
|
|
|
|
|
|
|
String payload = measurements.toString(false, fwMode, wifiConnector.RSSI());
|
2025-03-23 21:48:46 +07:00
|
|
|
if (agClient->httpPostMeasures(payload.c_str()) == false) {
|
2025-03-21 04:40:27 +07:00
|
|
|
Serial.println();
|
|
|
|
Serial.println("Online mode and isPostToAirGradient = true");
|
|
|
|
Serial.println();
|
2025-01-25 03:01:32 +07:00
|
|
|
}
|
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
// Log current free heap size
|
|
|
|
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
|
|
|
|
}
|
|
|
|
|
|
|
|
void postUsingCellular() {
|
2025-03-17 02:20:43 +07:00
|
|
|
// Aquire queue mutex to get queue size
|
|
|
|
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
|
2025-03-16 02:22:38 +07:00
|
|
|
|
2025-03-16 16:13:14 +07:00
|
|
|
// Make sure measurement cycle available
|
|
|
|
int queueSize = measurementCycleQueue.size();
|
|
|
|
if (queueSize == 0) {
|
|
|
|
Serial.println("Skipping transmission, measurementCycle empty");
|
|
|
|
xSemaphoreGive(mutexMeasurementCycleQueue);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
// Build payload include all measurements from queue
|
2025-03-26 16:18:48 +07:00
|
|
|
std::string payload;
|
|
|
|
payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds
|
2025-03-21 04:40:27 +07:00
|
|
|
for (int i = 0; i < queueSize; i++) {
|
|
|
|
auto mc = measurementCycleQueue.at(i);
|
|
|
|
payload += ",";
|
2025-03-28 13:45:07 +07:00
|
|
|
payload += measurements.buildMeasuresPayload(mc);
|
2025-03-21 04:40:27 +07:00
|
|
|
}
|
2025-03-17 02:20:43 +07:00
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
// Release before actually post measures that might takes too long
|
|
|
|
xSemaphoreGive(mutexMeasurementCycleQueue);
|
2025-03-17 02:20:43 +07:00
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
// Attempt to send
|
2025-03-26 16:18:48 +07:00
|
|
|
if (agClient->httpPostMeasures(payload) == false) {
|
2025-03-21 04:40:27 +07:00
|
|
|
// Consider network has a problem, retry in next schedule
|
|
|
|
Serial.println("Post measures failed, retry in next schedule");
|
|
|
|
return;
|
|
|
|
}
|
2025-03-17 02:20:43 +07:00
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
// Post success, remove the data that previously sent from queue
|
|
|
|
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
|
|
|
|
measurementCycleQueue.erase(measurementCycleQueue.begin(),
|
|
|
|
measurementCycleQueue.begin() + queueSize);
|
2025-03-27 14:34:26 +07:00
|
|
|
|
|
|
|
if (measurementCycleQueue.capacity() > RESERVED_MEASUREMENT_CYCLE_CAPACITY) {
|
|
|
|
Serial.println("measurementCycleQueue capacity more than reserved space, resizing..");
|
|
|
|
measurementCycleQueue.resize(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
|
|
|
|
}
|
2025-03-21 04:40:27 +07:00
|
|
|
xSemaphoreGive(mutexMeasurementCycleQueue);
|
|
|
|
}
|
2025-03-16 02:22:38 +07:00
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
void sendDataToServer(void) {
|
|
|
|
if (configuration.isPostDataToAirGradient() == false) {
|
|
|
|
Serial.println("Skipping transmission of data to AG server, post data to server disabled");
|
|
|
|
agClient->resetPostMeasuresStatus();
|
|
|
|
return;
|
|
|
|
}
|
2025-03-17 02:20:43 +07:00
|
|
|
|
2025-03-21 04:40:27 +07:00
|
|
|
if (networkOption == UseWifi) {
|
|
|
|
postUsingWifi();
|
2025-03-26 16:18:48 +07:00
|
|
|
} else if (networkOption == UseCellular) {
|
2025-03-21 04:40:27 +07:00
|
|
|
postUsingCellular();
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tempHumUpdate(void) {
|
2024-04-08 10:29:58 +07:00
|
|
|
delay(100);
|
2024-03-23 16:58:17 +07:00
|
|
|
if (ag->sht.measure()) {
|
2024-10-14 02:05:30 +07:00
|
|
|
float temp = ag->sht.getTemperature();
|
|
|
|
float rhum = ag->sht.getRelativeHumidity();
|
2024-03-23 16:58:17 +07:00
|
|
|
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::Temperature, temp);
|
|
|
|
measurements.update(Measurements::Humidity, rhum);
|
2024-03-23 16:58:17 +07:00
|
|
|
|
|
|
|
// Update compensation temperature and humidity for SGP41
|
2024-04-07 16:39:01 +07:00
|
|
|
if (configuration.hasSensorSGP) {
|
2024-10-14 02:05:30 +07:00
|
|
|
ag->sgp41.setCompensationTemperatureHumidity(temp, rhum);
|
2024-03-23 16:58:17 +07:00
|
|
|
}
|
|
|
|
} else {
|
2024-10-19 01:32:41 +07:00
|
|
|
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
|
|
|
|
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
|
2024-03-23 16:58:17 +07:00
|
|
|
Serial.println("SHT read failed");
|
|
|
|
}
|
2024-10-20 23:20:16 +07:00
|
|
|
}
|
|
|
|
|
2024-10-20 23:27:27 +07:00
|
|
|
/* Set max period for each measurement type based on sensor update interval*/
|
|
|
|
void setMeasurementMaxPeriod() {
|
2024-10-22 17:13:15 +07:00
|
|
|
int max;
|
|
|
|
|
2024-10-20 23:27:27 +07:00
|
|
|
/// Max period for S8 sensors measurements
|
|
|
|
measurements.maxPeriod(Measurements::CO2, calculateMaxPeriod(SENSOR_CO2_UPDATE_INTERVAL));
|
2024-10-22 17:13:15 +07:00
|
|
|
|
2024-10-20 23:27:27 +07:00
|
|
|
/// Max period for SGP sensors measurements
|
2024-10-22 17:13:15 +07:00
|
|
|
max = calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL);
|
|
|
|
measurements.maxPeriod(Measurements::TVOC, max);
|
|
|
|
measurements.maxPeriod(Measurements::TVOCRaw, max);
|
|
|
|
measurements.maxPeriod(Measurements::NOx, max);
|
|
|
|
measurements.maxPeriod(Measurements::NOxRaw, max);
|
|
|
|
|
2024-10-20 23:27:27 +07:00
|
|
|
/// Max period for PMS sensors measurements
|
2024-10-22 17:13:15 +07:00
|
|
|
max = calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL);
|
|
|
|
measurements.maxPeriod(Measurements::PM25, max);
|
|
|
|
measurements.maxPeriod(Measurements::PM01, max);
|
|
|
|
measurements.maxPeriod(Measurements::PM10, max);
|
|
|
|
measurements.maxPeriod(Measurements::PM25_SP, max);
|
|
|
|
measurements.maxPeriod(Measurements::PM01_SP, max);
|
|
|
|
measurements.maxPeriod(Measurements::PM10_SP, max);
|
|
|
|
measurements.maxPeriod(Measurements::PM03_PC, max);
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.maxPeriod(Measurements::PM05_PC, max);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.maxPeriod(Measurements::PM01_PC, max);
|
|
|
|
measurements.maxPeriod(Measurements::PM25_PC, max);
|
2024-10-29 23:51:19 +07:00
|
|
|
measurements.maxPeriod(Measurements::PM5_PC, max);
|
2024-10-22 17:13:15 +07:00
|
|
|
measurements.maxPeriod(Measurements::PM10_PC, max);
|
|
|
|
|
2024-10-20 23:27:27 +07:00
|
|
|
// Temperature and Humidity
|
|
|
|
if (configuration.hasSensorSHT) {
|
|
|
|
/// Max period for SHT sensors measurements
|
|
|
|
measurements.maxPeriod(Measurements::Temperature,
|
|
|
|
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
|
|
|
|
measurements.maxPeriod(Measurements::Humidity,
|
|
|
|
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
|
|
|
|
} else {
|
|
|
|
/// Temp and hum data retrieved from PMS5003T sensor
|
|
|
|
measurements.maxPeriod(Measurements::Temperature,
|
|
|
|
calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
|
|
|
measurements.maxPeriod(Measurements::Humidity, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-20 23:20:16 +07:00
|
|
|
int calculateMaxPeriod(int updateInterval) {
|
2024-11-09 20:53:52 +07:00
|
|
|
// 0.8 is 80% reduced interval for max period
|
2025-04-04 10:54:03 +07:00
|
|
|
// NOTE: Both network option use the same measurement interval
|
|
|
|
return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval;
|
2025-03-16 02:22:38 +07:00
|
|
|
}
|
|
|
|
|
2025-03-26 16:18:48 +07:00
|
|
|
|
|
|
|
void networkSignalCheck() {
|
|
|
|
if (networkOption == UseWifi) {
|
|
|
|
Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI());
|
|
|
|
} else if (networkOption == UseCellular) {
|
|
|
|
auto result = cell->retrieveSignal();
|
|
|
|
if (result.status != CellReturnStatus::Ok) {
|
2025-03-27 16:10:56 +07:00
|
|
|
agClient->setClientReady(false);
|
2025-04-07 16:28:37 +07:00
|
|
|
lastCellSignalQuality = 99;
|
2025-03-27 16:10:56 +07:00
|
|
|
return;
|
|
|
|
}
|
2025-04-07 16:28:37 +07:00
|
|
|
|
|
|
|
// Save last signal quality
|
|
|
|
lastCellSignalQuality = result.data;
|
|
|
|
|
2025-03-27 16:10:56 +07:00
|
|
|
if (result.data == 99) {
|
|
|
|
// 99 indicate cellular not attached to network
|
|
|
|
agClient->setClientReady(false);
|
2025-03-26 16:18:48 +07:00
|
|
|
return;
|
|
|
|
}
|
2025-04-07 16:28:37 +07:00
|
|
|
|
|
|
|
Serial.printf("Cellular signal quality %d\n", result.data);
|
2025-03-26 16:18:48 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-16 02:22:38 +07:00
|
|
|
void networkingTask(void *args) {
|
2025-03-17 22:17:59 +07:00
|
|
|
// OTA check on boot
|
|
|
|
#ifdef ESP8266
|
|
|
|
// ota not supported
|
|
|
|
#else
|
|
|
|
// because cellular it takes too long, watchdog triggered
|
|
|
|
checkForFirmwareUpdate();
|
|
|
|
checkForUpdateSchedule.update();
|
|
|
|
#endif
|
|
|
|
|
2025-03-26 16:18:48 +07:00
|
|
|
// Because cellular interval is longer, needs to send first measures cycle on
|
|
|
|
// boot to indicate that its online
|
|
|
|
if (networkOption == UseCellular) {
|
|
|
|
Serial.println("Prepare first measures cycle to send on boot for 20s");
|
|
|
|
delay(20000);
|
2025-04-07 16:28:37 +07:00
|
|
|
networkSignalCheck();
|
2025-03-26 16:18:48 +07:00
|
|
|
newMeasurementCycle();
|
|
|
|
sendDataToServer();
|
|
|
|
measurementSchedule.update();
|
|
|
|
}
|
2025-03-16 22:47:21 +07:00
|
|
|
|
2025-04-09 15:51:54 +07:00
|
|
|
// Default value is 0, indicate its not started yet
|
|
|
|
uint32_t startTimeClientNotReady = 0;
|
|
|
|
|
2025-03-16 22:47:21 +07:00
|
|
|
// Reset scheduler
|
|
|
|
configSchedule.update();
|
|
|
|
transmissionSchedule.update();
|
2025-03-16 23:15:01 +07:00
|
|
|
|
2025-03-16 02:22:38 +07:00
|
|
|
while (1) {
|
|
|
|
// Handle reconnection based on mode
|
2025-03-16 16:13:14 +07:00
|
|
|
if (networkOption == UseWifi) {
|
|
|
|
wifiConnector.handle();
|
|
|
|
if (wifiConnector.isConnected() == false) {
|
|
|
|
delay(1000);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (networkOption == UseCellular) {
|
2025-03-27 16:10:56 +07:00
|
|
|
if (agClient->isClientReady() == false) {
|
2025-04-09 15:51:54 +07:00
|
|
|
// Start time if value still default
|
|
|
|
if (startTimeClientNotReady == 0) {
|
|
|
|
startTimeClientNotReady = millis();
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Need to handle if millis is overflow (back to 0)
|
|
|
|
|
|
|
|
// Enable at command debug
|
2025-03-31 13:53:56 +07:00
|
|
|
agSerial->setDebug(true);
|
2025-04-09 15:51:54 +07:00
|
|
|
|
|
|
|
// If in 2 hours still not ready, then restart esp
|
|
|
|
if ((millis() - startTimeClientNotReady) > (2 * 60 * 60000)) {
|
|
|
|
Serial.println("CLIENT NOT READY TAKING TOO LONG! Rebooting ...");
|
|
|
|
int i = 3;
|
|
|
|
while (i != 0) {
|
|
|
|
if (ag->isOne()) {
|
|
|
|
String tmp = "Reboot in " + String(i);
|
|
|
|
oledDisplay.setText("CE error", "too long", tmp.c_str());
|
|
|
|
} else {
|
|
|
|
Serial.println("Rebooting... " + String(i));
|
|
|
|
}
|
|
|
|
i = i - 1;
|
|
|
|
delay(1000);
|
|
|
|
}
|
|
|
|
oledDisplay.setBrightness(0);
|
|
|
|
esp_restart();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Starting from 1 hour since its not ready, power cycling the module
|
|
|
|
bool resetModule = true;
|
|
|
|
if ((millis() - startTimeClientNotReady) > (60 * 60000)) {
|
|
|
|
Serial.println("Power cycling module");
|
|
|
|
cell->powerOff();
|
|
|
|
delay(2000);
|
|
|
|
cell->powerOn();
|
|
|
|
delay(10000);
|
|
|
|
resetModule = false; // No need to reset module anymore
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2025-03-16 22:47:21 +07:00
|
|
|
continue;
|
|
|
|
}
|
2025-04-09 15:51:54 +07:00
|
|
|
|
|
|
|
// Client is ready
|
|
|
|
startTimeClientNotReady = 0; // reset to default
|
|
|
|
agSerial->setDebug(false); // disable at command debug
|
2025-03-16 22:47:21 +07:00
|
|
|
}
|
2025-03-16 16:13:14 +07:00
|
|
|
}
|
2025-03-16 02:22:38 +07:00
|
|
|
|
2025-03-16 16:13:14 +07:00
|
|
|
// If connection to AirGradient server disable don't run config and transmission schedule
|
|
|
|
if (configuration.isCloudConnectionDisabled()) {
|
|
|
|
delay(1000);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run scheduler
|
2025-03-26 16:18:48 +07:00
|
|
|
networkSignalCheckSchedule.run();
|
2025-03-16 02:22:38 +07:00
|
|
|
configSchedule.run();
|
|
|
|
transmissionSchedule.run();
|
2025-03-17 15:12:11 +07:00
|
|
|
checkForUpdateSchedule.run();
|
2025-03-16 02:22:38 +07:00
|
|
|
|
|
|
|
delay(1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
vTaskDelete(handleNetworkTask);
|
2024-11-03 14:38:37 +07:00
|
|
|
}
|
2025-03-16 02:22:38 +07:00
|
|
|
|
|
|
|
void newMeasurementCycle() {
|
|
|
|
if (xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY) == pdTRUE) {
|
2025-03-21 04:40:27 +07:00
|
|
|
// Make sure queue not overflow
|
|
|
|
if (measurementCycleQueue.size() >= MAXIMUM_MEASUREMENT_CYCLE_QUEUE) {
|
|
|
|
// Remove the oldest data from queue if queue reach max
|
|
|
|
measurementCycleQueue.erase(measurementCycleQueue.begin());
|
|
|
|
}
|
|
|
|
|
2025-04-07 16:28:37 +07:00
|
|
|
// Get current measures
|
2025-03-28 13:45:07 +07:00
|
|
|
auto mc = measurements.getMeasures();
|
2025-04-07 16:28:37 +07:00
|
|
|
mc.signal = cell->csqToDbm(lastCellSignalQuality); // convert to RSSI
|
|
|
|
|
2025-03-16 02:22:38 +07:00
|
|
|
measurementCycleQueue.push_back(mc);
|
|
|
|
Serial.println("New measurement cycle added to queue");
|
|
|
|
// Release mutex
|
|
|
|
xSemaphoreGive(mutexMeasurementCycleQueue);
|
|
|
|
// Log current free heap size
|
|
|
|
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|