diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b138b70..099b69d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -7,6 +7,7 @@ jobs: example: - "BASIC" - "DiyProIndoorV4_2" + - "DiyProIndoorV3_7" - "TestCO2" - "TestPM" - "TestSht" @@ -26,6 +27,8 @@ jobs: fqbn: "esp32:esp32:esp32c3" - example: "DiyProIndoorV4_2" fqbn: "esp32:esp32:esp32c3" + - example: "DiyProIndoorV3_7" + fqbn: "esp32:esp32:esp32c3" - example: "OneOpenAir" fqbn: "esp8266:esp8266:d1_mini" runs-on: ubuntu-latest diff --git a/examples/DiyProIndoorV3_7/DiyProIndoorV3_7.ino b/examples/DiyProIndoorV3_7/DiyProIndoorV3_7.ino new file mode 100644 index 0000000..e59ab44 --- /dev/null +++ b/examples/DiyProIndoorV3_7/DiyProIndoorV3_7.ino @@ -0,0 +1,618 @@ +/* +This is the code for the AirGradient DIY PRO 3.7 Air Quality Monitor with an D1 +ESP8266 Microcontroller. + +It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a +small display 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/diy-v4/ + +Please make sure you have esp8266 board manager installed. Tested with +version 3.1.2. + +Set board to "LOLIN(WEMOS) D1 R2 & mini" + +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 "AgApiClient.h" +#include "AgConfigure.h" +#include "AgSchedule.h" +#include "AgWiFiConnector.h" +#include "LocalServer.h" +#include "OpenMetrics.h" +#include "MqttClient.h" +#include +#include +#include +#include +#include + +#define LED_BAR_ANIMATION_PERIOD 100 /** ms */ +#define DISP_UPDATE_INTERVAL 2500 /** ms */ +#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** 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 FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */ + +static AirGradient ag(DIY_PRO_INDOOR_V3_7); +static Configuration configuration(Serial); +static AgApiClient apiClient(Serial, configuration); +static Measurements measurements; +static OledDisplay oledDisplay(configuration, measurements, Serial); +static StateMachine stateMachine(oledDisplay, Serial, measurements, + configuration); +static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine, + configuration); +static OpenMetrics openMetrics(measurements, configuration, wifiConnector, + apiClient); +static LocalServer localServer(Serial, openMetrics, measurements, configuration, + wifiConnector); +static MqttClient mqttClient(Serial); + +static int pmFailCount = 0; +static int getCO2FailCount = 0; +static AgFirmwareMode fwMode = FW_MODE_I_37PS; + +static String fwNewVersion; + +static void boardInit(void); +static void failedHandler(String msg); +static void configurationUpdateSchedule(void); +static void appDispHandler(void); +static void oledDisplaySchedule(void); +static void updateTvoc(void); +static void updatePm(void); +static void sendDataToServer(void); +static void tempHumUpdate(void); +static void co2Update(void); +static void mdnsInit(void); +static void initMqtt(void); +static void factoryConfigReset(void); +static void wdgFeedUpdate(void); +static bool sgp41Init(void); +static void wifiFactoryConfigure(void); +static void mqttHandle(void); + +AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule); +AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL, + configurationUpdateSchedule); +AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer); +AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update); +AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm); +AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate); +AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc); +AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate); +AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle); + +void setup() { + /** Serial for print debug message */ + Serial.begin(115200); + delay(100); /** For bester show log */ + + /** Print device ID into log */ + Serial.println("Serial nr: " + ag.deviceId()); + + /** Initialize local configure */ + configuration.begin(); + + /** Init I2C */ + Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin()); + delay(1000); + + configuration.setAirGradient(&ag); + oledDisplay.setAirGradient(&ag); + stateMachine.setAirGradient(&ag); + wifiConnector.setAirGradient(&ag); + apiClient.setAirGradient(&ag); + openMetrics.setAirGradient(&ag); + localServer.setAirGraident(&ag); + + /** Init sensor */ + boardInit(); + + /** Connecting wifi */ + bool connectToWifi = false; + + connectToWifi = !configuration.isOfflineMode(); + if (connectToWifi) { + apiClient.begin(); + + if (wifiConnector.connect()) { + if (wifiConnector.isConnected()) { + mdnsInit(); + localServer.begin(); + initMqtt(); + sendDataToAg(); + + apiClient.fetchServerConfiguration(); + configSchedule.update(); + if (apiClient.isFetchConfigureFailed()) { + if (apiClient.isNotAvailableOnDashboard()) { + stateMachine.displaySetAddToDashBoard(); + stateMachine.displayHandle( + AgStateMachineWiFiOkServerOkSensorConfigFailed); + } else { + stateMachine.displayClearAddToDashBoard(); + } + delay(DISPLAY_DELAY_SHOW_CONTENT_MS); + } + } else { + if (wifiConnector.isConfigurePorttalTimeout()) { + oledDisplay.showRebooting(); + delay(2500); + oledDisplay.setText("", "", ""); + ESP.restart(); + } + } + } + } + /** Set offline mode without saving, cause wifi is not configured */ + if (wifiConnector.hasConfigurated() == false) { + Serial.println("Set offline mode cause wifi is not configurated"); + configuration.setOfflineModeWithoutSave(true); + } + + /** Show display Warning up */ + oledDisplay.setText("Warming Up", "Serial Number:", ag.deviceId().c_str()); + delay(DISPLAY_DELAY_SHOW_CONTENT_MS); + + Serial.println("Display brightness: " + + String(configuration.getDisplayBrightness())); + oledDisplay.setBrightness(configuration.getDisplayBrightness()); + + appDispHandler(); +} + +void loop() { + /** Handle schedule */ + dispLedSchedule.run(); + configSchedule.run(); + agApiPostSchedule.run(); + + if (configuration.hasSensorS8) { + co2Schedule.run(); + } + if (configuration.hasSensorPMS1) { + pmsSchedule.run(); + ag.pms5003.handle(); + } + if (configuration.hasSensorSHT) { + tempHumSchedule.run(); + } + if (configuration.hasSensorSGP) { + tvocSchedule.run(); + } + + /** Auto reset watchdog timer if offline mode or postDataToAirGradient */ + if (configuration.isOfflineMode() || + (configuration.isPostDataToAirGradient() == false)) { + watchdogFeedSchedule.run(); + } + + /** Check for handle WiFi reconnect */ + wifiConnector.handle(); + + /** factory reset handle */ + // factoryConfigReset(); + + /** check that local configura changed then do some action */ + configUpdateHandle(); + + localServer._handle(); + + ag.sgp41.handle(); + + MDNS.update(); + + mqttSchedule.run(); + mqttClient.handle(); +} + +static void co2Update(void) { + int value = ag.s8.getCo2(); + if (value >= 0) { + measurements.CO2 = value; + getCO2FailCount = 0; + Serial.printf("CO2 (ppm): %d\r\n", measurements.CO2); + } else { + getCO2FailCount++; + Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount); + if (getCO2FailCount >= 3) { + measurements.CO2 = -1; + } + } +} + +static void mdnsInit(void) { + Serial.println("mDNS init"); + if (!MDNS.begin(localServer.getHostname().c_str())) { + Serial.println("Init mDNS failed"); + return; + } + + MDNS.addService("_airgradient", "_tcp", 80); + MDNS.addServiceTxt("_airgradient", "_tcp", "model", + AgFirmwareModeName(fwMode)); + MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId()); + MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion()); + MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient"); + + MDNS.announce(); +} + +static void initMqtt(void) { + if (mqttClient.begin(configuration.getMqttBrokerUri())) { + Serial.println("Setup connect to MQTT broker successful"); + } else { + Serial.println("setup Connect to MQTT broker failed"); + } +} + +static void factoryConfigReset(void) { +#if 0 + 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 + if (ag.isOne() || ag.isPro4_2()) { + oledDisplay.setText("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); + String str = "for " + String(count) + " sec"; + oledDisplay.setText("Factory reset", "keep pressed", str.c_str()); + + count--; + if (count == 0) { + /** Stop MQTT task first */ + // if (mqttTask) { + // vTaskDelete(mqttTask); + // mqttTask = NULL; + // } + + /** Reset WIFI */ + // WiFi.enableSTA(true); // Incase offline mode + // WiFi.disconnect(true, true); + wifiConnector.reset(); + + /** Reset local config */ + configuration.reset(); + + oledDisplay.setText("Factory reset", "successful", ""); + + delay(3000); + oledDisplay.setText("", "", ""); + ESP.restart(); + } + } + + /** Show current content cause reset ignore */ + factoryBtnPressTime = 0; + appDispHandler(); + } + } + } else { + if (factoryBtnPressTime != 0) { + appDispHandler(); + } + factoryBtnPressTime = 0; + } +#endif +} + +static void wdgFeedUpdate(void) { + ag.watchdog.reset(); + Serial.println(); + Serial.println("Offline mode or isPostToAirGradient = false: watchdog reset"); + Serial.println(); +} + +static bool sgp41Init(void) { + ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset()); + ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset()); + if (ag.sgp41.begin(Wire)) { + Serial.println("Init SGP41 success"); + configuration.hasSensorSGP = true; + return true; + } else { + Serial.println("Init SGP41 failuire"); + configuration.hasSensorSGP = false; + } + return false; +} + +static void wifiFactoryConfigure(void) { + WiFi.persistent(true); + WiFi.begin("airgradient", "cleanair"); + WiFi.persistent(false); + oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'"); + delay(2500); + oledDisplay.setText("Rebooting...", "", ""); + delay(2500); + oledDisplay.setText("", "", ""); + ESP.restart(); +} + +static void mqttHandle(void) { + if(mqttClient.isConnected() == false) { + mqttClient.connect(String("airgradient-") + ag.deviceId()); + } + + if (mqttClient.isConnected()) { + String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), + &ag, &configuration); + String topic = "airgradient/readings/" + ag.deviceId(); + if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) { + Serial.println("MQTT sync success"); + } else { + Serial.println("MQTT sync failure"); + } + } +} + +static void sendDataToAg() { + /** Change oledDisplay and led state */ + stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting); + + delay(1500); + if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) { + stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); + } else { + stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed); + } + delay(DISPLAY_DELAY_SHOW_CONTENT_MS); +} + +void dispSensorNotFound(String ss) { + ss = ss + " not found"; + oledDisplay.setText("Sensor init", "Error:", ss.c_str()); + delay(2000); +} + +static void boardInit(void) { + /** Display init */ + oledDisplay.begin(); + + /** Show boot display */ + Serial.println("Firmware Version: " + ag.getVersion()); + + oledDisplay.setText("AirGradient ONE", + "FW Version: ", ag.getVersion().c_str()); + delay(DISPLAY_DELAY_SHOW_CONTENT_MS); + + ag.watchdog.begin(); + + /** Show message init sensor */ + oledDisplay.setText("Sensor", "initializing...", ""); + + /** Init sensor SGP41 */ + if (sgp41Init() == false) { + dispSensorNotFound("SGP41"); + } + + /** Init SHT */ + if (ag.sht.begin(Wire) == false) { + Serial.println("SHTx sensor not found"); + configuration.hasSensorSHT = false; + dispSensorNotFound("SHT"); + } + + /** Init S8 CO2 sensor */ + if (ag.s8.begin(&Serial) == false) { + Serial.println("CO2 S8 sensor not found"); + configuration.hasSensorS8 = false; + dispSensorNotFound("S8"); + } + + /** Init PMS5003 */ + configuration.hasSensorPMS1 = true; + configuration.hasSensorPMS2 = false; + if (ag.pms5003.begin(&Serial) == false) { + Serial.println("PMS sensor not found"); + configuration.hasSensorPMS1 = false; + + dispSensorNotFound("PMS"); + } + + /** Set S8 CO2 abc days period */ + if (configuration.hasSensorS8) { + if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) { + Serial.println("Set S8 AbcDays successful"); + } else { + Serial.println("Set S8 AbcDays failure"); + } + } + + localServer.setFwMode(FW_MODE_I_43PS); +} + +static void failedHandler(String msg) { + while (true) { + Serial.println(msg); + delay(1000); + } +} + +static void configurationUpdateSchedule(void) { + if (apiClient.fetchServerConfiguration()) { + configUpdateHandle(); + } +} + +static void configUpdateHandle() { + if (configuration.isUpdated() == false) { + return; + } + + stateMachine.executeCo2Calibration(); + + String mqttUri = configuration.getMqttBrokerUri(); + if (mqttClient.isCurrentUri(mqttUri) == false) { + mqttClient.end(); + initMqtt(); + } + + if (configuration.hasSensorSGP) { + if (configuration.noxLearnOffsetChanged() || + configuration.tvocLearnOffsetChanged()) { + ag.sgp41.end(); + + int oldTvocOffset = ag.sgp41.getTvocLearningOffset(); + int oldNoxOffset = ag.sgp41.getNoxLearningOffset(); + bool result = sgp41Init(); + const char *resultStr = "successful"; + if (!result) { + resultStr = "failure"; + } + if (oldTvocOffset != configuration.getTvocLearningOffset()) { + Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n", + oldTvocOffset, configuration.getTvocLearningOffset(), + resultStr); + } + if (oldNoxOffset != configuration.getNoxLearningOffset()) { + Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n", + oldNoxOffset, configuration.getNoxLearningOffset(), + resultStr); + } + } + } + + if (configuration.isDisplayBrightnessChanged()) { + oledDisplay.setBrightness(configuration.getDisplayBrightness()); + } + + appDispHandler(); +} + +static void appDispHandler(void) { + AgStateMachineState state = AgStateMachineNormal; + + /** Only show display status on online mode. */ + if (configuration.isOfflineMode() == false) { + if (wifiConnector.isConnected() == false) { + state = AgStateMachineWiFiLost; + } else if (apiClient.isFetchConfigureFailed()) { + state = AgStateMachineSensorConfigFailed; + if (apiClient.isNotAvailableOnDashboard()) { + stateMachine.displaySetAddToDashBoard(); + } else { + stateMachine.displayClearAddToDashBoard(); + } + } else if (apiClient.isPostToServerFailed()) { + state = AgStateMachineServerLost; + } + } + stateMachine.displayHandle(state); +} + +static void oledDisplaySchedule(void) { + + appDispHandler(); +} + +static void updateTvoc(void) { + measurements.TVOC = ag.sgp41.getTvocIndex(); + measurements.TVOCRaw = ag.sgp41.getTvocRaw(); + measurements.NOx = ag.sgp41.getNoxIndex(); + measurements.NOxRaw = ag.sgp41.getNoxRaw(); + + Serial.println(); + Serial.printf("TVOC index: %d\r\n", measurements.TVOC); + Serial.printf("TVOC raw: %d\r\n", measurements.TVOCRaw); + Serial.printf("NOx index: %d\r\n", measurements.NOx); + Serial.printf("NOx raw: %d\r\n", measurements.NOxRaw); +} + +static void updatePm(void) { + if (ag.pms5003.isFailed() == false) { + measurements.pm01_1 = ag.pms5003.getPm01Ae(); + measurements.pm25_1 = ag.pms5003.getPm25Ae(); + measurements.pm10_1 = ag.pms5003.getPm10Ae(); + measurements.pm03PCount_1 = ag.pms5003.getPm03ParticleCount(); + + Serial.println(); + Serial.printf("PM1 ug/m3: %d\r\n", measurements.pm01_1); + Serial.printf("PM2.5 ug/m3: %d\r\n", measurements.pm25_1); + Serial.printf("PM10 ug/m3: %d\r\n", measurements.pm10_1); + Serial.printf("PM0.3 Count: %d\r\n", measurements.pm03PCount_1); + pmFailCount = 0; + } else { + pmFailCount++; + Serial.printf("PMS read failed: %d\r\n", pmFailCount); + if (pmFailCount >= 3) { + measurements.pm01_1 = -1; + measurements.pm25_1 = -1; + measurements.pm10_1 = -1; + measurements.pm03PCount_1 = -1; + } + } +} + +static void sendDataToServer(void) { + /** Ignore send data to server if postToAirGradient disabled */ + if (configuration.isPostDataToAirGradient() == false || + configuration.isOfflineMode()) { + return; + } + + String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), + &ag, &configuration); + if (apiClient.postToServer(syncData)) { + ag.watchdog.reset(); + Serial.println(); + Serial.println( + "Online mode and isPostToAirGradient = true: watchdog reset"); + Serial.println(); + } + + measurements.bootCount++; +} + +static void tempHumUpdate(void) { + delay(100); + if (ag.sht.measure()) { + measurements.Temperature = ag.sht.getTemperature(); + measurements.Humidity = ag.sht.getRelativeHumidity(); + + Serial.printf("Temperature in C: %0.2f\r\n", measurements.Temperature); + Serial.printf("Relative Humidity: %d\r\n", measurements.Humidity); + Serial.printf("Temperature compensated in C: %0.2f\r\n", + measurements.Temperature); + Serial.printf("Relative Humidity compensated: %d\r\n", + measurements.Humidity); + + // Update compensation temperature and humidity for SGP41 + if (configuration.hasSensorSGP) { + ag.sgp41.setCompensationTemperatureHumidity(measurements.Temperature, + measurements.Humidity); + } + } else { + Serial.println("SHT read failed"); + } +} diff --git a/examples/DiyProIndoorV3_7/LocalServer.cpp b/examples/DiyProIndoorV3_7/LocalServer.cpp new file mode 100644 index 0000000..8970ece --- /dev/null +++ b/examples/DiyProIndoorV3_7/LocalServer.cpp @@ -0,0 +1,61 @@ +#include "LocalServer.h" + +LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics, + Measurements &measure, Configuration &config, + WifiConnector &wifiConnector) + : PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure), + config(config), wifiConnector(wifiConnector), server(80) {} + +LocalServer::~LocalServer() {} + +bool LocalServer::begin(void) { + server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); }); + server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); }); + server.on("/config", HTTP_GET, [this]() { _GET_config(); }); + server.on("/config", HTTP_PUT, [this]() { _PUT_config(); }); + server.begin(); + logInfo("Init: " + getHostname() + ".local"); + + return true; +} + +void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; } + +String LocalServer::getHostname(void) { + return "airgradient_" + ag->deviceId(); +} + +void LocalServer::_handle(void) { server.handleClient(); } + +void LocalServer::_GET_config(void) { + if(ag->isOne()) { + server.send(200, "application/json", config.toString()); + } else { + server.send(200, "application/json", config.toString(fwMode)); + } +} + +void LocalServer::_PUT_config(void) { + String data = server.arg(0); + String response = ""; + int statusCode = 400; // Status code for data invalid + if (config.parse(data, true)) { + statusCode = 200; + response = "Success"; + } else { + response = config.getFailedMesage(); + } + server.send(statusCode, "text/plain", response); +} + +void LocalServer::_GET_metrics(void) { + server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload()); +} + +void LocalServer::_GET_measure(void) { + server.send( + 200, "application/json", + measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config)); +} + +void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; } diff --git a/examples/DiyProIndoorV3_7/LocalServer.h b/examples/DiyProIndoorV3_7/LocalServer.h new file mode 100644 index 0000000..1a943b8 --- /dev/null +++ b/examples/DiyProIndoorV3_7/LocalServer.h @@ -0,0 +1,38 @@ +#ifndef _LOCAL_SERVER_H_ +#define _LOCAL_SERVER_H_ + +#include "AgConfigure.h" +#include "AgValue.h" +#include "AirGradient.h" +#include "OpenMetrics.h" +#include "AgWiFiConnector.h" +#include +#include + +class LocalServer : public PrintLog { +private: + AirGradient *ag; + OpenMetrics &openMetrics; + Measurements &measure; + Configuration &config; + WifiConnector &wifiConnector; + ESP8266WebServer server; + AgFirmwareMode fwMode; + +public: + LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure, + Configuration &config, WifiConnector& wifiConnector); + ~LocalServer(); + + bool begin(void); + void setAirGraident(AirGradient *ag); + String getHostname(void); + void setFwMode(AgFirmwareMode fwMode); + void _handle(void); + void _GET_config(void); + void _PUT_config(void); + void _GET_metrics(void); + void _GET_measure(void); +}; + +#endif /** _LOCAL_SERVER_H_ */ diff --git a/examples/DiyProIndoorV3_7/OpenMetrics.cpp b/examples/DiyProIndoorV3_7/OpenMetrics.cpp new file mode 100644 index 0000000..5270768 --- /dev/null +++ b/examples/DiyProIndoorV3_7/OpenMetrics.cpp @@ -0,0 +1,186 @@ +#include "OpenMetrics.h" + +OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config, + WifiConnector &wifiConnector, AgApiClient &apiClient) + : measure(measure), config(config), wifiConnector(wifiConnector), + apiClient(apiClient) {} + +OpenMetrics::~OpenMetrics() {} + +void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; } + +const char *OpenMetrics::getApiContentType(void) { + return "application/openmetrics-text; version=1.0.0; charset=utf-8"; +} + +const char *OpenMetrics::getApi(void) { return "/metrics"; } + +String OpenMetrics::getPayload(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=\"" + ag->deviceId() + + "\",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("", apiClient.isFetchConfigureFailed() ? "0" : "1"); + + add_metric( + "post_ok", + "1 if the AirGradient device was able to successfully send to the server", + "gauge"); + add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1"); + + add_metric( + "wifi_rssi", + "WiFi signal strength from the AirGradient device perspective, in dBm", + "gauge", "dbm"); + add_metric_point("", String(wifiConnector.RSSI())); + + if (config.hasSensorS8 && measure.CO2 >= 0) { + add_metric("co2", + "Carbon dioxide concentration as measured by the AirGradient S8 " + "sensor, in parts per million", + "gauge", "ppm"); + add_metric_point("", String(measure.CO2)); + } + + float _temp = -1001; + float _hum = -1; + int pm01 = -1; + int pm25 = -1; + int pm10 = -1; + int pm03PCount = -1; + int atmpCompensated = -1; + int ahumCompensated = -1; + + if (config.hasSensorSHT) { + _temp = measure.Temperature; + _hum = measure.Humidity; + atmpCompensated = _temp; + ahumCompensated = _hum; + } + + if (config.hasSensorPMS1) { + pm01 = measure.pm01_1; + pm25 = measure.pm25_1; + pm10 = measure.pm10_1; + pm03PCount = measure.pm03PCount_1; + } + + if (config.hasSensorPMS1) { + 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 (config.hasSensorSGP) { + if (measure.TVOC >= 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(measure.TVOC)); + } + if (measure.TVOCRaw >= 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(measure.TVOCRaw)); + } + if (measure.NOx >= 0) { + add_metric("nox_index", + "The processed Nitrous Oxide (NOx) index as measured by the " + "AirGradient SGP sensor", + "gauge"); + add_metric_point("", String(measure.NOx)); + } + if (measure.NOxRaw >= 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(measure.NOxRaw)); + } + } + + if (_temp > -1001) { + add_metric( + "temperature", + "The ambient temperature as measured by the AirGradient SHT / PMS " + "sensor, in degrees Celsius", + "gauge", "celsius"); + add_metric_point("", String(_temp)); + } + if (atmpCompensated > -1001) { + add_metric("temperature_compensated", + "The compensated ambient temperature as measured by the " + "AirGradient SHT / PMS " + "sensor, in degrees Celsius", + "gauge", "celsius"); + add_metric_point("", String(atmpCompensated)); + } + if (_hum >= 0) { + add_metric( + "humidity", + "The relative humidity as measured by the AirGradient SHT sensor", + "gauge", "percent"); + add_metric_point("", String(_hum)); + } + if (ahumCompensated >= 0) { + add_metric("humidity_compensated", + "The compensated relative humidity as measured by the " + "AirGradient SHT / PMS sensor", + "gauge", "percent"); + add_metric_point("", String(ahumCompensated)); + } + + response += "# EOF\n"; + return response; +} diff --git a/examples/DiyProIndoorV3_7/OpenMetrics.h b/examples/DiyProIndoorV3_7/OpenMetrics.h new file mode 100644 index 0000000..ed890f5 --- /dev/null +++ b/examples/DiyProIndoorV3_7/OpenMetrics.h @@ -0,0 +1,28 @@ +#ifndef _OPEN_METRICS_H_ +#define _OPEN_METRICS_H_ + +#include "AgConfigure.h" +#include "AgValue.h" +#include "AgWiFiConnector.h" +#include "AirGradient.h" +#include "AgApiClient.h" + +class OpenMetrics { +private: + AirGradient *ag; + Measurements &measure; + Configuration &config; + WifiConnector &wifiConnector; + AgApiClient &apiClient; + +public: + OpenMetrics(Measurements &measure, Configuration &conig, + WifiConnector &wifiConnector, AgApiClient& apiClient); + ~OpenMetrics(); + void setAirGradient(AirGradient *ag); + const char *getApiContentType(void); + const char* getApi(void); + String getPayload(void); +}; + +#endif /** _OPEN_METRICS_H_ */ diff --git a/src/AgStateMachine.cpp b/src/AgStateMachine.cpp index 39f7462..5cdc9e4 100644 --- a/src/AgStateMachine.cpp +++ b/src/AgStateMachine.cpp @@ -224,7 +224,7 @@ void StateMachine::co2Calibration(void) { /** Count down to 0 then start */ for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) { - if (ag->isOne() || (ag->isPro4_2())) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7()) { String str = "after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec"; disp.setText("Start CO2 calib", str.c_str(), ""); @@ -236,13 +236,13 @@ void StateMachine::co2Calibration(void) { } if (ag->s8.setBaselineCalibration()) { - if (ag->isOne() || (ag->isPro4_2())) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7()) { disp.setText("Calibration", "success", ""); } else { logInfo("CO2 Calibration: success"); } delay(1000); - if (ag->isOne() || (ag->isPro4_2())) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7()) { disp.setText("Wait for", "calib finish", "..."); } else { logInfo("CO2 Calibration: Wait for calibration finish..."); @@ -254,7 +254,7 @@ void StateMachine::co2Calibration(void) { delay(1000); count++; } - if (ag->isOne() || (ag->isPro4_2())) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7()) { String str = "after " + String(count); disp.setText("Calib finish", str.c_str(), "sec"); } else { @@ -262,7 +262,7 @@ void StateMachine::co2Calibration(void) { } delay(2000); } else { - if (ag->isOne() || (ag->isPro4_2())) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7()) { disp.setText("Calibration", "failure!!!", ""); } else { logInfo("CO2 Calibration: failure!!!"); @@ -399,7 +399,7 @@ StateMachine::~StateMachine() {} */ void StateMachine::displayHandle(AgStateMachineState state) { // Ignore handle if not ONE_INDOOR board - if (!(ag->isOne() || (ag->isPro4_2()))) { + if (!(ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7())) { if (state == AgStateMachineCo2Calibration) { co2Calibration(); } diff --git a/src/AgValue.cpp b/src/AgValue.cpp index 7ac5383..2069c57 100644 --- a/src/AgValue.cpp +++ b/src/AgValue.cpp @@ -19,7 +19,7 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, } } - if (ag->isOne() || (ag->isPro4_2())) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7()) { if (config->hasSensorPMS1) { if (this->pm01_1 >= 0) { root["pm01"] = this->pm01_1; diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 348aad7..897d1a0 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -49,7 +49,7 @@ bool WifiConnector::connect(void) { WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); }); WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); }); WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();}); - if (ag->isOne() || (ag->isPro4_2())) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7()) { disp.setText("Connecting to", "WiFi", "..."); } else { logInfo("Connecting to WiFi..."); @@ -142,7 +142,7 @@ bool WifiConnector::connect(void) { /** Show display wifi connect result failed */ if (WiFi.isConnected() == false) { sm.handleLeds(AgStateMachineWiFiManagerConnectFailed); - if (ag->isOne() || ag->isPro4_2()) { + if (ag->isOne() || ag->isPro4_2() || ag->isPro3_7()) { sm.displayHandle(AgStateMachineWiFiManagerConnectFailed); } delay(6000); @@ -247,7 +247,7 @@ void WifiConnector::_wifiProcess() { if (WiFi.isConnected() == false) { /** Display countdown */ uint32_t ms; - if (ag->isOne() || (ag->isPro4_2())) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_7()) { ms = (uint32_t)(millis() - dispPeriod); if (ms >= 1000) { dispPeriod = millis(); diff --git a/src/AirGradient.cpp b/src/AirGradient.cpp index 31365b6..93ecdee 100644 --- a/src/AirGradient.cpp +++ b/src/AirGradient.cpp @@ -62,6 +62,10 @@ bool AirGradient::isPro4_2(void) { return boardType == BoardType::DIY_PRO_INDOOR_V4_2; } +bool AirGradient::isPro3_7(void) { + return boardType == BoardType::DIY_PRO_INDOOR_V3_7; +} + String AirGradient::deviceId(void) { String mac = WiFi.macAddress(); mac.replace(":", ""); diff --git a/src/AirGradient.h b/src/AirGradient.h index b864080..cf875ef 100644 --- a/src/AirGradient.h +++ b/src/AirGradient.h @@ -141,6 +141,13 @@ public: * @return false No */ bool isPro4_2(void); + /** + * @brief Check that Airgradient object is DIY_PRO 3.7 indoor + * + * @return true Yes + * @return false No + */ + bool isPro3_7(void); /** * @brief Get device Id diff --git a/src/Main/BoardDef.cpp b/src/Main/BoardDef.cpp index 738c2e9..95afbe6 100644 --- a/src/Main/BoardDef.cpp +++ b/src/Main/BoardDef.cpp @@ -235,92 +235,168 @@ const BoardDef bsps[_BOARD_MAX] = { .name = "ONE_INDOOR", }, /** OPEN_AIR_OUTDOOR */ - [OPEN_AIR_OUTDOOR] = { - .SenseAirS8 = - { - .uart_tx_pin = 1, - .uart_rx_pin = 0, + [OPEN_AIR_OUTDOOR] = + { + .SenseAirS8 = + { + .uart_tx_pin = 1, + .uart_rx_pin = 0, #if defined(ESP8266) - .supported = false, + .supported = false, #else - .supported = true, + .supported = true, #endif - }, - /** Use UART0 don't use define pin number */ - .Pms5003 = - { - .uart_tx_pin = -1, - .uart_rx_pin = -1, + }, + /** Use UART0 don't use define pin number */ + .Pms5003 = + { + .uart_tx_pin = -1, + .uart_rx_pin = -1, #if defined(ESP8266) - .supported = false, + .supported = false, #else - .supported = true, + .supported = true, #endif - }, - .I2C = - { - .sda_pin = 7, - .scl_pin = 6, + }, + .I2C = + { + .sda_pin = 7, + .scl_pin = 6, #if defined(ESP8266) - .supported = false, + .supported = false, #else - .supported = true, + .supported = true, #endif - }, - .SW = - { + }, + .SW = + { #if defined(ESP8266) - .pin = -1, - .activeLevel = 1, - .supported = false, + .pin = -1, + .activeLevel = 1, + .supported = false, #else - .pin = 9, - .activeLevel = 0, - .supported = true, + .pin = 9, + .activeLevel = 0, + .supported = true, #endif - }, - .LED = - { + }, + .LED = + { #if defined(ESP8266) - .pin = -1, - .rgbNum = 0, - .onState = 0, - .supported = false, - .rgbSupported = false, + .pin = -1, + .rgbNum = 0, + .onState = 0, + .supported = false, + .rgbSupported = false, #else - .pin = 10, - .rgbNum = 0, - .onState = 1, - .supported = true, - .rgbSupported = false, + .pin = 10, + .rgbNum = 0, + .onState = 1, + .supported = true, + .rgbSupported = false, #endif - }, - .OLED = - { + }, + .OLED = + { #if defined(ESP8266) - .width = 0, - .height = 0, - .addr = 0, - .supported = false, + .width = 0, + .height = 0, + .addr = 0, + .supported = false, #else - .width = 128, - .height = 64, - .addr = 0x3C, - .supported = true, + .width = 128, + .height = 64, + .addr = 0x3C, + .supported = true, #endif - }, - .WDG = - { + }, + .WDG = + { #if defined(ESP8266) - .resetPin = -1, - .supported = false, + .resetPin = -1, + .supported = false, #else - .resetPin = 2, - .supported = true, + .resetPin = 2, + .supported = true, #endif - }, - .name = "OPEN_AIR_OUTDOOR", - }}; + }, + .name = "OPEN_AIR_OUTDOOR", + }, + /** DIY_PRO_INDOOR_V3_7 */ + [DIY_PRO_INDOOR_V3_7] = + { + .SenseAirS8 = + { + .uart_tx_pin = 2, + .uart_rx_pin = 0, +#if defined(ESP8266) + .supported = true, +#else + .supported = false, +#endif + }, + .Pms5003 = + { + .uart_tx_pin = 14, + .uart_rx_pin = 12, +#if defined(ESP8266) + .supported = true, +#else + .supported = false, +#endif + }, + .I2C = + { + .sda_pin = 4, + .scl_pin = 5, +#if defined(ESP8266) + .supported = true, +#else + .supported = false, +#endif + }, + .SW = + { +#if defined(ESP8266) + .pin = -1, /** D7 */ + .activeLevel = 0, + .supported = false, +#else + .pin = -1, + .activeLevel = 1, + .supported = false, +#endif + }, + .LED = + { + .pin = -1, + .rgbNum = 0, + .onState = 0, + .supported = false, + .rgbSupported = false, + }, + .OLED = + { +#if defined(ESP8266) + .width = 128, + .height = 64, + .addr = 0x3C, + .supported = true, +#else + .width = 0, + .height = 0, + .addr = 0, + .supported = false, +#endif + }, + .WDG = + { + .resetPin = -1, + .supported = false, + }, + .name = "DIY_PRO_INDOOR_V3_7", + }, +}; /** * @brief Get Board Support Package @@ -337,9 +413,9 @@ const BoardDef *getBoardDef(BoardType def) { /** * @brief Get the Board Name - * + * * @param type BoarType - * @return const char* + * @return const char* */ const char *getBoardDefName(BoardType type) { if (type >= _BOARD_MAX) { diff --git a/src/Main/BoardDef.h b/src/Main/BoardDef.h index 1498f7d..8aa79ea 100644 --- a/src/Main/BoardDef.h +++ b/src/Main/BoardDef.h @@ -21,6 +21,7 @@ enum BoardType { DIY_PRO_INDOOR_V4_2 = 0x01, ONE_INDOOR = 0x02, OPEN_AIR_OUTDOOR = 0x03, + DIY_PRO_INDOOR_V3_7 = 0x04, _BOARD_MAX };