diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index cf815db1..51bb68de 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -78,6 +78,8 @@ jobs: examples/${{ matrix.example }} libraries: | - source-path: ./ + - name: NimBLE-Arduino + version: 2.3.7 cli-compile-flags: | - --warnings - none diff --git a/docs/howto-compile.md b/docs/howto-compile.md index 2bad278d..70e08c40 100644 --- a/docs/howto-compile.md +++ b/docs/howto-compile.md @@ -26,6 +26,11 @@ Using library manager install the latest version (Tools ➝ Manage Libraries... - With **git** cli, execute this command `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor` - Restart Arduino IDE +#### Version >= 3.6.0 + +- Ensure `NimBLE-Arduino` by h2zero library version `2.3.7` is installed using Arduino library manager +- Follow steps of ">= 3.3.0" + 3. On tools tab, follow settings below ``` diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index d541fdf2..8d294118 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -59,24 +59,24 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License #include "esp_system.h" #include "freertos/projdefs.h" -#define LED_BAR_ANIMATION_PERIOD 100 /** ms */ -#define DISP_UPDATE_INTERVAL 2500 /** ms */ -#define WIFI_SERVER_CONFIG_SYNC_INTERVAL 1 * 60000 /** ms */ -#define WIFI_MEASUREMENT_INTERVAL 1 * 60000 /** ms */ -#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */ -#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */ -#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */ -#define CELLULAR_TRANSMISSION_INTERVAL 3 * 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 6000 /** ms */ -#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */ -#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */ +#define LED_BAR_ANIMATION_PERIOD 100 /** ms */ +#define DISP_UPDATE_INTERVAL 2500 /** ms */ +#define WIFI_SERVER_CONFIG_SYNC_INTERVAL 1 * 60000 /** ms */ +#define WIFI_MEASUREMENT_INTERVAL 1 * 60000 /** ms */ +#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */ +#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */ +#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */ +#define CELLULAR_TRANSMISSION_INTERVAL 3 * 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 6000 /** ms */ +#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */ +#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */ #define TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE (1 * 60) /** minutes */ -#define TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY (2 * 60) /** minutes */ +#define TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY (2 * 60) /** minutes */ #define MEASUREMENT_TRANSMIT_CYCLE 3 #define MAXIMUM_MEASUREMENT_CYCLE_QUEUE 80 @@ -88,7 +88,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License #define OLED_I2C_ADDR 0x3C /** Power pin */ -#define GPIO_POWER_MODULE_PIN 5 +#define GPIO_POWER_MODULE_PIN 5 #define GPIO_EXPANSION_CARD_POWER 4 #define GPIO_IIC_RESET 3 @@ -100,21 +100,15 @@ static Configuration configuration(Serial); static Measurements measurements(configuration); static AirGradient *ag; static OledDisplay oledDisplay(configuration, measurements, Serial); -static StateMachine stateMachine(oledDisplay, Serial, measurements, - configuration); -static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine, - configuration); +static StateMachine stateMachine(oledDisplay, Serial, measurements, configuration); +static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine, configuration); static OpenMetrics openMetrics(measurements, configuration, wifiConnector); -static LocalServer localServer(Serial, openMetrics, measurements, configuration, - wifiConnector); +static LocalServer localServer(Serial, openMetrics, measurements, configuration, wifiConnector); static AgSerial *agSerial; static CellularModule *cellularCard; static AirgradientClient *agClient; -enum NetworkOption { - UseWifi, - UseCellular -}; +enum NetworkOption { UseWifi, UseCellular }; NetworkOption networkOption; TaskHandle_t handleNetworkTask = NULL; static bool firmwareUpdateInProgress = false; @@ -162,8 +156,7 @@ static void networkSignalCheck(); static void networkingTask(void *args); AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar); -AgSchedule configSchedule(WIFI_SERVER_CONFIG_SYNC_INTERVAL, - configurationUpdateSchedule); +AgSchedule configSchedule(WIFI_SERVER_CONFIG_SYNC_INTERVAL, configurationUpdateSchedule); AgSchedule transmissionSchedule(WIFI_TRANSMISSION_INTERVAL, sendDataToServer); AgSchedule measurementSchedule(WIFI_MEASUREMENT_INTERVAL, newMeasurementCycle); AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update); @@ -226,17 +219,15 @@ void setup() { /** Show message confirm offline mode, should me perform if LED bar button * test pressed */ if (ledBarButtonTest == false) { - oledDisplay.setText( - "Press now for", - configuration.isOfflineMode() ? "online mode" : "offline mode", ""); + 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", ""); + oledDisplay.setText("Offline Mode", + configuration.isOfflineMode() ? " = True" : " = False", ""); delay(1000); break; } @@ -256,12 +247,7 @@ void setup() { if (connectToNetwork) { oledDisplay.setText("Initialize", "network...", ""); initializeNetwork(); - } - - /** Set offline mode without saving, cause wifi is not configured */ - if (wifiConnector.hasConfigurated() == false && networkOption == UseWifi) { - Serial.println("Set offline mode cause wifi is not configurated"); - configuration.setOfflineModeWithoutSave(true); + wifiConnector.stopBLE(); } /** Show display Warning up */ @@ -274,7 +260,6 @@ void setup() { delay(DISPLAY_DELAY_SHOW_CONTENT_MS); } - if (networkOption == UseCellular) { // If using cellular re-set scheduler interval configSchedule.setPeriod(CELLULAR_SERVER_CONFIG_SYNC_INTERVAL); @@ -291,7 +276,7 @@ void setup() { // Only run network task if monitor is not in offline mode if (configuration.isOfflineMode() == false) { BaseType_t xReturned = - xTaskCreate(networkingTask, "NetworkingTask", 4096, null, 5, &handleNetworkTask); + xTaskCreate(networkingTask, "NetworkingTask", 4096, null, 5, &handleNetworkTask); if (xReturned == pdPASS) { Serial.println("Success create networking task"); } else { @@ -302,11 +287,9 @@ void setup() { // Log monitor mode for debugging purpose if (configuration.isOfflineMode()) { Serial.println("Running monitor in offline mode"); - } - else if (configuration.isCloudConnectionDisabled()) { + } else if (configuration.isCloudConnectionDisabled()) { Serial.println("Running monitor without connection to AirGradient server"); } - } void loop() { @@ -353,7 +336,7 @@ void loop() { static bool pmsConnected = false; if (pmsConnected != ag->pms5003.connected()) { pmsConnected = ag->pms5003.connected(); - Serial.printf("PMS sensor %s \n", pmsConnected?"connected":"removed"); + Serial.printf("PMS sensor %s \n", pmsConnected ? "connected" : "removed"); } } } else { @@ -392,9 +375,7 @@ static void co2Update(void) { } } -void printMeasurements() { - measurements.printCurrentAverage(); -} +void printMeasurements() { measurements.printCurrentAverage(); } static void mdnsInit(void) { if (!MDNS.begin(localServer.getHostname().c_str())) { @@ -403,8 +384,7 @@ static void mdnsInit(void) { } MDNS.addService("_airgradient", "_tcp", 80); - MDNS.addServiceTxt("_airgradient", "_tcp", "model", - AgFirmwareModeName(fwMode)); + 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"); @@ -428,8 +408,7 @@ static void createMqttTask(void) { String payload = measurements.toString(true, fwMode, wifiConnector.RSSI()); String topic = "airgradient/readings/" + ag->deviceId(); - if (mqttClient.publish(topic.c_str(), payload.c_str(), - payload.length())) { + if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) { Serial.println("MQTT sync success"); } else { Serial.println("MQTT sync failure"); @@ -447,8 +426,7 @@ static void createMqttTask(void) { static void initMqtt(void) { String mqttUri = configuration.getMqttBrokerUri(); if (mqttUri.isEmpty()) { - Serial.println( - "MQTT is not configured, skipping initialization of MQTT client"); + Serial.println("MQTT is not configured, skipping initialization of MQTT client"); return; } @@ -509,7 +487,7 @@ static void factoryConfigReset(void) { Serial.println("Factory reset successful"); } delay(3000); - oledDisplay.setText("","",""); + oledDisplay.setText("", "", ""); ESP.restart(); } } @@ -547,7 +525,7 @@ static void ledBarEnabledUpdate(void) { ag->ledBar.setBrightness(brightness); ag->ledBar.setEnable(configuration.getLedBarMode() != LedBarModeOff); } - ag->ledBar.show(); + ag->ledBar.show(); } } @@ -618,11 +596,11 @@ void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) { displayExecuteOta(result, "", std::stoi(msg)); break; case AirgradientOTA::Failed: - displayExecuteOta(result, "", 0); - if (configuration.hasSensorSGP && networkOption == UseCellular) { - ag->sgp41.resume(); - } - break; + displayExecuteOta(result, "", 0); + if (configuration.hasSensorSGP && networkOption == UseCellular) { + ag->sgp41.resume(); + } + break; case AirgradientOTA::Skipped: case AirgradientOTA::AlreadyUpToDate: displayExecuteOta(result, "", 0); @@ -638,7 +616,7 @@ void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) { static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int processing) { switch (result) { - case AirgradientOTA::Starting: + case AirgradientOTA::Starting: if (ag->isOne()) { oledDisplay.showFirmwareUpdateVersion(msg); } else { @@ -707,6 +685,7 @@ static void sendDataToAg() { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnecting); + wifiConnector.bleNotifyStatus(PROV_CONNECTING_TO_SERVER); /** Task handle led connecting animation */ xTaskCreate( @@ -714,8 +693,7 @@ static void sendDataToAg() { for (;;) { // ledSmHandler(); stateMachine.handleLeds(); - if (stateMachine.getLedState() != - AgStateMachineWiFiOkServerConnecting) { + if (stateMachine.getLedState() != AgStateMachineWiFiOkServerConnecting) { break; } delay(LED_BAR_ANIMATION_PERIOD); @@ -736,11 +714,13 @@ static void sendDataToAg() { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnected); + wifiConnector.bleNotifyStatus(PROV_SERVER_REACHABLE); } else { if (ag->isOne()) { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed); + wifiConnector.bleNotifyStatus(PROV_ERR_SERVER_UNREACHABLE); } stateMachine.handleLeds(AgStateMachineNormal); @@ -761,8 +741,7 @@ static void oneIndoorInit(void) { /** Show boot display */ Serial.println("Firmware Version: " + ag->getVersion()); - oledDisplay.setText("AirGradient ONE", - "FW Version: ", ag->getVersion().c_str()); + oledDisplay.setText("AirGradient ONE", "FW Version: ", ag->getVersion().c_str()); delay(DISPLAY_DELAY_SHOW_CONTENT_MS); ag->ledBar.begin(); @@ -793,9 +772,9 @@ static void oneIndoorInit(void) { WiFi.begin("airgradient", "cleanair"); oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'"); delay(2500); - oledDisplay.setText("Rebooting...", "",""); + oledDisplay.setText("Rebooting...", "", ""); delay(2500); - oledDisplay.setText("","",""); + oledDisplay.setText("", "", ""); ESP.restart(); } } @@ -921,8 +900,7 @@ static void openAirInit(void) { } if (fwMode == FW_MODE_O_1PP) { - int count = (configuration.hasSensorPMS1 ? 1 : 0) + - (configuration.hasSensorPMS2 ? 1 : 0); + int count = (configuration.hasSensorPMS1 ? 1 : 0) + (configuration.hasSensorPMS2 ? 1 : 0); if (count == 1) { fwMode = FW_MODE_O_1P; } @@ -1014,22 +992,18 @@ void initializeNetwork() { } if (networkOption == UseWifi) { - if (!wifiConnector.connect()) { + String modelName = AgFirmwareModeName(fwMode); + if (!wifiConnector.connect(modelName)) { Serial.println("Cannot initiate wifi connection"); return; } if (!wifiConnector.isConnected()) { Serial.println("Failed connect to WiFi"); - if (wifiConnector.isConfigurePorttalTimeout()) { - oledDisplay.showRebooting(); - delay(2500); - oledDisplay.setText("", "", ""); - ESP.restart(); - } - - // Directly return because the rest of the function applied if wifi is connect only - return; + oledDisplay.showRebooting(); + delay(2500); + oledDisplay.setText("", "", ""); + ESP.restart(); } // Initiate local network configuration @@ -1064,21 +1038,22 @@ void initializeNetwork() { if (agClient->isRegisteredOnAgServer() == false) { stateMachine.displaySetAddToDashBoard(); stateMachine.displayHandle(AgStateMachineWiFiOkServerOkSensorConfigFailed); + wifiConnector.bleNotifyStatus(PROV_ERR_MONITOR_NOT_REGISTERED); } else { stateMachine.displayClearAddToDashBoard(); + wifiConnector.bleNotifyStatus(PROV_ERR_GET_MONITOR_CONFIG_FAILED); } } stateMachine.handleLeds(AgStateMachineWiFiOkServerOkSensorConfigFailed); delay(DISPLAY_DELAY_SHOW_CONTENT_MS); - } - else { + } else { ledBarEnabledUpdate(); + wifiConnector.bleNotifyStatus(PROV_MONITOR_CONFIGURED); } } static void configurationUpdateSchedule(void) { - if (configuration.getConfigurationControl() == - ConfigurationControl::ConfigurationControlLocal) { + if (configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) { Serial.println("Ignore fetch server configuration, configurationControl set to local"); agClient->resetFetchConfigurationStatus(); return; @@ -1112,8 +1087,7 @@ static void configUpdateHandle() { } if (configuration.hasSensorSGP) { - if (configuration.noxLearnOffsetChanged() || - configuration.tvocLearnOffsetChanged()) { + if (configuration.noxLearnOffsetChanged() || configuration.tvocLearnOffsetChanged()) { ag->sgp41.end(); int oldTvocOffset = ag->sgp41.getTvocLearningOffset(); @@ -1124,14 +1098,12 @@ static void configUpdateHandle() { resultStr = "failure"; } if (oldTvocOffset != configuration.getTvocLearningOffset()) { - Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n", - oldTvocOffset, configuration.getTvocLearningOffset(), - resultStr); + 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); + Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n", oldNoxOffset, + configuration.getNoxLearningOffset(), resultStr); } } } @@ -1153,7 +1125,7 @@ static void configUpdateHandle() { if (configuration.getLedBarBrightness() == 0) { ag->ledBar.setEnable(false); } else { - if(configuration.getLedBarMode() == LedBarMode::LedBarModeOff) { + if (configuration.getLedBarMode() == LedBarMode::LedBarModeOff) { ag->ledBar.setEnable(false); } else { ag->ledBar.setEnable(true); @@ -1191,9 +1163,8 @@ static void updateDisplayAndLedBar(void) { stateMachine.handleLeds(AgStateMachineWiFiLost); return; } - } - else if (networkOption == UseCellular) { - if (agClient->isClientReady() == false) { + } else if (networkOption == UseCellular) { + if (agClient->isClientReady() == false) { // Same action as wifi stateMachine.displayHandle(AgStateMachineWiFiLost); stateMachine.handleLeds(AgStateMachineWiFiLost); @@ -1390,8 +1361,8 @@ void postUsingWifi() { } /** -* forcePost to force post without checking transmit cycle -*/ + * forcePost to force post without checking transmit cycle + */ void postUsingCellular(bool forcePost) { // Aquire queue mutex to get queue size xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY); @@ -1531,7 +1502,6 @@ int calculateMaxPeriod(int updateInterval) { return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval; } - void networkSignalCheck() { if (networkOption == UseWifi) { Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI()); @@ -1557,12 +1527,11 @@ void networkSignalCheck() { } /** -* If in 2 hours cellular client still not ready, then restart system -*/ + * If in 2 hours cellular client still not ready, then restart system + */ void restartIfCeClientIssueOverTwoHours() { if (agCeClientProblemDetectedTime > 0 && - (MINUTES() - agCeClientProblemDetectedTime) > - TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY) { + (MINUTES() - agCeClientProblemDetectedTime) > TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY) { // Give up wait Serial.println("Rebooting because CE client issues for 2 hours detected"); int i = 3; @@ -1613,8 +1582,7 @@ void networkingTask(void *args) { delay(1000); continue; } - } - else if (networkOption == UseCellular) { + } else if (networkOption == UseCellular) { if (agClient->isClientReady() == false) { // Start time if value still default if (agCeClientProblemDetectedTime == 0) { @@ -1652,7 +1620,7 @@ void networkingTask(void *args) { // Client is ready agCeClientProblemDetectedTime = 0; // reset to default - agSerial->setDebug(false); // disable at command debug + agSerial->setDebug(false); // disable at command debug } } @@ -1694,4 +1662,3 @@ void newMeasurementCycle() { Serial.printf("Free heap: %u\n", ESP.getFreeHeap()); } } - diff --git a/platformio.ini b/platformio.ini index 73f77125..c9592c9a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,6 +26,7 @@ lib_deps = WiFiClientSecure Update DNSServer + h2zero/NimBLE-Arduino@^2.1.0 [env:esp8266] platform = espressif8266 @@ -38,7 +39,7 @@ lib_deps = ESP8266HTTPClient ESP8266WebServer DNSServer - +; monitor_filters = time [platformio] diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index a620c82b..8951fe71 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -19,10 +19,7 @@ static unsigned char OFFLINE_BITS[] = { 0xE6, 0x00, 0xFE, 0x1F, 0xFE, 0x1F, 0xE6, 0x00, 0x62, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, }; -// { -// 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x62, 0x00, 0xE2, 0x00, -// 0xFE, 0x1F, 0xFE, 0x1F, 0xE2, 0x00, 0x62, 0x00, 0x60, 0x00, 0x30, 0x00, -// 0x00, 0x00, 0x00, 0x00, }; + /** * @brief Show dashboard temperature and humdity * @@ -270,6 +267,37 @@ void OledDisplay::setText(const char *line1, const char *line2, } } +void OledDisplay::showWiFiProvisioning(bool firstRun, int countdown) { + if (firstRun) { + DISP()->clearBuffer(); + DISP()->setFont(u8g2_font_t0_16_tf); + DISP()->drawStr(1, 25, "to WiFi hotspot:"); + DISP()->drawStr(1, 40, "\"airgradient-"); + DISP()->drawStr(1, 55, (ag->deviceId() + "\"").c_str()); + } + + // Now just update countdown area + char buf[16]; + snprintf(buf, sizeof(buf), "%ds to connect", countdown); + DISP()->setDrawColor(0); // erase previous text + DISP()->drawBox(0, 0, 128, 14); // clear top region + DISP()->setDrawColor(1); // draw new text in white + DISP()->setFont(u8g2_font_t0_16_tf); + DISP()->drawStr(1, 10, buf); + + // Blink the BLE mark section + if (countdown % 2 == 0) { + DISP()->setFont(u8g2_font_t0_12b_tf); + DISP()->drawStr(108, 60, "BLE"); + } else { + DISP()->setDrawColor(0); + DISP()->drawBox(108, 48, 20, 16); + DISP()->setDrawColor(1); + } + + DISP()->sendBuffer(); +} + /** * @brief Update dashboard content * diff --git a/src/AgOledDisplay.h b/src/AgOledDisplay.h index 8f71802e..3217f77f 100644 --- a/src/AgOledDisplay.h +++ b/src/AgOledDisplay.h @@ -48,6 +48,7 @@ class OledDisplay : public PrintLog { void setText(String &line1, String &line2, String &line3, String &line4); void setText(const char *line1, const char *line2, const char *line3, const char *line4); + void showWiFiProvisioning(bool firstRun, int countdown); void showDashboard(void); void showDashboard(DashboardStatus status); void setBrightness(int percent); diff --git a/src/AgStateMachine.cpp b/src/AgStateMachine.cpp index 13d51d6e..584276a3 100644 --- a/src/AgStateMachine.cpp +++ b/src/AgStateMachine.cpp @@ -494,13 +494,10 @@ void StateMachine::displayHandle(AgStateMachineState state) { if (ag->isBasic()) { String ssid = "\"airgradient-" + ag->deviceId() + "\" " + String(wifiConnectCountDown) + String("s"); - disp.setText("Connect tohotspot:", ssid.c_str(), ""); + disp.setText("Connect to hotspot:", 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); + // NOTE: This bool is hardcoded! + disp.showWiFiProvisioning((wifiConnectCountDown == 180), wifiConnectCountDown); } wifiConnectCountDown--; } @@ -648,7 +645,7 @@ void StateMachine::handleLeds(AgStateMachineState state) { ag->ledBar.clear(); ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2); } else { - ag->statusLed.setToggle(); + ag->statusLed.setStep(); } break; } diff --git a/src/AgValue.h b/src/AgValue.h index 175bb880..33b44e2b 100644 --- a/src/AgValue.h +++ b/src/AgValue.h @@ -9,6 +9,7 @@ #include #include #include +#include class Measurements { private: diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index e13ca966..cb094ac8 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -1,5 +1,19 @@ #include "AgWiFiConnector.h" +#include "Arduino.h" #include "Libraries/WiFiManager/WiFiManager.h" +#include "Libraries/Arduino_JSON/src/Arduino_JSON.h" + +#ifdef ESP32 +#include "WiFiType.h" +#include "esp32-hal.h" + +#define BLE_SERVICE_UUID "acbcfea8-e541-4c40-9bfd-17820f16c95c" +#define BLE_CRED_CHAR_UUID "703fa252-3d2a-4da9-a05c-83b0d9cacb8e" +#define BLE_SCAN_CHAR_UUID "467a080f-e50f-42c9-b9b2-a2ab14d82725" + +#define BLE_CRED_BIT (1 << 0) +#define BLE_SCAN_BIT (1 << 1) +#endif // ESP32 #define WIFI_CONNECT_COUNTDOWN_MAX 180 #define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair" @@ -32,7 +46,7 @@ WifiConnector::~WifiConnector() {} * @return true Success * @return false Failure */ -bool WifiConnector::connect(void) { +bool WifiConnector::connect(String modelName) { if (wifi == NULL) { wifi = new WiFiManager(); if (wifi == NULL) { @@ -61,61 +75,89 @@ bool WifiConnector::connect(void) { break; } } - } - - WIFI()->setConfigPortalBlocking(false); - WIFI()->setConnectTimeout(15); - WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX); - WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); }); - WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); }); - WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); }); - WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();}); - if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) { - disp.setText("Connecting to", "WiFi", "..."); + if (!WiFi.isConnected()) { + // Erase already saved default credentials + WiFi.disconnect(false, true); + } } else { - logInfo("Connecting to WiFi..."); + Serial.printf("Attempt connect to configured ssid: %d\n", wifiSSID.c_str()); + // WiFi.begin() already called before, it will attempt connect when wifi creds already persist + + sm.ledAnimationInit(); + sm.handleLeds(AgStateMachineWiFiManagerStaConnecting); + sm.displayHandle(AgStateMachineWiFiManagerStaConnecting); + + uint32_t ledPeriod = millis(); + uint32_t startTime = millis(); + while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 15000) { + /** LED animations */ + if ((millis() - ledPeriod) >= 100) { + ledPeriod = millis(); + sm.handleLeds(); + } + delay(1); + } + + if (!WiFi.isConnected()) { + // WiFi not connect, show indicator. + sm.ledAnimationInit(); + sm.handleLeds(AgStateMachineWiFiManagerConnectFailed); + sm.displayHandle(AgStateMachineWiFiManagerConnectFailed); + delay(3000); + } } - ssid = "airgradient-" + ag->deviceId(); - // ssid = "AG-" + String(ESP.getChipId(), HEX); - WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX); + if (WiFi.isConnected()) { + sm.handleLeds(AgStateMachineWiFiManagerStaConnected); + return true; + } + // Enable provision by both BLE and WiFi portal WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T", 2, "type=\"checkbox\" ", WFM_LABEL_AFTER); - WIFI()->addParameter(&disableCloud); WiFiManagerParameter disableCloudInfo( "

Prevent connection to the AirGradient Server. Important: Only enable " "it if you are sure you don't want to use any AirGradient cloud " "features. As a result you will not receive automatic firmware updates, " "configuration settings from cloud and the measure data will not reach the AirGradient dashboard.

"); - WIFI()->addParameter(&disableCloudInfo); - - WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT); - - logInfo("Wait for configure portal"); + setupProvisionByPortal(&disableCloud, &disableCloudInfo); #ifdef ESP32 - // Task handle WiFi connection. + // Provision by BLE only for ESP32 + setupProvisionByBLE(modelName.c_str()); + + // Task handling WiFi portal xTaskCreate( - [](void *obj) { - WifiConnector *connector = (WifiConnector *)obj; - while (connector->_wifiConfigPortalActive()) { - connector->_wifiProcess(); - vTaskDelay(1); + [](void *obj) { + WifiConnector *connector = (WifiConnector *)obj; + while (connector->_wifiConfigPortalActive()) { + if (connector->isBleClientConnected()) { + Serial.println("Stopping portal because BLE connected"); + connector->_wifiStop(); + connector->provisionMethod = ProvisionMethod::BLE; + break; } - vTaskDelete(NULL); - }, - "wifi_cfg", 4096, this, 10, NULL); + connector->_wifiProcess(); + vTaskDelay(1); + } + vTaskDelete(NULL); + }, + "wifi_cfg", 4096, this, 10, NULL); - /** Wait for WiFi connect and show LED, display status */ + + // Wait for WiFi connect and show LED, display status uint32_t dispPeriod = millis(); uint32_t ledPeriod = millis(); bool clientConnectChanged = false; + // By default wifi portal loops run first + // Provision method defined when either wifi or ble client connected first + // If wifi client connect, then ble server will be stopped + // If ble client connect, then wifi portal will be stopped (see wifi_cfg task) AgStateMachineState stateOld = sm.getDisplayState(); while (WIFI()->getConfigPortalActive()) { - /** LED animatoin and display update content */ + /** LED animation and display update content */ if (WiFi.isConnected() == false) { /** Display countdown */ uint32_t ms; @@ -145,6 +187,11 @@ bool WifiConnector::connect(void) { clientConnectChanged = clientConnected; if (clientConnectChanged) { sm.handleLeds(AgStateMachineWiFiManagerPortalActive); + if (bleServerRunning) { + Serial.println("Stopping BLE since wifi is connected"); + stopBLE(); + provisionMethod = ProvisionMethod::WiFi; + } } else { sm.ledAnimationInit(); sm.handleLeds(AgStateMachineWiFiManagerMode); @@ -157,6 +204,74 @@ bool WifiConnector::connect(void) { delay(1); // avoid watchdog timer reset. } + + if (provisionMethod == ProvisionMethod::BLE) { + disp.setText("Provision by", "BLE", ""); + sm.ledAnimationInit(); + sm.handleLeds(AgStateMachineWiFiManagerPortalActive); + + uint32_t wdMillis = 0; + + // Loop until the BLE client disconnected or WiFi connected + while (isBleClientConnected() && !WiFi.isConnected()) { + EventBits_t bits = xEventGroupWaitBits( + bleEventGroup, + BLE_SCAN_BIT | BLE_CRED_BIT, + pdTRUE, + pdFALSE, + 10 / portTICK_PERIOD_MS + ); + + if (bits & BLE_CRED_BIT) { + Serial.printf("Connecting to %s...\n", ssid.c_str()); + wifiConnecting = true; + + sm.ledAnimationInit(); + sm.handleLeds(AgStateMachineWiFiManagerStaConnecting); + sm.displayHandle(AgStateMachineWiFiManagerStaConnecting); + + uint32_t startTime = millis(); + while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 15000) { + // Led animations + if ((millis() - ledPeriod) >= 100) { + ledPeriod = millis(); + sm.handleLeds(); + } + delay(1); + } + + if (WiFi.status() != WL_CONNECTED) { + Serial.println("Failed connect to WiFi"); + // If not connect send status through BLE while also turn led and display indicator + WiFi.disconnect(); + wifiConnecting = false; + bleNotifyStatus(PROV_ERR_WIFI_CONNECT_FAILED); + + // Show failed inficator then revert back to provision mode + sm.ledAnimationInit(); + sm.handleLeds(AgStateMachineWiFiManagerConnectFailed); + sm.displayHandle(AgStateMachineWiFiManagerConnectFailed); + delay(3000); + sm.ledAnimationInit(); + disp.setText("Provision by", "BLE", ""); + sm.handleLeds(AgStateMachineWiFiManagerPortalActive); + } + } + else if (bits & BLE_SCAN_BIT) { + handleBleScanRequest(); + } + + // Ensure watchdog fed every minute + if ((millis() - wdMillis) >= 60000) { + wdMillis = millis(); + ag->watchdog.reset(); + } + + delay(1); + } + + Serial.println("Exit provision by BLE"); + } #else _wifiProcess(); #endif @@ -180,6 +295,7 @@ bool WifiConnector::connect(void) { config.setDisableCloudConnection(result == "T"); } hasPortalConfig = false; + bleNotifyStatus(PROV_WIFI_CONNECT); } return true; @@ -206,6 +322,11 @@ bool WifiConnector::wifiClientConnected(void) { return WiFi.softAPgetStationNum() ? true : false; } + +bool WifiConnector::isBleClientConnected() { + return bleClientConnected; +} + /** * @brief Handle WiFiManage softAP setup completed callback * @@ -248,6 +369,10 @@ bool WifiConnector::_wifiConfigPortalActive(void) { } void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; } +void WifiConnector::_wifiStop() { + WIFI()->stopConfigPortal(); +} + /** * @brief Process WiFiManager connection * @@ -411,3 +536,333 @@ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; } void WifiConnector::setDefault(void) { WiFi.begin("airgradient", "cleanair"); } + +void WifiConnector::setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo) { + WIFI()->setConfigPortalBlocking(false); + WIFI()->setConnectTimeout(15); + WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX); + WIFI()->setBreakAfterConfig(true); + + WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); }); + WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); }); + WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); }); + WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();}); + if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) { + disp.setText("Connecting to", "WiFi", "..."); + } else { + logInfo("Connecting to WiFi..."); + } + ssid = "airgradient-" + ag->deviceId(); + + // ssid = "AG-" + String(ESP.getChipId(), HEX); + WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX); + + WIFI()->addParameter(disableCloudParam); + WIFI()->addParameter(disableCloudInfo); + + WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT); + + logInfo("Wait for configure portal"); +} + +void WifiConnector::bleNotifyStatus(int status) { +#ifdef ESP32 + if (!bleServerRunning) { + return; + } + + if (pServer->getConnectedCount()) { + NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID); + if (pSvc) { + NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_CRED_CHAR_UUID); + if (pChr) { + char tosend[50]; + memset(tosend, 0, 50); + sprintf(tosend, "{\"status\":%d}", status); + Serial.printf("BLE Notify >> %s \n", tosend); + pChr->setValue(String(tosend)); + pChr->notify(); + } + } + } +#endif // ESP32 +} + +#ifdef ESP32 + +int WifiConnector::scanAndFilterWiFi(WiFiNetwork networks[], int maxResults) { + Serial.println("Scanning for Wi-Fi networks..."); + int n = WiFi.scanNetworks(false, true); // async=false, show_hidden=true + Serial.printf("Found %d networks\n", n); + + const int MAX_NETWORKS = 50; + + if (n <= 0) { + Serial.println("No networks found"); + return 0; + } + + WiFiNetwork allNetworks[MAX_NETWORKS]; + int allCount = 0; + + // Collect valid networks (filter weak or empty SSID) + for (int i = 0; i < n && allCount < MAX_NETWORKS; ++i) { + String ssid = WiFi.SSID(i); + int32_t rssi = WiFi.RSSI(i); + bool open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN); + + if (ssid.length() == 0 || rssi < -75) continue; + + allNetworks[allCount++] = {ssid, rssi, open}; + } + + // Remove duplicates (keep the strongest) + WiFiNetwork uniqueNetworks[MAX_NETWORKS]; + int uniqueCount = 0; + + for (int i = 0; i < allCount; i++) { + bool exists = false; + for (int j = 0; j < uniqueCount; j++) { + if (uniqueNetworks[j].ssid == allNetworks[i].ssid) { + exists = true; + if (allNetworks[i].rssi > uniqueNetworks[j].rssi) + uniqueNetworks[j] = allNetworks[i]; // keep stronger one + break; + } + } + if (!exists && uniqueCount < MAX_NETWORKS) { + uniqueNetworks[uniqueCount++] = allNetworks[i]; + } + } + + // Sort by RSSI descending (simple bubble sort for small lists) + for (int i = 0; i < uniqueCount - 1; i++) { + for (int j = i + 1; j < uniqueCount; j++) { + if (uniqueNetworks[j].rssi > uniqueNetworks[i].rssi) { + WiFiNetwork temp = uniqueNetworks[i]; + uniqueNetworks[i] = uniqueNetworks[j]; + uniqueNetworks[j] = temp; + } + } + } + + // Copy to output array + int resultCount = (uniqueCount > maxResults) ? maxResults : uniqueCount; + for (int i = 0; i < resultCount; i++) { + networks[i] = uniqueNetworks[i]; + } + + Serial.printf("Returning %d filtered networks\n", resultCount); + return resultCount; +} + +String WifiConnector::buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalFound, + int page, int batchSize, int totalPages) { + // Calculate start and end indices for this page + int startIdx = (page - 1) * batchSize; + int endIdx = startIdx + batchSize; + if (endIdx > totalFound) { + endIdx = totalFound; + } + + // Build JSON object with pagination + JSONVar jsonRoot; + JSONVar jsonArray; + + for (int i = startIdx; i < endIdx; i++) { + JSONVar obj; + obj["s"] = networks[i].ssid; + obj["r"] = networks[i].rssi; + obj["o"] = networks[i].open ? 1 : 0; + jsonArray[i - startIdx] = obj; + } + + jsonRoot["wifi"] = jsonArray; + jsonRoot["page"] = page; + jsonRoot["tpage"] = totalPages; + jsonRoot["found"] = totalFound; + + String jsonString = JSON.stringify(jsonRoot); + + Serial.printf("Page %d/%d JSON: %s\n", page, totalPages, jsonString.c_str()); + + return jsonString; +} + +void WifiConnector::handleBleScanRequest() { + const int BATCH_SIZE = 3; + const int MAX_RESULTS = 30; + WiFiNetwork networks[MAX_RESULTS]; + + // Scan and filter networks once + int networkCount = scanAndFilterWiFi(networks, MAX_RESULTS); + + // Calculate total pages + int totalFound = (networkCount + BATCH_SIZE - 1) / BATCH_SIZE; + + NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID); + if (!pSvc) { + Serial.println("BLE service not found"); + return; + } + + NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_SCAN_CHAR_UUID); + if (!pChr) { + Serial.println("BLE scan characteristic not found"); + return; + } + + if (networkCount == 0) { + Serial.println("No networks found to send"); + String tosend = "{\"found\":0}"; + pChr->setValue(tosend); + pChr->notify(); + return; + } + + // Send results in batches + for (int page = 1; page <= totalFound; page++) { + String batchJson = buildPaginatedWiFiJSON(networks, networkCount, + page, BATCH_SIZE, totalFound); + pChr->setValue(batchJson); + pChr->notify(); + + Serial.printf("Sent WiFi scan page %d/%d through BLE notify\n", page, totalFound); + + // Delay between batches (except last one) + if (page < totalFound) { + delay(100); + } + } + + Serial.println("All WiFi scan pages sent successfully"); +} + +void WifiConnector::setupProvisionByBLE(const char *modelName) { + NimBLEDevice::init("AirGradient"); + NimBLEDevice::setPower(3); /** +3db */ + + /** bonding, MITM, don't need BLE secure connections as we are using passkey pairing */ + NimBLEDevice::setSecurityAuth(false, false, true); + NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT); + + pServer = NimBLEDevice::createServer(); + pServer->setCallbacks(new ServerCallbacks(this)); + + // Service and characteristics for device information + NimBLEService *pServDeviceInfo = pServer->createService("180A"); + NimBLECharacteristic *pModelCharacteristic = pServDeviceInfo->createCharacteristic("2A24", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC); + pModelCharacteristic->setValue(modelName); + NimBLECharacteristic *pSerialCharacteristic = pServDeviceInfo->createCharacteristic("2A25", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC); + pSerialCharacteristic->setValue(ag->deviceId().c_str()); + NimBLECharacteristic *pFwCharacteristic = pServDeviceInfo->createCharacteristic("2A26", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC); + pFwCharacteristic->setValue(ag->getVersion().c_str()); + NimBLECharacteristic *pManufCharacteristic = pServDeviceInfo->createCharacteristic("2A29", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC); + pManufCharacteristic->setValue("AirGradient"); + + // Service and characteristics for wifi provisioning + NimBLEService *pServProvisioning = pServer->createService(BLE_SERVICE_UUID); + auto characteristicCallback = new CharacteristicCallbacks(this); + NimBLECharacteristic *pCredentialCharacteristic = + pServProvisioning->createCharacteristic(BLE_CRED_CHAR_UUID, + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | + NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY); + pCredentialCharacteristic->setCallbacks(characteristicCallback); + NimBLECharacteristic *pScanCharacteristic = + pServProvisioning->createCharacteristic(BLE_SCAN_CHAR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY); + pScanCharacteristic->setCallbacks(characteristicCallback); + + // Start services + pServProvisioning->start(); + pServDeviceInfo->start(); + + // Advertise + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + // Format advertising data + String mdata; + mdata += (char)0xFF; + mdata += (char)0xFF; + mdata += modelName; + mdata += '#'; + mdata += ag->deviceId(); + pAdvertising->setManufacturerData(mdata.c_str()); + // Start advertise + pAdvertising->start(); + bleServerRunning = true; + + // Create event group + bleEventGroup = xEventGroupCreate(); + if (bleEventGroup == NULL) { + Serial.println("Failed to create BLE event group!"); + // This case is very unlikely + } + + Serial.println("Provision by BLE ready"); +} + +void WifiConnector::stopBLE() { + if (bleServerRunning) { + Serial.println("Stopping BLE"); + NimBLEDevice::deinit(); + } + bleServerRunning = false; +} + +// +// BLE innerclass implementation +// + +WifiConnector::ServerCallbacks::ServerCallbacks(WifiConnector* parent) + : parent(parent) {} + +void WifiConnector::ServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) { + Serial.printf("Client address: %s\n", connInfo.getAddress().toString().c_str()); + parent->bleClientConnected = true; + NimBLEDevice::stopAdvertising(); +} + +void WifiConnector::ServerCallbacks::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) { + Serial.printf("Client disconnected - start advertising\n"); + NimBLEDevice::startAdvertising(); + parent->bleClientConnected = false; +} + +void WifiConnector::ServerCallbacks::onAuthenticationComplete(NimBLEConnInfo& connInfo) { + Serial.println("\n========== PAIRING COMPLETE =========="); + Serial.printf("Peer Address: %s\n", connInfo.getAddress().toString().c_str()); + Serial.printf("Encrypted: %s\n", connInfo.isEncrypted() ? "YES" : "NO"); + Serial.printf("Authenticated: %s\n", connInfo.isAuthenticated() ? "YES" : "NO"); + Serial.printf("Key Size: %d bits\n", connInfo.getSecKeySize() * 8); + Serial.println("======================================\n"); +} + +WifiConnector::CharacteristicCallbacks::CharacteristicCallbacks(WifiConnector* parent) + : parent(parent) {} + +void WifiConnector::CharacteristicCallbacks::onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) { + Serial.printf("%s : onRead(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), + pCharacteristic->getValue().c_str()); +} + +void WifiConnector::CharacteristicCallbacks::onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) { + Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), + pCharacteristic->getValue().c_str()); + + auto bleCred = NimBLEUUID(BLE_CRED_CHAR_UUID); + if (pCharacteristic->getUUID().equals(bleCred)) { + if (!parent->wifiConnecting) { + JSONVar root = JSON.parse(pCharacteristic->getValue().c_str()); + + String ssid = root["ssid"]; + String pass = root["password"]; + + WiFi.begin(ssid.c_str(), pass.c_str()); + xEventGroupSetBits(parent->bleEventGroup, BLE_CRED_BIT); + } + } else { + xEventGroupSetBits(parent->bleEventGroup, BLE_SCAN_BIT); + } + +} + +#endif // ESP32 diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index e22ea0fc..6fa26e57 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -1,20 +1,57 @@ #ifndef _AG_WIFI_CONNECTOR_H_ #define _AG_WIFI_CONNECTOR_H_ +#include #include "AgOledDisplay.h" #include "AgStateMachine.h" #include "AirGradient.h" #include "AgConfigure.h" +#include "Libraries/WiFiManager/WiFiManager.h" #include "Main/PrintLog.h" -#include +#ifdef ESP32 +#include "esp32-hal.h" +#include +#include "NimBLECharacteristic.h" +#include "NimBLEService.h" + +#endif + +// Provisioning Status Codes +#define PROV_WIFI_CONNECT 0 // WiFi Connect +#define PROV_CONNECTING_TO_SERVER 1 // Connecting to server +#define PROV_SERVER_REACHABLE 2 // Server reachable +#define PROV_MONITOR_CONFIGURED 3 // Monitor configured properly on dashboard + +// Provisioning Error Codes +#define PROV_ERR_WIFI_CONNECT_FAILED 10 // Failed to connect to WiFi +#define PROV_ERR_SERVER_UNREACHABLE 11 // Server unreachable +#define PROV_ERR_GET_MONITOR_CONFIG_FAILED 12 // Failed to get monitor configuration from dashboard +#define PROV_ERR_MONITOR_NOT_REGISTERED 13 // Monitor is not registered on dashboard class WifiConnector : public PrintLog { +public: + enum class ProvisionMethod { + Unknown = 0, + WiFi, + BLE + }; + + struct WiFiNetwork { + String ssid; + int32_t rssi; + bool open; + }; + private: AirGradient *ag; OledDisplay &disp; StateMachine &sm; Configuration &config; + #ifdef ESP32 + NimBLEServer *pServer; + EventGroupHandle_t bleEventGroup; + #endif // ESP32 String ssid; void *wifi = NULL; @@ -22,16 +59,55 @@ class WifiConnector : public PrintLog { uint32_t lastRetry; bool hasPortalConfig = false; bool connectorTimeout = false; + bool bleServerRunning = false; + bool bleClientConnected = false; + bool wifiConnecting = false; + ProvisionMethod provisionMethod = ProvisionMethod::Unknown; bool wifiClientConnected(void); + bool isBleClientConnected(); +#ifdef ESP32 + int scanAndFilterWiFi(WiFiNetwork networks[], int maxResults); + String buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalCount, + int page, int batchSize, int totalPages); + void handleBleScanRequest(); + + // BLE server handler + class ServerCallbacks : public NimBLEServerCallbacks { + public: + explicit ServerCallbacks(WifiConnector *parent); + void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override; + void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override; + void onAuthenticationComplete(NimBLEConnInfo &connInfo) override; + + private: + WifiConnector *parent; + }; + + // BLE Characteristics handler + class CharacteristicCallbacks : public NimBLECharacteristicCallbacks { + public: + explicit CharacteristicCallbacks(WifiConnector *parent); + void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override; + void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override; + private: + WifiConnector *parent; + }; + +#endif // ESP32 public: void setAirGradient(AirGradient *ag); - WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config); + WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config); ~WifiConnector(); - bool connect(void); + #ifdef ESP32 + void setupProvisionByBLE(const char *modelName); + void stopBLE(); + #endif // ESP32 + void setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo); + bool connect(String modelName = ""); void disconnect(void); void handle(void); void _wifiApCallback(void); @@ -39,6 +115,7 @@ class WifiConnector : public PrintLog { void _wifiSaveParamCallback(void); bool _wifiConfigPortalActive(void); void _wifiTimeoutCallback(void); + void _wifiStop(); void _wifiProcess(); bool isConnected(void); void reset(void); @@ -47,8 +124,10 @@ class WifiConnector : public PrintLog { bool hasConfigurated(void); bool isConfigurePorttalTimeout(void); - const char* defaultSsid = "airgradient"; - const char* defaultPassword = "cleanair"; + void bleNotifyStatus(int status); + + const char *defaultSsid = "airgradient"; + const char *defaultPassword = "cleanair"; void setDefault(void); }; diff --git a/src/Main/StatusLed.cpp b/src/Main/StatusLed.cpp index d0451e88..9c4e80b5 100644 --- a/src/Main/StatusLed.cpp +++ b/src/Main/StatusLed.cpp @@ -72,6 +72,36 @@ void StatusLed::setToggle(void) { } } + +void StatusLed::setStep(void) { + static uint8_t step = 0; + + // Pattern definition + const bool pattern[] = { + true, // 0: ON + false, // 1: OFF + true, // 2: ON + false, // 3: OFF + false, // 4: OFF + false, // 5: OFF + false, // 6: OFF + false, // 7: OFF + false, // 8: OFF + false // 9: OFF + }; + + if (pattern[step]) { + this->setOn(); + } else { + this->setOff(); + } + + step++; + if (step >= sizeof(pattern)) { + step = 0; // restart pattern + } +} + /** * @brief Get current LED state * diff --git a/src/Main/StatusLed.h b/src/Main/StatusLed.h index f0eda5f0..43d5d858 100644 --- a/src/Main/StatusLed.h +++ b/src/Main/StatusLed.h @@ -25,6 +25,7 @@ class StatusLed { void setOn(void); void setOff(void); void setToggle(void); + void setStep(void); State getState(void); String toString(StatusLed::State state);