Compare commits

...

22 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
Samuel Siburian
75be7d9fc5 Merge pull request #342 from mtlynch/no-trailing-spaces
Eliminate trailing whitespace
2025-09-25 13:45:11 +07:00
Samuel Siburian
309d322100 Merge pull request #343 from sysboy/typo-fix-failuire
Simple typo fix
2025-09-25 11:37:54 +07:00
Steve
89ebe6c39f Simple typo fix 2025-09-23 15:40:36 +01:00
Michael Lynch
29db5469f5 Eliminate trailing whitespace 2025-09-17 20:06:50 -04:00
32 changed files with 855 additions and 278 deletions

View File

@@ -1,5 +1,36 @@
on: [push, pull_request]
jobs:
trailing-whitespace:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
- name: Check for trailing whitespace
run: |
set -u
# Don't enforce checks on vendored libraries.
readonly EXCLUDED_DIR='src/Libraries'
has_trailing_whitespace=false
while read -r line; do
if grep \
"\s$" \
--line-number \
--with-filename \
--binary-files=without-match \
"${line}"; then
has_trailing_whitespace=true
fi
done < <(git ls-files | grep --invert-match "^${EXCLUDED_DIR}/")
if [ "$has_trailing_whitespace" = true ]; then
echo "ERROR: Found trailing whitespace"
exit 1
fi
compile:
strategy:
fail-fast: false

View File

@@ -32,7 +32,7 @@ Each GitHub release note will also include the planned rollout date for wider av
## Help & Support
If you have any questions or problems, check out [our forum](https://forum.airgradient.com/).
If you have any questions or problems, check out [our forum](https://forum.airgradient.com/).
## Development

View File

@@ -16,15 +16,15 @@ Arduino IDE version 2.x ([download](https://www.arduino.cc/en/software))
#### Version < 3.2.0
Using library manager install the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
Using library manager install the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
![Aigradient Library](images/ag-lib.png)
#### Version >= 3.3.0
- From your terminal, go to Arduino libraries folder (windows and mac: `Documents/Arduino/libraries` or linux: `~/Arduino/Libraries`).
- With **git** cli, execute this command `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
- Restart Arduino IDE
- With **git** cli, execute this command `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
- Restart Arduino IDE
3. On tools tab, follow settings below
@@ -57,7 +57,7 @@ Upload Speed ➝ 921600
![board manager](images/esp8266-board.png)
3. Install AirGradient library on library manager using the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
3. Install AirGradient library on library manager using the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
![Aigradient Library](images/ag-lib.png)
@@ -65,7 +65,7 @@ Upload Speed ➝ 921600
![settings esp8266](images/settings-esp8266.png)
5. Open sketch to compile (File ➝ Examples ➝ AirGradient Air Quality Sensor ➝ `<Model Option>`). Depends on the DIY model, either `BASIC`, `DiyProIndoorV3_3` and `DiyProIndoorV4_2`
5. Open sketch to compile (File ➝ Examples ➝ AirGradient Air Quality Sensor ➝ `<Model Option>`). Depends on the DIY model, either `BASIC`, `DiyProIndoorV3_3` and `DiyProIndoorV4_2`
6. Compile
![compiled esp8266](images/compiled-esp8266.png)
@@ -78,13 +78,13 @@ ModuleNotFoundError: No module named serial
![Linux Failed](images/linux-failed.png)
Make sure python pyserial module installed globally in the environment by executing:
Make sure python pyserial module installed globally in the environment by executing:
`$ sudo apt install -y python3-pyserial`
or
or
`$ pip install pyserial`
`$ pip install pyserial`
Choose based on how python installed on your machine. But most user, using `apt` is better.

View File

@@ -1,6 +1,6 @@
## Local Server API
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
### Discovery
@@ -20,7 +20,7 @@ http://airgradient_ecda3b1eaaaf.local/measures/current
“ecda3b1eaaaf” being the serial number of your monitor.
You get the following response:
```json
```json
{
"wifi": -46,
"serialno": "ecda3b1eaaaf",
@@ -84,7 +84,7 @@ Compensated values apply correction algorithms to make the sensor values more ac
"/config" path returns the current configuration of the monitor.
```json
```json
{
"country": "TH",
"pmStandard": "ugm3",
@@ -118,22 +118,22 @@ Configuration parameters can be changed with a PUT request to the monitor, e.g.
Example to force CO2 calibration
```bash
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
```
Example to set monitor to Celsius
```bash
curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config
curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config
```
If you use command prompt on Windows, you need to escape the quotes:
``` -d "{\"param\":\"value\"}" ```
### Avoiding Conflicts with Configuration on AirGradient Server
If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
### Configuration Parameters (GET/PUT)
@@ -204,34 +204,34 @@ Example correction configuration:
Field Name: `pm02`
| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
| EPA 2021 | `"epa_2021"` | Use EPA 2021 correction factors on top of raw value | No |
| PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes |
| PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes |
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
**NOTES**:
**NOTES**:
- Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false`
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
**Examples**:
- PMS5003_20231030
- PMS5003_20231030
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":true}}}}'
```
- PMS5003_20231218
- PMS5003_20231218
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0.03525,"useEpa2021":true}}}}'
```
- PMS5003_20240104
- PMS5003_20240104
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}'
@@ -244,10 +244,10 @@ Field Name:
- Humidity: `rhum`
| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
| AirGradient Standard Correction | `"ag_pms5003t_2024"` | Using standard airgradient correction (for outdoor monitor)| No |
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
*Table above apply for both Temperature and Humidity*

View File

@@ -1,6 +1,6 @@
## OTA Updates
From [firmware version 3.1.1](https://github.com/airgradienthq/arduino/tree/3.1.1) onwards, the AirGradient ONE and Open Air monitors support over the air (OTA) updates.
From [firmware version 3.1.1](https://github.com/airgradienthq/arduino/tree/3.1.1) onwards, the AirGradient ONE and Open Air monitors support over the air (OTA) updates.
#### Mechanism
@@ -10,7 +10,7 @@ The device attempts to update to the latest version on startup and in regular in
http://hw.airgradient.com/sensors/{deviceId}/generic/os/firmware.bin?current_firmware={GIT_VERSION}
If does pass the version it is currently running on along to the server through URL parameter 'current_firmware'.
If does pass the version it is currently running on along to the server through URL parameter 'current_firmware'.
This allows the server to identify if the device is already running on the latest version or should update.
The following scenarios are possible

View File

@@ -294,7 +294,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
Serial.println("Init SGP41 failure");
configuration.hasSensorSGP = false;
}
return false;

View File

@@ -351,7 +351,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
Serial.println("Init SGP41 failure");
configuration.hasSensorSGP = false;
}
return false;

View File

@@ -249,7 +249,7 @@ void loop() {
configUpdateHandle();
localServer._handle();
if (configuration.hasSensorSGP) {
ag.sgp41.handle();
}
@@ -374,7 +374,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
Serial.println("Init SGP41 failure");
configuration.hasSensorSGP = false;
}
return false;

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 TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE (1 * 60) /** minutes */
#define TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY (2 * 60) /** minutes */
#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 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;
@@ -123,7 +117,7 @@ static uint32_t factoryBtnPressTime = 0;
static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
static bool ledBarButtonTest = false;
static String fwNewVersion;
static int lastCellSignalQuality = 99; // CSQ
static int lastCellSignalQuality = 99; // CSQ
// Default value is 0, indicate its not started yet
// In minutes
@@ -136,7 +130,7 @@ static void boardInit(void);
static void initializeNetwork();
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void configUpdateHandle(void);
static void configUpdateHandle(void);
static void updateDisplayAndLedBar(void);
static void updateTvoc(void);
static void updatePm(void);
@@ -157,13 +151,12 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod();
static void newMeasurementCycle();
static void restartIfCeClientIssueOverTwoHours();
static void restartIfCeClientIssueOverTwoHours();
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);
@@ -180,7 +173,7 @@ void setup() {
Serial.begin(115200);
delay(100); /** For bester show log */
// Enable cullular module power board
// Enable cullular module power board
pinMode(GPIO_EXPANSION_CARD_POWER, OUTPUT);
digitalWrite(GPIO_EXPANSION_CARD_POWER, HIGH);
@@ -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);
@@ -284,14 +269,14 @@ void setup() {
// Queue now only applied for cellular
// Allocate queue memory to avoid always reallocation
measurementCycleQueue.reserve(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
// Initialize mutex to access mesurementCycleQueue
// Initialize mutex to access mesurementCycleQueue
mutexMeasurementCycleQueue = xSemaphoreCreateMutex();
}
// 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,17 +287,15 @@ 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() {
if (networkOption == UseCellular) {
// Check if cellular client not ready until certain time
// Redundant check in both task to make sure its executed
// Redundant check in both task to make sure its executed
restartIfCeClientIssueOverTwoHours();
}
@@ -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();
}
}
@@ -559,7 +537,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
Serial.println("Init SGP41 failure");
configuration.hasSensorSGP = false;
}
return false;
@@ -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);
@@ -725,7 +703,7 @@ static void sendDataToAg() {
"task_led", 2048, NULL, 5, NULL);
delay(1500);
// Build payload to check connection to airgradient server
JSONVar root;
root["wifi"] = wifiConnector.RSSI();
@@ -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;
}
@@ -970,7 +948,7 @@ void initializeNetwork() {
agSerial->init(GPIO_IIC_RESET);
if (agSerial->open()) {
Serial.println("Cellular module found");
// Initialize cellular module and use cellular as agClient
// Initialize cellular module and use cellular as agClient
cellularCard = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN);
agClient = new AirgradientCellularClient(cellularCard);
networkOption = UseCellular;
@@ -984,7 +962,7 @@ void initializeNetwork() {
}
if (networkOption == UseCellular) {
// Enable serial stream debugging to check the AT command when doing registration
// Enable serial stream debugging to check the AT command when doing registration
agSerial->setDebug(true);
}
@@ -1005,7 +983,7 @@ void initializeNetwork() {
ESP.restart();
}
// Provide openmetrics to have access to last transmission result
// Provide openmetrics to have access to last transmission result
openMetrics.setAirgradientClient(agClient);
if (networkOption == UseCellular) {
@@ -1014,30 +992,26 @@ 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
mdnsInit();
localServer.begin();
// Apply mqtt connection if configured
initMqtt();
// Ignore the rest if cloud connection to AirGradient is disabled
if (configuration.isCloudConnectionDisabled()) {
return;
@@ -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);
@@ -1202,7 +1173,7 @@ static void updateDisplayAndLedBar(void) {
}
if (configuration.isCloudConnectionDisabled()) {
// Ignore API related check since cloud is disabled
// Ignore API related check since cloud is disabled
stateMachine.displayHandle(AgStateMachineNormal);
stateMachine.handleLeds(AgStateMachineNormal);
return;
@@ -1390,12 +1361,12 @@ 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);
// Make sure measurement cycle available
int queueSize = measurementCycleQueue.size();
if (queueSize == 0) {
@@ -1405,7 +1376,7 @@ void postUsingCellular(bool forcePost) {
}
// Check queue size if its ready to transmit
// It is ready if size is divisible by 3
// It is ready if size is divisible by 3
if (!forcePost && (queueSize % MEASUREMENT_TRANSMIT_CYCLE) > 0) {
Serial.printf("Not ready to transmit, queue size are %d\n", queueSize);
xSemaphoreGive(mutexMeasurementCycleQueue);
@@ -1426,7 +1397,7 @@ void postUsingCellular(bool forcePost) {
// Attempt to send
if (agClient->httpPostMeasures(payload) == false) {
// Consider network has a problem, retry in next schedule
// Consider network has a problem, retry in next schedule
Serial.println("Post measures failed, retry in next schedule");
return;
}
@@ -1527,11 +1498,10 @@ void setMeasurementMaxPeriod() {
int calculateMaxPeriod(int updateInterval) {
// 0.8 is 80% reduced interval for max period
// NOTE: Both network option use the same measurement interval
// NOTE: Both network option use the same measurement interval
return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval;
}
void networkSignalCheck() {
if (networkOption == UseWifi) {
Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI());
@@ -1544,7 +1514,7 @@ void networkSignalCheck() {
}
// Save last signal quality
lastCellSignalQuality = result.data;
lastCellSignalQuality = result.data;
if (result.data == 99) {
// 99 indicate cellular not attached to network
@@ -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) {
@@ -1622,10 +1590,10 @@ void networkingTask(void *args) {
}
// Enable at command debug
agSerial->setDebug(true);
agSerial->setDebug(true);
// Check if cellular client not ready until certain time
// Redundant check in both task to make sure its executed
// Redundant check in both task to make sure its executed
restartIfCeClientIssueOverTwoHours();
// Power cycling cellular module due to network issues for more than 1 hour
@@ -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
}
}
@@ -1676,14 +1644,14 @@ void networkingTask(void *args) {
void newMeasurementCycle() {
if (xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY) == pdTRUE) {
// Make sure queue not overflow
// Make sure queue not overflow
if (measurementCycleQueue.size() >= MAXIMUM_MEASUREMENT_CYCLE_QUEUE) {
// Remove the oldest data from queue if queue reach max
measurementCycleQueue.erase(measurementCycleQueue.begin());
}
// Get current measures
auto mc = measurements.getMeasures();
auto mc = measurements.getMeasures();
mc.signal = cellularCard->csqToDbm(lastCellSignalQuality); // convert to RSSI
measurementCycleQueue.push_back(mc);
@@ -1694,4 +1662,3 @@ void newMeasurementCycle() {
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
}
}

View File

@@ -6,8 +6,8 @@ OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) {
this->ag = ag;
void OpenMetrics::setAirGradient(AirGradient *ag) {
this->ag = ag;
}
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {

View File

@@ -15,7 +15,7 @@ framework = arduino
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D AG_LOG_LEVEL=AG_LOG_LEVEL_INFO -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
board_build.partitions = partitions.csv
monitor_speed = 115200
lib_deps =
lib_deps =
aglib=symlink://../arduino
EEPROM
WebServer
@@ -26,13 +26,14 @@ lib_deps =
WiFiClientSecure
Update
DNSServer
h2zero/NimBLE-Arduino@^2.1.0
[env:esp8266]
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 115200
lib_deps =
lib_deps =
aglib=symlink://../arduino
EEPROM
ESP8266HTTPClient

View File

@@ -47,7 +47,7 @@ bool AgApiClient::fetchServerConfiguration(void) {
}
#else
HTTPClient client;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
@@ -114,7 +114,7 @@ bool AgApiClient::postToServer(String data) {
}
#else
HTTPClient client;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
@@ -185,13 +185,13 @@ void AgApiClient::setAirGradient(AirGradient *ag) { this->ag = ag; }
/**
* @brief Send the package to check the connection with cloud
*
*
* @param rssi WiFi RSSI
* @param bootCount Boot count
* @return true Success
* @return false Failure
*/
bool AgApiClient::sendPing(int rssi, int bootCount) {
bool AgApiClient::sendPing(int rssi, int bootCount) {
JSONVar root;
root["wifi"] = rssi;
root["boot"] = bootCount;

View File

@@ -114,7 +114,7 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
const size_t enumSize = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;;
// Loop through enum values
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
@@ -122,7 +122,7 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
}
}
// If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections
// If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections
if (result == COR_ALGO_PM_UNKNOWN) {
// Check the substring "slr_PMS5003_xxxxxxxx"
if (algorithm.substring(0, 11) == "slr_PMS5003") {
@@ -749,7 +749,7 @@ bool Configuration::parse(String data, bool isLocal) {
}
else if (JSON.typeof_(root[jprop_mqttBrokerUrl]) == "null" and !isLocal) {
// So if its not available on the json and json comes from aigradient server
// then set its value to default (empty)
// then set its value to default (empty)
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
}
else {
@@ -1073,7 +1073,7 @@ String Configuration::getMqttBrokerUri(void) {
}
/**
* @brief Get HTTP domain for post measures and get configuration
* @brief Get HTTP domain for post measures and get configuration
*
* @return String http domain, might be empty string
*/
@@ -1562,13 +1562,13 @@ void Configuration::setDisableCloudConnection(bool disable) {
saveConfig();
}
bool Configuration::isLedBarModeChanged(void) {
bool Configuration::isLedBarModeChanged(void) {
bool changed = _ledBarModeChanged;
_ledBarModeChanged = false;
return changed;
}
bool Configuration::isMonitorDisplayCompensatedValues(void) {
bool Configuration::isMonitorDisplayCompensatedValues(void) {
return jconfig[jprop_monitorDisplayCompensatedValues];
}
@@ -1591,8 +1591,8 @@ bool Configuration::isPMCorrectionChanged(void) {
}
/**
* @brief Check if PM correction is enabled
*
* @brief Check if PM correction is enabled
*
* @return true if PM correction algorithm is not None, otherwise false
*/
bool Configuration::isPMCorrectionEnabled(void) {

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

@@ -568,7 +568,7 @@ float Measurements::getAverage(MeasurementType type, int ch) {
assert(0);
}
return measurementAverage;
return measurementAverage;
}
String Measurements::pms5003FirmwareVersion(int fwCode) {
@@ -895,7 +895,7 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
oss << ",";
// Temperature
// Temperature
if (utils::isValidTemperature(mc.temperature[0]) && utils::isValidTemperature(mc.temperature[1])) {
float temp = (mc.temperature[0] + mc.temperature[1]) / 2.0f;
oss << std::round(temp * 10);

View File

@@ -149,7 +149,7 @@ public:
*
* @param type measurement type that will be retrieve
* @param ch target type value channel
* @return moving average value of target measurements type
* @return moving average value of target measurements type
*/
float getAverage(MeasurementType type, int ch = 1);

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
*
@@ -358,7 +481,7 @@ bool WifiConnector::isConnected(void) { return WiFi.isConnected(); }
* this method
*
*/
void WifiConnector::reset(void) {
void WifiConnector::reset(void) {
if(this->wifi == NULL) {
this->wifi = new WiFiManager();
if(this->wifi == NULL){
@@ -366,7 +489,7 @@ void WifiConnector::reset(void) {
return;
}
}
WIFI()->resetSettings();
WIFI()->resetSettings();
}
/**
@@ -404,10 +527,336 @@ 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
*
*
*/
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

@@ -1,7 +1,7 @@
#include "AirGradient.h"
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else
#else
#include "WiFi.h"
#endif
@@ -57,7 +57,7 @@ String AirGradient::getBoardName(void) {
/**
* @brief Board Type is ONE_INDOOR
*
*
* @return true ONE_INDOOR
* @return false Other
*/
@@ -65,15 +65,15 @@ bool AirGradient::isOne(void) {
return boardType == BoardType::ONE_INDOOR;
}
bool AirGradient::isOpenAir(void) {
return boardType == BoardType::OPEN_AIR_OUTDOOR;
bool AirGradient::isOpenAir(void) {
return boardType == BoardType::OPEN_AIR_OUTDOOR;
}
bool AirGradient::isPro4_2(void) {
return boardType == BoardType::DIY_PRO_INDOOR_V4_2;
}
bool AirGradient::isPro3_3(void) {
bool AirGradient::isPro3_3(void) {
return boardType == BoardType::DIY_PRO_INDOOR_V3_3;
}

View File

@@ -177,9 +177,9 @@ public:
/**
* @brief Check that Airgradient object is OPEN_AIR
*
* @return true
* @return false
*
* @return true
* @return false
*/
bool isOpenAir(void);

View File

@@ -27,7 +27,7 @@ enum BoardType {
/**
* @brief Board definitions
*
*
*/
struct BoardDef {
/** Board Support CO2 SenseS8 */

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

@@ -6,7 +6,7 @@
/**
* @brief The class define how to handle the LED
*
*
*/
class StatusLed {
public:
@@ -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);

View File

@@ -316,10 +316,10 @@ int PMSBase::pm25ToAQI(int pm02) {
/**
* @brief SLR correction for PM2.5
*
* @brief SLR correction for PM2.5
*
* Reference: https://www.airgradient.com/blog/low-readings-from-pms5003/
*
*
* @param pm25 PM2.5 raw value
* @param pm003Count PM0.3 count
* @param scalingFactor Scaling factor

View File

@@ -35,7 +35,7 @@ public:
/** For PMS5003T*/
int16_t getTemp(void);
uint16_t getHum(void);
uint8_t getFirmwareVersion(void);
uint8_t getFirmwareVersion(void);
uint8_t getErrorCode(void);
int pm25ToAQI(int pm02);

View File

@@ -189,21 +189,21 @@ float PMS5003::compensate(float pm25, float humidity) { return pms.compensate(pm
/**
* @brief Get sensor firmware version
*
*
* @return int
*/
int PMS5003::getFirmwareVersion(void) { return _ver; }
/**
* @brief Get sensor error code
*
* @return uint8_t
*
* @return uint8_t
*/
uint8_t PMS5003::getErrorCode(void) { return pms.getErrorCode(); }
/**
* @brief Is sensor connect with device
*
*
* @return true Connected
* @return false Removed
*/
@@ -255,14 +255,14 @@ void PMS5003::resetFailCount(void) {
/**
* @brief Get number of fail count
*
* @return int
*
* @return int
*/
int PMS5003::getFailCount(void) { return pms.getFailCount(); }
/**
* @brief Get number of fail count max
*
* @return int
*
* @return int
*/
int PMS5003::getFailCountMax(void) { return pms.getFailCountMax(); }

View File

@@ -218,21 +218,21 @@ float PMS5003T::compensate(float pm25, float humidity) { return pms.compensate(p
/**
* @brief Get module(s) firmware version
*
*
* @return int Version code
*/
int PMS5003T::getFirmwareVersion(void) { return _ver; }
/**
* @brief Get sensor error code
*
* @return uint8_t
*
* @return uint8_t
*/
uint8_t PMS5003T::getErrorCode(void) { return pms.getErrorCode(); }
/**
* @brief Is sensor connect to device
*
*
* @return true Connected
* @return false Removed
*/
@@ -281,14 +281,14 @@ void PMS5003T::resetFailCount(void) {
/**
* @brief Get fail count
*
* @return int
*
* @return int
*/
int PMS5003T::getFailCount(void) { return pms.getFailCount(); }
/**
* @brief Get fail count max
*
* @return int
*
* @return int
*/
int PMS5003T::getFailCountMax(void) { return pms.getFailCountMax(); }

View File

@@ -6,11 +6,11 @@ PMS5003TBase::~PMS5003TBase() {}
/**
* @brief Compensate the temperature
*
*
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
*
* @param temp
* @return * float
*
* @param temp
* @return * float
*/
float PMS5003TBase::compensateTemp(float temp) {
if (temp < 10.0f) {
@@ -21,11 +21,11 @@ float PMS5003TBase::compensateTemp(float temp) {
/**
* @brief Compensate the humidity
*
*
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
*
* @param temp
* @return * float
*
* @param temp
* @return * float
*/
float PMS5003TBase::compensateHum(float hum) {
hum = hum * 1.259f + 7.34f;

View File

@@ -4,7 +4,7 @@
class PMS5003TBase
{
private:
public:
PMS5003TBase();
~PMS5003TBase();

View File

@@ -140,7 +140,7 @@ void Sgp41::pause() {
tvoc = utils::getInvalidVOC();
noxRaw = utils::getInvalidNOx();
nox = utils::getInvalidNOx();
}
}
void Sgp41::resume() {
onPause = false;