From d075d12011a867f57036fa1c047e73f1de6965a9 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Thu, 2 Oct 2025 18:17:18 +0700 Subject: [PATCH 01/22] First test --- examples/OneOpenAir/OneOpenAir.ino | 228 +++++++++++++++++------------ platformio.ini | 1 + 2 files changed, 136 insertions(+), 93 deletions(-) diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index d541fdf2..f02263dc 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 @@ -87,8 +87,13 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License #define I2C_SCL_PIN 6 #define OLED_I2C_ADDR 0x3C +#include +#include + +static NimBLEServer *pServer; + /** 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 +105,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 +161,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); @@ -175,6 +173,8 @@ AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, checkForFirmware AgSchedule networkSignalCheckSchedule(10000, networkSignalCheck); AgSchedule printMeasurementsSchedule(6000, printMeasurements); +static void setupBLE(); + void setup() { /** Serial for print debug message */ Serial.begin(115200); @@ -221,22 +221,25 @@ void setup() { boardInit(); setMeasurementMaxPeriod(); + setupBLE(); + oledDisplay.setText("BT", "ON", ""); + Serial.println("Bluetooth server ready"); + while(1) {delay(100);} + bool connectToNetwork = true; if (ag->isOne()) { // Offline mode only available for indoor monitor /** 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; } @@ -274,7 +277,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 +293,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 +304,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 +353,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 +392,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 +401,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 +425,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 +443,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 +504,7 @@ static void factoryConfigReset(void) { Serial.println("Factory reset successful"); } delay(3000); - oledDisplay.setText("","",""); + oledDisplay.setText("", "", ""); ESP.restart(); } } @@ -547,7 +542,7 @@ static void ledBarEnabledUpdate(void) { ag->ledBar.setBrightness(brightness); ag->ledBar.setEnable(configuration.getLedBarMode() != LedBarModeOff); } - ag->ledBar.show(); + ag->ledBar.show(); } } @@ -618,11 +613,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 +633,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 { @@ -714,8 +709,7 @@ static void sendDataToAg() { for (;;) { // ledSmHandler(); stateMachine.handleLeds(); - if (stateMachine.getLedState() != - AgStateMachineWiFiOkServerConnecting) { + if (stateMachine.getLedState() != AgStateMachineWiFiOkServerConnecting) { break; } delay(LED_BAR_ANIMATION_PERIOD); @@ -761,8 +755,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 +786,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 +914,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; } @@ -1070,15 +1062,13 @@ void initializeNetwork() { } stateMachine.handleLeds(AgStateMachineWiFiOkServerOkSensorConfigFailed); delay(DISPLAY_DELAY_SHOW_CONTENT_MS); - } - else { + } else { ledBarEnabledUpdate(); } } 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 +1102,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 +1113,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 +1140,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 +1178,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 +1376,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 +1517,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 +1542,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 +1597,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 +1635,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 } } @@ -1695,3 +1678,62 @@ void newMeasurementCycle() { } } +class ServerCallbacks : public NimBLEServerCallbacks { + void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override { + Serial.printf("Client address: %s\n", connInfo.getAddress().toString().c_str()); + } + + void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override { + Serial.printf("Client disconnected - start advertising\n"); + NimBLEDevice::startAdvertising(); + } + + void onAuthenticationComplete(NimBLEConnInfo &connInfo) override { + 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"); + } +}; + +/** Handler class for characteristic actions */ +class CharacteristicCallbacks : public NimBLECharacteristicCallbacks { + void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { + Serial.printf("%s : onRead(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), + pCharacteristic->getValue().c_str()); + } + + void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { + Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), + pCharacteristic->getValue().c_str()); + } +}; + +void setupBLE() { + 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); + + NimBLEServer *pServer = NimBLEDevice::createServer(); + pServer->setCallbacks(new ServerCallbacks()); + + NimBLEService *pService = pServer->createService("acbcfea8-e541-4c40-9bfd-17820f16c95c"); + NimBLECharacteristic *pSecureCharacteristic = + pService->createCharacteristic("703fa252-3d2a-4da9-a05c-83b0d9cacb8e", + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | + NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC); + pSecureCharacteristic->setCallbacks(new CharacteristicCallbacks()); + + pService->start(); + + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(pService->getUUID()); + pAdvertising->start(); +} diff --git a/platformio.ini b/platformio.ini index 73f77125..253c035c 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 From 16339acb973c70c4930b15b6b3e4eda559ee129e Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 24 Oct 2025 13:11:39 +0700 Subject: [PATCH 02/22] WIP --- examples/OneOpenAir/OneOpenAir.ino | 72 ------------------- src/AgWiFiConnector.cpp | 107 ++++++++++++++++++++++++++++- src/AgWiFiConnector.h | 9 +++ 3 files changed, 115 insertions(+), 73 deletions(-) diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index f02263dc..868e381e 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -87,11 +87,6 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License #define I2C_SCL_PIN 6 #define OLED_I2C_ADDR 0x3C -#include -#include - -static NimBLEServer *pServer; - /** Power pin */ #define GPIO_POWER_MODULE_PIN 5 #define GPIO_EXPANSION_CARD_POWER 4 @@ -173,8 +168,6 @@ AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, checkForFirmware AgSchedule networkSignalCheckSchedule(10000, networkSignalCheck); AgSchedule printMeasurementsSchedule(6000, printMeasurements); -static void setupBLE(); - void setup() { /** Serial for print debug message */ Serial.begin(115200); @@ -221,11 +214,6 @@ void setup() { boardInit(); setMeasurementMaxPeriod(); - setupBLE(); - oledDisplay.setText("BT", "ON", ""); - Serial.println("Bluetooth server ready"); - while(1) {delay(100);} - bool connectToNetwork = true; if (ag->isOne()) { // Offline mode only available for indoor monitor /** Show message confirm offline mode, should me perform if LED bar button @@ -1677,63 +1665,3 @@ void newMeasurementCycle() { Serial.printf("Free heap: %u\n", ESP.getFreeHeap()); } } - -class ServerCallbacks : public NimBLEServerCallbacks { - void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override { - Serial.printf("Client address: %s\n", connInfo.getAddress().toString().c_str()); - } - - void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override { - Serial.printf("Client disconnected - start advertising\n"); - NimBLEDevice::startAdvertising(); - } - - void onAuthenticationComplete(NimBLEConnInfo &connInfo) override { - 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"); - } -}; - -/** Handler class for characteristic actions */ -class CharacteristicCallbacks : public NimBLECharacteristicCallbacks { - void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { - Serial.printf("%s : onRead(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), - pCharacteristic->getValue().c_str()); - } - - void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { - Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), - pCharacteristic->getValue().c_str()); - } -}; - -void setupBLE() { - 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); - - NimBLEServer *pServer = NimBLEDevice::createServer(); - pServer->setCallbacks(new ServerCallbacks()); - - NimBLEService *pService = pServer->createService("acbcfea8-e541-4c40-9bfd-17820f16c95c"); - NimBLECharacteristic *pSecureCharacteristic = - pService->createCharacteristic("703fa252-3d2a-4da9-a05c-83b0d9cacb8e", - NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | - NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC); - pSecureCharacteristic->setCallbacks(new CharacteristicCallbacks()); - - pService->start(); - - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - pAdvertising->addServiceUUID(pService->getUUID()); - pAdvertising->start(); -} diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index e13ca966..c21adf8c 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -1,4 +1,5 @@ #include "AgWiFiConnector.h" +#include "Arduino.h" #include "Libraries/WiFiManager/WiFiManager.h" #define WIFI_CONNECT_COUNTDOWN_MAX 180 @@ -6,6 +7,46 @@ #define WIFI() ((WiFiManager *)(this->wifi)) +static bool g_isBLEConnect = false; + + +class ServerCallbacks : public NimBLEServerCallbacks { + void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override { + Serial.printf("Client address: %s\n", connInfo.getAddress().toString().c_str()); + g_isBLEConnect = true; + } + + void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override { + Serial.printf("Client disconnected - start advertising\n"); + NimBLEDevice::startAdvertising(); + g_isBLEConnect = false; + } + + void onAuthenticationComplete(NimBLEConnInfo &connInfo) override { + 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"); + } +}; + +/** Handler class for characteristic actions */ +class CharacteristicCallbacks : public NimBLECharacteristicCallbacks { + void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { + Serial.printf("%s : onRead(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), + pCharacteristic->getValue().c_str()); + } + + void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { + Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), + pCharacteristic->getValue().c_str()); + } +}; + /** * @brief Set reference AirGradient instance * @@ -101,6 +142,11 @@ bool WifiConnector::connect(void) { [](void *obj) { WifiConnector *connector = (WifiConnector *)obj; while (connector->_wifiConfigPortalActive()) { + if (g_isBLEConnect) { + Serial.println("Stopping portal because BLE connected"); + connector->_wifiStop(); + break; + } connector->_wifiProcess(); vTaskDelay(1); } @@ -113,9 +159,11 @@ bool WifiConnector::connect(void) { uint32_t ledPeriod = millis(); bool clientConnectChanged = false; + setupBLE(); + 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 +193,8 @@ bool WifiConnector::connect(void) { clientConnectChanged = clientConnected; if (clientConnectChanged) { sm.handleLeds(AgStateMachineWiFiManagerPortalActive); + Serial.println("Stopping BLE since wifi is connected"); + stopBLE(); } else { sm.ledAnimationInit(); sm.handleLeds(AgStateMachineWiFiManagerMode); @@ -161,6 +211,26 @@ bool WifiConnector::connect(void) { _wifiProcess(); #endif + while (1) { + delay(1000); + } + + + /** Set wifi connect */ + WiFi.begin("hbonfam", "51burian"); + + /** Wait for wifi connect to AP */ + int count = 0; + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + count++; + if (count >= 15) { + logError("Try connect to default wifi \"" + String(this->defaultSsid) + + String("\" failed")); + break; + } + } + /** Show display wifi connect result failed */ if (WiFi.isConnected() == false) { sm.handleLeds(AgStateMachineWiFiManagerConnectFailed); @@ -248,6 +318,10 @@ bool WifiConnector::_wifiConfigPortalActive(void) { } void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; } +void WifiConnector::_wifiStop() { + WIFI()->stopConfigPortal(); +} + /** * @brief Process WiFiManager connection * @@ -411,3 +485,34 @@ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; } void WifiConnector::setDefault(void) { WiFi.begin("airgradient", "cleanair"); } + + +void WifiConnector::setupBLE() { + 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); + + NimBLEServer *pServer = NimBLEDevice::createServer(); + pServer->setCallbacks(new ServerCallbacks()); + + NimBLEService *pService = pServer->createService("acbcfea8-e541-4c40-9bfd-17820f16c95c"); + NimBLECharacteristic *pSecureCharacteristic = + pService->createCharacteristic("703fa252-3d2a-4da9-a05c-83b0d9cacb8e", + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | + NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC); + pSecureCharacteristic->setCallbacks(new CharacteristicCallbacks()); + + pService->start(); + + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(pService->getUUID()); + pAdvertising->start(); +} + +void WifiConnector::stopBLE() { + NimBLEDevice::deinit(); + +} diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index e22ea0fc..4778dde0 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -6,8 +6,11 @@ #include "AirGradient.h" #include "AgConfigure.h" #include "Main/PrintLog.h" +#include "NimBLECharacteristic.h" +#include "NimBLEService.h" #include +#include class WifiConnector : public PrintLog { private: @@ -25,6 +28,11 @@ class WifiConnector : public PrintLog { bool wifiClientConnected(void); + void setupBLE(); + void stopBLE(); + + NimBLEServer *pServer; + public: void setAirGradient(AirGradient *ag); @@ -39,6 +47,7 @@ class WifiConnector : public PrintLog { void _wifiSaveParamCallback(void); bool _wifiConfigPortalActive(void); void _wifiTimeoutCallback(void); + void _wifiStop(); void _wifiProcess(); bool isConnected(void); void reset(void); From 19df574130937a64e1a0f9d379b7230943ec7fc6 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sun, 26 Oct 2025 23:25:20 +0700 Subject: [PATCH 03/22] POC BLE provision --- examples/OneOpenAir/OneOpenAir.ino | 7 ++ src/AgWiFiConnector.cpp | 184 ++++++++++++++++++----------- src/AgWiFiConnector.h | 48 +++++++- 3 files changed, 166 insertions(+), 73 deletions(-) diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index 868e381e..304269d8 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -247,6 +247,7 @@ void setup() { if (connectToNetwork) { oledDisplay.setText("Initialize", "network...", ""); initializeNetwork(); + wifiConnector.stopBLE(); } /** Set offline mode without saving, cause wifi is not configured */ @@ -690,6 +691,7 @@ static void sendDataToAg() { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnecting); + wifiConnector.bleNotifyStatus(1); /** Task handle led connecting animation */ xTaskCreate( @@ -718,11 +720,13 @@ static void sendDataToAg() { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnected); + wifiConnector.bleNotifyStatus(2); } else { if (ag->isOne()) { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed); + wifiConnector.bleNotifyStatus(11); } stateMachine.handleLeds(AgStateMachineNormal); @@ -1044,14 +1048,17 @@ void initializeNetwork() { if (agClient->isRegisteredOnAgServer() == false) { stateMachine.displaySetAddToDashBoard(); stateMachine.displayHandle(AgStateMachineWiFiOkServerOkSensorConfigFailed); + wifiConnector.bleNotifyStatus(13); } else { stateMachine.displayClearAddToDashBoard(); + wifiConnector.bleNotifyStatus(12); } } stateMachine.handleLeds(AgStateMachineWiFiOkServerOkSensorConfigFailed); delay(DISPLAY_DELAY_SHOW_CONTENT_MS); } else { ledBarEnabledUpdate(); + wifiConnector.bleNotifyStatus(3); } } diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index c21adf8c..174fbe72 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -1,51 +1,16 @@ #include "AgWiFiConnector.h" #include "Arduino.h" #include "Libraries/WiFiManager/WiFiManager.h" +#include "Libraries/Arduino_JSON/src/Arduino_JSON.h" #define WIFI_CONNECT_COUNTDOWN_MAX 180 #define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair" -#define WIFI() ((WiFiManager *)(this->wifi)) - -static bool g_isBLEConnect = false; - - -class ServerCallbacks : public NimBLEServerCallbacks { - void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override { - Serial.printf("Client address: %s\n", connInfo.getAddress().toString().c_str()); - g_isBLEConnect = true; - } - - void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override { - Serial.printf("Client disconnected - start advertising\n"); - NimBLEDevice::startAdvertising(); - g_isBLEConnect = false; - } - void onAuthenticationComplete(NimBLEConnInfo &connInfo) override { - Serial.println("\n========== PAIRING COMPLETE =========="); - Serial.printf("Peer Address: %s\n", connInfo.getAddress().toString().c_str()); +#define BLE_SERVICE_UUID "acbcfea8-e541-4c40-9bfd-17820f16c95c" +#define BLE_CHARACTERISTIC_UUID "703fa252-3d2a-4da9-a05c-83b0d9cacb8e" - 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"); - } -}; - -/** Handler class for characteristic actions */ -class CharacteristicCallbacks : public NimBLECharacteristicCallbacks { - void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { - Serial.printf("%s : onRead(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), - pCharacteristic->getValue().c_str()); - } - - void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { - Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), - pCharacteristic->getValue().c_str()); - } -}; +#define WIFI() ((WiFiManager *)(this->wifi)) /** * @brief Set reference AirGradient instance @@ -142,9 +107,10 @@ bool WifiConnector::connect(void) { [](void *obj) { WifiConnector *connector = (WifiConnector *)obj; while (connector->_wifiConfigPortalActive()) { - if (g_isBLEConnect) { + if (connector->isBleClientConnected()) { Serial.println("Stopping portal because BLE connected"); connector->_wifiStop(); + connector->provisionMethod = ProvisionMethod::BLE; break; } connector->_wifiProcess(); @@ -159,7 +125,7 @@ bool WifiConnector::connect(void) { uint32_t ledPeriod = millis(); bool clientConnectChanged = false; - setupBLE(); + setupBLE(ssid); AgStateMachineState stateOld = sm.getDisplayState(); while (WIFI()->getConfigPortalActive()) { @@ -193,8 +159,11 @@ bool WifiConnector::connect(void) { clientConnectChanged = clientConnected; if (clientConnectChanged) { sm.handleLeds(AgStateMachineWiFiManagerPortalActive); - Serial.println("Stopping BLE since wifi is connected"); - stopBLE(); + if (bleServerRunning) { + Serial.println("Stopping BLE since wifi is connected"); + stopBLE(); + provisionMethod = ProvisionMethod::WiFi; + } } else { sm.ledAnimationInit(); sm.handleLeds(AgStateMachineWiFiManagerMode); @@ -211,23 +180,23 @@ bool WifiConnector::connect(void) { _wifiProcess(); #endif - while (1) { - delay(1000); - } - + if (provisionMethod == ProvisionMethod::BLE) { + disp.setText("Provision by", "BLE", ""); - /** Set wifi connect */ - WiFi.begin("hbonfam", "51burian"); + while (isBleClientConnected() && !wifiConnecting) { + Serial.println("Wait for WiFi credentials through BLE"); + delay(1000); + } - /** Wait for wifi connect to AP */ - int count = 0; - while (WiFi.status() != WL_CONNECTED) { - delay(1000); - count++; - if (count >= 15) { - logError("Try connect to default wifi \"" + String(this->defaultSsid) + - String("\" failed")); - break; + int count = 0; + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + count++; + if (count >= 15) { + // give up + WiFi.disconnect(); + break; + } } } @@ -237,6 +206,7 @@ bool WifiConnector::connect(void) { if (ag->isOne() || ag->isPro4_2() || ag->isPro3_3() || ag->isBasic()) { sm.displayHandle(AgStateMachineWiFiManagerConnectFailed); } + bleNotifyStatus(10); delay(6000); } else { hasConfig = true; @@ -250,6 +220,7 @@ bool WifiConnector::connect(void) { config.setDisableCloudConnection(result == "T"); } hasPortalConfig = false; + bleNotifyStatus(0); } return true; @@ -276,6 +247,11 @@ bool WifiConnector::wifiClientConnected(void) { return WiFi.softAPgetStationNum() ? true : false; } + +bool WifiConnector::isBleClientConnected() { + return bleClientConnected; +} + /** * @brief Handle WiFiManage softAP setup completed callback * @@ -478,6 +454,24 @@ bool WifiConnector::hasConfigurated(void) { */ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; } + +void WifiConnector::bleNotifyStatus(int status) { + if (pServer->getConnectedCount()) { + NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID); + if (pSvc) { + NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_CHARACTERISTIC_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(); + } + } + } +} + /** * @brief Set wifi connect to default WiFi * @@ -487,32 +481,88 @@ void WifiConnector::setDefault(void) { } -void WifiConnector::setupBLE() { - NimBLEDevice::init("AirGradient"); +void WifiConnector::setupBLE(String bleName) { + NimBLEDevice::init(bleName.c_str()); 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); - NimBLEServer *pServer = NimBLEDevice::createServer(); - pServer->setCallbacks(new ServerCallbacks()); + pServer = NimBLEDevice::createServer(); + pServer->setCallbacks(new ServerCallbacks(this)); - NimBLEService *pService = pServer->createService("acbcfea8-e541-4c40-9bfd-17820f16c95c"); + NimBLEService *pService = pServer->createService(BLE_SERVICE_UUID); NimBLECharacteristic *pSecureCharacteristic = - pService->createCharacteristic("703fa252-3d2a-4da9-a05c-83b0d9cacb8e", + pService->createCharacteristic(BLE_CHARACTERISTIC_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | - NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC); - pSecureCharacteristic->setCallbacks(new CharacteristicCallbacks()); + NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY); + pSecureCharacteristic->setCallbacks(new CharacteristicCallbacks(this)); pService->start(); NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); pAdvertising->addServiceUUID(pService->getUUID()); pAdvertising->start(); + bleServerRunning = true; + Serial.println("Provision by BLE ready"); } void WifiConnector::stopBLE() { - NimBLEDevice::deinit(); + 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()); + + JSONVar root = JSON.parse(pCharacteristic->getValue().c_str()); + + String ssid = root["ssid"]; + String pass = root["password"]; + + Serial.printf("Connecting to %s...\n", ssid.c_str()); + WiFi.begin(ssid.c_str(), pass.c_str()); + parent->wifiConnecting = true; +} + diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index 4778dde0..56fa74f7 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -13,11 +13,19 @@ #include class WifiConnector : public PrintLog { +public: + enum class ProvisionMethod { + Unknown = 0, + WiFi, + BLE + }; + private: AirGradient *ag; OledDisplay &disp; StateMachine &sm; Configuration &config; + NimBLEServer *pServer; String ssid; void *wifi = NULL; @@ -25,20 +33,45 @@ 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(); - void setupBLE(); - void stopBLE(); + // 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; + }; - NimBLEServer *pServer; public: void setAirGradient(AirGradient *ag); - WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config); + WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config); ~WifiConnector(); + void setupBLE(String bleName); + void stopBLE(); bool connect(void); void disconnect(void); void handle(void); @@ -56,8 +89,11 @@ 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); }; From 449817a38486f90063b05423060a40d13ee7de23 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Tue, 28 Oct 2025 11:46:07 +0700 Subject: [PATCH 04/22] wifi scan characteristic more or less how the flow would be --- src/AgWiFiConnector.cpp | 193 ++++++++++++++++++++++++++++++++++------ src/AgWiFiConnector.h | 12 ++- 2 files changed, 177 insertions(+), 28 deletions(-) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 174fbe72..19d00e9d 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -8,7 +8,11 @@ #define BLE_SERVICE_UUID "acbcfea8-e541-4c40-9bfd-17820f16c95c" -#define BLE_CHARACTERISTIC_UUID "703fa252-3d2a-4da9-a05c-83b0d9cacb8e" +#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) #define WIFI() ((WiFiManager *)(this->wifi)) @@ -125,7 +129,7 @@ bool WifiConnector::connect(void) { uint32_t ledPeriod = millis(); bool clientConnectChanged = false; - setupBLE(ssid); + setupBLE(); AgStateMachineState stateOld = sm.getDisplayState(); while (WIFI()->getConfigPortalActive()) { @@ -183,21 +187,50 @@ bool WifiConnector::connect(void) { if (provisionMethod == ProvisionMethod::BLE) { disp.setText("Provision by", "BLE", ""); - while (isBleClientConnected() && !wifiConnecting) { - Serial.println("Wait for WiFi credentials through BLE"); - delay(1000); - } - int count = 0; - while (WiFi.status() != WL_CONNECTED) { - delay(1000); - count++; - if (count >= 15) { - // give up - WiFi.disconnect(); - break; + + // Loop until the BLE client disconnected or WiFi connected + while (isBleClientConnected() && !WiFi.isConnected()) { + Serial.println("Wait for BLE provision command"); + EventBits_t bits = xEventGroupWaitBits( + bleEventGroup, + BLE_SCAN_BIT | BLE_CRED_BIT, + pdTRUE, + pdFALSE, + portMAX_DELAY + ); + + if (bits & BLE_CRED_BIT) { + count = 0; + wifiConnecting = true; + Serial.printf("Connecting to %s...\n", ssid.c_str()); + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + Serial.print("."); + count++; + if (count >= 15) { + WiFi.disconnect(); + wifiConnecting = false; + bleNotifyStatus(10); + break; + } + } + } + else if (bits & BLE_SCAN_BIT) { + String result = scanFilteredWiFiJSON(); + NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID); + if (pSvc) { + NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_SCAN_CHAR_UUID); + if (pChr) { + pChr->setValue(result); + pChr->notify(); + Serial.println("List of scanned networks sent through BLE notify"); + } + } } } + + Serial.println("Exit provision by BLE"); } /** Show display wifi connect result failed */ @@ -206,7 +239,6 @@ bool WifiConnector::connect(void) { if (ag->isOne() || ag->isPro4_2() || ag->isPro3_3() || ag->isBasic()) { sm.displayHandle(AgStateMachineWiFiManagerConnectFailed); } - bleNotifyStatus(10); delay(6000); } else { hasConfig = true; @@ -456,10 +488,14 @@ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; } void WifiConnector::bleNotifyStatus(int status) { + if (!bleServerRunning) { + return; + } + if (pServer->getConnectedCount()) { NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID); if (pSvc) { - NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_CHARACTERISTIC_UUID); + NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_CRED_CHAR_UUID); if (pChr) { char tosend[50]; memset(tosend, 0, 50); @@ -480,9 +516,89 @@ void WifiConnector::setDefault(void) { WiFi.begin("airgradient", "cleanair"); } +String WifiConnector::scanFilteredWiFiJSON() { + 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; + const int MAX_RESULTS = 15; + + if (n <= 0) { + Serial.println("No networks found"); + return "[]"; + } + + 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 < -70) 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; + } + } + } + + // Limit to top X + if (uniqueCount > MAX_RESULTS) + uniqueCount = MAX_RESULTS; + + // Build JSON array + JSONVar jsonArray; + for (int i = 0; i < uniqueCount; i++) { + JSONVar obj; + obj["ssid"] = uniqueNetworks[i].ssid; + obj["rssi"] = uniqueNetworks[i].rssi; + obj["open"] = uniqueNetworks[i].open; + jsonArray[i] = obj; + } + + String jsonString = JSON.stringify(jsonArray); + + Serial.println("Filtered Wi-Fi Networks (JSON):"); + Serial.println(jsonString); + + return jsonString; +} + -void WifiConnector::setupBLE(String bleName) { - NimBLEDevice::init(bleName.c_str()); +void WifiConnector::setupBLE() { + Serial.printf("Setup BLE with device name %s\n", ssid.c_str()); + NimBLEDevice::init(ssid.c_str()); NimBLEDevice::setPower(3); /** +3db */ /** bonding, MITM, don't need BLE secure connections as we are using passkey pairing */ @@ -492,12 +608,20 @@ void WifiConnector::setupBLE(String bleName) { pServer = NimBLEDevice::createServer(); pServer->setCallbacks(new ServerCallbacks(this)); + + auto characteristicCallback = new CharacteristicCallbacks(this); + NimBLEService *pService = pServer->createService(BLE_SERVICE_UUID); - NimBLECharacteristic *pSecureCharacteristic = - pService->createCharacteristic(BLE_CHARACTERISTIC_UUID, + NimBLECharacteristic *pCredentialCharacteristic = + pService->createCharacteristic(BLE_CRED_CHAR_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY); - pSecureCharacteristic->setCallbacks(new CharacteristicCallbacks(this)); + pCredentialCharacteristic->setCallbacks(characteristicCallback); + + NimBLECharacteristic *pScanCharacteristic = + pService->createCharacteristic(BLE_SCAN_CHAR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY); + pScanCharacteristic->setCallbacks(characteristicCallback); + pService->start(); @@ -505,6 +629,14 @@ void WifiConnector::setupBLE(String bleName) { pAdvertising->addServiceUUID(pService->getUUID()); 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"); } @@ -556,13 +688,20 @@ void WifiConnector::CharacteristicCallbacks::onWrite(NimBLECharacteristic *pChar Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(), pCharacteristic->getValue().c_str()); - JSONVar root = JSON.parse(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"]; - 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); + } - Serial.printf("Connecting to %s...\n", ssid.c_str()); - WiFi.begin(ssid.c_str(), pass.c_str()); - parent->wifiConnecting = true; } diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index 56fa74f7..08fe96ce 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -8,6 +8,7 @@ #include "Main/PrintLog.h" #include "NimBLECharacteristic.h" #include "NimBLEService.h" +#include "esp32-hal.h" #include #include @@ -20,6 +21,12 @@ class WifiConnector : public PrintLog { BLE }; + struct WiFiNetwork { + String ssid; + int32_t rssi; + bool open; + }; + private: AirGradient *ag; OledDisplay &disp; @@ -27,6 +34,8 @@ class WifiConnector : public PrintLog { Configuration &config; NimBLEServer *pServer; + EventGroupHandle_t bleEventGroup; + String ssid; void *wifi = NULL; bool hasConfig; @@ -40,6 +49,7 @@ class WifiConnector : public PrintLog { bool wifiClientConnected(void); bool isBleClientConnected(); + String scanFilteredWiFiJSON(); // BLE server handler class ServerCallbacks : public NimBLEServerCallbacks { @@ -70,7 +80,7 @@ class WifiConnector : public PrintLog { WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config); ~WifiConnector(); - void setupBLE(String bleName); + void setupBLE(); void stopBLE(); bool connect(void); void disconnect(void); From 2f13c8fa665f4311db13919c0051679f97f5ea69 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 31 Oct 2025 01:50:39 +0700 Subject: [PATCH 05/22] Provision by ble stable candidate --- src/AgWiFiConnector.cpp | 187 +++++++++++++++++++++++++++------------- src/AgWiFiConnector.h | 4 +- 2 files changed, 132 insertions(+), 59 deletions(-) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 19d00e9d..e5c78215 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -2,6 +2,8 @@ #include "Arduino.h" #include "Libraries/WiFiManager/WiFiManager.h" #include "Libraries/Arduino_JSON/src/Arduino_JSON.h" +#include "WiFiType.h" +#include "esp32-hal.h" #define WIFI_CONNECT_COUNTDOWN_MAX 180 #define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair" @@ -58,6 +60,8 @@ bool WifiConnector::connect(void) { String(this->defaultSsid) + String("\"")); /** Set wifi connect */ + WiFi.persistent(false); + WiFi.disconnect(true); WiFi.begin(this->defaultSsid, this->defaultPassword); /** Wait for wifi connect to AP */ @@ -71,66 +75,87 @@ 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()) { + // // Set the persistence back + // WiFi.persistent(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 + WiFi.persistent(true); 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(); + + // Task handling WiFi portal xTaskCreate( - [](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; - } - 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; - setupBLE(); - + // 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 animation and display update content */ @@ -180,40 +205,53 @@ bool WifiConnector::connect(void) { delay(1); // avoid watchdog timer reset. } -#else - _wifiProcess(); -#endif if (provisionMethod == ProvisionMethod::BLE) { disp.setText("Provision by", "BLE", ""); - int count = 0; - // Loop until the BLE client disconnected or WiFi connected while (isBleClientConnected() && !WiFi.isConnected()) { - Serial.println("Wait for BLE provision command"); EventBits_t bits = xEventGroupWaitBits( bleEventGroup, BLE_SCAN_BIT | BLE_CRED_BIT, pdTRUE, pdFALSE, - portMAX_DELAY + 10 / portTICK_PERIOD_MS ); if (bits & BLE_CRED_BIT) { - count = 0; - wifiConnecting = true; Serial.printf("Connecting to %s...\n", ssid.c_str()); - while (WiFi.status() != WL_CONNECTED) { - delay(1000); - Serial.print("."); - count++; - if (count >= 15) { - WiFi.disconnect(); - wifiConnecting = false; - bleNotifyStatus(10); - break; + 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(10); + + // 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) { @@ -228,10 +266,15 @@ bool WifiConnector::connect(void) { } } } + + delay(1); } Serial.println("Exit provision by BLE"); } +#else + _wifiProcess(); +#endif /** Show display wifi connect result failed */ if (WiFi.isConnected() == false) { @@ -596,7 +639,35 @@ String WifiConnector::scanFilteredWiFiJSON() { } -void WifiConnector::setupBLE() { +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::setupProvisionByBLE() { Serial.printf("Setup BLE with device name %s\n", ssid.c_str()); NimBLEDevice::init(ssid.c_str()); NimBLEDevice::setPower(3); /** +3db */ diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index 08fe96ce..c1489025 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -5,6 +5,7 @@ #include "AgStateMachine.h" #include "AirGradient.h" #include "AgConfigure.h" +#include "Libraries/WiFiManager/WiFiManager.h" #include "Main/PrintLog.h" #include "NimBLECharacteristic.h" #include "NimBLEService.h" @@ -80,7 +81,8 @@ class WifiConnector : public PrintLog { WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config); ~WifiConnector(); - void setupBLE(); + void setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo); + void setupProvisionByBLE(); void stopBLE(); bool connect(void); void disconnect(void); From 3a56ee448d43a2cab2df0274210828bfa68ec65b Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 31 Oct 2025 02:08:02 +0700 Subject: [PATCH 06/22] Provision status BLE proper definition --- examples/OneOpenAir/OneOpenAir.ino | 12 ++++++------ src/AgWiFiConnector.cpp | 6 +++--- src/AgWiFiConnector.h | 12 ++++++++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index 304269d8..b3636e1c 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -691,7 +691,7 @@ static void sendDataToAg() { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnecting); - wifiConnector.bleNotifyStatus(1); + wifiConnector.bleNotifyStatus(PROV_CONNECTING_TO_SERVER); /** Task handle led connecting animation */ xTaskCreate( @@ -720,13 +720,13 @@ static void sendDataToAg() { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnected); - wifiConnector.bleNotifyStatus(2); + wifiConnector.bleNotifyStatus(PROV_SERVER_REACHABLE); } else { if (ag->isOne()) { stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed); } stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed); - wifiConnector.bleNotifyStatus(11); + wifiConnector.bleNotifyStatus(PROV_ERR_SERVER_UNREACHABLE); } stateMachine.handleLeds(AgStateMachineNormal); @@ -1048,17 +1048,17 @@ void initializeNetwork() { if (agClient->isRegisteredOnAgServer() == false) { stateMachine.displaySetAddToDashBoard(); stateMachine.displayHandle(AgStateMachineWiFiOkServerOkSensorConfigFailed); - wifiConnector.bleNotifyStatus(13); + wifiConnector.bleNotifyStatus(PROV_ERR_MONITOR_NOT_REGISTERED); } else { stateMachine.displayClearAddToDashBoard(); - wifiConnector.bleNotifyStatus(12); + wifiConnector.bleNotifyStatus(PROV_ERR_GET_MONITOR_CONFIG_FAILED); } } stateMachine.handleLeds(AgStateMachineWiFiOkServerOkSensorConfigFailed); delay(DISPLAY_DELAY_SHOW_CONTENT_MS); } else { ledBarEnabledUpdate(); - wifiConnector.bleNotifyStatus(3); + wifiConnector.bleNotifyStatus(PROV_MONITOR_CONFIGURED); } } diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index e5c78215..dac8aed4 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -242,7 +242,7 @@ bool WifiConnector::connect(void) { // If not connect send status through BLE while also turn led and display indicator WiFi.disconnect(); wifiConnecting = false; - bleNotifyStatus(10); + bleNotifyStatus(PROV_ERR_WIFI_CONNECT_FAILED); // Show failed inficator then revert back to provision mode sm.ledAnimationInit(); @@ -295,7 +295,7 @@ bool WifiConnector::connect(void) { config.setDisableCloudConnection(result == "T"); } hasPortalConfig = false; - bleNotifyStatus(0); + bleNotifyStatus(PROV_WIFI_CONNECT); } return true; @@ -715,7 +715,7 @@ void WifiConnector::stopBLE() { if (bleServerRunning) { Serial.println("Stopping BLE"); NimBLEDevice::deinit(); - } + } bleServerRunning = false; } diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index c1489025..cf51369c 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -14,6 +14,18 @@ #include #include +// 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 { From e196e6c6b88d6e79c089af75fb46e2cf5158fd94 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 31 Oct 2025 13:20:07 +0700 Subject: [PATCH 07/22] Fix wifi persistent --- src/AgWiFiConnector.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index dac8aed4..0a142a84 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -60,8 +60,6 @@ bool WifiConnector::connect(void) { String(this->defaultSsid) + String("\"")); /** Set wifi connect */ - WiFi.persistent(false); - WiFi.disconnect(true); WiFi.begin(this->defaultSsid, this->defaultPassword); /** Wait for wifi connect to AP */ @@ -76,10 +74,10 @@ bool WifiConnector::connect(void) { } } - // if (!WiFi.isConnected()) { - // // Set the persistence back - // WiFi.persistent(true); - // } + if (!WiFi.isConnected()) { + // Erase already saved default credentials + WiFi.disconnect(false, true); + } } else { 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 @@ -114,7 +112,6 @@ bool WifiConnector::connect(void) { } // Enable provision by both BLE and WiFi portal - WiFi.persistent(true); WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T", 2, "type=\"checkbox\" ", WFM_LABEL_AFTER); WiFiManagerParameter disableCloudInfo( From f1110905ca90aacc02fb32afe280f58317f1f7eb Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Mon, 10 Nov 2025 14:36:29 +0700 Subject: [PATCH 08/22] Add device information service --- examples/OneOpenAir/OneOpenAir.ino | 3 ++- src/AgWiFiConnector.cpp | 40 +++++++++++++++++++----------- src/AgWiFiConnector.h | 4 +-- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index b3636e1c..d03f5380 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -998,7 +998,8 @@ void initializeNetwork() { } if (networkOption == UseWifi) { - if (!wifiConnector.connect()) { + String modelName = AgFirmwareModeName(fwMode); + if (!wifiConnector.connect(modelName)) { Serial.println("Cannot initiate wifi connection"); return; } diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 0a142a84..711f7696 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -44,7 +44,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) { @@ -123,7 +123,7 @@ bool WifiConnector::connect(void) { #ifdef ESP32 // Provision by BLE only for ESP32 - setupProvisionByBLE(); + setupProvisionByBLE(modelName.c_str()); // Task handling WiFi portal xTaskCreate( @@ -664,9 +664,8 @@ void WifiConnector::setupProvisionByPortal(WiFiManagerParameter *disableCloudPar logInfo("Wait for configure portal"); } -void WifiConnector::setupProvisionByBLE() { - Serial.printf("Setup BLE with device name %s\n", ssid.c_str()); - NimBLEDevice::init(ssid.c_str()); +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 */ @@ -676,25 +675,38 @@ void WifiConnector::setupProvisionByBLE() { 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); - - NimBLEService *pService = pServer->createService(BLE_SERVICE_UUID); NimBLECharacteristic *pCredentialCharacteristic = - pService->createCharacteristic(BLE_CRED_CHAR_UUID, + 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 = - pService->createCharacteristic(BLE_SCAN_CHAR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY); + 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(); - pService->start(); - + // Advertise NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - pAdvertising->addServiceUUID(pService->getUUID()); + // pAdvertising->setName(ssid.c_str()); + pAdvertising->addServiceUUID(pServDeviceInfo->getUUID()); + pAdvertising->addServiceUUID(pServProvisioning->getUUID()); pAdvertising->start(); bleServerRunning = true; diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index cf51369c..9ad54975 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -94,9 +94,9 @@ class WifiConnector : public PrintLog { ~WifiConnector(); void setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo); - void setupProvisionByBLE(); + void setupProvisionByBLE(const char *modelName); void stopBLE(); - bool connect(void); + bool connect(String modelName = ""); void disconnect(void); void handle(void); void _wifiApCallback(void); From 38bcf4c05eebb545d22e3f11185052d53aa96d77 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Mon, 10 Nov 2025 15:59:35 +0700 Subject: [PATCH 09/22] Show BT icon on countdown --- src/AgOledDisplay.cpp | 34 ++++++++++++++++++++++++++++++---- src/AgOledDisplay.h | 1 + src/AgStateMachine.cpp | 9 +++------ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index a620c82b..f73d99f9 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -19,10 +19,14 @@ 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, }; + +static unsigned char BLUETOOTH_BITS[] = { + 0x00, 0x02, 0x00, 0x00, 0x06, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x3a, 0x00, 0x60, + 0x72, 0x00, 0xe0, 0x72, 0x00, 0xc0, 0x3b, 0x00, 0x80, 0x1f, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x0f, + 0x00, 0x80, 0x1f, 0x00, 0xc0, 0x3b, 0x00, 0xe0, 0x72, 0x00, 0x60, 0x72, 0x00, 0x00, 0x3a, 0x00, + 0x00, 0x1e, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x06, 0x00, 0x00, 0x02, 0x00 +}; + /** * @brief Show dashboard temperature and humdity * @@ -270,6 +274,28 @@ 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()); + DISP()->drawXBM(108, 44, 20, 20, BLUETOOTH_BITS); + DISP()->sendBuffer(); + } + + // 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, 16); // clear top region + DISP()->setDrawColor(1); // draw new text in white + DISP()->setFont(u8g2_font_t0_16_tf); + DISP()->drawStr(1, 10, buf); + 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..b4781aa7 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--; } From 94bd0adf8bf269ebfa30da6b6009939f35e0f21b Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Mon, 10 Nov 2025 16:21:22 +0700 Subject: [PATCH 10/22] Fix led connected state --- src/AgWiFiConnector.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 711f7696..9c114567 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -205,6 +205,8 @@ bool WifiConnector::connect(String modelName) { if (provisionMethod == ProvisionMethod::BLE) { disp.setText("Provision by", "BLE", ""); + sm.ledAnimationInit(); + sm.handleLeds(AgStateMachineWiFiManagerPortalActive); // Loop until the BLE client disconnected or WiFi connected while (isBleClientConnected() && !WiFi.isConnected()) { From b8768e0ed63a64d2c02b2a37c79aec86a888fd0c Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Wed, 26 Nov 2025 00:05:38 +0700 Subject: [PATCH 11/22] Send wifi scanned result in batch --- src/AgWiFiConnector.cpp | 109 +++++++++++++++++++++++++++++++--------- src/AgWiFiConnector.h | 5 +- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 9c114567..58d2f1b7 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -254,16 +254,7 @@ bool WifiConnector::connect(String modelName) { } } else if (bits & BLE_SCAN_BIT) { - String result = scanFilteredWiFiJSON(); - NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID); - if (pSvc) { - NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_SCAN_CHAR_UUID); - if (pChr) { - pChr->setValue(result); - pChr->notify(); - Serial.println("List of scanned networks sent through BLE notify"); - } - } + handleBleScanRequest(); } delay(1); @@ -558,17 +549,16 @@ void WifiConnector::setDefault(void) { WiFi.begin("airgradient", "cleanair"); } -String WifiConnector::scanFilteredWiFiJSON() { +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; - const int MAX_RESULTS = 15; if (n <= 0) { Serial.println("No networks found"); - return "[]"; + return 0; } WiFiNetwork allNetworks[MAX_NETWORKS]; @@ -615,28 +605,97 @@ String WifiConnector::scanFilteredWiFiJSON() { } } - // Limit to top X - if (uniqueCount > MAX_RESULTS) - uniqueCount = MAX_RESULTS; + // 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 array + // Build JSON object with pagination + JSONVar jsonRoot; JSONVar jsonArray; - for (int i = 0; i < uniqueCount; i++) { + + for (int i = startIdx; i < endIdx; i++) { JSONVar obj; - obj["ssid"] = uniqueNetworks[i].ssid; - obj["rssi"] = uniqueNetworks[i].rssi; - obj["open"] = uniqueNetworks[i].open; - jsonArray[i] = obj; + obj["ssid"] = networks[i].ssid; + obj["rssi"] = networks[i].rssi; + obj["open"] = networks[i].open; + jsonArray[i - startIdx] = obj; } - String jsonString = JSON.stringify(jsonArray); + jsonRoot["wifi"] = jsonArray; + jsonRoot["page"] = page; + jsonRoot["totalPage"] = totalPages; + jsonRoot["found"] = totalFound; + + String jsonString = JSON.stringify(jsonRoot); - Serial.println("Filtered Wi-Fi Networks (JSON):"); - Serial.println(jsonString); + 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::setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo) { WIFI()->setConfigPortalBlocking(false); diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index 9ad54975..be455260 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -62,7 +62,10 @@ class WifiConnector : public PrintLog { bool wifiClientConnected(void); bool isBleClientConnected(); - String scanFilteredWiFiJSON(); + 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 { From 49eed70863059401d9d48122d40b2ad3fc23e82a Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Wed, 26 Nov 2025 00:07:06 +0700 Subject: [PATCH 12/22] Increase the rssi limit to -75 as valid network --- src/AgWiFiConnector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 58d2f1b7..69f0ab2b 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -570,7 +570,7 @@ int WifiConnector::scanAndFilterWiFi(WiFiNetwork networks[], int maxResults) { int32_t rssi = WiFi.RSSI(i); bool open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN); - if (ssid.length() == 0 || rssi < -70) continue; + if (ssid.length() == 0 || rssi < -75) continue; allNetworks[allCount++] = {ssid, rssi, open}; } From ae247ead48318c7eb479ff722c25860ca97f49fb Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Wed, 26 Nov 2025 00:34:57 +0700 Subject: [PATCH 13/22] Save some bytes on the wifi scanned result payload format --- src/AgWiFiConnector.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 69f0ab2b..bb9e5512 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -630,15 +630,15 @@ String WifiConnector::buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalFo for (int i = startIdx; i < endIdx; i++) { JSONVar obj; - obj["ssid"] = networks[i].ssid; - obj["rssi"] = networks[i].rssi; - obj["open"] = networks[i].open; + 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["totalPage"] = totalPages; + jsonRoot["tpage"] = totalPages; jsonRoot["found"] = totalFound; String jsonString = JSON.stringify(jsonRoot); From da694edcab0e5412382e6f409c97dcefbd6c6d40 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 29 Nov 2025 21:49:32 +0700 Subject: [PATCH 14/22] Reboot when wifi is not connected when provision failed Only applied for network option is wifi --- examples/OneOpenAir/OneOpenAir.ino | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index d03f5380..8d294118 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -250,12 +250,6 @@ void setup() { wifiConnector.stopBLE(); } - /** 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); - } - /** Show display Warning up */ if (ag->isOne()) { oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str()); @@ -1006,15 +1000,10 @@ void initializeNetwork() { 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 From a55677422ba088b98aef5e9b0ee5324d1b2a0a8c Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Sat, 29 Nov 2025 22:29:07 +0700 Subject: [PATCH 15/22] Advertise serial number and model --- src/AgWiFiConnector.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index bb9e5512..e7a4b18a 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -765,9 +765,15 @@ void WifiConnector::setupProvisionByBLE(const char *modelName) { // Advertise NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - // pAdvertising->setName(ssid.c_str()); - pAdvertising->addServiceUUID(pServDeviceInfo->getUUID()); - pAdvertising->addServiceUUID(pServProvisioning->getUUID()); + // 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; From b178ad12a5aa141a168e916a7ea80700783b80d0 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Thu, 4 Dec 2025 10:41:17 +0700 Subject: [PATCH 16/22] Replace BT icon with BLE text blinking --- src/AgOledDisplay.cpp | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index f73d99f9..8951fe71 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -20,13 +20,6 @@ static unsigned char OFFLINE_BITS[] = { 0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, }; -static unsigned char BLUETOOTH_BITS[] = { - 0x00, 0x02, 0x00, 0x00, 0x06, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x3a, 0x00, 0x60, - 0x72, 0x00, 0xe0, 0x72, 0x00, 0xc0, 0x3b, 0x00, 0x80, 0x1f, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x0f, - 0x00, 0x80, 0x1f, 0x00, 0xc0, 0x3b, 0x00, 0xe0, 0x72, 0x00, 0x60, 0x72, 0x00, 0x00, 0x3a, 0x00, - 0x00, 0x1e, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x06, 0x00, 0x00, 0x02, 0x00 -}; - /** * @brief Show dashboard temperature and humdity * @@ -275,24 +268,33 @@ void OledDisplay::setText(const char *line1, const char *line2, } void OledDisplay::showWiFiProvisioning(bool firstRun, int countdown) { -if (firstRun) { + 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()); - DISP()->drawXBM(108, 44, 20, 20, BLUETOOTH_BITS); - DISP()->sendBuffer(); } // 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, 16); // clear top region + 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(); } From b5d89cf1180cded2f2478a00afdf3aa4df2f4c86 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Thu, 4 Dec 2025 11:32:30 +0700 Subject: [PATCH 17/22] Feed watchdog when BLE client is connected --- src/AgWiFiConnector.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index e7a4b18a..a1612bee 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -208,6 +208,8 @@ bool WifiConnector::connect(String modelName) { 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( @@ -257,6 +259,12 @@ bool WifiConnector::connect(String modelName) { handleBleScanRequest(); } + // Ensure watchdog fed every minute + if ((millis() - wdMillis) >= 60000) { + wdMillis = millis(); + ag->watchdog.reset(); + } + delay(1); } From 0f1846f04031e4e6b0d969ed64afab472b295a7b Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 5 Dec 2025 23:45:14 +0700 Subject: [PATCH 18/22] Update OpenAir provision ready led pattern --- src/AgStateMachine.cpp | 2 +- src/Main/StatusLed.cpp | 30 ++++++++++++++++++++++++++++++ src/Main/StatusLed.h | 1 + 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/AgStateMachine.cpp b/src/AgStateMachine.cpp index b4781aa7..584276a3 100644 --- a/src/AgStateMachine.cpp +++ b/src/AgStateMachine.cpp @@ -645,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/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); From a9f1923255f7ca9b81191fce9dd7f827e1c8da28 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 19 Dec 2025 14:57:31 +0700 Subject: [PATCH 19/22] Update how to compile Update workflow compile deps --- .github/workflows/check.yml | 2 ++ docs/howto-compile.md | 5 +++++ platformio.ini | 26 +++++++++++++------------- src/AgValue.h | 1 + src/AgWiFiConnector.cpp | 2 +- src/AgWiFiConnector.h | 6 +++--- 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index cf815db1..0cbfc2ad 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/platformio.ini b/platformio.ini index 253c035c..f99d4944 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,7 +16,7 @@ build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D AG_LO board_build.partitions = partitions.csv monitor_speed = 115200 lib_deps = - aglib=symlink://../arduino + aglib=symlink://../arduino-dev EEPROM WebServer ESPmDNS @@ -28,18 +28,18 @@ lib_deps = DNSServer h2zero/NimBLE-Arduino@^2.1.0 -[env:esp8266] -platform = espressif8266 -board = d1_mini -framework = arduino -monitor_speed = 115200 -lib_deps = - aglib=symlink://../arduino - EEPROM - ESP8266HTTPClient - ESP8266WebServer - DNSServer - +; [env:esp8266] +; platform = espressif8266 +; board = d1_mini +; framework = arduino +; monitor_speed = 115200 +; lib_deps = +; aglib=symlink://../arduino +; EEPROM +; ESP8266HTTPClient +; ESP8266WebServer +; DNSServer +; monitor_filters = time [platformio] 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 a1612bee..1538ea1e 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -755,7 +755,7 @@ void WifiConnector::setupProvisionByBLE(const char *modelName) { NimBLECharacteristic *pManufCharacteristic = pServDeviceInfo->createCharacteristic("2A29", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC); pManufCharacteristic->setValue("AirGradient"); - // Service and characteristics for wifi provisioning + // Service and characteristics for wifi provisioning NimBLEService *pServProvisioning = pServer->createService(BLE_SERVICE_UUID); auto characteristicCallback = new CharacteristicCallbacks(this); NimBLECharacteristic *pCredentialCharacteristic = diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index be455260..a838666a 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -1,18 +1,18 @@ #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 "NimBLECharacteristic.h" -#include "NimBLEService.h" #include "esp32-hal.h" -#include #include +#include "NimBLECharacteristic.h" +#include "NimBLEService.h" // Provisioning Status Codes #define PROV_WIFI_CONNECT 0 // WiFi Connect From 50fa3be0bd4eece266a6d02c2b70e416ac9a45d5 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 19 Dec 2025 14:58:10 +0700 Subject: [PATCH 20/22] Redo the platform io changes --- platformio.ini | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/platformio.ini b/platformio.ini index f99d4944..c9592c9a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,7 +16,7 @@ build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D AG_LO board_build.partitions = partitions.csv monitor_speed = 115200 lib_deps = - aglib=symlink://../arduino-dev + aglib=symlink://../arduino EEPROM WebServer ESPmDNS @@ -28,17 +28,17 @@ lib_deps = DNSServer h2zero/NimBLE-Arduino@^2.1.0 -; [env:esp8266] -; platform = espressif8266 -; board = d1_mini -; framework = arduino -; monitor_speed = 115200 -; lib_deps = -; aglib=symlink://../arduino -; EEPROM -; ESP8266HTTPClient -; ESP8266WebServer -; DNSServer +[env:esp8266] +platform = espressif8266 +board = d1_mini +framework = arduino +monitor_speed = 115200 +lib_deps = + aglib=symlink://../arduino + EEPROM + ESP8266HTTPClient + ESP8266WebServer + DNSServer ; monitor_filters = time From 9c1fa3a8f410c1345b0ba26ce7f31986a5487c4a Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 19 Dec 2025 15:01:25 +0700 Subject: [PATCH 21/22] Fix github action --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0cbfc2ad..51bb68de 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -78,7 +78,7 @@ jobs: examples/${{ matrix.example }} libraries: | - source-path: ./ - - name: NimBLE-Arduino + - name: NimBLE-Arduino version: 2.3.7 cli-compile-flags: | - --warnings From 684574a40a7b417e3094d761de09aa7b6e6ce2f2 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 19 Dec 2025 15:20:02 +0700 Subject: [PATCH 22/22] Fix esp8266 compile error --- src/AgWiFiConnector.cpp | 84 ++++++++++++++++++++++------------------- src/AgWiFiConnector.h | 15 ++++++-- 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/src/AgWiFiConnector.cpp b/src/AgWiFiConnector.cpp index 1538ea1e..cb094ac8 100644 --- a/src/AgWiFiConnector.cpp +++ b/src/AgWiFiConnector.cpp @@ -2,19 +2,21 @@ #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 WIFI_CONNECT_COUNTDOWN_MAX 180 -#define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair" - - #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" #define WIFI() ((WiFiManager *)(this->wifi)) @@ -527,8 +529,44 @@ bool WifiConnector::hasConfigurated(void) { */ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; } +/** + * @brief Set wifi connect to default WiFi + * + */ +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; } @@ -547,15 +585,10 @@ void WifiConnector::bleNotifyStatus(int status) { } } } +#endif // ESP32 } -/** - * @brief Set wifi connect to default WiFi - * - */ -void WifiConnector::setDefault(void) { - WiFi.begin("airgradient", "cleanair"); -} +#ifdef ESP32 int WifiConnector::scanAndFilterWiFi(WiFiNetwork networks[], int maxResults) { Serial.println("Scanning for Wi-Fi networks..."); @@ -705,34 +738,6 @@ void WifiConnector::handleBleScanRequest() { Serial.println("All WiFi scan pages sent successfully"); } -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::setupProvisionByBLE(const char *modelName) { NimBLEDevice::init("AirGradient"); NimBLEDevice::setPower(3); /** +3db */ @@ -860,3 +865,4 @@ void WifiConnector::CharacteristicCallbacks::onWrite(NimBLECharacteristic *pChar } +#endif // ESP32 diff --git a/src/AgWiFiConnector.h b/src/AgWiFiConnector.h index a838666a..6fa26e57 100644 --- a/src/AgWiFiConnector.h +++ b/src/AgWiFiConnector.h @@ -8,12 +8,15 @@ #include "AgConfigure.h" #include "Libraries/WiFiManager/WiFiManager.h" #include "Main/PrintLog.h" -#include "esp32-hal.h" +#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 @@ -45,9 +48,10 @@ class WifiConnector : public PrintLog { OledDisplay &disp; StateMachine &sm; Configuration &config; + #ifdef ESP32 NimBLEServer *pServer; - EventGroupHandle_t bleEventGroup; + #endif // ESP32 String ssid; void *wifi = NULL; @@ -62,6 +66,7 @@ class WifiConnector : public PrintLog { 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); @@ -89,6 +94,7 @@ class WifiConnector : public PrintLog { WifiConnector *parent; }; +#endif // ESP32 public: void setAirGradient(AirGradient *ag); @@ -96,9 +102,11 @@ class WifiConnector : public PrintLog { WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config); ~WifiConnector(); - void setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo); + #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); @@ -116,7 +124,6 @@ class WifiConnector : public PrintLog { bool hasConfigurated(void); bool isConfigurePorttalTimeout(void); - void bleNotifyStatus(int status); const char *defaultSsid = "airgradient";