diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index aa51da8a..a3a21c41 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,6 +6,8 @@ jobs: matrix: example: - "BASIC" + - "DiyProIndoorV4_2" + - "DiyProIndoorV3_3" - "TestCO2" - "TestPM" - "TestSht" @@ -23,6 +25,10 @@ jobs: exclude: - example: "BASIC" fqbn: "esp32:esp32:esp32c3" + - example: "DiyProIndoorV4_2" + fqbn: "esp32:esp32:esp32c3" + - example: "DiyProIndoorV3_3" + fqbn: "esp32:esp32:esp32c3" - example: "OneOpenAir" fqbn: "esp8266:esp8266:d1_mini" runs-on: ubuntu-latest diff --git a/README.md b/README.md index a991bafb..375c53fb 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Local server API documentation is available in [/docs/local-server.md](/docs/loc - [Sensirion I2C SHT](https://github.com/Sensirion/arduino-sht) - [WiFiManager](https://github.com/tzapu/WiFiManager) - [Arduino_JSON](https://github.com/arduino-libraries/Arduino_JSON) +- [PubSubClient](https://github.com/knolleary/pubsubclient) ## License CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License diff --git a/examples/BASIC/BASIC.ino b/examples/BASIC/BASIC.ino index eed48180..8928c682 100644 --- a/examples/BASIC/BASIC.ino +++ b/examples/BASIC/BASIC.ino @@ -31,187 +31,375 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License #include "AgConfigure.h" #include "AgSchedule.h" #include "AgWiFiConnector.h" +#include "LocalServer.h" +#include "OpenMetrics.h" +#include "MqttClient.h" #include #include #include +#include #include -#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 5000 /** ms */ -#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */ -#define SERVER_SYNC_INTERVAL 60000 /** ms */ -#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */ -#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */ -#define SENSOR_CO2_UPDATE_INTERVAL 5000 /** ms */ -#define SENSOR_PM_UPDATE_INTERVAL 5000 /** 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 \ - */ - -/** Create airgradient instance for 'DIY_BASIC' board */ -static AirGradient ag = AirGradient(DIY_BASIC); +#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_BASIC); static Configuration configuration(Serial); static AgApiClient apiClient(Serial, configuration); -static WifiConnector wifiConnector(Serial); - -static int co2Ppm = -1; -static int pm25 = -1; -static float temp = -1001; -static int hum = -1; -static long val; +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_BASIC_40PS; + +static String fwNewVersion; static void boardInit(void); static void failedHandler(String msg); -static void executeCo2Calibration(void); -static void updateServerConfiguration(void); -static void co2Update(void); -static void pmUpdate(void); -static void tempHumUpdate(void); +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 dispHandler(void); -static String getDevId(void); -static void showNr(void); - -bool hasSensorS8 = true; -bool hasSensorPMS = true; -bool hasSensorSHT = true; -int pmFailCount = 0; -int getCO2FailCount = 0; +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, - updateServerConfiguration); -AgSchedule serverSchedule(SERVER_SYNC_INTERVAL, sendDataToServer); -AgSchedule dispSchedule(DISP_UPDATE_INTERVAL, dispHandler); + configurationUpdateSchedule); +AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer); AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update); -AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, pmUpdate); +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); - showNr(); + 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); - /** Board init */ - boardInit(); - - /** Init AirGradient server */ - apiClient.begin(); - apiClient.setAirGradient(&ag); configuration.setAirGradient(&ag); + oledDisplay.setAirGradient(&ag); + stateMachine.setAirGradient(&ag); wifiConnector.setAirGradient(&ag); + apiClient.setAirGradient(&ag); + openMetrics.setAirGradient(&ag); + localServer.setAirGraident(&ag); - /** Show boot display */ - displayShowText("DIY basic", "Lib:" + ag.getVersion(), ""); - delay(2000); - - /** WiFi connect */ - // connectToWifi(); - if (wifiConnector.connect()) { - if (WiFi.status() == WL_CONNECTED) { - sendDataToAg(); + /** Init sensor */ + boardInit(); - apiClient.fetchServerConfiguration(); - if (configuration.isCo2CalibrationRequested()) { - executeCo2Calibration(); + /** 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(); + } } } } - /** Show serial number display */ - ag.display.clear(); - ag.display.setCursor(1, 1); - ag.display.setText("Warm Up"); - ag.display.setCursor(1, 15); - ag.display.setText("Serial#"); - ag.display.setCursor(1, 29); - String id = getNormalizedMac(); - Serial.println("Device id: " + id); - String id1 = id.substring(0, 9); - String id2 = id.substring(9, 12); - ag.display.setText("\'" + id1); - ag.display.setCursor(1, 40); - ag.display.setText(id2 + "\'"); - ag.display.show(); - - delay(5000); + /** 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 */ + String sn = "SN:" + ag.deviceId(); + oledDisplay.setText("Warming Up", sn.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(); - serverSchedule.run(); - dispSchedule.run(); - if (hasSensorS8) { + agApiPostSchedule.run(); + + if (configuration.hasSensorS8) { co2Schedule.run(); } - if (hasSensorPMS) { + if (configuration.hasSensorPMS1) { pmsSchedule.run(); + ag.pms5003.handle(); } - if (hasSensorSHT) { + 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(); - /** Read PMS on loop */ - ag.pms5003.handle(); + /** factory reset handle */ + // factoryConfigReset(); + + /** check that local configura changed then do some action */ + configUpdateHandle(); + + localServer._handle(); + + if (configuration.hasSensorSGP) { + ag.sgp41.handle(); + } + + MDNS.update(); + + mqttSchedule.run(); + mqttClient.handle(); } -static void sendDataToAg() { - // delay(1500); - if (apiClient.sendPing(wifiConnector.RSSI(), 0)) { - // Ping Server succses +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 { - // Ping server failed + Serial.println("setup Connect to MQTT broker failed"); } - // delay(DISPLAY_DELAY_SHOW_CONTENT_MS); } -void displayShowText(String ln1, String ln2, String ln3) { - char buf[9]; - ag.display.clear(); +static void wdgFeedUpdate(void) { + ag.watchdog.reset(); + Serial.println(); + Serial.println("Offline mode or isPostToAirGradient = false: watchdog reset"); + Serial.println(); +} - ag.display.setCursor(1, 1); - ag.display.setText(ln1); - ag.display.setCursor(1, 19); - ag.display.setText(ln2); - ag.display.setCursor(1, 37); - ag.display.setText(ln3); +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; +} - ag.display.show(); - delay(100); +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) { + oledDisplay.setText("Sensor", ss.c_str(), "not found"); + delay(2000); } static void boardInit(void) { - /** Init SHT sensor */ + /** Display init */ + oledDisplay.begin(); + + /** Show boot display */ + Serial.println("Firmware Version: " + ag.getVersion()); + + if (ag.isBasic()) { + oledDisplay.setText("DIY Basic", ag.getVersion().c_str(), ""); + } else { + 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", "init...", ""); + + /** Init sensor SGP41 */ + configuration.hasSensorSGP = false; + // if (sgp41Init() == false) { + // dispSensorNotFound("SGP41"); + // } + + /** Init SHT */ if (ag.sht.begin(Wire) == false) { - hasSensorSHT = false; - Serial.println("SHT sensor not found"); + Serial.println("SHTx sensor not found"); + configuration.hasSensorSHT = false; + dispSensorNotFound("SHT"); } - /** CO2 init */ + /** Init S8 CO2 sensor */ if (ag.s8.begin(&Serial) == false) { - Serial.println("CO2 S8 snsor not found"); - hasSensorS8 = false; + Serial.println("CO2 S8 sensor not found"); + configuration.hasSensorS8 = false; + dispSensorNotFound("S8"); } - /** PMS init */ + /** Init PMS5003 */ + configuration.hasSensorPMS1 = true; + configuration.hasSensorPMS2 = false; if (ag.pms5003.begin(&Serial) == false) { Serial.println("PMS sensor not found"); - hasSensorPMS = false; + configuration.hasSensorPMS1 = false; + + dispSensorNotFound("PMS"); } - /** Display init */ - ag.display.begin(Wire); - ag.display.setTextColor(1); - ag.display.clear(); - ag.display.show(); - delay(100); + /** 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(fwMode); } static void failedHandler(String msg) { @@ -221,181 +409,160 @@ static void failedHandler(String msg) { } } -static void executeCo2Calibration(void) { - /** Count down for co2CalibCountdown secs */ - for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) { - displayShowText("CO2 calib.", "after", - String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec"); - delay(1000); +static void configurationUpdateSchedule(void) { + if (apiClient.fetchServerConfiguration()) { + configUpdateHandle(); } +} - if (ag.s8.setBaselineCalibration()) { - displayShowText("Calib", "success", ""); - delay(1000); - displayShowText("Wait to", "complete", "..."); - int count = 0; - while (ag.s8.isBaseLineCalibrationDone() == false) { - delay(1000); - count++; - } - displayShowText("Finished", "after", String(count) + " sec"); - delay(DISPLAY_DELAY_SHOW_CONTENT_MS); - } else { - displayShowText("Calibration", "failure", ""); - delay(DISPLAY_DELAY_SHOW_CONTENT_MS); +static void configUpdateHandle() { + if (configuration.isUpdated() == false) { + return; } -} -static void updateServerConfiguration(void) { - if (apiClient.fetchServerConfiguration()) { - if (configuration.isCo2CalibrationRequested()) { - if (hasSensorS8) { - executeCo2Calibration(); - } else { - Serial.println("CO2 S8 not available, calib ignored"); + 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 (configuration.getCO2CalibrationAbcDays() > 0) { - if (hasSensorS8) { - int newHour = configuration.getCO2CalibrationAbcDays() * 24; - Serial.printf("abcDays config: %d days(%d hours)\r\n", - configuration.getCO2CalibrationAbcDays(), newHour); - int curHour = ag.s8.getAbcPeriod(); - Serial.printf("Current config: %d (hours)\r\n", curHour); - if (curHour == newHour) { - Serial.println("set 'abcDays' ignored"); - } else { - if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * - 24) == false) { - Serial.println("Set S8 abcDays period calib failed"); - } else { - Serial.println("Set S8 abcDays period calib success"); - } - } - } else { - Serial.println("CO2 S8 not available, set 'abcDays' ignored"); + 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); } } } -} -static void co2Update() { - int value = ag.s8.getCo2(); - if (value >= 0) { - co2Ppm = value; - getCO2FailCount = 0; - Serial.printf("CO2 index: %d\r\n", co2Ppm); - } else { - getCO2FailCount++; - Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount); - if (getCO2FailCount >= 3) { - co2Ppm = -1; - } + if (configuration.isDisplayBrightnessChanged()) { + oledDisplay.setBrightness(configuration.getDisplayBrightness()); } + + appDispHandler(); } -void pmUpdate() { - if (ag.pms5003.isFailed() == false) { - pm25 = ag.pms5003.getPm25Ae(); - Serial.printf("PMS2.5: %d\r\n", pm25); - pmFailCount = 0; - } else { - Serial.printf("PM read failed, %d", pmFailCount); - pmFailCount++; - if (pmFailCount >= 3) { - pm25 = -1; +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 tempHumUpdate() { - if (ag.sht.measure()) { - temp = ag.sht.getTemperature(); - hum = ag.sht.getRelativeHumidity(); - Serial.printf("Temperature: %0.2f\r\n", temp); - Serial.printf(" Humidity: %d\r\n", hum); - } else { - Serial.println("Meaure SHT failed"); - } +static void oledDisplaySchedule(void) { + + appDispHandler(); } -static void sendDataToServer() { - String wifi = "\"wifi\":" + String(WiFi.RSSI()); - String rco2 = ""; - if(co2Ppm >= 0){ - rco2 = ",\"rco2\":" + String(co2Ppm); - } - String pm02 = ""; - if(pm25) { - pm02 = ",\"pm02\":" + String(pm25); - } - String rhum = ""; - if(hum >= 0){ - rhum = ",\"rhum\":" + String(rhum); - } - String payload = "{" + wifi + rco2 + pm02 + rhum + "}"; +static void updateTvoc(void) { + measurements.TVOC = ag.sgp41.getTvocIndex(); + measurements.TVOCRaw = ag.sgp41.getTvocRaw(); + measurements.NOx = ag.sgp41.getNoxIndex(); + measurements.NOxRaw = ag.sgp41.getNoxRaw(); - if (apiClient.postToServer(payload) == false) { - Serial.println("Post to server failed"); - } + 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 dispHandler() { - String ln1 = ""; - String ln2 = ""; - String ln3 = ""; - - if (configuration.isPmStandardInUSAQI()) { - if (pm25 < 0) { - ln1 = "AQI: -"; - } else { - ln1 = "AQI:" + String(ag.pms5003.convertPm25ToUsAqi(pm25)); - } +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 { - if (pm25 < 0) { - ln1 = "PM :- ug"; - - } else { - ln1 = "PM :" + String(pm25) + " ug"; + 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; } } - if (co2Ppm > -1001) { - ln2 = "CO2:" + String(co2Ppm); - } else { - ln2 = "CO2: -"; +} + +static void sendDataToServer(void) { + /** Ignore send data to server if postToAirGradient disabled */ + if (configuration.isPostDataToAirGradient() == false || + configuration.isOfflineMode()) { + return; } - String _hum = "-"; - if (hum > 0) { - _hum = String(hum); + 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(); } - String _temp = "-"; + measurements.bootCount++; +} - if (configuration.isTemperatureUnitInF()) { - if (temp > -1001) { - _temp = String((temp * 9 / 5) + 32).substring(0, 4); +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); } - ln3 = _temp + " " + _hum + "%"; } else { - if (temp > -1001) { - _temp = String(temp).substring(0, 4); - } - ln3 = _temp + " " + _hum + "%"; + Serial.println("SHT read failed"); } - displayShowText(ln1, ln2, ln3); -} - -static String getDevId(void) { return getNormalizedMac(); } - -static void showNr(void) { - Serial.println(); - Serial.println("Serial nr: " + getDevId()); -} - -String getNormalizedMac() { - String mac = WiFi.macAddress(); - mac.replace(":", ""); - mac.toLowerCase(); - return mac; } diff --git a/examples/BASIC/LocalServer.cpp b/examples/BASIC/LocalServer.cpp new file mode 100644 index 00000000..8970ece1 --- /dev/null +++ b/examples/BASIC/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/BASIC/LocalServer.h b/examples/BASIC/LocalServer.h new file mode 100644 index 00000000..1a943b8d --- /dev/null +++ b/examples/BASIC/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/BASIC/OpenMetrics.cpp b/examples/BASIC/OpenMetrics.cpp new file mode 100644 index 00000000..52707683 --- /dev/null +++ b/examples/BASIC/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/BASIC/OpenMetrics.h b/examples/BASIC/OpenMetrics.h new file mode 100644 index 00000000..ed890f57 --- /dev/null +++ b/examples/BASIC/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/examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino b/examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino new file mode 100644 index 00000000..d1eaa763 --- /dev/null +++ b/examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino @@ -0,0 +1,620 @@ +/* +This is the code for the AirGradient DIY PRO 3.3 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_3); +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_33PS; + +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(); + + if (configuration.hasSensorSGP) { + 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_33PS); +} + +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_3/LocalServer.cpp b/examples/DiyProIndoorV3_3/LocalServer.cpp new file mode 100644 index 00000000..8970ece1 --- /dev/null +++ b/examples/DiyProIndoorV3_3/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_3/LocalServer.h b/examples/DiyProIndoorV3_3/LocalServer.h new file mode 100644 index 00000000..1a943b8d --- /dev/null +++ b/examples/DiyProIndoorV3_3/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_3/OpenMetrics.cpp b/examples/DiyProIndoorV3_3/OpenMetrics.cpp new file mode 100644 index 00000000..52707683 --- /dev/null +++ b/examples/DiyProIndoorV3_3/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_3/OpenMetrics.h b/examples/DiyProIndoorV3_3/OpenMetrics.h new file mode 100644 index 00000000..ed890f57 --- /dev/null +++ b/examples/DiyProIndoorV3_3/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/examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino b/examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino new file mode 100644 index 00000000..3b167951 --- /dev/null +++ b/examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino @@ -0,0 +1,663 @@ +/* +This is the code for the AirGradient DIY PRO 4.2 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_V4_2); +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 uint32_t factoryBtnPressTime = 0; +static int getCO2FailCount = 0; +static AgFirmwareMode fwMode = FW_MODE_I_42PS; + +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; + + /** Show message confirm offline mode, should me perform if LED bar button + * test pressed */ + + oledDisplay.setText( + "Press now for", + configuration.isOfflineMode() ? "online mode" : "offline mode", ""); + uint32_t startTime = millis(); + while (true) { + if (ag.button.getState() == ag.button.BUTTON_PRESSED) { + configuration.setOfflineMode(!configuration.isOfflineMode()); + + oledDisplay.setText( + "Offline Mode", + configuration.isOfflineMode() ? " = True" : " = False", ""); + delay(1000); + break; + } + uint32_t periodMs = (uint32_t)(millis() - startTime); + if (periodMs >= 3000) { + Serial.println("Set for offline mode timeout"); + break; + } + + delay(1); + } + 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(); + + if (configuration.hasSensorSGP) { + 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 (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; + } +} + +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.button.begin(); + ag.watchdog.begin(); + + /** Run LED test on start up if button pressed */ + oledDisplay.setText("Press now for", "factory WiFi", "configure"); + + uint32_t stime = millis(); + while (true) { + if (ag.button.getState() == ag.button.BUTTON_PRESSED) { + wifiFactoryConfigure(); + } + delay(1); + uint32_t ms = (uint32_t)(millis() - stime); + if (ms >= 3000) { + break; + } + delay(1); + } + + /** 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_42PS); +} + +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) { + if (factoryBtnPressTime == 0) { + 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/DiyProIndoorV4_2/LocalServer.cpp b/examples/DiyProIndoorV4_2/LocalServer.cpp new file mode 100644 index 00000000..8970ece1 --- /dev/null +++ b/examples/DiyProIndoorV4_2/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/DiyProIndoorV4_2/LocalServer.h b/examples/DiyProIndoorV4_2/LocalServer.h new file mode 100644 index 00000000..1a943b8d --- /dev/null +++ b/examples/DiyProIndoorV4_2/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/DiyProIndoorV4_2/OpenMetrics.cpp b/examples/DiyProIndoorV4_2/OpenMetrics.cpp new file mode 100644 index 00000000..52707683 --- /dev/null +++ b/examples/DiyProIndoorV4_2/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/DiyProIndoorV4_2/OpenMetrics.h b/examples/DiyProIndoorV4_2/OpenMetrics.h new file mode 100644 index 00000000..ed890f57 --- /dev/null +++ b/examples/DiyProIndoorV4_2/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/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index ecda5c91..96633a63 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -221,8 +221,11 @@ void setup() { if (apiClient.isFetchConfigureFailed()) { if (ag->isOne()) { if (apiClient.isNotAvailableOnDashboard()) { + stateMachine.displaySetAddToDashBoard(); stateMachine.displayHandle( AgStateMachineWiFiOkServerOkSensorConfigFailed); + } else { + stateMachine.displayClearAddToDashBoard(); } } stateMachine.handleLeds( @@ -949,7 +952,6 @@ static void appLedHandler(void) { if (wifiConnector.isConnected() == false) { state = AgStateMachineWiFiLost; } else if (apiClient.isFetchConfigureFailed()) { - stateMachine.displaySetAddToDashBoard(); state = AgStateMachineSensorConfigFailed; } else if (apiClient.isPostToServerFailed()) { state = AgStateMachineServerLost; @@ -969,6 +971,11 @@ static void appDispHandler(void) { state = AgStateMachineWiFiLost; } else if (apiClient.isFetchConfigureFailed()) { state = AgStateMachineSensorConfigFailed; + if (apiClient.isNotAvailableOnDashboard()) { + stateMachine.displaySetAddToDashBoard(); + } else { + stateMachine.displayClearAddToDashBoard(); + } } else if (apiClient.isPostToServerFailed()) { state = AgStateMachineServerLost; } diff --git a/library.properties b/library.properties index 2d349562..a1065af5 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=AirGradient Air Quality Sensor -version=3.1.3 +version=3.1.4 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/platformio.ini b/platformio.ini index fed429cd..09e2b026 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,6 @@ lib_deps = WiFiClientSecure Update DNSServer -monitor_filters = time [env:esp8266] platform = espressif8266 @@ -45,6 +44,8 @@ monitor_filters = time [platformio] src_dir = examples/OneOpenAir ; src_dir = examples/BASIC +; src_dir = examples/DiyProIndoorV4_2 +; src_dir = examples/DiyProIndoorV3_3 ; src_dir = examples/TestCO2 ; src_dir = examples/TestPM ; src_dir = examples/TestSht diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 0173edb5..4518f244 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -43,7 +43,7 @@ JSON_PROP_DEF(ledBarTestRequested); JSON_PROP_DEF(offlineMode); #define jprop_model_default "" -#define jprop_country_default "" +#define jprop_country_default "TH" #define jprop_pmStandard_default getPMStandardString(false) #define jprop_ledBarMode_default getLedBarModeName(LedBarMode::LedBarModeCO2) #define jprop_abcDays_default 8 @@ -153,9 +153,15 @@ void Configuration::defaultConfig(void) { jconfig[jprop_pmStandard] = jprop_pmStandard_default; jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default; jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default; - jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default; - jconfig[jprop_displayBrightness] = jprop_displayBrightness_default; - jconfig[jprop_ledBarMode] = jprop_ledBarBrightness_default; + if (ag->isOne()) { + jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default; + } + if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2() || ag->isBasic()) { + jconfig[jprop_displayBrightness] = jprop_displayBrightness_default; + } + if (ag->isOne()) { + jconfig[jprop_ledBarMode] = jprop_ledBarBrightness_default; + } jconfig[jprop_tvocLearningOffset] = jprop_tvocLearningOffset_default; jconfig[jprop_noxLearningOffset] = jprop_noxLearningOffset_default; jconfig[jprop_abcDays] = jprop_abcDays_default; @@ -235,7 +241,10 @@ bool Configuration::parse(String data, bool isLocal) { bool changed = false; /** Get ConfigurationControl */ - String lasCtrl = jconfig[jprop_configurationControl]; + String lastCtrl = jconfig[jprop_configurationControl]; + const char *msg = "Monitor set to accept only configuration from the " + "cloud. Use property configurationControl to change."; + if (isLocal) { if (JSON.typeof_(root[jprop_configurationControl]) == "string") { String ctrl = root[jprop_configurationControl]; @@ -248,9 +257,19 @@ bool Configuration::parse(String data, bool isLocal) { ctrl == String(CONFIGURATION_CONTROL_NAME [ConfigurationControl::ConfigurationControlCloud])) { - if (ctrl != lasCtrl) { + if (ctrl != lastCtrl) { jconfig[jprop_configurationControl] = ctrl; - changed = true; + saveConfig(); + configLogInfo(String(jprop_configurationControl), lastCtrl, + jconfig[jprop_configurationControl]); + } + + /** Check to return result if configurationControl is 'cloud' */ + if (ctrl == + String(CONFIGURATION_CONTROL_NAME + [ConfigurationControl::ConfigurationControlCloud])) { + failedMessage = String(msg); + return true; } } else { failedMessage = @@ -267,18 +286,11 @@ bool Configuration::parse(String data, bool isLocal) { } } - if (changed) { - changed = false; - saveConfig(); - configLogInfo(String(jprop_configurationControl), lasCtrl, - jconfig[jprop_configurationControl]); - } - + /** Ignore all configuration value if 'configurationControl' is 'cloud' */ if (jconfig[jprop_configurationControl] == String(CONFIGURATION_CONTROL_NAME [ConfigurationControl::ConfigurationControlCloud])) { - failedMessage = "Monitor set to accept only configuration from the " - "cloud. Use property configurationControl to change."; + failedMessage = String(msg); jsonInvalid(); return false; } @@ -614,15 +626,18 @@ bool Configuration::parse(String data, bool isLocal) { } } - if (JSON.typeof_(root["targetFirmware"]) == "string") { - String newVer = root["targetFirmware"]; - String curVer = String(GIT_VERSION); - if (curVer != newVer) { - logInfo("Detected new firmware version: " + newVer); - otaNewFirmwareVersion = newVer; - udpated = true; - } else { - otaNewFirmwareVersion = String(""); + if (ag->getBoardType() == ONE_INDOOR || + ag->getBoardType() == OPEN_AIR_OUTDOOR) { + if (JSON.typeof_(root["targetFirmware"]) == "string") { + String newVer = root["targetFirmware"]; + String curVer = String(GIT_VERSION); + if (curVer != newVer) { + logInfo("Detected new firmware version: " + newVer); + otaNewFirmwareVersion = newVer; + udpated = true; + } else { + otaNewFirmwareVersion = String(""); + } } } diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index 329d56ff..22d25b2b 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -6,8 +6,8 @@ /** * @brief Show dashboard temperature and humdity - * - * @param hasStatus + * + * @param hasStatus */ void OledDisplay::showTempHum(bool hasStatus) { char buf[10]; @@ -58,20 +58,20 @@ void OledDisplay::setCentralText(int y, const char *text) { DISP()->drawStr(x, y, text); } - /** * @brief Construct a new Ag Oled Display:: Ag Oled Display object - * + * * @param config AgConfiguration * @param value Measurements * @param log Serial Stream */ -OledDisplay::OledDisplay(Configuration &config, Measurements &value, Stream &log) +OledDisplay::OledDisplay(Configuration &config, Measurements &value, + Stream &log) : PrintLog(log, "OledDisplay"), config(config), value(value) {} /** * @brief Set AirGradient instance - * + * * @param ag Point to AirGradient instance */ void OledDisplay::setAirGradient(AirGradient *ag) { this->ag = ag; } @@ -90,23 +90,31 @@ bool OledDisplay::begin(void) { return true; } - /** Create u8g2 instance */ - u8g2 = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, U8X8_PIN_NONE); - if (u8g2 == NULL) { - logError("Create 'U8G2' failed"); - return false; - } + if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) { + /** Create u8g2 instance */ + u8g2 = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, U8X8_PIN_NONE); + if (u8g2 == NULL) { + logError("Create 'U8G2' failed"); + return false; + } - /** Init u8g2 */ - if (DISP()->begin() == false) { - logError("U8G2 'begin' failed"); - return false; + /** Init u8g2 */ + if (DISP()->begin() == false) { + logError("U8G2 'begin' failed"); + return false; + } + } else if (ag->isBasic()) { + logInfo("DIY_BASIC init"); + ag->display.begin(Wire); + ag->display.setTextColor(1); + ag->display.clear(); + ag->display.show(); } /** Show low brightness on startup. then it's completely turn off on main * application */ int brightness = config.getDisplayBrightness(); - if(brightness == 0) { + if (brightness == 0) { setBrightness(1); } @@ -125,9 +133,13 @@ void OledDisplay::end(void) { return; } - /** Free u8g2 */ - delete DISP(); - u8g2 = NULL; + if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) { + /** Free u8g2 */ + delete DISP(); + u8g2 = NULL; + } else if (ag->isBasic()) { + ag->display.end(); + } isBegin = false; logInfo("end"); @@ -135,10 +147,10 @@ void OledDisplay::end(void) { /** * @brief Show text on 3 line of display - * - * @param line1 - * @param line2 - * @param line3 + * + * @param line1 + * @param line2 + * @param line3 */ void OledDisplay::setText(String &line1, String &line2, String &line3) { setText(line1.c_str(), line2.c_str(), line3.c_str()); @@ -146,190 +158,256 @@ void OledDisplay::setText(String &line1, String &line2, String &line3) { /** * @brief Show text on 3 line of display - * - * @param line1 - * @param line2 - * @param line3 + * + * @param line1 + * @param line2 + * @param line3 */ void OledDisplay::setText(const char *line1, const char *line2, - const char *line3) { + const char *line3) { if (isDisplayOff) { return; } - DISP()->firstPage(); - do { - DISP()->setFont(u8g2_font_t0_16_tf); - DISP()->drawStr(1, 10, line1); - DISP()->drawStr(1, 30, line2); - DISP()->drawStr(1, 50, line3); - } while (DISP()->nextPage()); + if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) { + DISP()->firstPage(); + do { + DISP()->setFont(u8g2_font_t0_16_tf); + DISP()->drawStr(1, 10, line1); + DISP()->drawStr(1, 30, line2); + DISP()->drawStr(1, 50, line3); + } while (DISP()->nextPage()); + } else if (ag->isBasic()) { + ag->display.clear(); + + ag->display.setCursor(1, 1); + ag->display.setText(line1); + ag->display.setCursor(1, 17); + ag->display.setText(line2); + ag->display.setCursor(1, 33); + ag->display.setText(line3); + + ag->display.show(); + } } /** * @brief Set Text on 4 line - * - * @param line1 - * @param line2 - * @param line3 - * @param line4 + * + * @param line1 + * @param line2 + * @param line3 + * @param line4 */ void OledDisplay::setText(String &line1, String &line2, String &line3, - String &line4) { + String &line4) { setText(line1.c_str(), line2.c_str(), line3.c_str(), line4.c_str()); } /** * @brief Set Text on 4 line - * - * @param line1 - * @param line2 - * @param line3 - * @param line4 + * + * @param line1 + * @param line2 + * @param line3 + * @param line4 */ void OledDisplay::setText(const char *line1, const char *line2, - const char *line3, const char *line4) { + const char *line3, const char *line4) { if (isDisplayOff) { return; } - DISP()->firstPage(); - do { - DISP()->setFont(u8g2_font_t0_16_tf); - DISP()->drawStr(1, 10, line1); - DISP()->drawStr(1, 25, line2); - DISP()->drawStr(1, 40, line3); - DISP()->drawStr(1, 55, line4); - } while (DISP()->nextPage()); + if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) { + DISP()->firstPage(); + do { + DISP()->setFont(u8g2_font_t0_16_tf); + DISP()->drawStr(1, 10, line1); + DISP()->drawStr(1, 25, line2); + DISP()->drawStr(1, 40, line3); + DISP()->drawStr(1, 55, line4); + } while (DISP()->nextPage()); + } else if (ag->isBasic()) { + ag->display.clear(); + ag->display.setCursor(0, 0); + ag->display.setText(line1); + ag->display.setCursor(0, 10); + ag->display.setText(line2); + ag->display.setCursor(0, 20); + ag->display.setText(line3); + ag->display.show(); + } } /** * @brief Update dashboard content - * + * */ void OledDisplay::showDashboard(void) { showDashboard(NULL); } /** * @brief Update dashboard content and error status - * + * */ void OledDisplay::showDashboard(const char *status) { if (isDisplayOff) { return; } - char strBuf[10]; + char strBuf[16]; + if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) { + DISP()->firstPage(); + do { + DISP()->setFont(u8g2_font_t0_16_tf); + if ((status == NULL) || (strlen(status) == 0)) { + showTempHum(false); + } else { + String strStatus = "Show status: " + String(status); + logInfo(strStatus); + + int strWidth = DISP()->getStrWidth(status); + DISP()->drawStr((DISP()->getWidth() - strWidth) / 2, 10, status); - DISP()->firstPage(); - do { - DISP()->setFont(u8g2_font_t0_16_tf); - if ((status == NULL) || (strlen(status) == 0)) { - showTempHum(false); - } else { - String strStatus = "Show status: " + String(status); - logInfo(strStatus); + /** Show WiFi NA*/ + if (strcmp(status, "WiFi N/A") == 0) { + DISP()->setFont(u8g2_font_t0_12_tf); + showTempHum(true); + } + } - int strWidth = DISP()->getStrWidth(status); - DISP()->drawStr((DISP()->getWidth() - strWidth) / 2, 10, status); + /** Draw horizonal line */ + DISP()->drawLine(1, 13, 128, 13); - /** Show WiFi NA*/ - if (strcmp(status, "WiFi N/A") == 0) { - DISP()->setFont(u8g2_font_t0_12_tf); - showTempHum(true); + /** Show CO2 label */ + DISP()->setFont(u8g2_font_t0_12_tf); + DISP()->drawUTF8(1, 27, "CO2"); + + DISP()->setFont(u8g2_font_t0_22b_tf); + if (value.CO2 > 0) { + int val = 9999; + if (value.CO2 < 10000) { + val = value.CO2; + } + sprintf(strBuf, "%d", val); + } else { + sprintf(strBuf, "%s", "-"); } - } + DISP()->drawStr(1, 48, strBuf); - /** Draw horizonal line */ - DISP()->drawLine(1, 13, 128, 13); + /** Show CO2 value index */ + DISP()->setFont(u8g2_font_t0_12_tf); + DISP()->drawStr(1, 61, "ppm"); - /** Show CO2 label */ - DISP()->setFont(u8g2_font_t0_12_tf); - DISP()->drawUTF8(1, 27, "CO2"); + /** Draw vertical line */ + DISP()->drawLine(45, 14, 45, 64); + DISP()->drawLine(82, 14, 82, 64); - DISP()->setFont(u8g2_font_t0_22b_tf); - if (value.CO2 > 0) { - int val = 9999; - if (value.CO2 < 10000) { - val = value.CO2; + /** Draw PM2.5 label */ + DISP()->setFont(u8g2_font_t0_12_tf); + DISP()->drawStr(48, 27, "PM2.5"); + + /** Draw PM2.5 value */ + DISP()->setFont(u8g2_font_t0_22b_tf); + if (config.isPmStandardInUSAQI()) { + if (value.pm25_1 >= 0) { + sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(value.pm25_1)); + } else { + sprintf(strBuf, "%s", "-"); + } + DISP()->drawStr(48, 48, strBuf); + DISP()->setFont(u8g2_font_t0_12_tf); + DISP()->drawUTF8(48, 61, "AQI"); + } else { + if (value.pm25_1 >= 0) { + sprintf(strBuf, "%d", value.pm25_1); + } else { + sprintf(strBuf, "%s", "-"); + } + DISP()->drawStr(48, 48, strBuf); + DISP()->setFont(u8g2_font_t0_12_tf); + DISP()->drawUTF8(48, 61, "ug/m³"); } - sprintf(strBuf, "%d", val); - } else { - sprintf(strBuf, "%s", "-"); - } - DISP()->drawStr(1, 48, strBuf); - /** Show CO2 value index */ - DISP()->setFont(u8g2_font_t0_12_tf); - DISP()->drawStr(1, 61, "ppm"); - - /** Draw vertical line */ - DISP()->drawLine(45, 14, 45, 64); - DISP()->drawLine(82, 14, 82, 64); + /** Draw tvocIndexlabel */ + DISP()->setFont(u8g2_font_t0_12_tf); + DISP()->drawStr(85, 27, "tvoc:"); - /** Draw PM2.5 label */ - DISP()->setFont(u8g2_font_t0_12_tf); - DISP()->drawStr(48, 27, "PM2.5"); + /** Draw tvocIndexvalue */ + if (value.TVOC >= 0) { + sprintf(strBuf, "%d", value.TVOC); + } else { + sprintf(strBuf, "%s", "-"); + } + DISP()->drawStr(85, 39, strBuf); - /** Draw PM2.5 value */ - DISP()->setFont(u8g2_font_t0_22b_tf); - if (config.isPmStandardInUSAQI()) { - if (value.pm25_1 >= 0) { - sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(value.pm25_1)); + /** Draw NOx label */ + DISP()->drawStr(85, 53, "NOx:"); + if (value.NOx >= 0) { + sprintf(strBuf, "%d", value.NOx); } else { sprintf(strBuf, "%s", "-"); } - DISP()->drawStr(48, 48, strBuf); - DISP()->setFont(u8g2_font_t0_12_tf); - DISP()->drawUTF8(48, 61, "AQI"); + DISP()->drawStr(85, 63, strBuf); + } while (DISP()->nextPage()); + } else if (ag->isBasic()) { + ag->display.clear(); + + /** Set CO2 */ + snprintf(strBuf, sizeof(strBuf), "CO2:%d", value.CO2); + ag->display.setCursor(0, 0); + ag->display.setText(strBuf); + + /** Set PM */ + ag->display.setCursor(0, 12); + snprintf(strBuf, sizeof(strBuf), "PM2.5:%d", value.pm25_1); + ag->display.setText(strBuf); + + /** Set temperature and humidity */ + if (value.Temperature <= -1001.0f) { + if (config.isTemperatureUnitInF()) { + snprintf(strBuf, sizeof(strBuf), "T:-F"); + } else { + snprintf(strBuf, sizeof(strBuf), "T:-C"); + } } else { - if (value.pm25_1 >= 0) { - sprintf(strBuf, "%d", value.pm25_1); + if (config.isTemperatureUnitInF()) { + float tempF = (value.Temperature * 9) / 5 + 32; + snprintf(strBuf, sizeof(strBuf), "T:%d F", (int)tempF); } else { - sprintf(strBuf, "%s", "-"); + snprintf(strBuf, sizeof(strBuf), "T:%d C", (int)value.Temperature); } - DISP()->drawStr(48, 48, strBuf); - DISP()->setFont(u8g2_font_t0_12_tf); - DISP()->drawUTF8(48, 61, "ug/m³"); } + ag->display.setCursor(0, 24); + ag->display.setText(strBuf); - /** Draw tvocIndexlabel */ - DISP()->setFont(u8g2_font_t0_12_tf); - DISP()->drawStr(85, 27, "tvoc:"); - - /** Draw tvocIndexvalue */ - if (value.TVOC >= 0) { - sprintf(strBuf, "%d", value.TVOC); - } else { - sprintf(strBuf, "%s", "-"); - } - DISP()->drawStr(85, 39, strBuf); + snprintf(strBuf, sizeof(strBuf), "H:%d %%", (int)value.Humidity); + ag->display.setCursor(0, 36); + ag->display.setText(strBuf); - /** Draw NOx label */ - DISP()->drawStr(85, 53, "NOx:"); - if (value.NOx >= 0) { - sprintf(strBuf, "%d", value.NOx); - } else { - sprintf(strBuf, "%s", "-"); - } - DISP()->drawStr(85, 63, strBuf); - } while (DISP()->nextPage()); + ag->display.show(); + } } void OledDisplay::setBrightness(int percent) { - if (percent == 0) { - isDisplayOff = true; + if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) { + if (percent == 0) { + isDisplayOff = true; - // Clear display. - DISP()->firstPage(); - do { - } while (DISP()->nextPage()); + // Clear display. + DISP()->firstPage(); + do { + } while (DISP()->nextPage()); - } else { - isDisplayOff = false; - DISP()->setContrast((127 * percent) / 100); + } else { + isDisplayOff = false; + DISP()->setContrast((127 * percent) / 100); + } + } else if (ag->isBasic()) { + ag->display.setContrast((255 * percent) / 100); } } +#ifdef ESP32 void OledDisplay::showFirmwareUpdateVersion(String version) { if (isDisplayOff) { return; @@ -410,13 +488,25 @@ void OledDisplay::showFirmwareUpdateUpToDate(void) { setCentralText(40, "up to date"); } while (DISP()->nextPage()); } +#else + +#endif void OledDisplay::showRebooting(void) { - DISP()->firstPage(); - do { - DISP()->setFont(u8g2_font_t0_16_tf); - // setCentralText(20, "Firmware Update"); - setCentralText(40, "Rebooting..."); - // setCentralText(60, String("Retry after 24h")); - } while (DISP()->nextPage()); + if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) { + DISP()->firstPage(); + do { + DISP()->setFont(u8g2_font_t0_16_tf); + // setCentralText(20, "Firmware Update"); + setCentralText(40, "Reboot..."); + // setCentralText(60, String("Retry after 24h")); + } while (DISP()->nextPage()); + } else if (ag->isBasic()) { + ag->display.clear(); + + ag->display.setCursor(0, 20); + ag->display.setText("Rebooting..."); + + ag->display.show(); + } } diff --git a/src/AgOledDisplay.h b/src/AgOledDisplay.h index 85bd2e00..28a0cbad 100644 --- a/src/AgOledDisplay.h +++ b/src/AgOledDisplay.h @@ -36,12 +36,16 @@ class OledDisplay : public PrintLog { void showDashboard(void); void showDashboard(const char *status); void setBrightness(int percent); +#ifdef ESP32 void showFirmwareUpdateVersion(String version); void showFirmwareUpdateProgress(int percent); void showFirmwareUpdateSuccess(int count); void showFirmwareUpdateFailed(void); void showFirmwareUpdateSkipped(void); void showFirmwareUpdateUpToDate(void); +#else + +#endif void showRebooting(void); }; diff --git a/src/AgStateMachine.cpp b/src/AgStateMachine.cpp index 38e2d3eb..58d777cf 100644 --- a/src/AgStateMachine.cpp +++ b/src/AgStateMachine.cpp @@ -7,11 +7,11 @@ #define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */ -#define RGB_COLOR_R 255, 0, 0 /** Red */ -#define RGB_COLOR_G 0, 255, 0 /** Green */ -#define RGB_COLOR_Y 255, 255, 0 /** Yellow */ -#define RGB_COLOR_O 255, 165, 0 /** Organge */ -#define RGB_COLOR_P 160, 32, 240 /** Purple */ +#define RGB_COLOR_R 255, 0, 0 /** Red */ +#define RGB_COLOR_G 0, 255, 0 /** Green */ +#define RGB_COLOR_Y 255, 255, 0 /** Yellow */ +#define RGB_COLOR_O 255, 165, 0 /** Organge */ +#define RGB_COLOR_P 160, 32, 240 /** Purple */ /** * @brief Animation LED bar with color @@ -224,10 +224,13 @@ void StateMachine::co2Calibration(void) { /** Count down to 0 then start */ for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) { - if (ag->isOne()) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) { String str = "after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec"; disp.setText("Start CO2 calib", str.c_str(), ""); + } else if (ag->isBasic()) { + String str = String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec"; + disp.setText("CO2 Calib", "after", str.c_str()); } else { logInfo("Start CO2 calib after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec"); @@ -236,14 +239,16 @@ void StateMachine::co2Calibration(void) { } if (ag->s8.setBaselineCalibration()) { - if (ag->isOne()) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) { disp.setText("Calibration", "success", ""); + } else if (ag->isBasic()) { + disp.setText("CO2 Calib", "success", ""); } else { logInfo("CO2 Calibration: success"); } delay(1000); - if (ag->isOne()) { - disp.setText("Wait for", "calib finish", "..."); + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) { + disp.setText("Wait for", "calib done", "..."); } else { logInfo("CO2 Calibration: Wait for calibration finish..."); } @@ -254,16 +259,18 @@ void StateMachine::co2Calibration(void) { delay(1000); count++; } - if (ag->isOne()) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) { String str = "after " + String(count); - disp.setText("Calib finish", str.c_str(), "sec"); + disp.setText("Calib done", str.c_str(), "sec"); } else { logInfo("CO2 Calibration: finish after " + String(count) + " sec"); } delay(2000); } else { - if (ag->isOne()) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) { disp.setText("Calibration", "failure!!!", ""); + } else if (ag->isBasic()) { + disp.setText("CO2 calib", "failure!!!", ""); } else { logInfo("CO2 Calibration: failure!!!"); } @@ -279,15 +286,16 @@ void StateMachine::co2Calibration(void) { if (ag->s8.setAbcPeriod(config.getCO2CalibrationAbcDays() * 24)) { resultStr = "successful"; } - String fromStr = String(curHour/24) + " days"; - if(curHour == 0){ + String fromStr = String(curHour / 24) + " days"; + if (curHour == 0) { fromStr = "off"; } String toStr = String(config.getCO2CalibrationAbcDays()) + " days"; - if(config.getCO2CalibrationAbcDays() == 0) { + if (config.getCO2CalibrationAbcDays() == 0) { toStr = "off"; } - String msg = "Setting S8 from " + fromStr + " to " + toStr + " " + resultStr; + String msg = + "Setting S8 from " + fromStr + " to " + toStr + " " + resultStr; logInfo(msg); } } else { @@ -314,9 +322,7 @@ void StateMachine::ledBarTest(void) { } } -void StateMachine::ledBarPowerUpTest(void) { - ledBarRunTest(); -} +void StateMachine::ledBarPowerUpTest(void) { ledBarRunTest(); } void StateMachine::ledBarRunTest(void) { disp.setText("LED Test", "running", "....."); @@ -398,8 +404,8 @@ StateMachine::~StateMachine() {} * @param state */ void StateMachine::displayHandle(AgStateMachineState state) { - // Ignore handle if not ONE_INDOOR board - if (!ag->isOne()) { + // Ignore handle if not support display + if (!(ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic())) { if (state == AgStateMachineCo2Calibration) { co2Calibration(); } @@ -417,11 +423,17 @@ void StateMachine::displayHandle(AgStateMachineState state) { case AgStateMachineWiFiManagerMode: case AgStateMachineWiFiManagerPortalActive: { if (wifiConnectCountDown >= 0) { - String line1 = String(wifiConnectCountDown) + "s to connect"; - String line2 = "to WiFi hotspot:"; - String line3 = "\"airgradient-"; - String line4 = ag->deviceId() + "\""; - disp.setText(line1, line2, line3, line4); + if (ag->isBasic()) { + String ssid = "\"airgradient-" + ag->deviceId() + "\" " + + String(wifiConnectCountDown) + String("s"); + disp.setText("Connect tohotspot:", ssid.c_str(), ""); + } else { + String line1 = String(wifiConnectCountDown) + "s to connect"; + String line2 = "to WiFi hotspot:"; + String line3 = "\"airgradient-"; + String line4 = ag->deviceId() + "\""; + disp.setText(line1, line2, line3, line4); + } wifiConnectCountDown--; } break; @@ -435,7 +447,12 @@ void StateMachine::displayHandle(AgStateMachineState state) { break; } case AgStateMachineWiFiOkServerConnecting: { - disp.setText("Connecting to", "Server", "..."); + if (ag->isBasic()) { + disp.setText("Connecting", "to", "Server..."); + } else { + disp.setText("Connecting to", "Server", "..."); + } + break; } case AgStateMachineWiFiOkServerConnected: { @@ -451,7 +468,11 @@ void StateMachine::displayHandle(AgStateMachineState state) { break; } case AgStateMachineWiFiOkServerOkSensorConfigFailed: { - disp.setText("Monitor not", "setup on", "dashboard"); + if (ag->isBasic()) { + disp.setText("Monitor", "not on", "dashboard"); + } else { + disp.setText("Monitor not", "setup on", "dashboard"); + } break; } case AgStateMachineWiFiLost: { @@ -502,7 +523,7 @@ void StateMachine::displayHandle(void) { displayHandle(dispState); } * */ void StateMachine::displaySetAddToDashBoard(void) { - if(addToDashBoard == false) { + if (addToDashBoard == false) { addToDashboardTime = 0; addToDashBoardToggle = true; } @@ -527,11 +548,18 @@ void StateMachine::displayWiFiConnectCountDown(int count) { void StateMachine::ledAnimationInit(void) { ledBarAnimationCount = -1; } /** - * @brief Handle LED from state + * @brief Handle LED from state, only handle LED if board type is: One Indoor or + * Open Air * * @param state */ void StateMachine::handleLeds(AgStateMachineState state) { + /** Ignore if board type if not ONE_INDOOR or OPEN_AIR_OUTDOOR */ + if ((ag->getBoardType() != BoardType::ONE_INDOOR) && + (ag->getBoardType() != BoardType::OPEN_AIR_OUTDOOR)) { + return; + } + if (state > AgStateMachineNormal) { logError("ledHandle: state invalid"); return; diff --git a/src/AgValue.cpp b/src/AgValue.cpp index c3bcc859..bd729ea1 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()) { + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) { if (config->hasSensorPMS1) { if (this->pm01_1 >= 0) { root["pm01"] = this->pm01_1; @@ -177,7 +177,9 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, root["bootCount"] = bootCount; if (localServer) { - root["ledMode"] = config->getLedBarModeName(); + if (ag->isOne()) { + root["ledMode"] = config->getLedBarModeName(); + } root["firmware"] = ag->getVersion(); root["model"] = AgFirmwareModeName(fwMode); } diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index f71a9cd0..a64ed83d 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -13,7 +13,6 @@ */ void WifiConnector::setAirGradient(AirGradient *ag) { this->ag = ag; } -#ifdef ESP32 /** * @brief Construct a new Ag Wi Fi Connector:: Ag Wi Fi Connector object * @@ -24,9 +23,6 @@ void WifiConnector::setAirGradient(AirGradient *ag) { this->ag = ag; } WifiConnector::WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config) : PrintLog(log, "WifiConnector"), disp(disp), sm(sm), config(config) {} -#else -WifiConnector::WifiConnector(Stream &log) : PrintLog(log, "WiFiConnector") {} -#endif WifiConnector::~WifiConnector() {} @@ -49,20 +45,18 @@ bool WifiConnector::connect(void) { WIFI()->setConnectTimeout(15); WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX); -#ifdef ESP32 WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); }); WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); }); WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); }); - WIFI()->setConfigPortalTimeoutCallback([this](){}); - if (ag->isOne()) { - disp.setText("Connecting to", "WiFi", "..."); + WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();}); + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) { + disp.setText("Connect to", "WiFi", "..."); } else { logInfo("Connecting to WiFi..."); } ssid = "airgradient-" + ag->deviceId(); -#else - ssid = "AG-" + String(ESP.getChipId(), HEX); -#endif + + // ssid = "AG-" + String(ESP.getChipId(), HEX); WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX); WiFiManagerParameter postToAg("chbPostToAg", @@ -78,6 +72,8 @@ bool WifiConnector::connect(void) { WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT); + logInfo("Wait for configure portal"); + #ifdef ESP32 // Task handle WiFi connection. xTaskCreate( @@ -85,6 +81,7 @@ bool WifiConnector::connect(void) { WifiConnector *connector = (WifiConnector *)obj; while (connector->_wifiConfigPortalActive()) { connector->_wifiProcess(); + vTaskDelay(1); } vTaskDelete(NULL); }, @@ -139,10 +136,14 @@ bool WifiConnector::connect(void) { delay(1); // avoid watchdog timer reset. } +#else + _wifiProcess(); +#endif + /** Show display wifi connect result failed */ if (WiFi.isConnected() == false) { sm.handleLeds(AgStateMachineWiFiManagerConnectFailed); - if (ag->isOne()) { + if (ag->isOne() || ag->isPro4_2() || ag->isPro3_3() || ag->isBasic()) { sm.displayHandle(AgStateMachineWiFiManagerConnectFailed); } delay(6000); @@ -160,9 +161,7 @@ bool WifiConnector::connect(void) { } hasPortalConfig = false; } -#else - _wifiProcess(); -#endif + return true; } @@ -177,24 +176,6 @@ void WifiConnector::disconnect(void) { } } -#ifdef ESP32 -#else -void WifiConnector::displayShowText(String ln1, String ln2, String ln3) { - char buf[9]; - ag->display.clear(); - - ag->display.setCursor(1, 1); - ag->display.setText(ln1); - ag->display.setCursor(1, 19); - ag->display.setText(ln2); - ag->display.setCursor(1, 37); - ag->display.setText(ln3); - - ag->display.show(); - delay(100); -} -#endif - /** * @brief Has wifi STA connected to WIFI softAP (this device) * @@ -205,7 +186,6 @@ bool WifiConnector::wifiClientConnected(void) { return WiFi.softAPgetStationNum() ? true : false; } -#ifdef ESP32 /** * @brief Handle WiFiManage softAP setup completed callback * @@ -247,7 +227,7 @@ bool WifiConnector::_wifiConfigPortalActive(void) { return WIFI()->getConfigPortalActive(); } void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; } -#endif + /** * @brief Process WiFiManager connection * @@ -256,33 +236,67 @@ void WifiConnector::_wifiProcess() { #ifdef ESP32 WIFI()->process(); #else - int count = WIFI_CONNECT_COUNTDOWN_MAX; - displayShowText(String(WIFI_CONNECT_COUNTDOWN_MAX) + " sec", "SSID:", ssid); + /** Wait for WiFi connect and show LED, display status */ + uint32_t dispPeriod = millis(); + uint32_t ledPeriod = millis(); + bool clientConnectChanged = false; + AgStateMachineState stateOld = sm.getDisplayState(); + while (WIFI()->getConfigPortalActive()) { WIFI()->process(); - uint32_t lastTime = millis(); - uint32_t ms = (uint32_t)(millis() - lastTime); - if (ms >= 1000) { - lastTime = millis(); - - displayShowText(String(count) + " sec", "SSID:", ssid); + if (WiFi.isConnected() == false) { + /** Display countdown */ + uint32_t ms; + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) { + ms = (uint32_t)(millis() - dispPeriod); + if (ms >= 1000) { + dispPeriod = millis(); + sm.displayHandle(); + logInfo("displayHandle state: " + String(sm.getDisplayState())); + } else { + if (stateOld != sm.getDisplayState()) { + stateOld = sm.getDisplayState(); + sm.displayHandle(); + } + } + } - count--; + /** LED animations */ + ms = (uint32_t)(millis() - ledPeriod); + if (ms >= 100) { + ledPeriod = millis(); + sm.handleLeds(); + } - // Timeout - if (count == 0) { - break; + /** Check for client connect to change led color */ + bool clientConnected = wifiClientConnected(); + if (clientConnected != clientConnectChanged) { + clientConnectChanged = clientConnected; + if (clientConnectChanged) { + sm.handleLeds(AgStateMachineWiFiManagerPortalActive); + } else { + sm.ledAnimationInit(); + sm.handleLeds(AgStateMachineWiFiManagerMode); + if (ag->isOne()) { + sm.displayHandle(AgStateMachineWiFiManagerMode); + } + } } } + + delay(1); } - if (!WiFi.isConnected()) { - displayShowText("Booting", "offline", "mode"); - Serial.println("failed to connect and hit timeout"); - delay(2500); - } else { - hasConfig = true; + // TODO This is for basic + if (ag->getBoardType() == DIY_BASIC) { + if (!WiFi.isConnected()) { + // disp.setText("Booting", "offline", "mode"); + Serial.println("failed to connect and hit timeout"); + delay(2500); + } else { + hasConfig = true; + } } #endif } @@ -307,8 +321,6 @@ void WifiConnector::handle(void) { if (ms >= 10000) { lastRetry = millis(); WiFi.reconnect(); - - // Serial.printf("Re-Connect WiFi\r\n"); logInfo("Re-Connect WiFi"); } } @@ -326,7 +338,16 @@ bool WifiConnector::isConnected(void) { return WiFi.isConnected(); } * this method * */ -void WifiConnector::reset(void) { WIFI()->resetSettings(); } +void WifiConnector::reset(void) { + if(this->wifi == NULL) { + this->wifi = new WiFiManager(); + if(this->wifi == NULL){ + logInfo("reset failed"); + return; + } + } + WIFI()->resetSettings(); +} /** * @brief Get wifi RSSI @@ -344,7 +365,7 @@ String WifiConnector::localIpStr(void) { return WiFi.localIP().toString(); } /** * @brief Get status that wifi has configurated - * + * * @return true Configurated * @return false Not Configurated */ @@ -357,8 +378,8 @@ bool WifiConnector::hasConfigurated(void) { /** * @brief Get WiFi connection porttal timeout. - * - * @return true - * @return false + * + * @return true + * @return false */ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; } diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index b4e76a76..10efba60 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -12,13 +12,10 @@ class WifiConnector : public PrintLog { private: AirGradient *ag; -#ifdef ESP32 OledDisplay &disp; StateMachine &sm; Configuration &config; -#else - void displayShowText(String ln1, String ln2, String ln3); -#endif + String ssid; void *wifi = NULL; bool hasConfig; @@ -30,23 +27,18 @@ class WifiConnector : public PrintLog { public: void setAirGradient(AirGradient *ag); -#ifdef ESP32 + WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config); -#else - WifiConnector(Stream &log); -#endif ~WifiConnector(); bool connect(void); void disconnect(void); void handle(void); -#ifdef ESP32 void _wifiApCallback(void); void _wifiSaveConfig(void); void _wifiSaveParamCallback(void); bool _wifiConfigPortalActive(void); void _wifiTimeoutCallback(void); -#endif void _wifiProcess(); bool isConnected(void); void reset(void); diff --git a/src/AirGradient.cpp b/src/AirGradient.cpp index 9c24a976..e0369820 100644 --- a/src/AirGradient.cpp +++ b/src/AirGradient.cpp @@ -58,6 +58,16 @@ bool AirGradient::isOne(void) { return boardType == BoardType::ONE_INDOOR; } +bool AirGradient::isPro4_2(void) { + return boardType == BoardType::DIY_PRO_INDOOR_V4_2; +} + +bool AirGradient::isPro3_3(void) { + return boardType == BoardType::DIY_PRO_INDOOR_V3_3; +} + +bool AirGradient::isBasic(void) { return boardType == BoardType::DIY_BASIC; } + String AirGradient::deviceId(void) { String mac = WiFi.macAddress(); mac.replace(":", ""); diff --git a/src/AirGradient.h b/src/AirGradient.h index 6c4eb889..5c47e1bf 100644 --- a/src/AirGradient.h +++ b/src/AirGradient.h @@ -134,6 +134,29 @@ class AirGradient { */ bool isOne(void); + /** + * @brief Check that Airgradient object is DIY_PRO 4.2 indoor + * + * @return true Yes + * @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_3(void); + + /** + * @brief Check that Airgradient object is DIY_BASIC + * + * @return true Yes + * @return false No + */ + bool isBasic(void); + /** * @brief Get device Id * diff --git a/src/App/AppDef.cpp b/src/App/AppDef.cpp index ad2d75d5..0b422326 100644 --- a/src/App/AppDef.cpp +++ b/src/App/AppDef.cpp @@ -14,6 +14,12 @@ const char *AgFirmwareModeName(AgFirmwareMode mode) { return "0-1PS"; case FW_MODE_O_1P: return "O-1P"; + case FW_MODE_I_42PS: + return "DIY-PRO-I-4.2PS"; + case FW_MODE_I_33PS: + return "DIY-PRO-I-3.3PS"; + case FW_MODE_I_BASIC_40PS: + return "DIY-BASIC-I-4.0PS"; default: break; } diff --git a/src/App/AppDef.h b/src/App/AppDef.h index a4d9cfc2..3bb93207 100644 --- a/src/App/AppDef.h +++ b/src/App/AppDef.h @@ -101,6 +101,9 @@ enum AgFirmwareMode { FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */ FW_MODE_O_1PS, /** PMS5003T, S8 */ FW_MODE_O_1P, /** PMS5003T */ + FW_MODE_I_42PS, /** DIY_PRO 4.2 */ + FW_MODE_I_33PS, /** DIY_PRO 3.3 */ + FW_MODE_I_BASIC_40PS, /** DIY_BASIC 4.0 */ }; const char *AgFirmwareModeName(AgFirmwareMode mode); diff --git a/src/Libraries/pubsubclient-2.8/.gitignore b/src/Libraries/pubsubclient-2.8/.gitignore new file mode 100644 index 00000000..a42cc406 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/.gitignore @@ -0,0 +1,5 @@ +tests/bin +.pioenvs +.piolibdeps +.clang_complete +.gcc-flags.json diff --git a/src/Libraries/pubsubclient-2.8/.travis.yml b/src/Libraries/pubsubclient-2.8/.travis.yml new file mode 100644 index 00000000..e7b28cb9 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/.travis.yml @@ -0,0 +1,7 @@ +sudo: false +language: cpp +compiler: + - g++ +script: cd tests && make && make test +os: + - linux diff --git a/src/Libraries/pubsubclient-2.8/CHANGES.txt b/src/Libraries/pubsubclient-2.8/CHANGES.txt new file mode 100644 index 00000000..e23d5315 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/CHANGES.txt @@ -0,0 +1,85 @@ +2.8 + * Add setBufferSize() to override MQTT_MAX_PACKET_SIZE + * Add setKeepAlive() to override MQTT_KEEPALIVE + * Add setSocketTimeout() to overide MQTT_SOCKET_TIMEOUT + * Added check to prevent subscribe/unsubscribe to empty topics + * Declare wifi mode prior to connect in ESP example + * Use `strnlen` to avoid overruns + * Support pre-connected Client objects + +2.7 + * Fix remaining-length handling to prevent buffer overrun + * Add large-payload API - beginPublish/write/publish/endPublish + * Add yield call to improve reliability on ESP + * Add Clean Session flag to connect options + * Add ESP32 support for functional callback signature + * Various other fixes + +2.4 + * Add MQTT_SOCKET_TIMEOUT to prevent it blocking indefinitely + whilst waiting for inbound data + * Fixed return code when publishing >256 bytes + +2.3 + * Add publish(topic,payload,retained) function + +2.2 + * Change code layout to match Arduino Library reqs + +2.1 + * Add MAX_TRANSFER_SIZE def to chunk messages if needed + * Reject topic/payloads that exceed MQTT_MAX_PACKET_SIZE + +2.0 + * Add (and default to) MQTT 3.1.1 support + * Fix PROGMEM handling for Intel Galileo/ESP8266 + * Add overloaded constructors for convenience + * Add chainable setters for server/callback/client/stream + * Add state function to return connack return code + +1.9 + * Do not split MQTT packets over multiple calls to _client->write() + * API change: All constructors now require an instance of Client + to be passed in. + * Fixed example to match 1.8 api changes - dpslwk + * Added username/password support - WilHall + * Added publish_P - publishes messages from PROGMEM - jobytaffey + +1.8 + * KeepAlive interval is configurable in PubSubClient.h + * Maximum packet size is configurable in PubSubClient.h + * API change: Return boolean rather than int from various functions + * API change: Length parameter in message callback changed + from int to unsigned int + * Various internal tidy-ups around types +1.7 + * Improved keepalive handling + * Updated to the Arduino-1.0 API +1.6 + * Added the ability to publish a retained message + +1.5 + * Added default constructor + * Fixed compile error when used with arduino-0021 or later + +1.4 + * Fixed connection lost handling + +1.3 + * Fixed packet reading bug in PubSubClient.readPacket + +1.2 + * Fixed compile error when used with arduino-0016 or later + + +1.1 + * Reduced size of library + * Added support for Will messages + * Clarified licensing - see LICENSE.txt + + +1.0 + * Only Quality of Service (QOS) 0 messaging is supported + * The maximum message size, including header, is 128 bytes + * The keepalive interval is set to 30 seconds + * No support for Will messages diff --git a/src/Libraries/pubsubclient-2.8/LICENSE.txt b/src/Libraries/pubsubclient-2.8/LICENSE.txt new file mode 100644 index 00000000..12c1689e --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2008-2020 Nicholas O'Leary + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Libraries/pubsubclient-2.8/README.md b/src/Libraries/pubsubclient-2.8/README.md new file mode 100644 index 00000000..2e131718 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/README.md @@ -0,0 +1,50 @@ +# Arduino Client for MQTT + +This library provides a client for doing simple publish/subscribe messaging with +a server that supports MQTT. + +## Examples + +The library comes with a number of example sketches. See File > Examples > PubSubClient +within the Arduino application. + +Full API documentation is available here: https://pubsubclient.knolleary.net + +## Limitations + + - It can only publish QoS 0 messages. It can subscribe at QoS 0 or QoS 1. + - The maximum message size, including header, is **256 bytes** by default. This + is configurable via `MQTT_MAX_PACKET_SIZE` in `PubSubClient.h` or can be changed + by calling `PubSubClient::setBufferSize(size)`. + - The keepalive interval is set to 15 seconds by default. This is configurable + via `MQTT_KEEPALIVE` in `PubSubClient.h` or can be changed by calling + `PubSubClient::setKeepAlive(keepAlive)`. + - The client uses MQTT 3.1.1 by default. It can be changed to use MQTT 3.1 by + changing value of `MQTT_VERSION` in `PubSubClient.h`. + + +## Compatible Hardware + +The library uses the Arduino Ethernet Client api for interacting with the +underlying network hardware. This means it Just Works with a growing number of +boards and shields, including: + + - Arduino Ethernet + - Arduino Ethernet Shield + - Arduino YUN – use the included `YunClient` in place of `EthernetClient`, and + be sure to do a `Bridge.begin()` first + - Arduino WiFi Shield - if you want to send packets > 90 bytes with this shield, + enable the `MQTT_MAX_TRANSFER_SIZE` define in `PubSubClient.h`. + - Sparkfun WiFly Shield – [library](https://github.com/dpslwk/WiFly) + - TI CC3000 WiFi - [library](https://github.com/sparkfun/SFE_CC3000_Library) + - Intel Galileo/Edison + - ESP8266 + - ESP32 + +The library cannot currently be used with hardware based on the ENC28J60 chip – +such as the Nanode or the Nuelectronics Ethernet Shield. For those, there is an +[alternative library](https://github.com/njh/NanodeMQTT) available. + +## License + +This code is released under the MIT License. diff --git a/src/Libraries/pubsubclient-2.8/examples/mqtt_auth/mqtt_auth.ino b/src/Libraries/pubsubclient-2.8/examples/mqtt_auth/mqtt_auth.ino new file mode 100644 index 00000000..04bd7bb2 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/examples/mqtt_auth/mqtt_auth.ino @@ -0,0 +1,43 @@ +/* + Basic MQTT example with Authentication + + - connects to an MQTT server, providing username + and password + - publishes "hello world" to the topic "outTopic" + - subscribes to the topic "inTopic" +*/ + +#include +#include +#include + +// Update these with values suitable for your network. +byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; +IPAddress ip(172, 16, 0, 100); +IPAddress server(172, 16, 0, 2); + +void callback(char* topic, byte* payload, unsigned int length) { + // handle message arrived +} + +EthernetClient ethClient; +PubSubClient client(server, 1883, callback, ethClient); + +void setup() +{ + Ethernet.begin(mac, ip); + // Note - the default maximum packet size is 128 bytes. If the + // combined length of clientId, username and password exceed this use the + // following to increase the buffer size: + // client.setBufferSize(255); + + if (client.connect("arduinoClient", "testuser", "testpass")) { + client.publish("outTopic","hello world"); + client.subscribe("inTopic"); + } +} + +void loop() +{ + client.loop(); +} diff --git a/src/Libraries/pubsubclient-2.8/examples/mqtt_basic/mqtt_basic.ino b/src/Libraries/pubsubclient-2.8/examples/mqtt_basic/mqtt_basic.ino new file mode 100644 index 00000000..f545adef --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/examples/mqtt_basic/mqtt_basic.ino @@ -0,0 +1,77 @@ +/* + Basic MQTT example + + This sketch demonstrates the basic capabilities of the library. + It connects to an MQTT server then: + - publishes "hello world" to the topic "outTopic" + - subscribes to the topic "inTopic", printing out any messages + it receives. NB - it assumes the received payloads are strings not binary + + It will reconnect to the server if the connection is lost using a blocking + reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to + achieve the same result without blocking the main loop. + +*/ + +#include +#include +#include + +// Update these with values suitable for your network. +byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; +IPAddress ip(172, 16, 0, 100); +IPAddress server(172, 16, 0, 2); + +void callback(char* topic, byte* payload, unsigned int length) { + Serial.print("Message arrived ["); + Serial.print(topic); + Serial.print("] "); + for (int i=0;i Preferences -> Additional Boards Manager URLs": + http://arduino.esp8266.com/stable/package_esp8266com_index.json + - Open the "Tools -> Board -> Board Manager" and click install for the ESP8266" + - Select your ESP8266 in "Tools -> Board" +*/ + +#include +#include + +// Update these with values suitable for your network. + +const char* ssid = "........"; +const char* password = "........"; +const char* mqtt_server = "broker.mqtt-dashboard.com"; + +WiFiClient espClient; +PubSubClient client(espClient); +unsigned long lastMsg = 0; +#define MSG_BUFFER_SIZE (50) +char msg[MSG_BUFFER_SIZE]; +int value = 0; + +void setup_wifi() { + + delay(10); + // We start by connecting to a WiFi network + Serial.println(); + Serial.print("Connecting to "); + Serial.println(ssid); + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + + randomSeed(micros()); + + Serial.println(""); + Serial.println("WiFi connected"); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); +} + +void callback(char* topic, byte* payload, unsigned int length) { + Serial.print("Message arrived ["); + Serial.print(topic); + Serial.print("] "); + for (int i = 0; i < length; i++) { + Serial.print((char)payload[i]); + } + Serial.println(); + + // Switch on the LED if an 1 was received as first character + if ((char)payload[0] == '1') { + digitalWrite(BUILTIN_LED, LOW); // Turn the LED on (Note that LOW is the voltage level + // but actually the LED is on; this is because + // it is active low on the ESP-01) + } else { + digitalWrite(BUILTIN_LED, HIGH); // Turn the LED off by making the voltage HIGH + } + +} + +void reconnect() { + // Loop until we're reconnected + while (!client.connected()) { + Serial.print("Attempting MQTT connection..."); + // Create a random client ID + String clientId = "ESP8266Client-"; + clientId += String(random(0xffff), HEX); + // Attempt to connect + if (client.connect(clientId.c_str())) { + Serial.println("connected"); + // Once connected, publish an announcement... + client.publish("outTopic", "hello world"); + // ... and resubscribe + client.subscribe("inTopic"); + } else { + Serial.print("failed, rc="); + Serial.print(client.state()); + Serial.println(" try again in 5 seconds"); + // Wait 5 seconds before retrying + delay(5000); + } + } +} + +void setup() { + pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output + Serial.begin(115200); + setup_wifi(); + client.setServer(mqtt_server, 1883); + client.setCallback(callback); +} + +void loop() { + + if (!client.connected()) { + reconnect(); + } + client.loop(); + + unsigned long now = millis(); + if (now - lastMsg > 2000) { + lastMsg = now; + ++value; + snprintf (msg, MSG_BUFFER_SIZE, "hello world #%ld", value); + Serial.print("Publish message: "); + Serial.println(msg); + client.publish("outTopic", msg); + } +} diff --git a/src/Libraries/pubsubclient-2.8/examples/mqtt_large_message/mqtt_large_message.ino b/src/Libraries/pubsubclient-2.8/examples/mqtt_large_message/mqtt_large_message.ino new file mode 100644 index 00000000..e048c3ed --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/examples/mqtt_large_message/mqtt_large_message.ino @@ -0,0 +1,179 @@ +/* + Long message ESP8266 MQTT example + + This sketch demonstrates sending arbitrarily large messages in combination + with the ESP8266 board/library. + + It connects to an MQTT server then: + - publishes "hello world" to the topic "outTopic" + - subscribes to the topic "greenBottles/#", printing out any messages + it receives. NB - it assumes the received payloads are strings not binary + - If the sub-topic is a number, it publishes a "greenBottles/lyrics" message + with a payload consisting of the lyrics to "10 green bottles", replacing + 10 with the number given in the sub-topic. + + It will reconnect to the server if the connection is lost using a blocking + reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to + achieve the same result without blocking the main loop. + + To install the ESP8266 board, (using Arduino 1.6.4+): + - Add the following 3rd party board manager under "File -> Preferences -> Additional Boards Manager URLs": + http://arduino.esp8266.com/stable/package_esp8266com_index.json + - Open the "Tools -> Board -> Board Manager" and click install for the ESP8266" + - Select your ESP8266 in "Tools -> Board" + +*/ + +#include +#include + +// Update these with values suitable for your network. + +const char* ssid = "........"; +const char* password = "........"; +const char* mqtt_server = "broker.mqtt-dashboard.com"; + +WiFiClient espClient; +PubSubClient client(espClient); +long lastMsg = 0; +char msg[50]; +int value = 0; + +void setup_wifi() { + + delay(10); + // We start by connecting to a WiFi network + Serial.println(); + Serial.print("Connecting to "); + Serial.println(ssid); + + WiFi.begin(ssid, password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + + randomSeed(micros()); + + Serial.println(""); + Serial.println("WiFi connected"); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); +} + +void callback(char* topic, byte* payload, unsigned int length) { + Serial.print("Message arrived ["); + Serial.print(topic); + Serial.print("] "); + for (int i = 0; i < length; i++) { + Serial.print((char)payload[i]); + } + Serial.println(); + + // Find out how many bottles we should generate lyrics for + String topicStr(topic); + int bottleCount = 0; // assume no bottles unless we correctly parse a value from the topic + if (topicStr.indexOf('/') >= 0) { + // The topic includes a '/', we'll try to read the number of bottles from just after that + topicStr.remove(0, topicStr.indexOf('/')+1); + // Now see if there's a number of bottles after the '/' + bottleCount = topicStr.toInt(); + } + + if (bottleCount > 0) { + // Work out how big our resulting message will be + int msgLen = 0; + for (int i = bottleCount; i > 0; i--) { + String numBottles(i); + msgLen += 2*numBottles.length(); + if (i == 1) { + msgLen += 2*String(" green bottle, standing on the wall\n").length(); + } else { + msgLen += 2*String(" green bottles, standing on the wall\n").length(); + } + msgLen += String("And if one green bottle should accidentally fall\nThere'll be ").length(); + switch (i) { + case 1: + msgLen += String("no green bottles, standing on the wall\n\n").length(); + break; + case 2: + msgLen += String("1 green bottle, standing on the wall\n\n").length(); + break; + default: + numBottles = i-1; + msgLen += numBottles.length(); + msgLen += String(" green bottles, standing on the wall\n\n").length(); + break; + }; + } + + // Now we can start to publish the message + client.beginPublish("greenBottles/lyrics", msgLen, false); + for (int i = bottleCount; i > 0; i--) { + for (int j = 0; j < 2; j++) { + client.print(i); + if (i == 1) { + client.print(" green bottle, standing on the wall\n"); + } else { + client.print(" green bottles, standing on the wall\n"); + } + } + client.print("And if one green bottle should accidentally fall\nThere'll be "); + switch (i) { + case 1: + client.print("no green bottles, standing on the wall\n\n"); + break; + case 2: + client.print("1 green bottle, standing on the wall\n\n"); + break; + default: + client.print(i-1); + client.print(" green bottles, standing on the wall\n\n"); + break; + }; + } + // Now we're done! + client.endPublish(); + } +} + +void reconnect() { + // Loop until we're reconnected + while (!client.connected()) { + Serial.print("Attempting MQTT connection..."); + // Create a random client ID + String clientId = "ESP8266Client-"; + clientId += String(random(0xffff), HEX); + // Attempt to connect + if (client.connect(clientId.c_str())) { + Serial.println("connected"); + // Once connected, publish an announcement... + client.publish("outTopic", "hello world"); + // ... and resubscribe + client.subscribe("greenBottles/#"); + } else { + Serial.print("failed, rc="); + Serial.print(client.state()); + Serial.println(" try again in 5 seconds"); + // Wait 5 seconds before retrying + delay(5000); + } + } +} + +void setup() { + pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output + Serial.begin(115200); + setup_wifi(); + client.setServer(mqtt_server, 1883); + client.setCallback(callback); +} + +void loop() { + + if (!client.connected()) { + reconnect(); + } + client.loop(); +} diff --git a/src/Libraries/pubsubclient-2.8/examples/mqtt_publish_in_callback/mqtt_publish_in_callback.ino b/src/Libraries/pubsubclient-2.8/examples/mqtt_publish_in_callback/mqtt_publish_in_callback.ino new file mode 100644 index 00000000..42afb2a3 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/examples/mqtt_publish_in_callback/mqtt_publish_in_callback.ino @@ -0,0 +1,60 @@ +/* + Publishing in the callback + + - connects to an MQTT server + - subscribes to the topic "inTopic" + - when a message is received, republishes it to "outTopic" + + This example shows how to publish messages within the + callback function. The callback function header needs to + be declared before the PubSubClient constructor and the + actual callback defined afterwards. + This ensures the client reference in the callback function + is valid. + +*/ + +#include +#include +#include + +// Update these with values suitable for your network. +byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; +IPAddress ip(172, 16, 0, 100); +IPAddress server(172, 16, 0, 2); + +// Callback function header +void callback(char* topic, byte* payload, unsigned int length); + +EthernetClient ethClient; +PubSubClient client(server, 1883, callback, ethClient); + +// Callback function +void callback(char* topic, byte* payload, unsigned int length) { + // In order to republish this payload, a copy must be made + // as the orignal payload buffer will be overwritten whilst + // constructing the PUBLISH packet. + + // Allocate the correct amount of memory for the payload copy + byte* p = (byte*)malloc(length); + // Copy the payload to the new buffer + memcpy(p,payload,length); + client.publish("outTopic", p, length); + // Free the memory + free(p); +} + +void setup() +{ + + Ethernet.begin(mac, ip); + if (client.connect("arduinoClient")) { + client.publish("outTopic","hello world"); + client.subscribe("inTopic"); + } +} + +void loop() +{ + client.loop(); +} diff --git a/src/Libraries/pubsubclient-2.8/examples/mqtt_reconnect_nonblocking/mqtt_reconnect_nonblocking.ino b/src/Libraries/pubsubclient-2.8/examples/mqtt_reconnect_nonblocking/mqtt_reconnect_nonblocking.ino new file mode 100644 index 00000000..080b7391 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/examples/mqtt_reconnect_nonblocking/mqtt_reconnect_nonblocking.ino @@ -0,0 +1,67 @@ +/* + Reconnecting MQTT example - non-blocking + + This sketch demonstrates how to keep the client connected + using a non-blocking reconnect function. If the client loses + its connection, it attempts to reconnect every 5 seconds + without blocking the main loop. + +*/ + +#include +#include +#include + +// Update these with values suitable for your hardware/network. +byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; +IPAddress ip(172, 16, 0, 100); +IPAddress server(172, 16, 0, 2); + +void callback(char* topic, byte* payload, unsigned int length) { + // handle message arrived +} + +EthernetClient ethClient; +PubSubClient client(ethClient); + +long lastReconnectAttempt = 0; + +boolean reconnect() { + if (client.connect("arduinoClient")) { + // Once connected, publish an announcement... + client.publish("outTopic","hello world"); + // ... and resubscribe + client.subscribe("inTopic"); + } + return client.connected(); +} + +void setup() +{ + client.setServer(server, 1883); + client.setCallback(callback); + + Ethernet.begin(mac, ip); + delay(1500); + lastReconnectAttempt = 0; +} + + +void loop() +{ + if (!client.connected()) { + long now = millis(); + if (now - lastReconnectAttempt > 5000) { + lastReconnectAttempt = now; + // Attempt to reconnect + if (reconnect()) { + lastReconnectAttempt = 0; + } + } + } else { + // Client connected + + client.loop(); + } + +} diff --git a/src/Libraries/pubsubclient-2.8/examples/mqtt_stream/mqtt_stream.ino b/src/Libraries/pubsubclient-2.8/examples/mqtt_stream/mqtt_stream.ino new file mode 100644 index 00000000..67c22872 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/examples/mqtt_stream/mqtt_stream.ino @@ -0,0 +1,57 @@ +/* + Example of using a Stream object to store the message payload + + Uses SRAM library: https://github.com/ennui2342/arduino-sram + but could use any Stream based class such as SD + + - connects to an MQTT server + - publishes "hello world" to the topic "outTopic" + - subscribes to the topic "inTopic" +*/ + +#include +#include +#include +#include + +// Update these with values suitable for your network. +byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; +IPAddress ip(172, 16, 0, 100); +IPAddress server(172, 16, 0, 2); + +SRAM sram(4, SRAM_1024); + +void callback(char* topic, byte* payload, unsigned int length) { + sram.seek(1); + + // do something with the message + for(uint8_t i=0; i +maintainer=Nick O'Leary +sentence=A client library for MQTT messaging. +paragraph=MQTT is a lightweight messaging protocol ideal for small devices. This library allows you to send and receive MQTT messages. It supports the latest MQTT 3.1.1 protocol and can be configured to use the older MQTT 3.1 if needed. It supports all Arduino Ethernet Client compatible hardware, including the Intel Galileo/Edison, ESP8266 and TI CC3000. +category=Communication +url=http://pubsubclient.knolleary.net +architectures=* diff --git a/src/Libraries/pubsubclient-2.8/src/PubSubClient.cpp b/src/Libraries/pubsubclient-2.8/src/PubSubClient.cpp new file mode 100644 index 00000000..2b48d2b6 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/src/PubSubClient.cpp @@ -0,0 +1,769 @@ +/* + + PubSubClient.cpp - A simple client for MQTT. + Nick O'Leary + http://knolleary.net +*/ + +#include "PubSubClient.h" +#include "Arduino.h" + +PubSubClient::PubSubClient() { + this->_state = MQTT_DISCONNECTED; + this->_client = NULL; + this->stream = NULL; + setCallback(NULL); + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} + +PubSubClient::PubSubClient(Client& client) { + this->_state = MQTT_DISCONNECTED; + setClient(client); + this->stream = NULL; + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} + +PubSubClient::PubSubClient(IPAddress addr, uint16_t port, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(addr, port); + setClient(client); + this->stream = NULL; + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(IPAddress addr, uint16_t port, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(addr,port); + setClient(client); + setStream(stream); + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(IPAddress addr, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(addr, port); + setCallback(callback); + setClient(client); + this->stream = NULL; + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(IPAddress addr, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(addr,port); + setCallback(callback); + setClient(client); + setStream(stream); + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} + +PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(ip, port); + setClient(client); + this->stream = NULL; + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(ip,port); + setClient(client); + setStream(stream); + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(ip, port); + setCallback(callback); + setClient(client); + this->stream = NULL; + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(ip,port); + setCallback(callback); + setClient(client); + setStream(stream); + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} + +PubSubClient::PubSubClient(const char* domain, uint16_t port, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(domain,port); + setClient(client); + this->stream = NULL; + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(const char* domain, uint16_t port, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(domain,port); + setClient(client); + setStream(stream); + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(const char* domain, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) { + this->_state = MQTT_DISCONNECTED; + setServer(domain,port); + setCallback(callback); + setClient(client); + this->stream = NULL; + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} +PubSubClient::PubSubClient(const char* domain, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) { + this->_state = MQTT_DISCONNECTED; + setServer(domain,port); + setCallback(callback); + setClient(client); + setStream(stream); + this->bufferSize = 0; + setBufferSize(MQTT_MAX_PACKET_SIZE); + setKeepAlive(MQTT_KEEPALIVE); + setSocketTimeout(MQTT_SOCKET_TIMEOUT); +} + +PubSubClient::~PubSubClient() { + free(this->buffer); +} + +boolean PubSubClient::connect(const char *id) { + return connect(id,NULL,NULL,0,0,0,0,1); +} + +boolean PubSubClient::connect(const char *id, const char *user, const char *pass) { + return connect(id,user,pass,0,0,0,0,1); +} + +boolean PubSubClient::connect(const char *id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage) { + return connect(id,NULL,NULL,willTopic,willQos,willRetain,willMessage,1); +} + +boolean PubSubClient::connect(const char *id, const char *user, const char *pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage) { + return connect(id,user,pass,willTopic,willQos,willRetain,willMessage,1); +} + +boolean PubSubClient::connect(const char *id, const char *user, const char *pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage, boolean cleanSession) { + if (!connected()) { + int result = 0; + + + if(_client->connected()) { + result = 1; + } else { + if (domain != NULL) { + result = _client->connect(this->domain, this->port); + } else { + result = _client->connect(this->ip, this->port); + } + } + + if (result == 1) { + nextMsgId = 1; + // Leave room in the buffer for header and variable length field + uint16_t length = MQTT_MAX_HEADER_SIZE; + unsigned int j; + +#if MQTT_VERSION == MQTT_VERSION_3_1 + uint8_t d[9] = {0x00,0x06,'M','Q','I','s','d','p', MQTT_VERSION}; +#define MQTT_HEADER_VERSION_LENGTH 9 +#elif MQTT_VERSION == MQTT_VERSION_3_1_1 + uint8_t d[7] = {0x00,0x04,'M','Q','T','T',MQTT_VERSION}; +#define MQTT_HEADER_VERSION_LENGTH 7 +#endif + for (j = 0;jbuffer[length++] = d[j]; + } + + uint8_t v; + if (willTopic) { + v = 0x04|(willQos<<3)|(willRetain<<5); + } else { + v = 0x00; + } + if (cleanSession) { + v = v|0x02; + } + + if(user != NULL) { + v = v|0x80; + + if(pass != NULL) { + v = v|(0x80>>1); + } + } + this->buffer[length++] = v; + + this->buffer[length++] = ((this->keepAlive) >> 8); + this->buffer[length++] = ((this->keepAlive) & 0xFF); + + CHECK_STRING_LENGTH(length,id) + length = writeString(id,this->buffer,length); + if (willTopic) { + CHECK_STRING_LENGTH(length,willTopic) + length = writeString(willTopic,this->buffer,length); + CHECK_STRING_LENGTH(length,willMessage) + length = writeString(willMessage,this->buffer,length); + } + + if(user != NULL) { + CHECK_STRING_LENGTH(length,user) + length = writeString(user,this->buffer,length); + if(pass != NULL) { + CHECK_STRING_LENGTH(length,pass) + length = writeString(pass,this->buffer,length); + } + } + + write(MQTTCONNECT,this->buffer,length-MQTT_MAX_HEADER_SIZE); + + lastInActivity = lastOutActivity = millis(); + + while (!_client->available()) { + unsigned long t = millis(); + if (t-lastInActivity >= ((int32_t) this->socketTimeout*1000UL)) { + _state = MQTT_CONNECTION_TIMEOUT; + _client->stop(); + return false; + } + } + uint8_t llen; + uint32_t len = readPacket(&llen); + + if (len == 4) { + if (buffer[3] == 0) { + lastInActivity = millis(); + pingOutstanding = false; + _state = MQTT_CONNECTED; + return true; + } else { + _state = buffer[3]; + } + } + _client->stop(); + } else { + _state = MQTT_CONNECT_FAILED; + } + return false; + } + return true; +} + +// reads a byte into result +boolean PubSubClient::readByte(uint8_t * result) { + uint32_t previousMillis = millis(); + while(!_client->available()) { + yield(); + uint32_t currentMillis = millis(); + if(currentMillis - previousMillis >= ((int32_t) this->socketTimeout * 1000)){ + return false; + } + } + *result = _client->read(); + return true; +} + +// reads a byte into result[*index] and increments index +boolean PubSubClient::readByte(uint8_t * result, uint16_t * index){ + uint16_t current_index = *index; + uint8_t * write_address = &(result[current_index]); + if(readByte(write_address)){ + *index = current_index + 1; + return true; + } + return false; +} + +uint32_t PubSubClient::readPacket(uint8_t* lengthLength) { + uint16_t len = 0; + if(!readByte(this->buffer, &len)) return 0; + bool isPublish = (this->buffer[0]&0xF0) == MQTTPUBLISH; + uint32_t multiplier = 1; + uint32_t length = 0; + uint8_t digit = 0; + uint16_t skip = 0; + uint32_t start = 0; + + do { + if (len == 5) { + // Invalid remaining length encoding - kill the connection + _state = MQTT_DISCONNECTED; + _client->stop(); + return 0; + } + if(!readByte(&digit)) return 0; + this->buffer[len++] = digit; + length += (digit & 127) * multiplier; + multiplier <<=7; //multiplier *= 128 + } while ((digit & 128) != 0); + *lengthLength = len-1; + + if (isPublish) { + // Read in topic length to calculate bytes to skip over for Stream writing + if(!readByte(this->buffer, &len)) return 0; + if(!readByte(this->buffer, &len)) return 0; + skip = (this->buffer[*lengthLength+1]<<8)+this->buffer[*lengthLength+2]; + start = 2; + if (this->buffer[0]&MQTTQOS1) { + // skip message id + skip += 2; + } + } + uint32_t idx = len; + + for (uint32_t i = start;istream) { + if (isPublish && idx-*lengthLength-2>skip) { + this->stream->write(digit); + } + } + + if (len < this->bufferSize) { + this->buffer[len] = digit; + len++; + } + idx++; + } + + if (!this->stream && idx > this->bufferSize) { + len = 0; // This will cause the packet to be ignored. + } + return len; +} + +boolean PubSubClient::loop() { + if (connected()) { + unsigned long t = millis(); + if ((t - lastInActivity > this->keepAlive*1000UL) || (t - lastOutActivity > this->keepAlive*1000UL)) { + if (pingOutstanding) { + this->_state = MQTT_CONNECTION_TIMEOUT; + _client->stop(); + return false; + } else { + this->buffer[0] = MQTTPINGREQ; + this->buffer[1] = 0; + _client->write(this->buffer,2); + lastOutActivity = t; + lastInActivity = t; + pingOutstanding = true; + } + } + if (_client->available()) { + uint8_t llen; + uint16_t len = readPacket(&llen); + uint16_t msgId = 0; + uint8_t *payload; + if (len > 0) { + lastInActivity = t; + uint8_t type = this->buffer[0]&0xF0; + if (type == MQTTPUBLISH) { + if (callback) { + uint16_t tl = (this->buffer[llen+1]<<8)+this->buffer[llen+2]; /* topic length in bytes */ + memmove(this->buffer+llen+2,this->buffer+llen+3,tl); /* move topic inside buffer 1 byte to front */ + this->buffer[llen+2+tl] = 0; /* end the topic as a 'C' string with \x00 */ + char *topic = (char*) this->buffer+llen+2; + // msgId only present for QOS>0 + if ((this->buffer[0]&0x06) == MQTTQOS1) { + msgId = (this->buffer[llen+3+tl]<<8)+this->buffer[llen+3+tl+1]; + payload = this->buffer+llen+3+tl+2; + callback(topic,payload,len-llen-3-tl-2); + + this->buffer[0] = MQTTPUBACK; + this->buffer[1] = 2; + this->buffer[2] = (msgId >> 8); + this->buffer[3] = (msgId & 0xFF); + _client->write(this->buffer,4); + lastOutActivity = t; + + } else { + payload = this->buffer+llen+3+tl; + callback(topic,payload,len-llen-3-tl); + } + } + } else if (type == MQTTPINGREQ) { + this->buffer[0] = MQTTPINGRESP; + this->buffer[1] = 0; + _client->write(this->buffer,2); + } else if (type == MQTTPINGRESP) { + pingOutstanding = false; + } + } else if (!connected()) { + // readPacket has closed the connection + return false; + } + } + return true; + } + return false; +} + +boolean PubSubClient::publish(const char* topic, const char* payload) { + return publish(topic,(const uint8_t*)payload, payload ? strnlen(payload, this->bufferSize) : 0,false); +} + +boolean PubSubClient::publish(const char* topic, const char* payload, boolean retained) { + return publish(topic,(const uint8_t*)payload, payload ? strnlen(payload, this->bufferSize) : 0,retained); +} + +boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength) { + return publish(topic, payload, plength, false); +} + +boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) { + if (connected()) { + if (this->bufferSize < MQTT_MAX_HEADER_SIZE + 2+strnlen(topic, this->bufferSize) + plength) { + // Too long + return false; + } + // Leave room in the buffer for header and variable length field + uint16_t length = MQTT_MAX_HEADER_SIZE; + length = writeString(topic,this->buffer,length); + + // Add payload + uint16_t i; + for (i=0;ibuffer[length++] = payload[i]; + } + + // Write the header + uint8_t header = MQTTPUBLISH; + if (retained) { + header |= 1; + } + return write(header,this->buffer,length-MQTT_MAX_HEADER_SIZE); + } + return false; +} + +boolean PubSubClient::publish_P(const char* topic, const char* payload, boolean retained) { + return publish_P(topic, (const uint8_t*)payload, payload ? strnlen(payload, this->bufferSize) : 0, retained); +} + +boolean PubSubClient::publish_P(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) { + uint8_t llen = 0; + uint8_t digit; + unsigned int rc = 0; + uint16_t tlen; + unsigned int pos = 0; + unsigned int i; + uint8_t header; + unsigned int len; + int expectedLength; + + if (!connected()) { + return false; + } + + tlen = strnlen(topic, this->bufferSize); + + header = MQTTPUBLISH; + if (retained) { + header |= 1; + } + this->buffer[pos++] = header; + len = plength + 2 + tlen; + do { + digit = len & 127; //digit = len %128 + len >>= 7; //len = len / 128 + if (len > 0) { + digit |= 0x80; + } + this->buffer[pos++] = digit; + llen++; + } while(len>0); + + pos = writeString(topic,this->buffer,pos); + + rc += _client->write(this->buffer,pos); + + for (i=0;iwrite((char)pgm_read_byte_near(payload + i)); + } + + lastOutActivity = millis(); + + expectedLength = 1 + llen + 2 + tlen + plength; + + return (rc == expectedLength); +} + +boolean PubSubClient::beginPublish(const char* topic, unsigned int plength, boolean retained) { + if (connected()) { + // Send the header and variable length field + uint16_t length = MQTT_MAX_HEADER_SIZE; + length = writeString(topic,this->buffer,length); + uint8_t header = MQTTPUBLISH; + if (retained) { + header |= 1; + } + size_t hlen = buildHeader(header, this->buffer, plength+length-MQTT_MAX_HEADER_SIZE); + uint16_t rc = _client->write(this->buffer+(MQTT_MAX_HEADER_SIZE-hlen),length-(MQTT_MAX_HEADER_SIZE-hlen)); + lastOutActivity = millis(); + return (rc == (length-(MQTT_MAX_HEADER_SIZE-hlen))); + } + return false; +} + +int PubSubClient::endPublish() { + return 1; +} + +size_t PubSubClient::write(uint8_t data) { + lastOutActivity = millis(); + return _client->write(data); +} + +size_t PubSubClient::write(const uint8_t *buffer, size_t size) { + lastOutActivity = millis(); + return _client->write(buffer,size); +} + +size_t PubSubClient::buildHeader(uint8_t header, uint8_t* buf, uint16_t length) { + uint8_t lenBuf[4]; + uint8_t llen = 0; + uint8_t digit; + uint8_t pos = 0; + uint16_t len = length; + do { + + digit = len & 127; //digit = len %128 + len >>= 7; //len = len / 128 + if (len > 0) { + digit |= 0x80; + } + lenBuf[pos++] = digit; + llen++; + } while(len>0); + + buf[4-llen] = header; + for (int i=0;i 0) && result) { + bytesToWrite = (bytesRemaining > MQTT_MAX_TRANSFER_SIZE)?MQTT_MAX_TRANSFER_SIZE:bytesRemaining; + rc = _client->write(writeBuf,bytesToWrite); + result = (rc == bytesToWrite); + bytesRemaining -= rc; + writeBuf += rc; + } + return result; +#else + rc = _client->write(buf+(MQTT_MAX_HEADER_SIZE-hlen),length+hlen); + lastOutActivity = millis(); + return (rc == hlen+length); +#endif +} + +boolean PubSubClient::subscribe(const char* topic) { + return subscribe(topic, 0); +} + +boolean PubSubClient::subscribe(const char* topic, uint8_t qos) { + size_t topicLength = strnlen(topic, this->bufferSize); + if (topic == 0) { + return false; + } + if (qos > 1) { + return false; + } + if (this->bufferSize < 9 + topicLength) { + // Too long + return false; + } + if (connected()) { + // Leave room in the buffer for header and variable length field + uint16_t length = MQTT_MAX_HEADER_SIZE; + nextMsgId++; + if (nextMsgId == 0) { + nextMsgId = 1; + } + this->buffer[length++] = (nextMsgId >> 8); + this->buffer[length++] = (nextMsgId & 0xFF); + length = writeString((char*)topic, this->buffer,length); + this->buffer[length++] = qos; + return write(MQTTSUBSCRIBE|MQTTQOS1,this->buffer,length-MQTT_MAX_HEADER_SIZE); + } + return false; +} + +boolean PubSubClient::unsubscribe(const char* topic) { + size_t topicLength = strnlen(topic, this->bufferSize); + if (topic == 0) { + return false; + } + if (this->bufferSize < 9 + topicLength) { + // Too long + return false; + } + if (connected()) { + uint16_t length = MQTT_MAX_HEADER_SIZE; + nextMsgId++; + if (nextMsgId == 0) { + nextMsgId = 1; + } + this->buffer[length++] = (nextMsgId >> 8); + this->buffer[length++] = (nextMsgId & 0xFF); + length = writeString(topic, this->buffer,length); + return write(MQTTUNSUBSCRIBE|MQTTQOS1,this->buffer,length-MQTT_MAX_HEADER_SIZE); + } + return false; +} + +void PubSubClient::disconnect() { + this->buffer[0] = MQTTDISCONNECT; + this->buffer[1] = 0; + _client->write(this->buffer,2); + _state = MQTT_DISCONNECTED; + _client->flush(); + _client->stop(); + lastInActivity = lastOutActivity = millis(); +} + +uint16_t PubSubClient::writeString(const char* string, uint8_t* buf, uint16_t pos) { + const char* idp = string; + uint16_t i = 0; + pos += 2; + while (*idp) { + buf[pos++] = *idp++; + i++; + } + buf[pos-i-2] = (i >> 8); + buf[pos-i-1] = (i & 0xFF); + return pos; +} + + +boolean PubSubClient::connected() { + boolean rc; + if (_client == NULL ) { + rc = false; + } else { + rc = (int)_client->connected(); + if (!rc) { + if (this->_state == MQTT_CONNECTED) { + this->_state = MQTT_CONNECTION_LOST; + _client->flush(); + _client->stop(); + } + } else { + return this->_state == MQTT_CONNECTED; + } + } + return rc; +} + +PubSubClient& PubSubClient::setServer(uint8_t * ip, uint16_t port) { + IPAddress addr(ip[0],ip[1],ip[2],ip[3]); + return setServer(addr,port); +} + +PubSubClient& PubSubClient::setServer(IPAddress ip, uint16_t port) { + this->ip = ip; + this->port = port; + this->domain = NULL; + return *this; +} + +PubSubClient& PubSubClient::setServer(const char * domain, uint16_t port) { + this->domain = domain; + this->port = port; + return *this; +} + +PubSubClient& PubSubClient::setCallback(MQTT_CALLBACK_SIGNATURE) { + this->callback = callback; + return *this; +} + +PubSubClient& PubSubClient::setClient(Client& client){ + this->_client = &client; + return *this; +} + +PubSubClient& PubSubClient::setStream(Stream& stream){ + this->stream = &stream; + return *this; +} + +int PubSubClient::state() { + return this->_state; +} + +boolean PubSubClient::setBufferSize(uint16_t size) { + if (size == 0) { + // Cannot set it back to 0 + return false; + } + if (this->bufferSize == 0) { + this->buffer = (uint8_t*)malloc(size); + } else { + uint8_t* newBuffer = (uint8_t*)realloc(this->buffer, size); + if (newBuffer != NULL) { + this->buffer = newBuffer; + } else { + return false; + } + } + this->bufferSize = size; + return (this->buffer != NULL); +} + +uint16_t PubSubClient::getBufferSize() { + return this->bufferSize; +} +PubSubClient& PubSubClient::setKeepAlive(uint16_t keepAlive) { + this->keepAlive = keepAlive; + return *this; +} +PubSubClient& PubSubClient::setSocketTimeout(uint16_t timeout) { + this->socketTimeout = timeout; + return *this; +} diff --git a/src/Libraries/pubsubclient-2.8/src/PubSubClient.h b/src/Libraries/pubsubclient-2.8/src/PubSubClient.h new file mode 100644 index 00000000..c70d9fd3 --- /dev/null +++ b/src/Libraries/pubsubclient-2.8/src/PubSubClient.h @@ -0,0 +1,184 @@ +/* + PubSubClient.h - A simple client for MQTT. + Nick O'Leary + http://knolleary.net +*/ + +#ifndef PubSubClient_h +#define PubSubClient_h + +#include +#include "IPAddress.h" +#include "Client.h" +#include "Stream.h" + +#define MQTT_VERSION_3_1 3 +#define MQTT_VERSION_3_1_1 4 + +// MQTT_VERSION : Pick the version +//#define MQTT_VERSION MQTT_VERSION_3_1 +#ifndef MQTT_VERSION +#define MQTT_VERSION MQTT_VERSION_3_1_1 +#endif + +// MQTT_MAX_PACKET_SIZE : Maximum packet size. Override with setBufferSize(). +#ifndef MQTT_MAX_PACKET_SIZE +#define MQTT_MAX_PACKET_SIZE 256 +#endif + +// MQTT_KEEPALIVE : keepAlive interval in Seconds. Override with setKeepAlive() +#ifndef MQTT_KEEPALIVE +#define MQTT_KEEPALIVE 15 +#endif + +// MQTT_SOCKET_TIMEOUT: socket timeout interval in Seconds. Override with setSocketTimeout() +#ifndef MQTT_SOCKET_TIMEOUT +#define MQTT_SOCKET_TIMEOUT 15 +#endif + +// MQTT_MAX_TRANSFER_SIZE : limit how much data is passed to the network client +// in each write call. Needed for the Arduino Wifi Shield. Leave undefined to +// pass the entire MQTT packet in each write call. +//#define MQTT_MAX_TRANSFER_SIZE 80 + +// Possible values for client.state() +#define MQTT_CONNECTION_TIMEOUT -4 +#define MQTT_CONNECTION_LOST -3 +#define MQTT_CONNECT_FAILED -2 +#define MQTT_DISCONNECTED -1 +#define MQTT_CONNECTED 0 +#define MQTT_CONNECT_BAD_PROTOCOL 1 +#define MQTT_CONNECT_BAD_CLIENT_ID 2 +#define MQTT_CONNECT_UNAVAILABLE 3 +#define MQTT_CONNECT_BAD_CREDENTIALS 4 +#define MQTT_CONNECT_UNAUTHORIZED 5 + +#define MQTTCONNECT 1 << 4 // Client request to connect to Server +#define MQTTCONNACK 2 << 4 // Connect Acknowledgment +#define MQTTPUBLISH 3 << 4 // Publish message +#define MQTTPUBACK 4 << 4 // Publish Acknowledgment +#define MQTTPUBREC 5 << 4 // Publish Received (assured delivery part 1) +#define MQTTPUBREL 6 << 4 // Publish Release (assured delivery part 2) +#define MQTTPUBCOMP 7 << 4 // Publish Complete (assured delivery part 3) +#define MQTTSUBSCRIBE 8 << 4 // Client Subscribe request +#define MQTTSUBACK 9 << 4 // Subscribe Acknowledgment +#define MQTTUNSUBSCRIBE 10 << 4 // Client Unsubscribe request +#define MQTTUNSUBACK 11 << 4 // Unsubscribe Acknowledgment +#define MQTTPINGREQ 12 << 4 // PING Request +#define MQTTPINGRESP 13 << 4 // PING Response +#define MQTTDISCONNECT 14 << 4 // Client is Disconnecting +#define MQTTReserved 15 << 4 // Reserved + +#define MQTTQOS0 (0 << 1) +#define MQTTQOS1 (1 << 1) +#define MQTTQOS2 (2 << 1) + +// Maximum size of fixed header and variable length size header +#define MQTT_MAX_HEADER_SIZE 5 + +#if defined(ESP8266) || defined(ESP32) +#include +#define MQTT_CALLBACK_SIGNATURE std::function callback +#else +#define MQTT_CALLBACK_SIGNATURE void (*callback)(char*, uint8_t*, unsigned int) +#endif + +#define CHECK_STRING_LENGTH(l,s) if (l+2+strnlen(s, this->bufferSize) > this->bufferSize) {_client->stop();return false;} + +class PubSubClient : public Print { +private: + Client* _client; + uint8_t* buffer; + uint16_t bufferSize; + uint16_t keepAlive; + uint16_t socketTimeout; + uint16_t nextMsgId; + unsigned long lastOutActivity; + unsigned long lastInActivity; + bool pingOutstanding; + MQTT_CALLBACK_SIGNATURE; + uint32_t readPacket(uint8_t*); + boolean readByte(uint8_t * result); + boolean readByte(uint8_t * result, uint16_t * index); + boolean write(uint8_t header, uint8_t* buf, uint16_t length); + uint16_t writeString(const char* string, uint8_t* buf, uint16_t pos); + // Build up the header ready to send + // Returns the size of the header + // Note: the header is built at the end of the first MQTT_MAX_HEADER_SIZE bytes, so will start + // (MQTT_MAX_HEADER_SIZE - ) bytes into the buffer + size_t buildHeader(uint8_t header, uint8_t* buf, uint16_t length); + IPAddress ip; + const char* domain; + uint16_t port; + Stream* stream; + int _state; +public: + PubSubClient(); + PubSubClient(Client& client); + PubSubClient(IPAddress, uint16_t, Client& client); + PubSubClient(IPAddress, uint16_t, Client& client, Stream&); + PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client); + PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&); + PubSubClient(uint8_t *, uint16_t, Client& client); + PubSubClient(uint8_t *, uint16_t, Client& client, Stream&); + PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client); + PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&); + PubSubClient(const char*, uint16_t, Client& client); + PubSubClient(const char*, uint16_t, Client& client, Stream&); + PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client); + PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&); + + ~PubSubClient(); + + PubSubClient& setServer(IPAddress ip, uint16_t port); + PubSubClient& setServer(uint8_t * ip, uint16_t port); + PubSubClient& setServer(const char * domain, uint16_t port); + PubSubClient& setCallback(MQTT_CALLBACK_SIGNATURE); + PubSubClient& setClient(Client& client); + PubSubClient& setStream(Stream& stream); + PubSubClient& setKeepAlive(uint16_t keepAlive); + PubSubClient& setSocketTimeout(uint16_t timeout); + + boolean setBufferSize(uint16_t size); + uint16_t getBufferSize(); + + boolean connect(const char* id); + boolean connect(const char* id, const char* user, const char* pass); + boolean connect(const char* id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage); + boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage); + boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage, boolean cleanSession); + void disconnect(); + boolean publish(const char* topic, const char* payload); + boolean publish(const char* topic, const char* payload, boolean retained); + boolean publish(const char* topic, const uint8_t * payload, unsigned int plength); + boolean publish(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained); + boolean publish_P(const char* topic, const char* payload, boolean retained); + boolean publish_P(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained); + // Start to publish a message. + // This API: + // beginPublish(...) + // one or more calls to write(...) + // endPublish() + // Allows for arbitrarily large payloads to be sent without them having to be copied into + // a new buffer and held in memory at one time + // Returns 1 if the message was started successfully, 0 if there was an error + boolean beginPublish(const char* topic, unsigned int plength, boolean retained); + // Finish off this publish message (started with beginPublish) + // Returns 1 if the packet was sent successfully, 0 if there was an error + int endPublish(); + // Write a single byte of payload (only to be used with beginPublish/endPublish) + virtual size_t write(uint8_t); + // Write size bytes from buffer into the payload (only to be used with beginPublish/endPublish) + // Returns the number of bytes written + virtual size_t write(const uint8_t *buffer, size_t size); + boolean subscribe(const char* topic); + boolean subscribe(const char* topic, uint8_t qos); + boolean unsubscribe(const char* topic); + boolean loop(); + boolean connected(); + int state(); + +}; + + +#endif diff --git a/src/Main/BoardDef.cpp b/src/Main/BoardDef.cpp index 738c2e91..42d8494b 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_3 */ + [DIY_PRO_INDOOR_V3_3] = + { + .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_3", + }, +}; /** * @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 1498f7d8..58748a78 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_3 = 0x04, _BOARD_MAX }; diff --git a/src/Main/LedBar.cpp b/src/Main/LedBar.cpp index 63aed352..97fb7ea5 100644 --- a/src/Main/LedBar.cpp +++ b/src/Main/LedBar.cpp @@ -116,6 +116,9 @@ void LedBar::setColor(uint8_t red, uint8_t green, uint8_t blue) { */ void LedBar::show(void) { // Ignore update the LED if LED bar disabled + if(this->isBegin() == false) { + return; + } if (enabled == false) { return; } diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp index fcbefea2..66c71e6e 100644 --- a/src/MqttClient.cpp +++ b/src/MqttClient.cpp @@ -1,11 +1,19 @@ -#ifdef ESP32 - #include "MqttClient.h" +#include "Libraries/pubsubclient-2.8/src/PubSubClient.h" +#ifdef ESP32 static void __mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); +#else +#define CLIENT() ((PubSubClient *)client) +#endif -MqttClient::MqttClient(Stream &debugLog) : PrintLog(debugLog, "MqttClient") {} +MqttClient::MqttClient(Stream &debugLog) : PrintLog(debugLog, "MqttClient") { +#ifdef ESP32 +#else + client = NULL; +#endif +} MqttClient::~MqttClient() {} @@ -22,6 +30,7 @@ bool MqttClient::begin(String uri) { this->uri = uri; logInfo("Init uri: " + uri); +#ifdef ESP32 /** config esp_mqtt client */ esp_mqtt_client_config_t config = { .uri = this->uri.c_str(), @@ -45,6 +54,108 @@ bool MqttClient::begin(String uri) { logError("Client start failed"); return false; } +#else + // mqtt://:@: + bool hasUser = false; + for (unsigned int i = 0; i < this->uri.length(); i++) { + if (this->uri[i] == '@') { + hasUser = true; + break; + } + } + + user = ""; + password = ""; + server = ""; + port = 0; + + char *serverPort = NULL; + char *buf = (char *)this->uri.c_str(); + if (hasUser) { + // mqtt://:@: + char *userPass = strtok(buf, "@"); + serverPort = strtok(NULL, "@"); + + if (userPass == NULL) { + logError("User and Password invalid"); + return false; + } else { + if ((userPass[5] == '/') && (userPass[6] == '/')) { /** Check mqtt:// */ + userPass = &userPass[7]; + } else if ((userPass[6] == '/') && + (userPass[7] == '/')) { /** Check mqtts:// */ + userPass = &userPass[8]; + } else { + logError("Server invalid"); + return false; + } + + buf = strtok(userPass, ":"); + if (buf == NULL) { + logError("User invalid"); + return false; + } + user = String(buf); + + buf = strtok(NULL, "@"); + if (buf == NULL) { + logError("Password invalid"); + return false; + } + password = String(buf); + + logInfo("Username: " + user); + logInfo("Password: " + password); + } + + if (serverPort == NULL) { + logError("Server and port invalid"); + return false; + } + } else { + // mqtt://: + if ((buf[5] == '/') && (buf[6] == '/')) { /** Check mqtt:// */ + serverPort = &buf[7]; + } else if ((buf[6] == '/') && (buf[7] == '/')) { /** Check mqtts:// */ + serverPort = &buf[8]; + } else { + logError("Server invalid"); + return false; + } + } + + if (serverPort == NULL) { + logError("Server and port invalid"); + return false; + } + + buf = strtok(serverPort, ":"); + if (buf == NULL) { + logError("Server invalid"); + return false; + } + server = String(buf); + logInfo("Server: " + server); + + buf = strtok(NULL, ":"); + if (buf == NULL) { + logError("Port invalid"); + return false; + } + port = (uint16_t)String(buf).toInt(); + logInfo("Port: " + String(port)); + + if (client == NULL) { + client = new PubSubClient(__wifiClient); + if (client == NULL) { + return false; + } + } + + CLIENT()->setServer(server.c_str(), port); + CLIENT()->setBufferSize(1024); + connected = false; +#endif isBegin = true; connectionFailedCount = 0; @@ -56,12 +167,16 @@ void MqttClient::end(void) { logWarning("Already end, call 'begin' and try again"); return; } - +#ifdef ESP32 esp_mqtt_client_disconnect(client); esp_mqtt_client_stop(client); esp_mqtt_client_destroy(client); client = NULL; +#else + CLIENT()->disconnect(); +#endif isBegin = false; + this->uri = ""; logInfo("end"); } @@ -86,10 +201,17 @@ bool MqttClient::publish(const char *topic, const char *payload, int len) { return false; } +#ifdef ESP32 if (esp_mqtt_client_publish(client, topic, payload, len, 0, 0) == ESP_OK) { logInfo("Publish success"); return true; } +#else + if (CLIENT()->publish(topic, payload)) { + logInfo("Publish success"); + return true; + } +#endif logError("Publish failed"); return false; } @@ -114,7 +236,9 @@ bool MqttClient::isCurrentUri(String &uri) { * @return true Connected * @return false Disconnected */ -bool MqttClient::isConnected(void) { return connected; } +bool MqttClient::isConnected(void) { + return connected; +} /** * @brief Get number of connection failed @@ -123,6 +247,35 @@ bool MqttClient::isConnected(void) { return connected; } */ int MqttClient::getConnectionFailedCount(void) { return connectionFailedCount; } +#ifdef ESP8266 +bool MqttClient::connect(String id) { + if (isBegin == false) { + return false; + } + + if (this->uri.isEmpty()) { + return false; + } + + connected = false; + if (user.isEmpty()) { + logInfo("Connect without auth"); + if(CLIENT()->connect(id.c_str())) { + connected = true; + } + return connected; + } + return CLIENT()->connect(id.c_str(), user.c_str(), password.c_str()); +} +void MqttClient::handle(void) { + if (isBegin == false) { + return; + } + CLIENT()->loop(); +} +#endif + +#ifdef ESP32 static void __mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { MqttClient *mqtt = (MqttClient *)handler_args; @@ -164,5 +317,4 @@ static void __mqtt_event_handler(void *handler_args, esp_event_base_t base, break; } } - -#endif /** ESP32 */ +#endif diff --git a/src/MqttClient.h b/src/MqttClient.h index cc557c0d..222f9107 100644 --- a/src/MqttClient.h +++ b/src/MqttClient.h @@ -2,8 +2,10 @@ #define _AG_MQTT_CLIENT_H_ #ifdef ESP32 - #include "mqtt_client.h" +#else +#include +#endif /** ESP32 */ #include "Main/PrintLog.h" #include @@ -11,7 +13,16 @@ class MqttClient: public PrintLog { private: bool isBegin = false; String uri; +#ifdef ESP32 esp_mqtt_client_handle_t client; +#else + WiFiClient __wifiClient; + void* client; + String password; + String user; + String server; + uint16_t port; +#endif bool connected = false; int connectionFailedCount = 0; @@ -26,8 +37,10 @@ class MqttClient: public PrintLog { bool isCurrentUri(String &uri); bool isConnected(void); int getConnectionFailedCount(void); +#ifdef ESP8266 + bool connect(String id); + void handle(void); +#endif }; -#endif /** ESP32 */ - #endif /** _AG_MQTT_CLIENT_H_ */ diff --git a/src/Sgp41/Sgp41.cpp b/src/Sgp41/Sgp41.cpp index b9801f5d..502dcb93 100644 --- a/src/Sgp41/Sgp41.cpp +++ b/src/Sgp41/Sgp41.cpp @@ -104,6 +104,8 @@ void Sgp41::handle(void) { } else { uint16_t srawVoc, srawNox; if (getRawSignal(srawVoc, srawNox)) { + tvocRaw = srawVoc; + noxRaw = srawNox; nox = noxAlgorithm()->process(srawNox); tvoc = vocAlgorithm()->process(srawVoc); // AgLog("Polling SGP41 success: tvoc: %d, nox: %d", tvoc, nox);