From 53285ab4ff6338f20021e7a02677ff4652078420 Mon Sep 17 00:00:00 2001 From: Phat Nguyen Date: Sat, 23 Mar 2024 16:58:17 +0700 Subject: [PATCH 1/8] Combine One and OpenAir into one: Just worked --- examples/OneOpenAir/OneOpenAir.ino | 2976 ++++++++++++++++++++++++++++ 1 file changed, 2976 insertions(+) create mode 100644 examples/OneOpenAir/OneOpenAir.ino diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino new file mode 100644 index 00000000..d5ae6681 --- /dev/null +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -0,0 +1,2976 @@ +/* +This is the code for the AirGradient ONE open-source hardware indoor Air Quality +Monitor with an ESP32-C3 Microcontroller. + +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/ + +Build Instructions: https://www.airgradient.com/documentation/one-v9/ + +The codes needs the following libraries installed: +“WifiManager by tzapu, tablatronix” tested with version 2.0.16-rc.2 +"Arduino_JSON" by Arduino version 0.2.0 +"U8g2" by oliver version 2.34.22 + +Please make sure you have esp32 board manager installed. Tested with +version 2.0.11. + +Important flashing settings: +- Set board to "ESP32C3 Dev Module" +- Enable "USB CDC On Boot" +- Flash frequency "80Mhz" +- Flash mode "QIO" +- Flash size "4MB" +- Partition scheme "Default 4MB with spiffs (1.2MB APP/1,5MB SPIFFS)" +- JTAG adapter "Disabled" + +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 + +*/ + +#include "mqtt_client.h" +#include +#include +#include + +#include "EEPROM.h" +#include +#include +#include +#include +#include + +/** + * @brief Application state machine state + * + */ +enum { + APP_SM_WIFI_MANAGER_MODE, /** In WiFi Manger Mode */ + APP_SM_WIFI_MANAGER_PORTAL_ACTIVE, /** WiFi Manager has connected to mobile + phone */ + APP_SM_WIFI_MANAGER_STA_CONNECTING, /** After SSID and PW entered and OK + clicked, connection to WiFI network is + attempted*/ + APP_SM_WIFI_MANAGER_STA_CONNECTED, /** Connecting to WiFi worked */ + APP_SM_WIFI_OK_SERVER_CONNECTING, /** Once connected to WiFi an attempt to + reach the server is performed */ + APP_SM_WIFI_OK_SERVER_CONNECTED, /** Server is reachable, all fine */ + /** Exceptions during WIFi Setup */ + APP_SM_WIFI_MANAGER_CONNECT_FAILED, /** Cannot connect to WiFi (e.g. wrong + password, WPA Enterprise etc.) */ + APP_SM_WIFI_OK_SERVER_CONNECT_FAILED, /** Connected to WiFi but server not + reachable, e.g. firewall block/ + whitelisting needed etc. */ + APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED, /** Server reachable but sensor + not configured correctly*/ + + /** During Normal Operation */ + APP_SM_WIFI_LOST, /** Connection to WiFi network failed credentials incorrect + encryption not supported etc. */ + APP_SM_SERVER_LOST, /** Connected to WiFi network but the server cannot be + reached through the internet, e.g. blocked by firewall + */ + APP_SM_SENSOR_CONFIG_FAILED, /** Server is reachable but there is some + configuration issue to be fixed on the server + side */ + APP_SM_NORMAL, +}; + +#define LED_FAST_BLINK_DELAY 250 /** ms */ +#define LED_SLOW_BLINK_DELAY 1000 /** ms */ +#define LED_SHORT_BLINK_DELAY 500 /** ms */ +#define LED_LONG_BLINK_DELAY 2000 /** ms */ +#define WIFI_CONNECT_COUNTDOWN_MAX 180 /** sec */ +#define WIFI_CONNECT_RETRY_MS 10000 /** ms */ +#define LED_BAR_COUNT_INIT_VALUE (-1) /** */ +#define LED_BAR_ANIMATION_PERIOD 100 /** ms */ +#define DISP_UPDATE_INTERVAL 2500 /** ms */ +#define SERVER_CONFIG_UPDATE_INTERVAL 15000 /** ms */ +#define SERVER_SYNC_INTERVAL 60000 /** ms */ +#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 2000 /** ms */ +#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */ +#define WIFI_HOTSPOT_PASSWORD_DEFAULT \ + "cleanair" /** default WiFi AP password \ + */ + +#define I2C_SDA_PIN 7 +#define I2C_SCL_PIN 6 +#define OLED_I2C_ADDR 0x3C + +/** + * @brief Use use LED bar state + */ +typedef enum { + UseLedBarOff, /** Don't use LED bar */ + UseLedBarPM, /** Use LED bar for PMS */ + UseLedBarCO2, /** Use LED bar for CO2 */ +} UseLedBar; + +enum { + FW_MODE_I_1PSL, /** ONE_INDOOR */ + FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */ + FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */ + FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */ + FW_MDOE_O_1PS /** PMS5003T, S8 */ +}; + +/** + * @brief Schedule handle with timing period + * + */ +class AgSchedule { +public: + AgSchedule(int period, void (*handler)(void)) + : period(period), handler(handler) {} + void run(void) { + uint32_t ms = (uint32_t)(millis() - count); + if (ms >= period) { + /** Call handler */ + handler(); + + // Serial.printf("[AgSchedule] handle 0x%08x, period: %d(ms)\r\n", + // (unsigned int)handler, period); + + /** Update period time */ + count = millis(); + } + } + void setPeriod(int period) { this->period = period; } + +private: + void (*handler)(void); + int period; + int count; +}; + +/** + * @brief AirGradient server configuration and sync data + * + */ +class AgServer { +public: + void begin(void) { + configFailed = false; + serverFailed = false; + loadConfig(); + } + + /** + * @brief Reset local config into default value. + * + */ + void defaultReset(void) { + config.inF = false; + config.inUSAQI = false; + memset(config.models, 0, sizeof(config.models)); + memset(config.mqttBrokers, 0, sizeof(config.mqttBrokers)); + config.useRGBLedBar = UseLedBarCO2; + saveConfig(); + } + + /** + * @brief Fetch server configuration, if get sucessed and configuratrion + * parameter has changed store into local storage + * + * @param id Device ID + * @return true Success + * @return false Failure + */ + bool fetchServerConfiguration(String id) { + String uri = + "http://hw.airgradient.com/sensors/airgradient:" + id + "/one/config"; + + /** Init http client */ + HTTPClient client; + if (client.begin(uri) == false) { + configFailed = true; + return false; + } + + /** Get */ + int retCode = client.GET(); + if (retCode != 200) { + client.end(); + configFailed = true; + return false; + } + + /** clear failed */ + configFailed = false; + + /** Get response string */ + String respContent = client.getString(); + client.end(); + Serial.println("Get server config: " + respContent); + + /** Parse JSON */ + JSONVar root = JSON.parse(respContent); + if (JSON.typeof(root) == "undefined") { + /** JSON invalid */ + return false; + } + + /** Get "country" */ + bool inF = false; + if (JSON.typeof_(root["country"]) == "string") { + String _country = root["country"]; + country = _country; + + if (country == "US") { + inF = true; + } else { + inF = false; + } + } + + /** Get "pmsStandard" */ + bool inUSAQI = false; + if (JSON.typeof_(root["pmStandard"]) == "string") { + String standard = root["pmStandard"]; + if (standard == "ugm3") { + inUSAQI = false; + } else { + inUSAQI = true; + } + } + + /** Get "co2CalibrationRequested" */ + if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") { + co2Calib = root["co2CalibrationRequested"]; + } else { + co2Calib = false; + } + + /** Get "ledBarMode" */ + uint8_t ledBarMode = UseLedBarOff; + if (JSON.typeof_(root["ledBarMode"]) == "string") { + String mode = root["ledBarMode"]; + ledBarMode = parseLedBarMode(mode); + } + + /** Get model */ + bool _saveConfig = false; + if (JSON.typeof_(root["model"]) == "string") { + String model = root["model"]; + if (model.length()) { + int len = model.length() < sizeof(config.models) + ? model.length() + : sizeof(config.models); + if (model != String(config.models)) { + memset(config.models, 0, sizeof(config.models)); + memcpy(config.models, model.c_str(), len); + _saveConfig = true; + } + } + } + + /** Get "mqttBrokerUrl" */ + if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") { + String mqtt = root["mqttBrokerUrl"]; + if (mqtt.length()) { + int len = mqtt.length() < sizeof(config.mqttBrokers) + ? mqtt.length() + : sizeof(config.mqttBrokers); + if (mqtt != String(config.mqttBrokers)) { + memset(config.mqttBrokers, 0, sizeof(config.mqttBrokers)); + memcpy(config.mqttBrokers, mqtt.c_str(), len); + _saveConfig = true; + } + } + } + + /** Get 'abcDays' */ + if (JSON.typeof_(root["abcDays"]) == "number") { + co2AbcCalib = root["abcDays"]; + } else { + co2AbcCalib = -1; + } + + /** Get "ledBarTestRequested" */ + if (JSON.typeof_(root["ledBarTestRequested"]) == "boolean") { + ledBarTestRequested = root["ledBarTestRequested"]; + } else { + ledBarTestRequested = false; + } + + /** Show configuration */ + showServerConfig(); + if (_saveConfig || (inF != config.inF) || (inUSAQI != config.inUSAQI) || + (ledBarMode != config.useRGBLedBar)) { + config.inF = inF; + config.inUSAQI = inUSAQI; + config.useRGBLedBar = ledBarMode; + + saveConfig(); + } + + return true; + } + + bool postToServer(String id, String payload) { + /** + * @brief Only post data if WiFi is connected + */ + if (WiFi.isConnected() == false) { + return false; + } + + Serial.printf("Post payload: %s\r\n", payload.c_str()); + + String uri = + "http://hw.airgradient.com/sensors/airgradient:" + id + "/measures"; + + WiFiClient wifiClient; + HTTPClient client; + if (client.begin(wifiClient, uri.c_str()) == false) { + return false; + } + client.addHeader("content-type", "application/json"); + int retCode = client.POST(payload); + client.end(); + + if ((retCode == 200) || (retCode == 429)) { + serverFailed = false; + return true; + } else { + Serial.printf("Post response failed code: %d\r\n", retCode); + } + serverFailed = true; + return false; + } + + /** + * @brief Get temperature configuration unit + * + * @return true F unit + * @return false C Unit + */ + bool isTemperatureUnitF(void) { return config.inF; } + + /** + * @brief Get PMS standard unit + * + * @return true USAQI + * @return false ugm3 + */ + bool isPMSinUSAQI(void) { return config.inUSAQI; } + + /** + * @brief Get status of get server configuration is failed + * + * @return true Failed + * @return false Success + */ + bool isConfigFailed(void) { return configFailed; } + + /** + * @brief Get status of post server configuration is failed + * + * @return true Failed + * @return false Success + */ + bool isServerFailed(void) { return serverFailed; } + + /** + * @brief Get request calibration CO2 + * + * @return true Requested. If result = true, it's clear after function call + * @return false Not-requested + */ + bool isCo2Calib(void) { + bool ret = co2Calib; + if (ret) { + co2Calib = false; + } + return ret; + } + + /** + * @brief Get request LedBar test + * + * @return true Requested. If result = true, it's clear after function call + * @return false Not-requested + */ + bool isLedBarTestRequested(void) { + bool ret = ledBarTestRequested; + if (ret) { + ledBarTestRequested = false; + } + return ret; + } + + /** + * @brief Get the Co2 auto calib period + * + * @return int days, -1 if invalid. + */ + int getCo2AbcDaysConfig(void) { return co2AbcCalib; } + + /** + * @brief Get device configuration model name + * + * @return String Model name, empty string if server failed + */ + String getModelName(void) { return String(config.models); } + + /** + * @brief Get mqttBroker url + * + * @return String Broker url, empty if server failed + */ + String getMqttBroker(void) { return String(config.mqttBrokers); } + + /** + * @brief Show server configuration parameter + */ + void showServerConfig(void) { + Serial.println("Server configuration: "); + Serial.printf("inF: %s\r\n", config.inF ? "true" : "false"); + Serial.printf("inUSAQI: %s\r\n", config.inUSAQI ? "true" : "false"); + Serial.printf("useRGBLedBar: %d\r\n", (int)config.useRGBLedBar); + Serial.printf("Model: %s\r\n", config.models); + Serial.printf("MQTT Broker: %s\r\n", config.mqttBrokers); + Serial.printf("S8 calibration period: %d\r\n", co2AbcCalib); + } + + /** + * @brief Get server config led bar mode + * + * @return UseLedBar + */ + UseLedBar getLedBarMode(void) { return (UseLedBar)config.useRGBLedBar; } + + /** + * @brief Return the name of the led bare mode. + * + * @return String + */ + String getLedBarModeName(void) { + UseLedBar ledBarMode = getLedBarMode(); + if (ledBarMode == UseLedBarOff) { + return String("off"); + } else if (ledBarMode == UseLedBarPM) { + return String("pm"); + } else if (ledBarMode == UseLedBarCO2) { + return String("co2"); + } else { + return String("off"); + } + } + + /** + * @brief Get the Country + * + * @return String + */ + String getCountry(void) { return country; } + +private: + bool configFailed; /** Flag indicate get server configuration failed */ + bool serverFailed; /** Flag indicate post data to server failed */ + bool co2Calib; /** Is co2Ppmcalibration requset */ + bool ledBarTestRequested; /** */ + int co2AbcCalib = -1; /** update auto calibration number of day */ + String country; /***/ + + struct config_s { + bool inF; + bool inUSAQI; + uint8_t useRGBLedBar; + char models[20]; + char mqttBrokers[256]; + uint32_t checksum; + }; + struct config_s config; + + void defaultConfig(void) { + config.inF = false; + config.inUSAQI = false; + memset(config.models, 0, sizeof(config.models)); + memset(config.mqttBrokers, 0, sizeof(config.mqttBrokers)); + + Serial.println("Load config default"); + saveConfig(); + } + + void loadConfig(void) { + if (EEPROM.readBytes(0, &config, sizeof(config)) != sizeof(config)) { + config.inF = false; + config.inUSAQI = false; + config.useRGBLedBar = UseLedBarCO2; // default use LED bar for CO2 + memset(config.models, 0, sizeof(config.models)); + memset(config.mqttBrokers, 0, sizeof(config.mqttBrokers)); + + Serial.println("Load configure failed"); + } else { + uint32_t sum = 0; + uint8_t *data = (uint8_t *)&config; + for (int i = 0; i < sizeof(config) - 4; i++) { + sum += data[i]; + } + if (sum != config.checksum) { + Serial.println("config checksum failed"); + defaultConfig(); + } + } + + showServerConfig(); + } + + void saveConfig(void) { + config.checksum = 0; + uint8_t *data = (uint8_t *)&config; + for (int i = 0; i < sizeof(config) - 4; i++) { + config.checksum += data[i]; + } + + EEPROM.writeBytes(0, &config, sizeof(config)); + EEPROM.commit(); + Serial.println("Save config"); + } + + UseLedBar parseLedBarMode(String mode) { + UseLedBar ledBarMode = UseLedBarOff; + if (mode == "co2") { + ledBarMode = UseLedBarCO2; + } else if (mode == "pm") { + ledBarMode = UseLedBarPM; + } else if (mode == "off") { + ledBarMode = UseLedBarOff; + } else { + ledBarMode = UseLedBarOff; + } + + return ledBarMode; + } +}; +AgServer agServer; + +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, + int32_t event_id, void *event_data); + +class AgMqtt { +private: + bool _isBegin = false; + String uri; + String hostname; + String user; + String pass; + int port; + esp_mqtt_client_handle_t client; + bool clientConnected = false; + int connectFailedCount = 0; + +public: + AgMqtt() {} + ~AgMqtt() {} + + /** + * @brief Initialize mqtt + * + * @param uri Complete mqtt uri, ex: + * mqtts://username:password@my.broker.com:4711 + * @return true Success + * @return false Failure + */ + bool begin(String uri) { + if (_isBegin) { + Serial.println("Mqtt already begin, call 'end' and try again"); + return true; + } + + if (uri.isEmpty()) { + Serial.println("Mqtt uri is empty"); + return false; + } + + this->uri = uri; + Serial.printf("mqtt init '%s'\r\n", uri.c_str()); + + /** config esp_mqtt client */ + esp_mqtt_client_config_t config = { + .uri = this->uri.c_str(), + }; + + /** init client */ + client = esp_mqtt_client_init(&config); + if (client == NULL) { + Serial.println("MQTT client init failed"); + return false; + } + + /** Register event */ + if (esp_mqtt_client_register_event(client, MQTT_EVENT_ANY, + mqtt_event_handler, NULL) != ESP_OK) { + Serial.println("MQTT client register event failed"); + return false; + } + + if (esp_mqtt_client_start(client) != ESP_OK) { + Serial.println("MQTT client start failed"); + return false; + } + + _isBegin = true; + return true; + } + + /** + * @brief Deinitialize mqtt + * + */ + void end(void) { + if (_isBegin == false) { + return; + } + + esp_mqtt_client_disconnect(client); + esp_mqtt_client_stop(client); + esp_mqtt_client_destroy(client); + _isBegin = false; + + Serial.println("mqtt de-init"); + } + + bool publish(const char *topic, const char *payload, int len) { + if ((_isBegin == false) || (clientConnected == false)) { + return false; + } + + if (esp_mqtt_client_publish(client, topic, payload, len, 0, 0) == ESP_OK) { + return true; + } + return false; + } + + /** + * @brief Get current complete mqtt uri + * + * @return String + */ + String getUri(void) { return uri; } + + void _connectionHandler(bool connected) { + clientConnected = connected; + if (clientConnected == false) { + connectFailedCount++; + } else { + connectFailedCount = 0; + } + } + + /** + * @brief Mqtt client connect status + * + * @return true Connected + * @return false Disconnected or Not initialize + */ + bool isConnected(void) { return (_isBegin && clientConnected); } + + /** + * @brief Get number of times connection failed + * + * @return int + */ + int connectionFailedCount(void) { return connectFailedCount; } +}; +AgMqtt agMqtt; +static TaskHandle_t mqttTask = NULL; + +/** Create airgradient instance for 'ONE_INDOOR' board */ +// AirGradient ag(ONE_INDOOR); +static AirGradient *ag; + +/** Create u8g2 instance */ +U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE); + +/** wifi manager instance */ +WiFiManager wifiManager; + +/** Web server instance */ +WebServer webServer; + +static bool wifiHasConfig = false; /** */ +static int connectCountDown; /** wifi configuration countdown */ +static int ledCount; /** For LED animation */ +static int ledSmState = APP_SM_NORMAL; /** Save display SM */ +static int dispSmState = APP_SM_NORMAL; /** Save LED SM */ + +static int tvocIndex = -1; +static int tvocRawIndex = -1; +static int noxIndex = -1; +static int noxRawIndex = -1; +static int co2Ppm = -1; +// static int pm25 = -1; +// static int pm01 = -1; +// static int pm10 = -1; +// static int pm03PCount = -1; +static float temp = -1001; +static int hum = -1; +static int bootCount; +static String wifiSSID = ""; + +static int pm25_1 = -1; +static int pm01_1 = -1; +static int pm10_1 = -1; +static int pm03PCount_1 = -1; +static float temp_1 = -1001; +static int hum_1 = -1; + +static int pm25_2 = -1; +static int pm01_2 = -1; +static int pm10_2 = -1; +static int pm03PCount_2 = -1; +static float temp_2 = -1001; +static int hum_2 = -1; + +static int pm1Value01; +static int pm1Value25; +static int pm1Value10; +static int pm1PCount; +static int pm1temp; +static int pm1hum; +static int pm2Value01; +static int pm2Value25; +static int pm2Value10; +static int pm2PCount; +static int pm2temp; +static int pm2hum; +static int countPosition; +const int targetCount = 20; + +static void boardInit(void); +static void failedHandler(String msg); +static void updateServerConfiguration(void); +static void co2Calibration(void); +static void setRGBledPMcolor(int pm25Value); +static void ledSmHandler(int sm); +static void ledSmHandler(void); +static void dispSmHandler(int sm); +static void sensorLedColorHandler(void); +static void appLedHandler(void); +static void appDispHandler(void); +static void updateWiFiConnect(void); +static void displayAndLedBarUpdate(void); +static void tvocUpdate(void); +static void pmUpdate(void); +static void sendDataToServer(void); +static void tempHumUpdate(void); +static void co2Update(void); +static void showNr(void); +static void webServerInit(void); +static String getServerSyncData(bool localServer); +static void createMqttTask(void); +static void factoryConfigReset(void); +static void wdgFeedUpdate(void); + +/** Init schedule */ +bool hasSensorS8 = true; +bool hasSensorPMS1 = true; +bool hasSensorPMS2 = true; +bool hasSensorSGP = true; +bool hasSensorSHT = true; +int pmFailCount = 0; +uint32_t factoryBtnPressTime = 0; +int getCO2FailCount = 0; +uint32_t addToDashboardTime; +bool isAddToDashboard = true; +bool offlineMode = false; +int fwMode = FW_MODE_I_1PSL; + +AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, displayAndLedBarUpdate); +AgSchedule configSchedule(SERVER_CONFIG_UPDATE_INTERVAL, + updateServerConfiguration); +AgSchedule serverSchedule(SERVER_SYNC_INTERVAL, sendDataToServer); +AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update); +AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, pmUpdate); +AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate); +AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, tvocUpdate); +AgSchedule wdgFeedSchedule(60000, wdgFeedUpdate); + +void setup() { + EEPROM.begin(512); + + /** Serial for print debug message */ + Serial.begin(115200); + delay(100); /** For bester show log */ + showNr(); + + /** Init I2C */ + Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); + delay(1000); + + /** Detect board type */ + Wire.beginTransmission(OLED_I2C_ADDR); + if (Wire.endTransmission() == 0x00) { + Serial.println("Detect ONE_INDOOR"); + ag = new AirGradient(BoardType::ONE_INDOOR); + } else { + Serial.println("Detect OPEN_AIR"); + ag = new AirGradient(BoardType::OPEN_AIR_OUTDOOR); + } + + /** Init AirGradient server */ + agServer.begin(); + + /** Init sensor */ + boardInit(); + + /** Connecting wifi */ + connectToWifi(); + + /** + * Send first data to ping server and get server configuration + */ + if (WiFi.isConnected()) { + webServerInit(); + + /** MQTT init */ + if (agServer.getMqttBroker().isEmpty() == false) { + if (agMqtt.begin(agServer.getMqttBroker())) { + createMqttTask(); + Serial.println("MQTT client init success"); + } else { + Serial.println("MQTT client init failure"); + } + } + + sendPing(); + Serial.println(F("WiFi connected!")); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); + + /** Get first connected to wifi */ + agServer.fetchServerConfiguration(getDevId()); + if (agServer.isConfigFailed()) { + if (isOneIndoor()) { + dispSmHandler(APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED); + } + ledSmHandler(APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED); + delay(DISPLAY_DELAY_SHOW_CONTENT_MS); + } else { + ag->ledBar.setEnable(agServer.getLedBarMode() != UseLedBarOff); + } + } else { + offlineMode = true; + } + + /** Show display Warning up */ + if (isOneIndoor()) { + displayShowText("Warming Up", "Serial Number:", String(getNormalizedMac())); + delay(DISPLAY_DELAY_SHOW_CONTENT_MS); + } + + appLedHandler(); + if (isOneIndoor()) { + appDispHandler(); + } +} + +void loop() { + /** Handle schedule */ + dispLedSchedule.run(); + configSchedule.run(); + serverSchedule.run(); + + if (hasSensorS8) { + co2Schedule.run(); + } + + if (hasSensorPMS1 || hasSensorPMS2) { + pmsSchedule.run(); + } + + if (isOneIndoor()) { + if (hasSensorSHT) { + delay(100); + tempHumSchedule.run(); + } + } + + if (hasSensorSGP) { + tvocSchedule.run(); + } + + if (offlineMode) { + wdgFeedSchedule.run(); + } + + /** Check for handle WiFi reconnect */ + updateWiFiConnect(); + + /** factory reset handle */ + factoryConfigReset(); + + /** Read PMS on loop */ + if (isOneIndoor()) { + if (hasSensorPMS1) { + ag->pms5003.handle(); + } + } else { + if (hasSensorPMS1) { + ag->pms5003t_1.handle(); + } + if (hasSensorPMS2) { + ag->pms5003t_2.handle(); + } + } +} + +static void setTestColor(char color) { + int r = 0; + int g = 0; + int b = 0; + switch (color) { + case 'g': + g = 255; + break; + case 'y': + r = 255; + g = 255; + break; + case 'o': + r = 255; + g = 128; + break; + case 'r': + r = 255; + break; + case 'b': + b = 255; + break; + case 'w': + r = 255; + g = 255; + b = 255; + break; + case 'p': + r = 153; + b = 153; + break; + case 'z': + r = 102; + break; + case 'n': + default: + break; + } + ag->ledBar.setColor(r, g, b); +} + +static void ledTest() { + displayShowText("LED Test", "running", "....."); + setTestColor('r'); + ag->ledBar.show(); + delay(1000); + setTestColor('g'); + ag->ledBar.show(); + delay(1000); + setTestColor('b'); + ag->ledBar.show(); + delay(1000); + setTestColor('w'); + ag->ledBar.show(); + delay(1000); + setTestColor('n'); + ag->ledBar.show(); + delay(1000); +} +static void ledTest2Min(void) { + uint32_t tstart = millis(); + + Serial.println("Start run LED test for 2 min"); + while (1) { + ledTest(); + uint32_t ms = (uint32_t)(millis() - tstart); + if (ms >= (60 * 1000 * 2)) { + Serial.println("LED test after 2 min finish"); + break; + } + } +} + +static void co2Update(void) { + int value = ag->s8.getCo2(); + if (value >= 0) { + co2Ppm = value; + getCO2FailCount = 0; + Serial.printf("CO2 (ppm): %d\r\n", co2Ppm); + } else { + getCO2FailCount++; + Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount); + if (getCO2FailCount >= 3) { + co2Ppm = 1; + } + } +} + +static void showNr(void) { Serial.println("Serial nr: " + getDevId()); } + +void webServerMeasureCurrentGet(void) { + webServer.send(200, "application/json", getServerSyncData(true)); +} + +/** + * Sends metrics in Prometheus/OpenMetrics format to the currently connected + * webServer client. + * + * For background, see: + * https://prometheus.io/docs/instrumenting/exposition_formats/ + */ +void webServerMetricsGet(void) { + String response; + String current_metric_name; + const auto add_metric = [&](const String &name, const String &help, + const String &type, const String &unit = "") { + current_metric_name = "airgradient_" + name; + if (!unit.isEmpty()) + current_metric_name += "_" + unit; + response += "# HELP " + current_metric_name + " " + help + "\n"; + response += "# TYPE " + current_metric_name + " " + type + "\n"; + if (!unit.isEmpty()) + response += "# UNIT " + current_metric_name + " " + unit + "\n"; + }; + const auto add_metric_point = [&](const String &labels, const String &value) { + response += current_metric_name + "{" + labels + "} " + value + "\n"; + }; + + add_metric("info", "AirGradient device information", "info"); + add_metric_point("airgradient_serial_number=\"" + getDevId() + + "\",airgradient_device_type=\"" + ag->getBoardName() + + "\",airgradient_library_version=\"" + ag->getVersion() + + "\"", + "1"); + + add_metric("config_ok", + "1 if the AirGradient device was able to successfully fetch its " + "configuration from the server", + "gauge"); + add_metric_point("", agServer.isConfigFailed() ? "0" : "1"); + + add_metric( + "post_ok", + "1 if the AirGradient device was able to successfully send to the server", + "gauge"); + add_metric_point("", agServer.isServerFailed() ? "0" : "1"); + + add_metric( + "wifi_rssi", + "WiFi signal strength from the AirGradient device perspective, in dBm", + "gauge", "dbm"); + add_metric_point("", String(WiFi.RSSI())); + + if (hasSensorS8 && co2Ppm >= 0) { + add_metric("co2", + "Carbon dioxide concentration as measured by the AirGradient S8 " + "sensor, in parts per million", + "gauge", "ppm"); + add_metric_point("", String(co2Ppm)); + } + + float _temp = -1001; + float _hum = -1; + int pm01 = -1; + int pm25 = -1; + int pm10 = -1; + int pm03PCount = -1; + if (hasSensorPMS1 && hasSensorPMS2) { + _temp = (temp_1 + temp_2) / 2.0f; + _hum = (hum_1 + hum_2) / 2.0f; + pm01 = (pm01_1 + pm01_2) / 2; + pm25 = (pm25_1 + pm25_2) / 2; + pm10 = (pm10_1 + pm10_2) / 2; + pm03PCount = (pm03PCount_1 + pm03PCount_2) / 2; + } else { + if (isOneIndoor()) { + if (hasSensorSHT) { + _temp = temp; + _hum = hum; + } + } else { + if (hasSensorPMS1) { + _temp = temp_1; + _hum = hum_1; + pm01 = pm01_1; + pm25 = pm25_1; + pm10 = pm10_1; + pm03PCount = pm03PCount_1; + } + if (hasSensorPMS2) { + _temp = temp_2; + _hum = hum_2; + pm01 = pm01_2; + pm25 = pm25_2; + pm10 = pm10_2; + pm03PCount = pm03PCount_2; + } + } + } + + if (hasSensorPMS1 || hasSensorPMS2) { + if (pm01 >= 0) { + add_metric("pm1", + "PM1.0 concentration as measured by the AirGradient PMS " + "sensor, in micrograms per cubic meter", + "gauge", "ugm3"); + add_metric_point("", String(pm01)); + } + if (pm25 >= 0) { + add_metric("pm2d5", + "PM2.5 concentration as measured by the AirGradient PMS " + "sensor, in micrograms per cubic meter", + "gauge", "ugm3"); + add_metric_point("", String(pm25)); + } + if (pm10 >= 0) { + add_metric("pm10", + "PM10 concentration as measured by the AirGradient PMS " + "sensor, in micrograms per cubic meter", + "gauge", "ugm3"); + add_metric_point("", String(pm10)); + } + if (pm03PCount >= 0) { + add_metric("pm0d3", + "PM0.3 concentration as measured by the AirGradient PMS " + "sensor, in number of particules per 100 milliliters", + "gauge", "p100ml"); + add_metric_point("", String(pm03PCount)); + } + } + + if (hasSensorSGP) { + if (tvocIndex >= 0) { + add_metric("tvoc_index", + "The processed Total Volatile Organic Compounds (TVOC) index " + "as measured by the AirGradient SGP sensor", + "gauge"); + add_metric_point("", String(tvocIndex)); + } + if (tvocRawIndex >= 0) { + add_metric("tvoc_raw", + "The raw input value to the Total Volatile Organic Compounds " + "(TVOC) index as measured by the AirGradient SGP sensor", + "gauge"); + add_metric_point("", String(tvocRawIndex)); + } + if (noxIndex >= 0) { + add_metric("nox_index", + "The processed Nitrous Oxide (NOx) index as measured by the " + "AirGradient SGP sensor", + "gauge"); + add_metric_point("", String(noxIndex)); + } + if (noxRawIndex >= 0) { + add_metric("nox_raw", + "The raw input value to the Nitrous Oxide (NOx) index as " + "measured by the AirGradient SGP sensor", + "gauge"); + add_metric_point("", String(noxRawIndex)); + } + } + + { + if (_temp > -1001) { + add_metric("temperature", + "The ambient temperature as measured by the AirGradient SHT " + "sensor, in degrees Celsius", + "gauge", "celsius"); + add_metric_point("", String(_temp)); + } + if (_hum >= 0) { + add_metric( + "humidity", + "The relative humidity as measured by the AirGradient SHT sensor", + "gauge", "percent"); + add_metric_point("", String(_hum)); + } + } + + response += "# EOF\n"; + webServer.send(200, + "application/openmetrics-text; version=1.0.0; charset=utf-8", + response); +} + +void webServerHandler(void *param) { + for (;;) { + webServer.handleClient(); + } +} + +static void webServerInit(void) { + String host = "airgradient_" + getDevId(); + if (!MDNS.begin(host)) { + Serial.println("Init mDNS failed"); + return; + } + + webServer.on("/measures/current", HTTP_GET, webServerMeasureCurrentGet); + // Make it possible to query this device from Prometheus/OpenMetrics. + webServer.on("/metrics", HTTP_GET, webServerMetricsGet); + webServer.begin(); + MDNS.addService("_airgradient", "_tcp", 80); + MDNS.addServiceTxt("_airgradient", "_tcp", "model", getFirmwareModeName()); + MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", getDevId()); + MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag->getVersion()); + MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient"); + + if (xTaskCreate(webServerHandler, "webserver", 1024 * 4, NULL, 5, NULL) != + pdTRUE) { + Serial.println("Create task handle webserver failed"); + } + Serial.printf("Webserver init: %s.local\r\n", host.c_str()); +} + +static String getServerSyncData(bool localServer) { + JSONVar root; + root["wifi"] = WiFi.RSSI(); + if (localServer) { + root["serialno"] = getDevId(); + } + if (hasSensorS8) { + if (co2Ppm >= 0) { + root["rco2"] = co2Ppm; + } + } + + if (isOneIndoor()) { + if (hasSensorPMS1) { + if (pm01_1 >= 0) { + root["pm01"] = pm01_1; + } + if (pm25_1 >= 0) { + root["pm02"] = pm25_1; + } + if (pm10_1 >= 0) { + root["pm10"] = pm10_1; + } + if (pm03PCount_1 >= 0) { + if (localServer) { + root["pm003Count"] = pm03PCount_1; + } else { + root["pm003_count"] = pm03PCount_1; + } + } + } + + if (hasSensorSHT) { + if (temp > -1001) { + root["atmp"] = ag->round2(temp); + } + if (hum >= 0) { + root["rhum"] = hum; + } + } + + } else { + if (hasSensorPMS1 && hasSensorPMS2) { + root["pm01"] = ag->round2((pm01_1 + pm01_2) / 2.0); + root["pm02"] = ag->round2((pm25_1 + pm25_2) / 2.0); + root["pm10"] = ag->round2((pm10_1 + pm10_2) / 2.0); + if (localServer) { + root["pm003Count"] = ag->round2((pm03PCount_1 + pm03PCount_2) / 2.0); + } else { + root["pm003_count"] = ag->round2((pm03PCount_1 + pm03PCount_2) / 2.0); + } + root["atmp"] = ag->round2((temp_1 + temp_2) / 2.0); + root["rhum"] = ag->round2((hum_1 + hum_2) / 2.0); + } + + if (fwMode == FW_MDOE_O_1PS || fwMode == FW_MODE_O_1PST) { + if (hasSensorPMS1) { + root["pm01"] = pm01_1; + root["pm02"] = pm25_1; + root["pm10"] = pm10_1; + if (localServer) { + root["pm003Count"] = pm03PCount_1; + } else { + root["pm003_count"] = pm03PCount_1; + } + root["atmp"] = ag->round2(temp_1); + root["rhum"] = hum_1; + } + if (hasSensorPMS2) { + root["pm01"] = pm01_2; + root["pm02"] = pm25_2; + root["pm10"] = pm10_2; + if (localServer) { + root["pm003Count"] = pm03PCount_2; + } else { + root["pm003_count"] = pm03PCount_2; + } + root["atmp"] = ag->round2(temp_2); + root["rhum"] = hum_2; + } + } else { + if (hasSensorPMS1) { + root["channels"]["1"]["pm01"] = pm01_1; + root["channels"]["1"]["pm02"] = pm25_1; + root["channels"]["1"]["pm10"] = pm10_1; + if (localServer) { + root["channels"]["1"]["pm003Count"] = pm03PCount_1; + } else { + root["channels"]["1"]["pm003_count"] = pm03PCount_1; + } + root["channels"]["1"]["atmp"] = ag->round2(temp_1); + root["channels"]["1"]["rhum"] = hum_1; + } + if (hasSensorPMS2) { + root["channels"]["2"]["pm01"] = pm01_2; + root["channels"]["2"]["pm02"] = pm25_2; + root["channels"]["2"]["pm10"] = pm10_2; + if (localServer) { + root["channels"]["2"]["pm003Count"] = pm03PCount_2; + } else { + root["channels"]["2"]["pm003_count"] = pm03PCount_2; + } + root["channels"]["2"]["atmp"] = ag->round2(temp_2); + root["channels"]["2"]["rhum"] = hum_2; + } + } + } + + if (hasSensorSGP) { + if (tvocIndex >= 0) { + if (localServer) { + root["tvocIndex"] = tvocIndex; + } else { + root["tvoc_index"] = tvocIndex; + } + } + if (tvocRawIndex >= 0) { + root["tvoc_raw"] = tvocRawIndex; + } + if (noxIndex >= 0) { + if (localServer) { + root["noxIndex"] = noxIndex; + } else { + root["nox_index"] = noxIndex; + } + } + if (noxRawIndex >= 0) { + root["nox_raw"] = noxRawIndex; + } + } + root["boot"] = bootCount; + + if (localServer) { + root["ledMode"] = agServer.getLedBarModeName(); + root["firmwareVersion"] = ag->getVersion(); + root["fwMode"] = getFirmwareModeName(); + } + + return JSON.stringify(root); +} + +static void createMqttTask(void) { + if (mqttTask) { + vTaskDelete(mqttTask); + mqttTask = NULL; + } + + xTaskCreate( + [](void *param) { + for (;;) { + delay(MQTT_SYNC_INTERVAL); + + /** Send data */ + if (agMqtt.isConnected()) { + String syncData = getServerSyncData(false); + String topic = "airgradient/readings/" + getDevId(); + if (agMqtt.publish(topic.c_str(), syncData.c_str(), + syncData.length())) { + Serial.println("MQTT sync success"); + } else { + Serial.println("MQTT sync failure"); + } + } + } + }, + "mqtt-task", 1024 * 3, NULL, 6, &mqttTask); + + if (mqttTask == NULL) { + Serial.println("Creat mqttTask failed"); + } +} + +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 + // Count display. + if (isOneIndoor()) { + displayShowText("Factory reset", "keep pressed", "for 8 sec"); + } else { + Serial.println("Factory reset, keep pressed for 8 sec"); + } + + int count = 7; + while (ag->button.getState() == ag->button.BUTTON_PRESSED) { + delay(1000); + if (isOneIndoor()) { + + displayShowText("Factory reset", "keep pressed", + "for " + String(count) + " sec"); + } else { + Serial.printf("Factory reset, keep pressed for %d sec\r\n", count); + } + count--; + // ms = (uint32_t)(millis() - factoryBtnPressTime); + if (count == 0) { + /** Stop MQTT task first */ + if (mqttTask) { + vTaskDelete(mqttTask); + mqttTask = NULL; + } + + /** Disconnect WIFI */ + WiFi.disconnect(); + wifiManager.resetSettings(); + + /** Reset local config */ + agServer.defaultReset(); + + if (isOneIndoor()) { + displayShowText("Factory reset", "successful", ""); + } else { + Serial.println("Factory reset successful"); + } + delay(3000); + ESP.restart(); + } + } + + /** Show current content cause reset ignore */ + factoryBtnPressTime = 0; + if (isOneIndoor()) { + appDispHandler(); + } + } + } + } else { + if (factoryBtnPressTime != 0) { + if (isOneIndoor()) { + /** Restore last display content */ + appDispHandler(); + } + } + factoryBtnPressTime = 0; + } +} + +static void wdgFeedUpdate(void) { + ag->watchdog.reset(); + Serial.println(); + Serial.println("External watchdog feed"); + Serial.println(); +} + +static void sendPing() { + JSONVar root; + root["wifi"] = WiFi.RSSI(); + root["boot"] = bootCount; + + /** Change disp and led state */ + if (isOneIndoor()) { + dispSmHandler(APP_SM_WIFI_OK_SERVER_CONNECTING); + } + ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNECTING); + + /** Task handle led connecting animation */ + xTaskCreate( + [](void *obj) { + for (;;) { + ledSmHandler(); + if (ledSmState != APP_SM_WIFI_OK_SERVER_CONNECTING) { + break; + } + delay(LED_BAR_ANIMATION_PERIOD); + } + vTaskDelete(NULL); + }, + "task_led", 2048, NULL, 5, NULL); + + delay(1500); + if (agServer.postToServer(getDevId(), JSON.stringify(root))) { + if (isOneIndoor()) { + dispSmHandler(APP_SM_WIFI_OK_SERVER_CONNECTED); + } + ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNECTED); + } else { + if (isOneIndoor()) { + dispSmHandler(APP_SM_WIFI_OK_SERVER_CONNECT_FAILED); + } + ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNECT_FAILED); + } + delay(DISPLAY_DELAY_SHOW_CONTENT_MS); + + ledSmHandler(APP_SM_NORMAL); +} + +static void displayShowWifiText(String ln1, String ln2, String ln3, + String ln4) { + u8g2.firstPage(); + do { + u8g2.setFont(u8g2_font_t0_16_tf); + u8g2.drawStr(1, 10, ln1.c_str()); + u8g2.drawStr(1, 25, ln2.c_str()); + u8g2.drawStr(1, 40, ln3.c_str()); + u8g2.drawStr(1, 55, ln4.c_str()); + } while (u8g2.nextPage()); +} + +static void displayShowText(String ln1, String ln2, String ln3) { + u8g2.firstPage(); + do { + u8g2.setFont(u8g2_font_t0_16_tf); + u8g2.drawStr(1, 10, String(ln1).c_str()); + u8g2.drawStr(1, 30, String(ln2).c_str()); + u8g2.drawStr(1, 50, String(ln3).c_str()); + } while (u8g2.nextPage()); +} + +static void displayShowDashboard(String err) { + char strBuf[10]; + + /** Clear display dashboard */ + u8g2.firstPage(); + + /** Show content to dashboard */ + do { + u8g2.setFont(u8g2_font_t0_16_tf); + if ((err == NULL) || err.isEmpty()) { + + /** Show temperature */ + if (agServer.isTemperatureUnitF()) { + if (temp > -1001) { + float tempF = (temp * 9 / 5) + 32; + sprintf(strBuf, "%.1f°F", tempF); + } else { + sprintf(strBuf, "-°F"); + } + u8g2.drawUTF8(1, 10, strBuf); + } else { + if (temp > -1001) { + sprintf(strBuf, "%.1f°C", temp); + } else { + sprintf(strBuf, "-°C"); + } + u8g2.drawUTF8(1, 10, strBuf); + } + + /** Show humidity */ + if (hum >= 0) { + sprintf(strBuf, "%d%%", hum); + } else { + sprintf(strBuf, " -%%"); + } + if (hum > 99) { + u8g2.drawStr(97, 10, strBuf); + } else { + u8g2.drawStr(105, 10, strBuf); + } + } else { + Serial.println("Display show error: " + err); + int strWidth = u8g2.getStrWidth(err.c_str()); + u8g2.drawStr((126 - strWidth) / 2, 10, err.c_str()); + + if (err == "WiFi N/A") { + u8g2.setFont(u8g2_font_t0_12_tf); + if (agServer.isTemperatureUnitF()) { + if (temp > -1001) { + float tempF = (temp * 9 / 5) + 32; + sprintf(strBuf, "%.1f", tempF); + } else { + sprintf(strBuf, "-"); + } + u8g2.drawUTF8(1, 10, strBuf); + } else { + if (temp > -1001) { + sprintf(strBuf, "%.1f", temp); + } else { + sprintf(strBuf, "-"); + } + u8g2.drawUTF8(1, 10, strBuf); + } + + /** Show humidity */ + if (hum >= 0) { + sprintf(strBuf, "%d%%", hum); + } else { + sprintf(strBuf, " -%%"); + } + if (hum > 99) { + u8g2.drawStr(97, 10, strBuf); + } else { + u8g2.drawStr(105, 10, strBuf); + } + } + } + + /** Draw horizontal line */ + u8g2.drawLine(1, 13, 128, 13); + + /** Show CO2 label */ + u8g2.setFont(u8g2_font_t0_12_tf); + u8g2.drawUTF8(1, 27, "CO2"); + + /** Show CO2 value */ + u8g2.setFont(u8g2_font_t0_22b_tf); + if (co2Ppm > 0) { + sprintf(strBuf, "%d", co2Ppm); + } else { + sprintf(strBuf, "%s", "-"); + } + u8g2.drawStr(1, 48, strBuf); + + /** Show CO2 value index */ + u8g2.setFont(u8g2_font_t0_12_tf); + u8g2.drawStr(1, 61, "ppm"); + + /** Draw vertical line */ + u8g2.drawLine(45, 14, 45, 64); + u8g2.drawLine(82, 14, 82, 64); + + /** Draw PM2.5 label */ + u8g2.setFont(u8g2_font_t0_12_tf); + u8g2.drawStr(48, 27, "PM2.5"); + + /** Draw PM2.5 value */ + u8g2.setFont(u8g2_font_t0_22b_tf); + if (agServer.isPMSinUSAQI()) { + if (pm25_1 >= 0) { + sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25_1)); + } else { + sprintf(strBuf, "%s", "-"); + } + u8g2.drawStr(48, 48, strBuf); + u8g2.setFont(u8g2_font_t0_12_tf); + u8g2.drawUTF8(48, 61, "AQI"); + } else { + if (pm25_1 >= 0) { + sprintf(strBuf, "%d", pm25_1); + } else { + sprintf(strBuf, "%s", "-"); + } + u8g2.drawStr(48, 48, strBuf); + u8g2.setFont(u8g2_font_t0_12_tf); + u8g2.drawUTF8(48, 61, "ug/m³"); + } + + /** Draw tvocIndexlabel */ + u8g2.setFont(u8g2_font_t0_12_tf); + u8g2.drawStr(85, 27, "tvoc:"); + + /** Draw tvocIndexvalue */ + if (tvocIndex >= 0) { + sprintf(strBuf, "%d", tvocIndex); + } else { + sprintf(strBuf, "%s", "-"); + } + u8g2.drawStr(85, 39, strBuf); + + /** Draw NOx label */ + u8g2.drawStr(85, 53, "NOx:"); + if (noxIndex >= 0) { + sprintf(strBuf, "%d", noxIndex); + } else { + sprintf(strBuf, "%s", "-"); + } + u8g2.drawStr(85, 63, strBuf); + } while (u8g2.nextPage()); +} + +/** + * @brief Must reset each 5min to avoid ESP32 reset + */ +static void resetWatchdog() { ag->watchdog.reset(); } + +static bool wifiMangerClientConnected(void) { + return WiFi.softAPgetStationNum() ? true : false; +} + +// Wifi Manager +static void connectToWifi() { + /** get wifi AP ssid */ + wifiSSID = "airgradient-" + String(getNormalizedMac()); + wifiManager.setConfigPortalBlocking(false); + wifiManager.setTimeout(WIFI_CONNECT_COUNTDOWN_MAX); + + wifiManager.setAPCallback([](WiFiManager *obj) { + /** This callback if wifi connnected failed and try to start configuration + * portal */ + connectCountDown = WIFI_CONNECT_COUNTDOWN_MAX; + ledCount = LED_BAR_COUNT_INIT_VALUE; + if (isOneIndoor()) { + dispSmState = APP_SM_WIFI_MANAGER_MODE; + } + ledSmState = APP_SM_WIFI_MANAGER_MODE; + }); + wifiManager.setSaveConfigCallback([]() { + /** Wifi connected save the configuration */ + dispSmState = APP_SM_WIFI_MANAGER_STA_CONNECTED; + ledSmHandler(APP_SM_WIFI_MANAGER_STA_CONNECTED); + }); + wifiManager.setSaveParamsCallback([]() { + /** Wifi set connect: ssid, password */ + ledCount = LED_BAR_COUNT_INIT_VALUE; + if (isOneIndoor()) { + dispSmState = APP_SM_WIFI_MANAGER_STA_CONNECTING; + } + ledSmState = APP_SM_WIFI_MANAGER_STA_CONNECTING; + }); + + if (isOneIndoor()) { + displayShowText("Connecting to", "WiFi", "..."); + } + wifiManager.autoConnect(wifiSSID.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT); + xTaskCreate( + [](void *obj) { + while (wifiManager.getConfigPortalActive()) { + wifiManager.process(); + } + vTaskDelete(NULL); + }, + "wifi_cfg", 4096, NULL, 10, NULL); + + /** Wait for WiFi connect and show LED, display status */ + uint32_t dispPeriod = millis(); + uint32_t ledPeriod = millis(); + bool clientConnectChanged = false; + + int dispSmStateOld = dispSmState; + while (wifiManager.getConfigPortalActive()) { + /** LED and display animation */ + if (WiFi.isConnected() == false) { + /** Display countdown */ + uint32_t ms = (uint32_t)(millis() - dispPeriod); + if (isOneIndoor()) { + if (ms >= 1000) { + dispPeriod = millis(); + dispSmHandler(dispSmState); + } else { + if (dispSmStateOld != dispSmState) { + dispSmStateOld = dispSmState; + dispSmHandler(dispSmState); + } + } + } + + /** LED animations */ + ms = (uint32_t)(millis() - ledPeriod); + if (ms >= LED_BAR_ANIMATION_PERIOD) { + ledPeriod = millis(); + ledSmHandler(); + } + + /** Check for client connect to change led color */ + bool clientConnected = wifiMangerClientConnected(); + if (clientConnected != clientConnectChanged) { + clientConnectChanged = clientConnected; + if (clientConnectChanged) { + ledSmHandler(APP_SM_WIFI_MANAGER_PORTAL_ACTIVE); + } else { + ledCount = LED_BAR_COUNT_INIT_VALUE; + ledSmHandler(APP_SM_WIFI_MANAGER_MODE); + if (isOneIndoor()) { + dispSmHandler(APP_SM_WIFI_MANAGER_MODE); + } + } + } + } + } + + /** Show display wifi connect result failed */ + if (WiFi.isConnected() == false) { + ledSmHandler(APP_SM_WIFI_MANAGER_CONNECT_FAILED); + if (isOneIndoor()) { + dispSmHandler(APP_SM_WIFI_MANAGER_CONNECT_FAILED); + } + delay(6000); + } else { + wifiHasConfig = true; + } + + /** Update LED bar color */ + appLedHandler(); +} + +static String getDevId(void) { return getNormalizedMac(); } + +static String getNormalizedMac() { + String mac = WiFi.macAddress(); + mac.replace(":", ""); + mac.toLowerCase(); + return mac; +} + +static void setRGBledCO2color(int co2Value) { + if (co2Value <= 400) { + /** G; 1 */ + ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + } else if (co2Value <= 700) { + /** GG; 2 */ + ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 2); + } else if (co2Value <= 1000) { + /** YYY; 3 */ + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3); + } else if (co2Value <= 1333) { + /** YYYY; 4 */ + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 4); + } else if (co2Value <= 1666) { + /** YYYYY; 5 */ + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 5); + } else if (co2Value <= 2000) { + /** RRRRRR; 6 */ + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + } else if (co2Value <= 2666) { + /** RRRRRRR; 7 */ + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7); + } else if (co2Value <= 3333) { + /** RRRRRRRR; 8 */ + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8); + } else if (co2Value <= 4000) { + /** RRRRRRRRR; 9 */ + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 9); + } else { /** > 4000 */ + /* PRPRPRPRP; 9 */ + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 7); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8); + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 9); + } +} + +static void setRGBledColor(char color) { + int r = 0; + int g = 0; + int b = 0; + switch (color) { + case 'g': + g = 255; + break; + case 'y': + r = 255; + g = 255; + break; + case 'o': + r = 255; + g = 128; + break; + case 'r': + r = 255; + break; + case 'b': + b = 255; + break; + case 'w': + r = 255; + g = 255; + b = 255; + break; + case 'p': + r = 153; + b = 153; + break; + case 'z': + r = 102; + break; + case 'n': + default: + break; + } + + /** Sensor LED indicator has only show status on last 2 LED on LED Bar */ + int ledNum = ag->ledBar.getNumberOfLeds() - 1; + ag->ledBar.setColor(r, g, b, ledNum); + + ledNum = ag->ledBar.getNumberOfLeds() - 2; + ag->ledBar.setColor(r, g, b, ledNum); +} + +void dispSensorNotFound(String ss) { + displayShowText("Sensor init", "Error:", ss + " not found"); + delay(2000); +} + +static void oneIndoorInit(void) { + /** Display init */ + u8g2.begin(); + + /** Show boot display */ + Serial.println("Firmware Version: " + ag->getVersion()); + displayShowText("AirGradient ONE", "FW Version: ", ag->getVersion()); + delay(DISPLAY_DELAY_SHOW_CONTENT_MS); + + ag->ledBar.begin(); + ag->button.begin(); + ag->watchdog.begin(); + + /** Init sensor SGP41 */ + if (ag->sgp41.begin(Wire) == false) { + Serial.println("SGP41 sensor not found"); + hasSensorSGP = false; + dispSensorNotFound("SGP41"); + } + + /** INit SHT */ + if (ag->sht.begin(Wire) == false) { + Serial.println("SHTx sensor not found"); + hasSensorSHT = false; + dispSensorNotFound("SHT"); + } + + /** Init S8 CO2 sensor */ + if (ag->s8.begin(Serial1) == false) { + // failedHandler("Init SenseAirS8 failed"); + Serial.println("CO2 S8 sensor not found"); + hasSensorS8 = false; + dispSensorNotFound("S8"); + } + + /** Init PMS5003 */ + if (ag->pms5003.begin(Serial0) == false) { + Serial.println("PMS sensor not found"); + hasSensorPMS1 = false; + + dispSensorNotFound("PMS"); + } + + /** Run LED test on start up */ + displayShowText("Press now for", "LED test &", "offline mode"); + bool test = false; + uint32_t stime = millis(); + while (true) { + if (ag->button.getState() == ag->button.BUTTON_PRESSED) { + test = true; + break; + } + delay(1); + uint32_t ms = (uint32_t)(millis() - stime); + if (ms >= 3000) { + break; + } + } + if (test) { + ledTest(); + } + + /** Check LED mode to disabled LED */ + if (agServer.getLedBarMode() == UseLedBarOff) { + ag->ledBar.setEnable(false); + } +} +static void openAirInit(void) { + 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) { + hasSensorS8 = false; + + Serial.println("CO2 S8 sensor not found"); + Serial.println("Can not detect S8 run mode 'PPT'"); + fwMode = FW_MODE_O_1PPT; + + Serial0.end(); + delay(200); + } else { + Serial.println("Found S8 on Serial0"); + serial0Available = false; + } + } else { + Serial.println("Found S8 on Serial1"); + serial1Available = false; + } + + if (ag->sgp41.begin(Wire) == false) { + hasSensorSGP = false; + Serial.println("SGP sensor not found"); + + if (hasSensorS8 == false) { + fwMode = FW_MODE_O_1PP; + Serial.println("Can not detect SGP run mode 'O-1PP'"); + } else { + Serial.println("Can not detect SGP run mode 'O-1PS'"); + fwMode = FW_MDOE_O_1PS; + } + } + + /** Try to find the PMS on other difference port with S8 */ + if (fwMode == FW_MODE_O_1PST) { + bool pmInitSuccess = false; + if (serial0Available) { + if (ag->pms5003t_1.begin(Serial0) == false) { + hasSensorPMS1 = false; + Serial.println("PMS1 sensor not found"); + } else { + serial0Available = false; + pmInitSuccess = true; + Serial.println("Found PMS 1 on Serial0"); + } + } + if (pmInitSuccess == false) { + if (serial1Available) { + if (ag->pms5003t_1.begin(Serial1) == false) { + hasSensorPMS1 = false; + Serial.println("PMS1 sensor not found"); + } else { + serial1Available = false; + Serial.println("Found PMS 1 on Serial1"); + } + } + } + hasSensorPMS2 = false; // Disable PM2 + } else { + if (ag->pms5003t_1.begin(Serial0) == false) { + hasSensorPMS1 = false; + Serial.println("PMS1 sensor not found"); + } else { + Serial.println("Found PMS 1 on Serial0"); + } + if (ag->pms5003t_2.begin(Serial1) == false) { + hasSensorPMS2 = false; + Serial.println("PMS2 sensor not found"); + } else { + Serial.println("Found PMS 2 on Serial1"); + } + } + + /** update the PMS poll period base on fw mode and sensor available */ + if (fwMode != FW_MODE_O_1PST) { + if (hasSensorPMS1 && hasSensorPMS2) { + pmsSchedule.setPeriod(2000); + } + } + + Serial.printf("Firmware Mode: %s\r\n", getFirmwareModeName()); +} + +static String getFirmwareModeName() { + switch (fwMode) { + case FW_MODE_I_1PSL: + return "I-1PSL"; + case FW_MODE_O_1PP: + return "O-1PP"; + case FW_MODE_O_1PPT: + return "O-1PPT"; + case FW_MODE_O_1PST: + return "O-1PST"; + case FW_MDOE_O_1PS: + return "0-1PS"; + default: + break; + } + return "UNKNOWN"; +} + +static bool isOneIndoor(void) { + return ag->getBoardType() == BoardType::ONE_INDOOR; +} + +/** + * @brief Initialize board + */ +static void boardInit(void) { + if (isOneIndoor()) { + oneIndoorInit(); + } else { + openAirInit(); + } +} + +/** + * @brief Failed handler + * + * @param msg Failure message + */ +static void failedHandler(String msg) { + while (true) { + Serial.println(msg); + vTaskDelay(1000); + } +} + +/** + * @brief Send data to server + */ +static void updateServerConfiguration(void) { + if (agServer.fetchServerConfiguration(getDevId())) { + if (agServer.isCo2Calib()) { + if (hasSensorS8) { + co2Calibration(); + } else { + Serial.println("CO2 S8 not available, calibration ignored"); + } + } + + // Update LED bar + ag->ledBar.setEnable(agServer.getLedBarMode() != UseLedBarOff); + + if (agServer.getCo2AbcDaysConfig() > 0) { + if (hasSensorS8) { + int newHour = agServer.getCo2AbcDaysConfig() * 24; + Serial.printf("Requested abcDays setting: %d days (%d hours)\r\n", + agServer.getCo2AbcDaysConfig(), newHour); + int curHour = ag->s8.getAbcPeriod(); + Serial.printf("Current S8 abcDays setting: %d (hours)\r\n", curHour); + if (curHour == newHour) { + Serial.println("'abcDays' unchanged"); + } else { + if (ag->s8.setAbcPeriod(agServer.getCo2AbcDaysConfig() * 24) == + false) { + Serial.println("Set S8 abcDays period failed"); + } else { + Serial.println("Set S8 abcDays period success"); + } + } + } else { + Serial.println("CO2 S8 not available, set 'abcDays' ignored"); + } + } + + if (agServer.isLedBarTestRequested()) { + if (agServer.getCountry() == "TH") { + ledTest2Min(); + } else { + ledTest(); + } + } + + String mqttUri = agServer.getMqttBroker(); + if (mqttUri != agMqtt.getUri()) { + agMqtt.end(); + + if (mqttTask != NULL) { + vTaskDelete(mqttTask); + mqttTask = NULL; + } + if (agMqtt.begin(mqttUri)) { + Serial.println("Connect to MQTT broker successful"); + createMqttTask(); + } else { + Serial.println("Connect to MQTT broker failed"); + } + } + } +} + +/** + * @brief Calibration CO2 sensor, it's base calibration, after calib complete + * the value will be start at 400 if do calib on clean environment + */ +static void co2Calibration(void) { + /** Count down for co2CalibCountdown secs */ + for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) { + displayShowText( + "Start CO2 calib", + "after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec", ""); + delay(1000); + } + + if (ag->s8.setBaselineCalibration()) { + displayShowText("Calibration", "success", ""); + delay(1000); + displayShowText("Wait for", "calib finish", "..."); + int count = 0; + while (ag->s8.isBaseLineCalibrationDone() == false) { + delay(1000); + count++; + } + displayShowText("Calib finish", "after " + String(count), "sec"); + delay(2000); + } else { + displayShowText("Calibration", "failure!!!", ""); + delay(2000); + } + + /** Update display */ + appDispHandler(); +} + +/** + * @brief Set LED color for special PMS value + * + * @param pm25Value PMS2.5 value + */ +static void setRGBledPMcolor(int pm25Value) { + if (pm25Value <= 5) { + /** G; 1 */ + ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + } else if (pm25Value <= 10) { + /** GG; 2 */ + ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 2); + } else if (pm25Value <= 20) { + /** YYY; 3 */ + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3); + } else if (pm25Value <= 35) { + /** YYYY; 4 */ + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 4); + } else if (pm25Value <= 45) { + /** YYYYY; 5 */ + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 5); + } else if (pm25Value <= 55) { + /** RRRRRR; 6 */ + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + } else if (pm25Value <= 65) { + /** RRRRRRR; 7 */ + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7); + } else if (pm25Value <= 150) { + /** RRRRRRRR; 8 */ + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8); + } else if (pm25Value <= 250) { + /** RRRRRRRRR; 9 */ + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 9); + } else { /** > 250 */ + /* PRPRPRPRP; 9 */ + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 1); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2); + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 3); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4); + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 5); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6); + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 7); + ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8); + ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 9); + } +} + +static void singleLedAnimation(uint8_t r, uint8_t g, uint8_t b) { + if (ledCount < 0) { + ledCount = 0; + ag->ledBar.setColor(r, g, b, ledCount); + } else { + ledCount++; + if (ledCount >= ag->ledBar.getNumberOfLeds()) { + ledCount = 0; + } + ag->ledBar.setColor(r, g, b, ledCount); + } +} + +/** + * @brief LED state machine handler + * + * @param sm APP state machine + */ +static void ledSmHandler(int sm) { + if (sm > APP_SM_NORMAL) { + return; + } + + ledSmState = sm; + if (isOneIndoor()) { + ag->ledBar.clear(); // Set all LED OFF + } + switch (sm) { + case APP_SM_WIFI_MANAGER_MODE: { + /** In WiFi Manager Mode */ + /** Turn LED OFF */ + /** Turn midle LED Color */ + if (isOneIndoor()) { + ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2); + } else { + ag->statusLed.setToggle(); + } + break; + } + case APP_SM_WIFI_MANAGER_PORTAL_ACTIVE: { + /** WiFi Manager has connected to mobile phone */ + if (isOneIndoor()) { + ag->ledBar.setColor(0, 0, 255); + } else { + ag->statusLed.setOn(); + } + break; + } + case APP_SM_WIFI_MANAGER_STA_CONNECTING: { + /** after SSID and PW entered and OK clicked, connection to WiFI network is + * attempted */ + if (isOneIndoor()) { + singleLedAnimation(255, 255, 255); + } else { + ag->statusLed.setOff(); + } + break; + } + case APP_SM_WIFI_MANAGER_STA_CONNECTED: { + /** Connecting to WiFi worked */ + if (isOneIndoor()) { + ag->ledBar.setColor(255, 255, 255); + } else { + ag->statusLed.setOff(); + } + break; + } + case APP_SM_WIFI_OK_SERVER_CONNECTING: { + /** once connected to WiFi an attempt to reach the server is performed */ + if (isOneIndoor()) { + singleLedAnimation(0, 255, 0); + } else { + ag->statusLed.setOff(); + } + break; + } + case APP_SM_WIFI_OK_SERVER_CONNECTED: { + /** Server is reachable, all fine */ + if (isOneIndoor()) { + ag->ledBar.setColor(0, 255, 0); + } else { + ag->statusLed.setOff(); + + /** two time slow blink, then off */ + for (int i = 0; i < 2; i++) { + ledBlinkDelay(LED_SLOW_BLINK_DELAY); + } + + ag->statusLed.setOff(); + } + break; + } + case APP_SM_WIFI_MANAGER_CONNECT_FAILED: { + /** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */ + if (isOneIndoor()) { + ag->ledBar.setColor(255, 0, 0); + } else { + ag->statusLed.setOff(); + + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 3; i++) { + ledBlinkDelay(LED_FAST_BLINK_DELAY); + } + delay(2000); + } + ag->statusLed.setOff(); + } + break; + } + case APP_SM_WIFI_OK_SERVER_CONNECT_FAILED: { + /** Connected to WiFi but server not reachable, e.g. firewall block/ + * whitelisting needed etc. */ + if (isOneIndoor()) { + ag->ledBar.setColor(233, 183, 54); /** orange */ + } else { + ag->statusLed.setOff(); + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 4; i++) { + ledBlinkDelay(LED_FAST_BLINK_DELAY); + } + delay(2000); + } + ag->statusLed.setOff(); + } + break; + } + case APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED: { + /** Server reachable but sensor not configured correctly */ + if (isOneIndoor()) { + ag->ledBar.setColor(139, 24, 248); /** violet */ + } else { + ag->statusLed.setOff(); + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 5; i++) { + ledBlinkDelay(LED_FAST_BLINK_DELAY); + } + delay(2000); + } + ag->statusLed.setOff(); + } + break; + } + case APP_SM_WIFI_LOST: { + /** Connection to WiFi network failed credentials incorrect encryption not + * supported etc. */ + if (isOneIndoor()) { + /** WIFI failed status LED color */ + ag->ledBar.setColor(255, 0, 0, 0); + /** Show CO2 or PM color status */ + sensorLedColorHandler(); + } else { + ag->statusLed.setOff(); + } + break; + } + case APP_SM_SERVER_LOST: { + /** Connected to WiFi network but the server cannot be reached through the + * internet, e.g. blocked by firewall */ + if (isOneIndoor()) { + ag->ledBar.setColor(233, 183, 54, 0); + + /** Show CO2 or PM color status */ + sensorLedColorHandler(); + } else { + ag->statusLed.setOff(); + } + break; + } + case APP_SM_SENSOR_CONFIG_FAILED: { + /** Server is reachable but there is some configuration issue to be fixed on + * the server side */ + if (isOneIndoor()) { + ag->ledBar.setColor(139, 24, 248, 0); + + /** Show CO2 or PM color status */ + sensorLedColorHandler(); + } else { + ag->statusLed.setOff(); + } + break; + } + case APP_SM_NORMAL: { + if (isOneIndoor()) { + sensorLedColorHandler(); + } else { + ag->statusLed.setOff(); + } + break; + } + default: + break; + } + + if (isOneIndoor()) { + ag->ledBar.show(); + } +} + +static void ledBlinkDelay(uint32_t tdelay) { + ag->statusLed.setOn(); + delay(tdelay); + ag->statusLed.setOff(); + delay(tdelay); +} + +/** + * @brief LED state machine handler + */ +static void ledSmHandler(void) { ledSmHandler(ledSmState); } + +/** + * @brief Display state machine handler + * + * @param sm APP state machine + */ +static void dispSmHandler(int sm) { + if (sm > APP_SM_NORMAL) { + return; + } + + dispSmState = sm; + + switch (sm) { + case APP_SM_WIFI_MANAGER_MODE: + case APP_SM_WIFI_MANAGER_PORTAL_ACTIVE: { + if (connectCountDown >= 0) { + displayShowWifiText(String(connectCountDown) + "s to connect", + "to WiFi hotspot:", "\"airgradient-", + getDevId() + "\""); + connectCountDown--; + } + break; + } + case APP_SM_WIFI_MANAGER_STA_CONNECTING: { + displayShowText("Trying to", "connect to WiFi", "..."); + break; + } + case APP_SM_WIFI_MANAGER_STA_CONNECTED: { + displayShowText("WiFi connection", "successful", ""); + break; + } + case APP_SM_WIFI_OK_SERVER_CONNECTING: { + displayShowText("Connecting to", "Server", "..."); + break; + } + case APP_SM_WIFI_OK_SERVER_CONNECTED: { + displayShowText("Server", "connection", "successful"); + break; + } + case APP_SM_WIFI_MANAGER_CONNECT_FAILED: { + displayShowText("WiFi not", "connected", ""); + break; + } + case APP_SM_WIFI_OK_SERVER_CONNECT_FAILED: { + // displayShowText("Server not", "reachable", ""); + break; + } + case APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED: { + displayShowText("Monitor not", "setup on", "dashboard"); + break; + } + case APP_SM_WIFI_LOST: { + displayShowDashboard("WiFi N/A"); + break; + } + case APP_SM_SERVER_LOST: { + displayShowDashboard("Server N/A"); + break; + } + case APP_SM_SENSOR_CONFIG_FAILED: { + uint32_t ms = (uint32_t)(millis() - addToDashboardTime); + if (ms >= 5000) { + addToDashboardTime = millis(); + if (isAddToDashboard) { + displayShowDashboard("Add to Dashboard"); + } else { + displayShowDashboard(getDevId()); + } + isAddToDashboard = !isAddToDashboard; + } + break; + } + case APP_SM_NORMAL: { + displayShowDashboard(""); + } + default: + break; + } +} + +/** + * @brief Handle change LED color base on sensor value of CO2 and PMS + */ +static void sensorLedColorHandler(void) { + switch (agServer.getLedBarMode()) { + case UseLedBarCO2: + setRGBledCO2color(co2Ppm); + break; + case UseLedBarPM: + setRGBledPMcolor(pm25_1); + break; + case UseLedBarOff: + ag->ledBar.clear(); + break; + default: + ag->ledBar.clear(); + break; + } +} + +/** + * @brief APP LED color handler + */ +static void appLedHandler(void) { + uint8_t state = APP_SM_NORMAL; + if (WiFi.isConnected() == false) { + state = APP_SM_WIFI_LOST; + } else if (agServer.isConfigFailed()) { + state = APP_SM_SENSOR_CONFIG_FAILED; + } else if (agServer.isServerFailed()) { + state = APP_SM_SERVER_LOST; + } + ledSmHandler(state); +} + +/** + * @brief APP display content handler + */ +static void appDispHandler(void) { + uint8_t state = APP_SM_NORMAL; + if (WiFi.isConnected() == false) { + state = APP_SM_WIFI_LOST; + } else if (agServer.isConfigFailed()) { + state = APP_SM_SENSOR_CONFIG_FAILED; + } else if (agServer.isServerFailed()) { + state = APP_SM_SERVER_LOST; + } + dispSmHandler(state); +} + +/** + * @brief WiFi reconnect handler + */ +static void updateWiFiConnect(void) { + static uint32_t lastRetry; + if (wifiHasConfig == false) { + return; + } + if (WiFi.isConnected()) { + lastRetry = millis(); + return; + } + uint32_t ms = (uint32_t)(millis() - lastRetry); + if (ms >= WIFI_CONNECT_RETRY_MS) { + lastRetry = millis(); + WiFi.reconnect(); + + Serial.printf("Re-Connect WiFi\r\n"); + } +} + +/** + * @brief APP display and LED handler + * + */ +static void displayAndLedBarUpdate(void) { + if (isOneIndoor()) { + if (factoryBtnPressTime == 0) { + appDispHandler(); + } + } + appLedHandler(); +} + +/** + * @brief Update tvocIndexindex + * + */ +static void tvocUpdate(void) { + tvocIndex = ag->sgp41.getTvocIndex(); + tvocRawIndex = ag->sgp41.getTvocRaw(); + noxIndex = ag->sgp41.getNoxIndex(); + noxRawIndex = ag->sgp41.getNoxRaw(); + + Serial.println(); + Serial.printf("TVOC index: %d\r\n", tvocIndex); + Serial.printf("TVOC raw: %d\r\n", tvocRawIndex); + Serial.printf("NOx index: %d\r\n", noxIndex); + Serial.printf("NOx raw: %d\r\n", noxRawIndex); +} + +/** + * @brief Update PMS data + * + */ +static void pmUpdate(void) { + if (isOneIndoor()) { + if (ag->pms5003.isFailed() == false) { + pm01_1 = ag->pms5003.getPm01Ae(); + pm25_1 = ag->pms5003.getPm25Ae(); + pm10_1 = ag->pms5003.getPm10Ae(); + pm03PCount_1 = ag->pms5003.getPm03ParticleCount(); + + Serial.println(); + Serial.printf("PM1 ug/m3: %d\r\n", pm01_1); + Serial.printf("PM2.5 ug/m3: %d\r\n", pm25_1); + Serial.printf("PM10 ug/m3: %d\r\n", pm10_1); + Serial.printf("PM0.3 Count: %d\r\n", pm03PCount_1); + pmFailCount = 0; + } else { + pmFailCount++; + Serial.printf("PMS read failed: %d\r\n", pmFailCount); + if (pmFailCount >= 3) { + pm01_1 = -1; + pm25_1 = -1; + pm10_1 = -1; + pm03PCount_1 = -1; + } + } + } else { + bool pmsResult_1 = false; + bool pmsResult_2 = false; + if (hasSensorPMS1 && (ag->pms5003t_1.isFailed() == false)) { + pm01_1 = ag->pms5003t_1.getPm01Ae(); + pm25_1 = ag->pms5003t_1.getPm25Ae(); + pm10_1 = ag->pms5003t_1.getPm10Ae(); + pm03PCount_1 = ag->pms5003t_1.getPm03ParticleCount(); + temp_1 = ag->pms5003t_1.getTemperature(); + hum_1 = ag->pms5003t_1.getRelativeHumidity(); + + pmsResult_1 = true; + + Serial.println(); + Serial.printf("[1] PM1 ug/m3: %d\r\n", pm01_1); + Serial.printf("[1] PM2.5 ug/m3: %d\r\n", pm25_1); + Serial.printf("[1] PM10 ug/m3: %d\r\n", pm10_1); + Serial.printf("[1] PM3.0 Count: %d\r\n", pm03PCount_1); + Serial.printf("[1] Temperature in C: %0.2f\r\n", temp_1); + Serial.printf("[1] Relative Humidity: %d\r\n", hum_1); + } else { + pm01_1 = -1; + pm25_1 = -1; + pm10_1 = -1; + pm03PCount_1 = -1; + temp_1 = -1001; + hum_1 = -1; + } + + if (hasSensorPMS2 && (ag->pms5003t_2.isFailed() == false)) { + pm01_2 = ag->pms5003t_2.getPm01Ae(); + pm25_2 = ag->pms5003t_2.getPm25Ae(); + pm10_2 = ag->pms5003t_2.getPm10Ae(); + pm03PCount_2 = ag->pms5003t_2.getPm03ParticleCount(); + temp_2 = ag->pms5003t_2.getTemperature(); + hum_2 = ag->pms5003t_2.getRelativeHumidity(); + + pmsResult_2 = true; + + Serial.println(); + Serial.printf("[2] PM1 ug/m3: %d\r\n", pm01_2); + Serial.printf("[2] PM2.5 ug/m3: %d\r\n", pm25_2); + Serial.printf("[2] PM10 ug/m3: %d\r\n", pm10_2); + Serial.printf("[2] PM3.0 Count: %d\r\n", pm03PCount_2); + Serial.printf("[2] Temperature in C: %0.2f\r\n", temp_2); + Serial.printf("[2] Relative Humidity: %d\r\n", hum_2); + } else { + pm01_2 = -1; + pm25_2 = -1; + pm10_2 = -1; + pm03PCount_2 = -1; + temp_2 = -1001; + hum_2 = -1; + } + + if (hasSensorPMS1 && hasSensorPMS2 && pmsResult_1 && pmsResult_2) { + /** Get total of PMS1*/ + pm1Value01 = pm1Value01 + pm01_1; + pm1Value25 = pm1Value25 + pm25_1; + pm1Value10 = pm1Value10 + pm10_1; + pm1PCount = pm1PCount + pm03PCount_1; + pm1temp = pm1temp + temp_1; + pm1hum = pm1hum + hum_1; + + /** Get total of PMS2 */ + pm2Value01 = pm2Value01 + pm01_2; + pm2Value25 = pm2Value25 + pm25_2; + pm2Value10 = pm2Value10 + pm10_2; + pm2PCount = pm2PCount + pm03PCount_2; + pm2temp = pm2temp + temp_2; + pm2hum = pm2hum + hum_2; + + countPosition++; + + /** Get average */ + if (countPosition == targetCount) { + pm01_1 = pm1Value01 / targetCount; + pm25_1 = pm1Value25 / targetCount; + pm10_1 = pm1Value10 / targetCount; + pm03PCount_1 = pm1PCount / targetCount; + temp_1 = pm1temp / targetCount; + hum_1 = pm1hum / targetCount; + + pm01_2 = pm2Value01 / targetCount; + pm25_2 = pm2Value25 / targetCount; + pm10_2 = pm2Value10 / targetCount; + pm03PCount_2 = pm2PCount / targetCount; + temp_2 = pm2temp / targetCount; + hum_2 = pm2hum / targetCount; + + countPosition = 0; + + pm1Value01 = 0; + pm1Value25 = 0; + pm1Value10 = 0; + pm1PCount = 0; + pm1temp = 0; + pm1hum = 0; + pm2Value01 = 0; + pm2Value25 = 0; + pm2Value10 = 0; + pm2PCount = 0; + pm2temp = 0; + pm2hum = 0; + } + } + + if (hasSensorSGP) { + float temp; + float hum; + if (pmsResult_1 && pmsResult_2) { + temp = (temp_1 + temp_2) / 2.0f; + hum = (hum_1 + hum_2) / 2.0f; + } else { + if (pmsResult_1) { + temp = temp_1; + hum = hum_1; + } + if (pmsResult_2) { + temp = temp_2; + hum = hum_2; + } + } + ag->sgp41.setCompensationTemperatureHumidity(temp, hum); + } + } +} + +/** + * @brief Send data to server + * + */ +static void sendDataToServer(void) { + String syncData = getServerSyncData(false); + if (agServer.postToServer(getDevId(), syncData)) { + resetWatchdog(); + } + + bootCount++; +} + +/** + * @brief Update temperature and humidity value + */ +static void tempHumUpdate(void) { + if (ag->sht.measure()) { + + temp = ag->sht.getTemperature(); + hum = ag->sht.getRelativeHumidity(); + + Serial.printf("Temperature in C: %0.2f\r\n", temp); + Serial.printf("Relative Humidity: %d\r\n", hum); + + // Update compensation temperature and humidity for SGP41 + if (hasSensorSGP) { + ag->sgp41.setCompensationTemperatureHumidity(temp, hum); + } + } else { + Serial.println("SHT read failed"); + } +} + +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, + int32_t event_id, void *event_data) { + esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + Serial.println("MQTT_EVENT_CONNECTED"); + // msg_id = esp_mqtt_client_subscribe(client, "helloworld", 0); + // Serial.printf("sent subscribe successful, msg_id=%d\r\n", msg_id); + agMqtt._connectionHandler(true); + break; + case MQTT_EVENT_DISCONNECTED: + Serial.println("MQTT_EVENT_DISCONNECTED"); + agMqtt._connectionHandler(false); + break; + case MQTT_EVENT_SUBSCRIBED: + break; + case MQTT_EVENT_UNSUBSCRIBED: + Serial.printf("MQTT_EVENT_UNSUBSCRIBED, msg_id=%d\r\n", event->msg_id); + break; + case MQTT_EVENT_PUBLISHED: + Serial.printf("MQTT_EVENT_PUBLISHED, msg_id=%d\r\n", event->msg_id); + break; + case MQTT_EVENT_DATA: + Serial.println("MQTT_EVENT_DATA"); + // add null terminal to data + // event->data[event->data_len] = 0; + // rpc_attritbutes_handler(event->data, event->data_len); + break; + case MQTT_EVENT_ERROR: + Serial.println("MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + Serial.printf("reported from esp-tls: %d", + event->error_handle->esp_tls_last_esp_err); + Serial.printf("reported from tls stack: %d", + event->error_handle->esp_tls_stack_err); + Serial.printf("captured as transport's socket errno: %d", + event->error_handle->esp_transport_sock_errno); + } + break; + default: + Serial.printf("Other event id:%d\r\n", event->event_id); + break; + } +} From 7cfa72268424614f450b335ffa1b62c3bb9ec696 Mon Sep 17 00:00:00 2001 From: Phat Nguyen Date: Sat, 23 Mar 2024 17:44:11 +0700 Subject: [PATCH 2/8] update show log log message and fix bug --- examples/OneOpenAir/OneOpenAir.ino | 169 +++++++++++++++++++---------- 1 file changed, 112 insertions(+), 57 deletions(-) diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index d5ae6681..d24ffa2c 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -108,6 +108,7 @@ enum { "cleanair" /** default WiFi AP password \ */ +/** I2C define */ #define I2C_SDA_PIN 7 #define I2C_SCL_PIN 6 #define OLED_I2C_ADDR 0x3C @@ -137,33 +138,41 @@ class AgSchedule { public: AgSchedule(int period, void (*handler)(void)) : period(period), handler(handler) {} + + /** + * @brief Handle schedule + * + */ void run(void) { uint32_t ms = (uint32_t)(millis() - count); if (ms >= period) { - /** Call handler */ handler(); - - // Serial.printf("[AgSchedule] handle 0x%08x, period: %d(ms)\r\n", - // (unsigned int)handler, period); - - /** Update period time */ count = millis(); } } + /** + * @brief Set schedule handle period + * + * @param period + */ void setPeriod(int period) { this->period = period; } private: - void (*handler)(void); - int period; - int count; + void (*handler)(void); /** Callback handle */ + int period; /** Schedule handle period */ + int count; /** Schedule time count check */ }; /** * @brief AirGradient server configuration and sync data - * */ class AgServer { public: + /** + * @brief Initialize airgradient server, it's load the server configuration if + * failed load it to default. + * + */ void begin(void) { configFailed = false; serverFailed = false; @@ -322,10 +331,15 @@ public: return true; } + /** + * @brief Post data to Airgradient server + * + * @param id Device Id + * @param payload Data payload + * @return true Success + * @return false Failure + */ bool postToServer(String id, String payload) { - /** - * @brief Only post data if WiFi is connected - */ if (WiFi.isConnected() == false) { return false; } @@ -498,6 +512,9 @@ private: }; struct config_s config; + /** + * @brief Set server configuration default + */ void defaultConfig(void) { config.inF = false; config.inUSAQI = false; @@ -508,6 +525,9 @@ private: saveConfig(); } + /** + * @brief Get local server configuration + */ void loadConfig(void) { if (EEPROM.readBytes(0, &config, sizeof(config)) != sizeof(config)) { config.inF = false; @@ -532,6 +552,9 @@ private: showServerConfig(); } + /** + * @brief Save server configuration + */ void saveConfig(void) { config.checksum = 0; uint8_t *data = (uint8_t *)&config; @@ -544,6 +567,12 @@ private: Serial.println("Save config"); } + /** + * @brief Parse LED bar mode + * + * @param mode LED mode + * @return UseLedBar + */ UseLedBar parseLedBarMode(String mode) { UseLedBar ledBarMode = UseLedBarOff; if (mode == "co2") { @@ -559,7 +588,6 @@ private: return ledBarMode; } }; -AgServer agServer; static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); @@ -616,7 +644,7 @@ public: /** Register event */ if (esp_mqtt_client_register_event(client, MQTT_EVENT_ANY, - mqtt_event_handler, NULL) != ESP_OK) { + mqtt_event_handler, this) != ESP_OK) { Serial.println("MQTT client register event failed"); return false; } @@ -665,6 +693,11 @@ public: */ String getUri(void) { return uri; } + /** + * @brief Update mqtt connection changed + * + * @param connected + */ void _connectionHandler(bool connected) { clientConnected = connected; if (clientConnected == false) { @@ -689,21 +722,15 @@ public: */ int connectionFailedCount(void) { return connectFailedCount; } }; -AgMqtt agMqtt; -static TaskHandle_t mqttTask = NULL; -/** Create airgradient instance for 'ONE_INDOOR' board */ -// AirGradient ag(ONE_INDOOR); +static AgMqtt agMqtt; +static TaskHandle_t mqttTask = NULL; +static AgServer agServer; static AirGradient *ag; - -/** Create u8g2 instance */ -U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE); - -/** wifi manager instance */ -WiFiManager wifiManager; - -/** Web server instance */ -WebServer webServer; +static U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, + /* reset=*/U8X8_PIN_NONE); +static WiFiManager wifiManager; +static WebServer webServer; static bool wifiHasConfig = false; /** */ static int connectCountDown; /** wifi configuration countdown */ @@ -711,15 +738,25 @@ static int ledCount; /** For LED animation */ static int ledSmState = APP_SM_NORMAL; /** Save display SM */ static int dispSmState = APP_SM_NORMAL; /** Save LED SM */ +/** Init schedule */ +static bool hasSensorS8 = true; +static bool hasSensorPMS1 = true; +static bool hasSensorPMS2 = true; +static bool hasSensorSGP = true; +static bool hasSensorSHT = true; +static int pmFailCount = 0; +static uint32_t factoryBtnPressTime = 0; +static int getCO2FailCount = 0; +static uint32_t addToDashboardTime; +static bool isAddToDashboard = true; +static bool offlineMode = false; +static int fwMode = FW_MODE_I_1PSL; + static int tvocIndex = -1; static int tvocRawIndex = -1; static int noxIndex = -1; static int noxRawIndex = -1; static int co2Ppm = -1; -// static int pm25 = -1; -// static int pm01 = -1; -// static int pm10 = -1; -// static int pm03PCount = -1; static float temp = -1001; static int hum = -1; static int bootCount; @@ -779,20 +816,6 @@ static void createMqttTask(void); static void factoryConfigReset(void); static void wdgFeedUpdate(void); -/** Init schedule */ -bool hasSensorS8 = true; -bool hasSensorPMS1 = true; -bool hasSensorPMS2 = true; -bool hasSensorSGP = true; -bool hasSensorSHT = true; -int pmFailCount = 0; -uint32_t factoryBtnPressTime = 0; -int getCO2FailCount = 0; -uint32_t addToDashboardTime; -bool isAddToDashboard = true; -bool offlineMode = false; -int fwMode = FW_MODE_I_1PSL; - AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, displayAndLedBarUpdate); AgSchedule configSchedule(SERVER_CONFIG_UPDATE_INTERVAL, updateServerConfiguration); @@ -1742,6 +1765,8 @@ static void connectToWifi() { if (isOneIndoor()) { displayShowText("Connecting to", "WiFi", "..."); + } else { + Serial.println("Connecting to WiFi..."); } wifiManager.autoConnect(wifiSSID.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT); xTaskCreate( @@ -1955,6 +1980,8 @@ void dispSensorNotFound(String ss) { } static void oneIndoorInit(void) { + hasSensorPMS2 = false; + /** Display init */ u8g2.begin(); @@ -2022,6 +2049,8 @@ static void oneIndoorInit(void) { } } static void openAirInit(void) { + hasSensorSHT = false; + fwMode = FW_MODE_O_1PST; Serial.println("Firmware Version: " + ag->getVersion()); @@ -2064,8 +2093,8 @@ static void openAirInit(void) { Serial.println("SGP sensor not found"); if (hasSensorS8 == false) { - fwMode = FW_MODE_O_1PP; Serial.println("Can not detect SGP run mode 'O-1PP'"); + fwMode = FW_MODE_O_1PP; } else { Serial.println("Can not detect SGP run mode 'O-1PS'"); fwMode = FW_MDOE_O_1PS; @@ -2236,32 +2265,56 @@ static void updateServerConfiguration(void) { * the value will be start at 400 if do calib on clean environment */ static void co2Calibration(void) { + Serial.println("co2Calibration: Start"); /** Count down for co2CalibCountdown secs */ for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) { - displayShowText( - "Start CO2 calib", - "after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec", ""); + if (isOneIndoor()) { + displayShowText( + "Start CO2 calib", + "after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec", ""); + } else { + Serial.printf("Start CO2 calib after %d sec\r\n", + SENSOR_CO2_CALIB_COUNTDOWN_MAX - i); + } delay(1000); } if (ag->s8.setBaselineCalibration()) { - displayShowText("Calibration", "success", ""); + if (isOneIndoor()) { + displayShowText("Calibration", "success", ""); + } else { + Serial.println("Calibration success"); + } delay(1000); - displayShowText("Wait for", "calib finish", "..."); + if (isOneIndoor()) { + displayShowText("Wait for", "calib finish", "..."); + } else { + Serial.println("Wait for calibration finish..."); + } int count = 0; while (ag->s8.isBaseLineCalibrationDone() == false) { delay(1000); count++; } - displayShowText("Calib finish", "after " + String(count), "sec"); + if (isOneIndoor()) { + displayShowText("Calib finish", "after " + String(count), "sec"); + } else { + Serial.printf("Calibration finish after %d sec\r\n", count); + } delay(2000); } else { - displayShowText("Calibration", "failure!!!", ""); + if (isOneIndoor()) { + displayShowText("Calibration", "failure!!!", ""); + } else { + Serial.println("Calibration failure!!!"); + } delay(2000); } /** Update display */ - appDispHandler(); + if (isOneIndoor()) { + appDispHandler(); + } } /** @@ -2930,6 +2983,8 @@ static void tempHumUpdate(void) { static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { + AgMqtt *mqtt = (AgMqtt *)handler_args; + esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; esp_mqtt_client_handle_t client = event->client; int msg_id; @@ -2938,11 +2993,11 @@ static void mqtt_event_handler(void *handler_args, esp_event_base_t base, Serial.println("MQTT_EVENT_CONNECTED"); // msg_id = esp_mqtt_client_subscribe(client, "helloworld", 0); // Serial.printf("sent subscribe successful, msg_id=%d\r\n", msg_id); - agMqtt._connectionHandler(true); + mqtt->_connectionHandler(true); break; case MQTT_EVENT_DISCONNECTED: Serial.println("MQTT_EVENT_DISCONNECTED"); - agMqtt._connectionHandler(false); + mqtt->_connectionHandler(false); break; case MQTT_EVENT_SUBSCRIBED: break; From 063612e08f330936b8e980375e6887a290855ccd Mon Sep 17 00:00:00 2001 From: Phat Nguyen Date: Sun, 24 Mar 2024 07:40:26 +0700 Subject: [PATCH 3/8] fix: Ledbar off in offline mode --- examples/ONE/ONE.ino | 7 +------ examples/OneOpenAir/OneOpenAir.ino | 8 ++------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/examples/ONE/ONE.ino b/examples/ONE/ONE.ino index b81d7240..aecb4683 100644 --- a/examples/ONE/ONE.ino +++ b/examples/ONE/ONE.ino @@ -493,13 +493,8 @@ private: void loadConfig(void) { if (EEPROM.readBytes(0, &config, sizeof(config)) != sizeof(config)) { - config.inF = false; - config.inUSAQI = false; - config.useRGBLedBar = UseLedBarCO2; // default use LED bar for CO2 - memset(config.models, 0, sizeof(config.models)); - memset(config.mqttBrokers, 0, sizeof(config.mqttBrokers)); - Serial.println("Load configure failed"); + defaultConfig(); } else { uint32_t sum = 0; uint8_t *data = (uint8_t *)&config; diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index d24ffa2c..bd7df0e4 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -518,6 +518,7 @@ private: void defaultConfig(void) { config.inF = false; config.inUSAQI = false; + config.useRGBLedBar = UseLedBarCO2; memset(config.models, 0, sizeof(config.models)); memset(config.mqttBrokers, 0, sizeof(config.mqttBrokers)); @@ -530,13 +531,8 @@ private: */ void loadConfig(void) { if (EEPROM.readBytes(0, &config, sizeof(config)) != sizeof(config)) { - config.inF = false; - config.inUSAQI = false; - config.useRGBLedBar = UseLedBarCO2; // default use LED bar for CO2 - memset(config.models, 0, sizeof(config.models)); - memset(config.mqttBrokers, 0, sizeof(config.mqttBrokers)); - Serial.println("Load configure failed"); + defaultConfig(); } else { uint32_t sum = 0; uint8_t *data = (uint8_t *)&config; From e47096feac33ca83000c85e7dab289173dc1e904 Mon Sep 17 00:00:00 2001 From: Phat Nguyen Date: Sun, 24 Mar 2024 07:43:40 +0700 Subject: [PATCH 4/8] Limit show CO2 index within 4 character number (max = 9999) --- examples/ONE/ONE.ino | 4 ++++ examples/OneOpenAir/OneOpenAir.ino | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/ONE/ONE.ino b/examples/ONE/ONE.ino index aecb4683..34d4d13a 100644 --- a/examples/ONE/ONE.ino +++ b/examples/ONE/ONE.ino @@ -1453,6 +1453,10 @@ static void displayShowDashboard(String err) { /** Show CO2 value */ u8g2.setFont(u8g2_font_t0_22b_tf); if (co2Ppm > 0) { + int val = 9999; + if (co2Ppm < 10000) { + val = co2Ppm; + } sprintf(strBuf, "%d", co2Ppm); } else { sprintf(strBuf, "%s", "-"); diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index bd7df0e4..fcf410a7 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -1656,7 +1656,11 @@ static void displayShowDashboard(String err) { /** Show CO2 value */ u8g2.setFont(u8g2_font_t0_22b_tf); if (co2Ppm > 0) { - sprintf(strBuf, "%d", co2Ppm); + int val = 9999; + if(co2Ppm < 10000){ + val = co2Ppm; + } + sprintf(strBuf, "%d", val); } else { sprintf(strBuf, "%s", "-"); } From d255e6ad047923e3a6526af2f1bacc0172193e1c Mon Sep 17 00:00:00 2001 From: Phat Nguyen Date: Sun, 24 Mar 2024 07:45:32 +0700 Subject: [PATCH 5/8] Update workflows for `OneOpenAir` --- .github/workflows/check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index bc009d52..d4ae02f1 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -11,6 +11,7 @@ jobs: - "TestCO2" - "TestPM" - "TestSht" + - "OneOpenAir" fqbn: - "esp8266:esp8266:d1_mini" - "esp32:esp32:esp32c3" @@ -28,6 +29,8 @@ jobs: fqbn: "esp8266:esp8266:d1_mini" - example: "Open_Air" fqbn: "esp8266:esp8266:d1_mini" + - example: "OneOpenAir" + fqbn: "esp8266:esp8266:d1_mini" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 3537a3012c4cc367ccae6e736f0be8706729897a Mon Sep 17 00:00:00 2001 From: Phat Nguyen Date: Sun, 24 Mar 2024 08:51:14 +0700 Subject: [PATCH 6/8] fix: WiFi connect after LedBar test with button. And update comments --- examples/OneOpenAir/OneOpenAir.ino | 41 ++++++++++++++++++------------ 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index fcf410a7..55827400 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -1,6 +1,6 @@ /* -This is the code for the AirGradient ONE open-source hardware indoor Air Quality -Monitor with an ESP32-C3 Microcontroller. +This is the combined firmware code for AirGradient ONE and AirGradient Open Air +open-source hardware Air Quality Monitor with ESP32-C3 Microcontroller. 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. @@ -9,7 +9,10 @@ Open source air quality monitors and kits are available: Indoor Monitor: https://www.airgradient.com/indoor/ Outdoor Monitor: https://www.airgradient.com/outdoor/ -Build Instructions: https://www.airgradient.com/documentation/one-v9/ +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/ The codes needs the following libraries installed: “WifiManager by tzapu, tablatronix” tested with version 2.0.16-rc.2 @@ -25,7 +28,7 @@ Important flashing settings: - Flash frequency "80Mhz" - Flash mode "QIO" - Flash size "4MB" -- Partition scheme "Default 4MB with spiffs (1.2MB APP/1,5MB SPIFFS)" +- Partition scheme "Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)" - JTAG adapter "Disabled" Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3) @@ -787,6 +790,8 @@ static int pm2hum; static int countPosition; const int targetCount = 20; +static bool ledBarButtonTest = false; + static void boardInit(void); static void failedHandler(String msg); static void updateServerConfiguration(void); @@ -851,7 +856,19 @@ void setup() { boardInit(); /** Connecting wifi */ - connectToWifi(); + if (isOneIndoor()) { + if (ledBarButtonTest) { + ledTest(); + } else { + /** Check LED mode to disabled LED */ + if (agServer.getLedBarMode() == UseLedBarOff) { + ag->ledBar.setEnable(false); + } + connectToWifi(); + } + } else { + connectToWifi(); + } /** * Send first data to ping server and get server configuration @@ -1657,7 +1674,7 @@ static void displayShowDashboard(String err) { u8g2.setFont(u8g2_font_t0_22b_tf); if (co2Ppm > 0) { int val = 9999; - if(co2Ppm < 10000){ + if (co2Ppm < 10000) { val = co2Ppm; } sprintf(strBuf, "%d", val); @@ -2026,11 +2043,11 @@ static void oneIndoorInit(void) { /** Run LED test on start up */ displayShowText("Press now for", "LED test &", "offline mode"); - bool test = false; + ledBarButtonTest = false; uint32_t stime = millis(); while (true) { if (ag->button.getState() == ag->button.BUTTON_PRESSED) { - test = true; + ledBarButtonTest = true; break; } delay(1); @@ -2039,14 +2056,6 @@ static void oneIndoorInit(void) { break; } } - if (test) { - ledTest(); - } - - /** Check LED mode to disabled LED */ - if (agServer.getLedBarMode() == UseLedBarOff) { - ag->ledBar.setEnable(false); - } } static void openAirInit(void) { hasSensorSHT = false; From a960d086e12d10f265310a897558408e36149009 Mon Sep 17 00:00:00 2001 From: Phat Nguyen Date: Sun, 24 Mar 2024 08:51:39 +0700 Subject: [PATCH 7/8] Update instruction comment and clean code. --- examples/ONE/ONE.ino | 4 ++-- examples/Open_Air/Open_Air.ino | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ONE/ONE.ino b/examples/ONE/ONE.ino index 34d4d13a..af49d10c 100644 --- a/examples/ONE/ONE.ino +++ b/examples/ONE/ONE.ino @@ -25,7 +25,7 @@ Important flashing settings: - Flash frequency "80Mhz" - Flash mode "QIO" - Flash size "4MB" -- Partition scheme "Default 4MB with spiffs (1.2MB APP/1,5MB SPIFFS)" +- Partition scheme "Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)" - JTAG adapter "Disabled" Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3) @@ -804,7 +804,7 @@ void setup() { /** * Send first data to ping server and get server configuration */ - if (WiFi.status() == WL_CONNECTED) { + if (WiFi.isConnected()) { webServerInit(); /** MQTT init */ diff --git a/examples/Open_Air/Open_Air.ino b/examples/Open_Air/Open_Air.ino index 33d6b267..d6a03fef 100644 --- a/examples/Open_Air/Open_Air.ino +++ b/examples/Open_Air/Open_Air.ino @@ -25,7 +25,7 @@ Important flashing settings: - Flash frequency "80Mhz" - Flash mode "QIO" - Flash size "4MB" -- Partition scheme "Default 4MB with spiffs (1.2MB APP/1,5MB SPIFFS)" +- Partition scheme "Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)" - JTAG adapter "Disabled" If you have any questions please visit our forum at From efe68a54a4e8f3fc410e78c9dcae285c228aa6c8 Mon Sep 17 00:00:00 2001 From: Phat Nguyen Date: Sun, 24 Mar 2024 20:04:48 +0700 Subject: [PATCH 8/8] update version `3.0.8` to `3.0.9` --- library.properties | 2 +- src/AirGradient.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library.properties b/library.properties index a5128b1a..d2706c70 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=AirGradient Air Quality Sensor -version=3.0.8 +version=3.0.9 author=AirGradient maintainer=AirGradient sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display. diff --git a/src/AirGradient.cpp b/src/AirGradient.cpp index bc296482..ff4ad552 100644 --- a/src/AirGradient.cpp +++ b/src/AirGradient.cpp @@ -1,6 +1,6 @@ #include "AirGradient.h" -#define AG_LIB_VER "3.0.8" +#define AG_LIB_VER "3.0.9" AirGradient::AirGradient(BoardType type) : pms5003(type), pms5003t_1(type), pms5003t_2(type), s8(type), sgp41(type),