Compare commits

...

18 Commits

Author SHA1 Message Date
samuelbles07
0f1846f040 Update OpenAir provision ready led pattern 2025-12-05 23:45:14 +07:00
samuelbles07
b5d89cf118 Feed watchdog when BLE client is connected 2025-12-04 11:32:30 +07:00
samuelbles07
b178ad12a5 Replace BT icon with BLE text blinking 2025-12-04 10:41:17 +07:00
samuelbles07
a55677422b Advertise serial number and model 2025-11-29 22:29:07 +07:00
samuelbles07
da694edcab Reboot when wifi is not connected when provision failed
Only applied for network option is wifi
2025-11-29 21:49:32 +07:00
samuelbles07
ae247ead48 Save some bytes on the wifi scanned result payload format 2025-11-26 00:34:57 +07:00
samuelbles07
49eed70863 Increase the rssi limit to -75 as valid network 2025-11-26 00:07:06 +07:00
samuelbles07
b8768e0ed6 Send wifi scanned result in batch 2025-11-26 00:05:38 +07:00
samuelbles07
94bd0adf8b Fix led connected state 2025-11-10 16:21:22 +07:00
samuelbles07
38bcf4c05e Show BT icon on countdown 2025-11-10 15:59:35 +07:00
samuelbles07
f1110905ca Add device information service 2025-11-10 14:36:29 +07:00
samuelbles07
e196e6c6b8 Fix wifi persistent 2025-10-31 13:20:07 +07:00
samuelbles07
3a56ee448d Provision status BLE proper definition 2025-10-31 02:08:02 +07:00
samuelbles07
2f13c8fa66 Provision by ble stable candidate 2025-10-31 01:50:39 +07:00
samuelbles07
449817a384 wifi scan characteristic
more or less how the flow would be
2025-10-28 11:46:07 +07:00
samuelbles07
19df574130 POC BLE provision 2025-10-26 23:25:20 +07:00
samuelbles07
16339acb97 WIP 2025-10-24 13:11:39 +07:00
samuelbles07
d075d12011 First test 2025-10-02 18:17:18 +07:00
9 changed files with 706 additions and 160 deletions

View File

@@ -59,24 +59,24 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include "esp_system.h"
#include "freertos/projdefs.h"
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define WIFI_SERVER_CONFIG_SYNC_INTERVAL 1 * 60000 /** ms */
#define WIFI_MEASUREMENT_INTERVAL 1 * 60000 /** ms */
#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */
#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */
#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */
#define CELLULAR_TRANSMISSION_INTERVAL 3 * 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define WIFI_SERVER_CONFIG_SYNC_INTERVAL 1 * 60000 /** ms */
#define WIFI_MEASUREMENT_INTERVAL 1 * 60000 /** ms */
#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */
#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */
#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */
#define CELLULAR_TRANSMISSION_INTERVAL 3 * 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
#define TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE (1 * 60) /** minutes */
#define TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY (2 * 60) /** minutes */
#define TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY (2 * 60) /** minutes */
#define MEASUREMENT_TRANSMIT_CYCLE 3
#define MAXIMUM_MEASUREMENT_CYCLE_QUEUE 80
@@ -88,7 +88,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#define OLED_I2C_ADDR 0x3C
/** Power pin */
#define GPIO_POWER_MODULE_PIN 5
#define GPIO_POWER_MODULE_PIN 5
#define GPIO_EXPANSION_CARD_POWER 4
#define GPIO_IIC_RESET 3
@@ -100,21 +100,15 @@ static Configuration configuration(Serial);
static Measurements measurements(configuration);
static AirGradient *ag;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration);
static StateMachine stateMachine(oledDisplay, Serial, measurements, configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine, configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector);
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static LocalServer localServer(Serial, openMetrics, measurements, configuration, wifiConnector);
static AgSerial *agSerial;
static CellularModule *cellularCard;
static AirgradientClient *agClient;
enum NetworkOption {
UseWifi,
UseCellular
};
enum NetworkOption { UseWifi, UseCellular };
NetworkOption networkOption;
TaskHandle_t handleNetworkTask = NULL;
static bool firmwareUpdateInProgress = false;
@@ -162,8 +156,7 @@ static void networkSignalCheck();
static void networkingTask(void *args);
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar);
AgSchedule configSchedule(WIFI_SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule configSchedule(WIFI_SERVER_CONFIG_SYNC_INTERVAL, configurationUpdateSchedule);
AgSchedule transmissionSchedule(WIFI_TRANSMISSION_INTERVAL, sendDataToServer);
AgSchedule measurementSchedule(WIFI_MEASUREMENT_INTERVAL, newMeasurementCycle);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
@@ -226,17 +219,15 @@ void setup() {
/** Show message confirm offline mode, should me perform if LED bar button
* test pressed */
if (ledBarButtonTest == false) {
oledDisplay.setText(
"Press now for",
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
oledDisplay.setText("Press now for",
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
uint32_t startTime = millis();
while (true) {
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
configuration.setOfflineMode(!configuration.isOfflineMode());
oledDisplay.setText(
"Offline Mode",
configuration.isOfflineMode() ? " = True" : " = False", "");
oledDisplay.setText("Offline Mode",
configuration.isOfflineMode() ? " = True" : " = False", "");
delay(1000);
break;
}
@@ -256,12 +247,7 @@ void setup() {
if (connectToNetwork) {
oledDisplay.setText("Initialize", "network...", "");
initializeNetwork();
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false && networkOption == UseWifi) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
wifiConnector.stopBLE();
}
/** Show display Warning up */
@@ -274,7 +260,6 @@ void setup() {
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
if (networkOption == UseCellular) {
// If using cellular re-set scheduler interval
configSchedule.setPeriod(CELLULAR_SERVER_CONFIG_SYNC_INTERVAL);
@@ -291,7 +276,7 @@ void setup() {
// Only run network task if monitor is not in offline mode
if (configuration.isOfflineMode() == false) {
BaseType_t xReturned =
xTaskCreate(networkingTask, "NetworkingTask", 4096, null, 5, &handleNetworkTask);
xTaskCreate(networkingTask, "NetworkingTask", 4096, null, 5, &handleNetworkTask);
if (xReturned == pdPASS) {
Serial.println("Success create networking task");
} else {
@@ -302,11 +287,9 @@ void setup() {
// Log monitor mode for debugging purpose
if (configuration.isOfflineMode()) {
Serial.println("Running monitor in offline mode");
}
else if (configuration.isCloudConnectionDisabled()) {
} else if (configuration.isCloudConnectionDisabled()) {
Serial.println("Running monitor without connection to AirGradient server");
}
}
void loop() {
@@ -353,7 +336,7 @@ void loop() {
static bool pmsConnected = false;
if (pmsConnected != ag->pms5003.connected()) {
pmsConnected = ag->pms5003.connected();
Serial.printf("PMS sensor %s \n", pmsConnected?"connected":"removed");
Serial.printf("PMS sensor %s \n", pmsConnected ? "connected" : "removed");
}
}
} else {
@@ -392,9 +375,7 @@ static void co2Update(void) {
}
}
void printMeasurements() {
measurements.printCurrentAverage();
}
void printMeasurements() { measurements.printCurrentAverage(); }
static void mdnsInit(void) {
if (!MDNS.begin(localServer.getHostname().c_str())) {
@@ -403,8 +384,7 @@ static void mdnsInit(void) {
}
MDNS.addService("_airgradient", "_tcp", 80);
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
AgFirmwareModeName(fwMode));
MDNS.addServiceTxt("_airgradient", "_tcp", "model", AgFirmwareModeName(fwMode));
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag->deviceId());
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag->getVersion());
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
@@ -428,8 +408,7 @@ static void createMqttTask(void) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String topic = "airgradient/readings/" + ag->deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(),
payload.length())) {
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
} else {
Serial.println("MQTT sync failure");
@@ -447,8 +426,7 @@ static void createMqttTask(void) {
static void initMqtt(void) {
String mqttUri = configuration.getMqttBrokerUri();
if (mqttUri.isEmpty()) {
Serial.println(
"MQTT is not configured, skipping initialization of MQTT client");
Serial.println("MQTT is not configured, skipping initialization of MQTT client");
return;
}
@@ -509,7 +487,7 @@ static void factoryConfigReset(void) {
Serial.println("Factory reset successful");
}
delay(3000);
oledDisplay.setText("","","");
oledDisplay.setText("", "", "");
ESP.restart();
}
}
@@ -547,7 +525,7 @@ static void ledBarEnabledUpdate(void) {
ag->ledBar.setBrightness(brightness);
ag->ledBar.setEnable(configuration.getLedBarMode() != LedBarModeOff);
}
ag->ledBar.show();
ag->ledBar.show();
}
}
@@ -618,11 +596,11 @@ void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) {
displayExecuteOta(result, "", std::stoi(msg));
break;
case AirgradientOTA::Failed:
displayExecuteOta(result, "", 0);
if (configuration.hasSensorSGP && networkOption == UseCellular) {
ag->sgp41.resume();
}
break;
displayExecuteOta(result, "", 0);
if (configuration.hasSensorSGP && networkOption == UseCellular) {
ag->sgp41.resume();
}
break;
case AirgradientOTA::Skipped:
case AirgradientOTA::AlreadyUpToDate:
displayExecuteOta(result, "", 0);
@@ -638,7 +616,7 @@ void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) {
static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int processing) {
switch (result) {
case AirgradientOTA::Starting:
case AirgradientOTA::Starting:
if (ag->isOne()) {
oledDisplay.showFirmwareUpdateVersion(msg);
} else {
@@ -707,6 +685,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnecting);
wifiConnector.bleNotifyStatus(PROV_CONNECTING_TO_SERVER);
/** Task handle led connecting animation */
xTaskCreate(
@@ -714,8 +693,7 @@ static void sendDataToAg() {
for (;;) {
// ledSmHandler();
stateMachine.handleLeds();
if (stateMachine.getLedState() !=
AgStateMachineWiFiOkServerConnecting) {
if (stateMachine.getLedState() != AgStateMachineWiFiOkServerConnecting) {
break;
}
delay(LED_BAR_ANIMATION_PERIOD);
@@ -736,11 +714,13 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnected);
wifiConnector.bleNotifyStatus(PROV_SERVER_REACHABLE);
} else {
if (ag->isOne()) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed);
wifiConnector.bleNotifyStatus(PROV_ERR_SERVER_UNREACHABLE);
}
stateMachine.handleLeds(AgStateMachineNormal);
@@ -761,8 +741,7 @@ static void oneIndoorInit(void) {
/** Show boot display */
Serial.println("Firmware Version: " + ag->getVersion());
oledDisplay.setText("AirGradient ONE",
"FW Version: ", ag->getVersion().c_str());
oledDisplay.setText("AirGradient ONE", "FW Version: ", ag->getVersion().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
ag->ledBar.begin();
@@ -793,9 +772,9 @@ static void oneIndoorInit(void) {
WiFi.begin("airgradient", "cleanair");
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
delay(2500);
oledDisplay.setText("Rebooting...", "","");
oledDisplay.setText("Rebooting...", "", "");
delay(2500);
oledDisplay.setText("","","");
oledDisplay.setText("", "", "");
ESP.restart();
}
}
@@ -921,8 +900,7 @@ static void openAirInit(void) {
}
if (fwMode == FW_MODE_O_1PP) {
int count = (configuration.hasSensorPMS1 ? 1 : 0) +
(configuration.hasSensorPMS2 ? 1 : 0);
int count = (configuration.hasSensorPMS1 ? 1 : 0) + (configuration.hasSensorPMS2 ? 1 : 0);
if (count == 1) {
fwMode = FW_MODE_O_1P;
}
@@ -1014,22 +992,18 @@ void initializeNetwork() {
}
if (networkOption == UseWifi) {
if (!wifiConnector.connect()) {
String modelName = AgFirmwareModeName(fwMode);
if (!wifiConnector.connect(modelName)) {
Serial.println("Cannot initiate wifi connection");
return;
}
if (!wifiConnector.isConnected()) {
Serial.println("Failed connect to WiFi");
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
// Directly return because the rest of the function applied if wifi is connect only
return;
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
// Initiate local network configuration
@@ -1064,21 +1038,22 @@ void initializeNetwork() {
if (agClient->isRegisteredOnAgServer() == false) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(AgStateMachineWiFiOkServerOkSensorConfigFailed);
wifiConnector.bleNotifyStatus(PROV_ERR_MONITOR_NOT_REGISTERED);
} else {
stateMachine.displayClearAddToDashBoard();
wifiConnector.bleNotifyStatus(PROV_ERR_GET_MONITOR_CONFIG_FAILED);
}
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerOkSensorConfigFailed);
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
else {
} else {
ledBarEnabledUpdate();
wifiConnector.bleNotifyStatus(PROV_MONITOR_CONFIGURED);
}
}
static void configurationUpdateSchedule(void) {
if (configuration.getConfigurationControl() ==
ConfigurationControl::ConfigurationControlLocal) {
if (configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration, configurationControl set to local");
agClient->resetFetchConfigurationStatus();
return;
@@ -1112,8 +1087,7 @@ static void configUpdateHandle() {
}
if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) {
if (configuration.noxLearnOffsetChanged() || configuration.tvocLearnOffsetChanged()) {
ag->sgp41.end();
int oldTvocOffset = ag->sgp41.getTvocLearningOffset();
@@ -1124,14 +1098,12 @@ static void configUpdateHandle() {
resultStr = "failure";
}
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
oldTvocOffset, configuration.getTvocLearningOffset(),
resultStr);
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n", oldTvocOffset,
configuration.getTvocLearningOffset(), resultStr);
}
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
oldNoxOffset, configuration.getNoxLearningOffset(),
resultStr);
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n", oldNoxOffset,
configuration.getNoxLearningOffset(), resultStr);
}
}
}
@@ -1153,7 +1125,7 @@ static void configUpdateHandle() {
if (configuration.getLedBarBrightness() == 0) {
ag->ledBar.setEnable(false);
} else {
if(configuration.getLedBarMode() == LedBarMode::LedBarModeOff) {
if (configuration.getLedBarMode() == LedBarMode::LedBarModeOff) {
ag->ledBar.setEnable(false);
} else {
ag->ledBar.setEnable(true);
@@ -1191,9 +1163,8 @@ static void updateDisplayAndLedBar(void) {
stateMachine.handleLeds(AgStateMachineWiFiLost);
return;
}
}
else if (networkOption == UseCellular) {
if (agClient->isClientReady() == false) {
} else if (networkOption == UseCellular) {
if (agClient->isClientReady() == false) {
// Same action as wifi
stateMachine.displayHandle(AgStateMachineWiFiLost);
stateMachine.handleLeds(AgStateMachineWiFiLost);
@@ -1390,8 +1361,8 @@ void postUsingWifi() {
}
/**
* forcePost to force post without checking transmit cycle
*/
* forcePost to force post without checking transmit cycle
*/
void postUsingCellular(bool forcePost) {
// Aquire queue mutex to get queue size
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
@@ -1531,7 +1502,6 @@ int calculateMaxPeriod(int updateInterval) {
return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval;
}
void networkSignalCheck() {
if (networkOption == UseWifi) {
Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI());
@@ -1557,12 +1527,11 @@ void networkSignalCheck() {
}
/**
* If in 2 hours cellular client still not ready, then restart system
*/
* If in 2 hours cellular client still not ready, then restart system
*/
void restartIfCeClientIssueOverTwoHours() {
if (agCeClientProblemDetectedTime > 0 &&
(MINUTES() - agCeClientProblemDetectedTime) >
TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY) {
(MINUTES() - agCeClientProblemDetectedTime) > TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY) {
// Give up wait
Serial.println("Rebooting because CE client issues for 2 hours detected");
int i = 3;
@@ -1613,8 +1582,7 @@ void networkingTask(void *args) {
delay(1000);
continue;
}
}
else if (networkOption == UseCellular) {
} else if (networkOption == UseCellular) {
if (agClient->isClientReady() == false) {
// Start time if value still default
if (agCeClientProblemDetectedTime == 0) {
@@ -1652,7 +1620,7 @@ void networkingTask(void *args) {
// Client is ready
agCeClientProblemDetectedTime = 0; // reset to default
agSerial->setDebug(false); // disable at command debug
agSerial->setDebug(false); // disable at command debug
}
}
@@ -1694,4 +1662,3 @@ void newMeasurementCycle() {
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
}
}

View File

@@ -26,6 +26,7 @@ lib_deps =
WiFiClientSecure
Update
DNSServer
h2zero/NimBLE-Arduino@^2.1.0
[env:esp8266]
platform = espressif8266

View File

@@ -19,10 +19,7 @@ static unsigned char OFFLINE_BITS[] = {
0xE6, 0x00, 0xFE, 0x1F, 0xFE, 0x1F, 0xE6, 0x00, 0x62, 0x00,
0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// {
// 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x62, 0x00, 0xE2, 0x00,
// 0xFE, 0x1F, 0xFE, 0x1F, 0xE2, 0x00, 0x62, 0x00, 0x60, 0x00, 0x30, 0x00,
// 0x00, 0x00, 0x00, 0x00, };
/**
* @brief Show dashboard temperature and humdity
*
@@ -270,6 +267,37 @@ void OledDisplay::setText(const char *line1, const char *line2,
}
}
void OledDisplay::showWiFiProvisioning(bool firstRun, int countdown) {
if (firstRun) {
DISP()->clearBuffer();
DISP()->setFont(u8g2_font_t0_16_tf);
DISP()->drawStr(1, 25, "to WiFi hotspot:");
DISP()->drawStr(1, 40, "\"airgradient-");
DISP()->drawStr(1, 55, (ag->deviceId() + "\"").c_str());
}
// Now just update countdown area
char buf[16];
snprintf(buf, sizeof(buf), "%ds to connect", countdown);
DISP()->setDrawColor(0); // erase previous text
DISP()->drawBox(0, 0, 128, 14); // clear top region
DISP()->setDrawColor(1); // draw new text in white
DISP()->setFont(u8g2_font_t0_16_tf);
DISP()->drawStr(1, 10, buf);
// Blink the BLE mark section
if (countdown % 2 == 0) {
DISP()->setFont(u8g2_font_t0_12b_tf);
DISP()->drawStr(108, 60, "BLE");
} else {
DISP()->setDrawColor(0);
DISP()->drawBox(108, 48, 20, 16);
DISP()->setDrawColor(1);
}
DISP()->sendBuffer();
}
/**
* @brief Update dashboard content
*

View File

@@ -48,6 +48,7 @@ public:
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);

View File

@@ -494,13 +494,10 @@ void StateMachine::displayHandle(AgStateMachineState state) {
if (ag->isBasic()) {
String ssid = "\"airgradient-" + ag->deviceId() + "\" " +
String(wifiConnectCountDown) + String("s");
disp.setText("Connect tohotspot:", ssid.c_str(), "");
disp.setText("Connect to hotspot:", ssid.c_str(), "");
} else {
String line1 = String(wifiConnectCountDown) + "s to connect";
String line2 = "to WiFi hotspot:";
String line3 = "\"airgradient-";
String line4 = ag->deviceId() + "\"";
disp.setText(line1, line2, line3, line4);
// NOTE: This bool is hardcoded!
disp.showWiFiProvisioning((wifiConnectCountDown == 180), wifiConnectCountDown);
}
wifiConnectCountDown--;
}
@@ -648,7 +645,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
ag->ledBar.clear();
ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2);
} else {
ag->statusLed.setToggle();
ag->statusLed.setStep();
}
break;
}

View File

@@ -1,9 +1,21 @@
#include "AgWiFiConnector.h"
#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"
#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)
#define WIFI() ((WiFiManager *)(this->wifi))
/**
@@ -32,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) {
@@ -61,61 +73,89 @@ bool WifiConnector::connect(void) {
break;
}
}
}
WIFI()->setConfigPortalBlocking(false);
WIFI()->setConnectTimeout(15);
WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); });
WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); });
WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); });
WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();});
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
disp.setText("Connecting to", "WiFi", "...");
if (!WiFi.isConnected()) {
// Erase already saved default credentials
WiFi.disconnect(false, true);
}
} else {
logInfo("Connecting to WiFi...");
Serial.printf("Attempt connect to configured ssid: %d\n", wifiSSID.c_str());
// WiFi.begin() already called before, it will attempt connect when wifi creds already persist
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerStaConnecting);
sm.displayHandle(AgStateMachineWiFiManagerStaConnecting);
uint32_t ledPeriod = millis();
uint32_t startTime = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 15000) {
/** LED animations */
if ((millis() - ledPeriod) >= 100) {
ledPeriod = millis();
sm.handleLeds();
}
delay(1);
}
if (!WiFi.isConnected()) {
// WiFi not connect, show indicator.
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerConnectFailed);
sm.displayHandle(AgStateMachineWiFiManagerConnectFailed);
delay(3000);
}
}
ssid = "airgradient-" + ag->deviceId();
// ssid = "AG-" + String(ESP.getChipId(), HEX);
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
if (WiFi.isConnected()) {
sm.handleLeds(AgStateMachineWiFiManagerStaConnected);
return true;
}
// Enable provision by both BLE and WiFi portal
WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&disableCloud);
WiFiManagerParameter disableCloudInfo(
"<p>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.</p>");
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.
xTaskCreate(
[](void *obj) {
WifiConnector *connector = (WifiConnector *)obj;
while (connector->_wifiConfigPortalActive()) {
connector->_wifiProcess();
vTaskDelay(1);
}
vTaskDelete(NULL);
},
"wifi_cfg", 4096, this, 10, NULL);
// Provision by BLE only for ESP32
setupProvisionByBLE(modelName.c_str());
/** Wait for WiFi connect and show LED, display status */
// 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);
}
vTaskDelete(NULL);
},
"wifi_cfg", 4096, this, 10, NULL);
// Wait for WiFi connect and show LED, display status
uint32_t dispPeriod = millis();
uint32_t ledPeriod = millis();
bool clientConnectChanged = false;
// By default wifi portal loops run first
// Provision method defined when either wifi or ble client connected first
// If wifi client connect, then ble server will be stopped
// If ble client connect, then wifi portal will be stopped (see wifi_cfg task)
AgStateMachineState stateOld = sm.getDisplayState();
while (WIFI()->getConfigPortalActive()) {
/** LED animatoin and display update content */
/** LED animation and display update content */
if (WiFi.isConnected() == false) {
/** Display countdown */
uint32_t ms;
@@ -145,6 +185,11 @@ bool WifiConnector::connect(void) {
clientConnectChanged = clientConnected;
if (clientConnectChanged) {
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
if (bleServerRunning) {
Serial.println("Stopping BLE since wifi is connected");
stopBLE();
provisionMethod = ProvisionMethod::WiFi;
}
} else {
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerMode);
@@ -157,6 +202,74 @@ bool WifiConnector::connect(void) {
delay(1); // avoid watchdog timer reset.
}
if (provisionMethod == ProvisionMethod::BLE) {
disp.setText("Provision by", "BLE", "");
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
uint32_t wdMillis = 0;
// Loop until the BLE client disconnected or WiFi connected
while (isBleClientConnected() && !WiFi.isConnected()) {
EventBits_t bits = xEventGroupWaitBits(
bleEventGroup,
BLE_SCAN_BIT | BLE_CRED_BIT,
pdTRUE,
pdFALSE,
10 / portTICK_PERIOD_MS
);
if (bits & BLE_CRED_BIT) {
Serial.printf("Connecting to %s...\n", ssid.c_str());
wifiConnecting = true;
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerStaConnecting);
sm.displayHandle(AgStateMachineWiFiManagerStaConnecting);
uint32_t startTime = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 15000) {
// Led animations
if ((millis() - ledPeriod) >= 100) {
ledPeriod = millis();
sm.handleLeds();
}
delay(1);
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Failed connect to WiFi");
// If not connect send status through BLE while also turn led and display indicator
WiFi.disconnect();
wifiConnecting = false;
bleNotifyStatus(PROV_ERR_WIFI_CONNECT_FAILED);
// Show failed inficator then revert back to provision mode
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerConnectFailed);
sm.displayHandle(AgStateMachineWiFiManagerConnectFailed);
delay(3000);
sm.ledAnimationInit();
disp.setText("Provision by", "BLE", "");
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
}
}
else if (bits & BLE_SCAN_BIT) {
handleBleScanRequest();
}
// Ensure watchdog fed every minute
if ((millis() - wdMillis) >= 60000) {
wdMillis = millis();
ag->watchdog.reset();
}
delay(1);
}
Serial.println("Exit provision by BLE");
}
#else
_wifiProcess();
#endif
@@ -180,6 +293,7 @@ bool WifiConnector::connect(void) {
config.setDisableCloudConnection(result == "T");
}
hasPortalConfig = false;
bleNotifyStatus(PROV_WIFI_CONNECT);
}
return true;
@@ -206,6 +320,11 @@ bool WifiConnector::wifiClientConnected(void) {
return WiFi.softAPgetStationNum() ? true : false;
}
bool WifiConnector::isBleClientConnected() {
return bleClientConnected;
}
/**
* @brief Handle WiFiManage softAP setup completed callback
*
@@ -248,6 +367,10 @@ bool WifiConnector::_wifiConfigPortalActive(void) {
}
void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; }
void WifiConnector::_wifiStop() {
WIFI()->stopConfigPortal();
}
/**
* @brief Process WiFiManager connection
*
@@ -404,6 +527,28 @@ bool WifiConnector::hasConfigurated(void) {
*/
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_CRED_CHAR_UUID);
if (pChr) {
char tosend[50];
memset(tosend, 0, 50);
sprintf(tosend, "{\"status\":%d}", status);
Serial.printf("BLE Notify >> %s \n", tosend);
pChr->setValue(String(tosend));
pChr->notify();
}
}
}
}
/**
* @brief Set wifi connect to default WiFi
*
@@ -411,3 +556,307 @@ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; }
void WifiConnector::setDefault(void) {
WiFi.begin("airgradient", "cleanair");
}
int WifiConnector::scanAndFilterWiFi(WiFiNetwork networks[], int maxResults) {
Serial.println("Scanning for Wi-Fi networks...");
int n = WiFi.scanNetworks(false, true); // async=false, show_hidden=true
Serial.printf("Found %d networks\n", n);
const int MAX_NETWORKS = 50;
if (n <= 0) {
Serial.println("No networks found");
return 0;
}
WiFiNetwork allNetworks[MAX_NETWORKS];
int allCount = 0;
// Collect valid networks (filter weak or empty SSID)
for (int i = 0; i < n && allCount < MAX_NETWORKS; ++i) {
String ssid = WiFi.SSID(i);
int32_t rssi = WiFi.RSSI(i);
bool open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN);
if (ssid.length() == 0 || rssi < -75) continue;
allNetworks[allCount++] = {ssid, rssi, open};
}
// Remove duplicates (keep the strongest)
WiFiNetwork uniqueNetworks[MAX_NETWORKS];
int uniqueCount = 0;
for (int i = 0; i < allCount; i++) {
bool exists = false;
for (int j = 0; j < uniqueCount; j++) {
if (uniqueNetworks[j].ssid == allNetworks[i].ssid) {
exists = true;
if (allNetworks[i].rssi > uniqueNetworks[j].rssi)
uniqueNetworks[j] = allNetworks[i]; // keep stronger one
break;
}
}
if (!exists && uniqueCount < MAX_NETWORKS) {
uniqueNetworks[uniqueCount++] = allNetworks[i];
}
}
// Sort by RSSI descending (simple bubble sort for small lists)
for (int i = 0; i < uniqueCount - 1; i++) {
for (int j = i + 1; j < uniqueCount; j++) {
if (uniqueNetworks[j].rssi > uniqueNetworks[i].rssi) {
WiFiNetwork temp = uniqueNetworks[i];
uniqueNetworks[i] = uniqueNetworks[j];
uniqueNetworks[j] = temp;
}
}
}
// Copy to output array
int resultCount = (uniqueCount > maxResults) ? maxResults : uniqueCount;
for (int i = 0; i < resultCount; i++) {
networks[i] = uniqueNetworks[i];
}
Serial.printf("Returning %d filtered networks\n", resultCount);
return resultCount;
}
String WifiConnector::buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalFound,
int page, int batchSize, int totalPages) {
// Calculate start and end indices for this page
int startIdx = (page - 1) * batchSize;
int endIdx = startIdx + batchSize;
if (endIdx > totalFound) {
endIdx = totalFound;
}
// Build JSON object with pagination
JSONVar jsonRoot;
JSONVar jsonArray;
for (int i = startIdx; i < endIdx; i++) {
JSONVar obj;
obj["s"] = networks[i].ssid;
obj["r"] = networks[i].rssi;
obj["o"] = networks[i].open ? 1 : 0;
jsonArray[i - startIdx] = obj;
}
jsonRoot["wifi"] = jsonArray;
jsonRoot["page"] = page;
jsonRoot["tpage"] = totalPages;
jsonRoot["found"] = totalFound;
String jsonString = JSON.stringify(jsonRoot);
Serial.printf("Page %d/%d JSON: %s\n", page, totalPages, jsonString.c_str());
return jsonString;
}
void WifiConnector::handleBleScanRequest() {
const int BATCH_SIZE = 3;
const int MAX_RESULTS = 30;
WiFiNetwork networks[MAX_RESULTS];
// Scan and filter networks once
int networkCount = scanAndFilterWiFi(networks, MAX_RESULTS);
// Calculate total pages
int totalFound = (networkCount + BATCH_SIZE - 1) / BATCH_SIZE;
NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID);
if (!pSvc) {
Serial.println("BLE service not found");
return;
}
NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_SCAN_CHAR_UUID);
if (!pChr) {
Serial.println("BLE scan characteristic not found");
return;
}
if (networkCount == 0) {
Serial.println("No networks found to send");
String tosend = "{\"found\":0}";
pChr->setValue(tosend);
pChr->notify();
return;
}
// Send results in batches
for (int page = 1; page <= totalFound; page++) {
String batchJson = buildPaginatedWiFiJSON(networks, networkCount,
page, BATCH_SIZE, totalFound);
pChr->setValue(batchJson);
pChr->notify();
Serial.printf("Sent WiFi scan page %d/%d through BLE notify\n", page, totalFound);
// Delay between batches (except last one)
if (page < totalFound) {
delay(100);
}
}
Serial.println("All WiFi scan pages sent successfully");
}
void WifiConnector::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 */
/** bonding, MITM, don't need BLE secure connections as we are using passkey pairing */
NimBLEDevice::setSecurityAuth(false, false, true);
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT);
pServer = NimBLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks(this));
// Service and characteristics for device information
NimBLEService *pServDeviceInfo = pServer->createService("180A");
NimBLECharacteristic *pModelCharacteristic = pServDeviceInfo->createCharacteristic("2A24", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
pModelCharacteristic->setValue(modelName);
NimBLECharacteristic *pSerialCharacteristic = pServDeviceInfo->createCharacteristic("2A25", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
pSerialCharacteristic->setValue(ag->deviceId().c_str());
NimBLECharacteristic *pFwCharacteristic = pServDeviceInfo->createCharacteristic("2A26", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
pFwCharacteristic->setValue(ag->getVersion().c_str());
NimBLECharacteristic *pManufCharacteristic = pServDeviceInfo->createCharacteristic("2A29", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
pManufCharacteristic->setValue("AirGradient");
// Service and characteristics for wifi provisioning
NimBLEService *pServProvisioning = pServer->createService(BLE_SERVICE_UUID);
auto characteristicCallback = new CharacteristicCallbacks(this);
NimBLECharacteristic *pCredentialCharacteristic =
pServProvisioning->createCharacteristic(BLE_CRED_CHAR_UUID,
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC |
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY);
pCredentialCharacteristic->setCallbacks(characteristicCallback);
NimBLECharacteristic *pScanCharacteristic =
pServProvisioning->createCharacteristic(BLE_SCAN_CHAR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY);
pScanCharacteristic->setCallbacks(characteristicCallback);
// Start services
pServProvisioning->start();
pServDeviceInfo->start();
// Advertise
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
// Format advertising data
String mdata;
mdata += (char)0xFF;
mdata += (char)0xFF;
mdata += modelName;
mdata += '#';
mdata += ag->deviceId();
pAdvertising->setManufacturerData(mdata.c_str());
// Start advertise
pAdvertising->start();
bleServerRunning = true;
// Create event group
bleEventGroup = xEventGroupCreate();
if (bleEventGroup == NULL) {
Serial.println("Failed to create BLE event group!");
// This case is very unlikely
}
Serial.println("Provision by BLE ready");
}
void WifiConnector::stopBLE() {
if (bleServerRunning) {
Serial.println("Stopping BLE");
NimBLEDevice::deinit();
}
bleServerRunning = false;
}
//
// BLE innerclass implementation
//
WifiConnector::ServerCallbacks::ServerCallbacks(WifiConnector* parent)
: parent(parent) {}
void WifiConnector::ServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) {
Serial.printf("Client address: %s\n", connInfo.getAddress().toString().c_str());
parent->bleClientConnected = true;
NimBLEDevice::stopAdvertising();
}
void WifiConnector::ServerCallbacks::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) {
Serial.printf("Client disconnected - start advertising\n");
NimBLEDevice::startAdvertising();
parent->bleClientConnected = false;
}
void WifiConnector::ServerCallbacks::onAuthenticationComplete(NimBLEConnInfo& connInfo) {
Serial.println("\n========== PAIRING COMPLETE ==========");
Serial.printf("Peer Address: %s\n", connInfo.getAddress().toString().c_str());
Serial.printf("Encrypted: %s\n", connInfo.isEncrypted() ? "YES" : "NO");
Serial.printf("Authenticated: %s\n", connInfo.isAuthenticated() ? "YES" : "NO");
Serial.printf("Key Size: %d bits\n", connInfo.getSecKeySize() * 8);
Serial.println("======================================\n");
}
WifiConnector::CharacteristicCallbacks::CharacteristicCallbacks(WifiConnector* parent)
: parent(parent) {}
void WifiConnector::CharacteristicCallbacks::onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) {
Serial.printf("%s : onRead(), value: %s\n", pCharacteristic->getUUID().toString().c_str(),
pCharacteristic->getValue().c_str());
}
void WifiConnector::CharacteristicCallbacks::onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) {
Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(),
pCharacteristic->getValue().c_str());
auto bleCred = NimBLEUUID(BLE_CRED_CHAR_UUID);
if (pCharacteristic->getUUID().equals(bleCred)) {
if (!parent->wifiConnecting) {
JSONVar root = JSON.parse(pCharacteristic->getValue().c_str());
String ssid = root["ssid"];
String pass = root["password"];
WiFi.begin(ssid.c_str(), pass.c_str());
xEventGroupSetBits(parent->bleEventGroup, BLE_CRED_BIT);
}
} else {
xEventGroupSetBits(parent->bleEventGroup, BLE_SCAN_BIT);
}
}

View File

@@ -5,16 +5,49 @@
#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 <Arduino.h>
#include <NimBLEDevice.h>
// Provisioning Status Codes
#define PROV_WIFI_CONNECT 0 // WiFi Connect
#define PROV_CONNECTING_TO_SERVER 1 // Connecting to server
#define PROV_SERVER_REACHABLE 2 // Server reachable
#define PROV_MONITOR_CONFIGURED 3 // Monitor configured properly on dashboard
// Provisioning Error Codes
#define PROV_ERR_WIFI_CONNECT_FAILED 10 // Failed to connect to WiFi
#define PROV_ERR_SERVER_UNREACHABLE 11 // Server unreachable
#define PROV_ERR_GET_MONITOR_CONFIG_FAILED 12 // Failed to get monitor configuration from dashboard
#define PROV_ERR_MONITOR_NOT_REGISTERED 13 // Monitor is not registered on dashboard
class WifiConnector : public PrintLog {
public:
enum class ProvisionMethod {
Unknown = 0,
WiFi,
BLE
};
struct WiFiNetwork {
String ssid;
int32_t rssi;
bool open;
};
private:
AirGradient *ag;
OledDisplay &disp;
StateMachine &sm;
Configuration &config;
NimBLEServer *pServer;
EventGroupHandle_t bleEventGroup;
String ssid;
void *wifi = NULL;
@@ -22,16 +55,51 @@ private:
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();
int scanAndFilterWiFi(WiFiNetwork networks[], int maxResults);
String buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalCount,
int page, int batchSize, int totalPages);
void handleBleScanRequest();
// BLE server handler
class ServerCallbacks : public NimBLEServerCallbacks {
public:
explicit ServerCallbacks(WifiConnector *parent);
void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override;
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override;
void onAuthenticationComplete(NimBLEConnInfo &connInfo) override;
private:
WifiConnector *parent;
};
// BLE Characteristics handler
class CharacteristicCallbacks : public NimBLECharacteristicCallbacks {
public:
explicit CharacteristicCallbacks(WifiConnector *parent);
void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
private:
WifiConnector *parent;
};
public:
void setAirGradient(AirGradient *ag);
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config);
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config);
~WifiConnector();
bool connect(void);
void setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo);
void setupProvisionByBLE(const char *modelName);
void stopBLE();
bool connect(String modelName = "");
void disconnect(void);
void handle(void);
void _wifiApCallback(void);
@@ -39,6 +107,7 @@ public:
void _wifiSaveParamCallback(void);
bool _wifiConfigPortalActive(void);
void _wifiTimeoutCallback(void);
void _wifiStop();
void _wifiProcess();
bool isConnected(void);
void reset(void);
@@ -47,8 +116,11 @@ public:
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);
};

View File

@@ -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
*

View File

@@ -25,6 +25,7 @@ public:
void setOn(void);
void setOff(void);
void setToggle(void);
void setStep(void);
State getState(void);
String toString(StatusLed::State state);