Compare commits

..

37 Commits

Author SHA1 Message Date
samuelbles07
227bd518c9 Fix response data is big 2024-12-09 18:22:05 +07:00
samuelbles07
d0caee99aa Fix close file after write
Better error handling when write and load measurement file
comment out spiffs format
2024-12-08 03:09:28 +07:00
samuelbles07
3162030800 Increase html font text size 2024-12-08 01:24:29 +07:00
samuelbles07
6b6116ab6d Format csv file
remove pm1.0 and pm10
Add tvoc raw and nox raw
2024-12-08 01:23:51 +07:00
samuelbles07
15dec1713d Display serial number to dashboard 2024-12-08 01:06:28 +07:00
samuelbles07
70e626cbc9 Display serial number to dashboard 2024-12-08 01:05:43 +07:00
samuelbles07
c003912d7a post storage and time return html 2024-12-08 01:05:21 +07:00
samuelbles07
902797ceb0 Redirect root path to dashboard 2024-12-08 01:03:53 +07:00
samuelbles07
430e908d88 Downloaded filename and AP ssid
ap ssid format have serial number
filname have last 4 digit serial number
2024-12-07 23:51:54 +07:00
samuelbles07
6cb06986c3 Remove set time reduce by 17 minutes 2024-12-07 23:32:38 +07:00
samuelbles07
e3156d438c Hotspot mode 2024-12-07 05:41:39 +07:00
samuelbles07
4ae0206e6b Switch button position 2024-12-07 05:40:43 +07:00
samuelbles07
83a4eddc37 Local storage mode using esp32 as AP 2024-12-07 05:39:59 +07:00
samuelbles07
67b71f583b Add esp32 timestamp to dashboard page
Hotfix timestamp off by 17 minutes when set system time
2024-12-07 05:13:19 +07:00
samuelbles07
e2798f1193 Dashboard page 2024-12-07 04:59:25 +07:00
samuelbles07
f4357cca7e Fix timezone 2024-12-07 04:16:00 +07:00
samuelbles07
20dcea20ad Notify write succes on oled
Disable led bar
Decrease oled brightness
2024-12-07 02:21:11 +07:00
samuelbles07
cfe6fa9fd5 Seperate reset and set time endpoints 2024-12-07 02:05:43 +07:00
samuelbles07
391186dd59 PM2.5 correction 2024-12-06 20:00:43 +07:00
samuelbles07
a9f7f72871 Fix typo 2024-12-06 19:40:32 +07:00
samuelbles07
9a3f71b33c Fix typo 2024-12-06 19:39:32 +07:00
samuelbles07
d8f433bd3e WiFi reconnection with indicator 2024-12-06 19:38:34 +07:00
samuelbles07
da414bf3fc Add tips to docs 2024-12-06 19:14:39 +07:00
samuelbles07
d225af623a Add local storage docs 2024-12-06 19:06:28 +07:00
samuelbles07
b7d22c2136 Fix SPIFFS usage percentage 2024-12-06 15:19:03 +07:00
samuelbles07
6cd5e9f4b8 Handle if spiffs full 2024-12-06 04:26:18 +07:00
samuelbles07
0cec71ceb6 Attempt connect to default wifi on boot
notify led when new measurement inserted to local storage
2024-12-06 03:57:49 +07:00
samuelbles07
424d1d89fa Init timezone on boot 2024-12-06 03:55:55 +07:00
samuelbles07
6186e3eca0 Fix get storage allocate based on size 2024-12-06 03:12:37 +07:00
samuelbles07
b79c4e74e2 Add timestamp to local storage measurements 2024-12-06 03:00:23 +07:00
samuelbles07
baa8601b5c Reset storage endpoints 2024-12-06 02:20:46 +07:00
samuelbles07
a9fa7b6e63 Delete local storage function 2024-12-06 02:20:07 +07:00
samuelbles07
1034f1892a Set and get system time 2024-12-06 02:19:25 +07:00
samuelbles07
859c1a7e92 Local server to get local storage measurements 2024-12-05 04:09:17 +07:00
samuelbles07
bce46445d6 Disable unnecessary scheduler 2024-12-05 04:07:57 +07:00
samuelbles07
be7ca28a0e Scheduler to run save measurements to local storage 2024-12-05 04:07:00 +07:00
samuelbles07
12e6f72b85 Save and get function local storage measurements 2024-12-05 04:05:42 +07:00
66 changed files with 1505 additions and 3332 deletions

View File

@@ -1,36 +1,5 @@
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
@@ -48,14 +17,11 @@ jobs:
- "esp32:esp32:esp32c3"
include:
- fqbn: "esp8266:esp8266:d1_mini"
core: "esp8266:esp8266"
core_version: "3.1.2"
core: "esp8266:esp8266@3.1.2"
core_url: "https://arduino.esp8266.com/stable/package_esp8266com_index.json"
- fqbn: "esp32:esp32:esp32c3"
core: "esp32:esp32"
core_version: "2.0.17"
core_url: "https://espressif.github.io/arduino-esp32/package_esp32_index.json"
board_options: "JTAGAdapter=default,CDCOnBoot=cdc,PartitionScheme=min_spiffs,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=verbose,EraseFlash=none"
core: "esp32:esp32@2.0.11"
exclude:
- example: "BASIC"
fqbn: "esp32:esp32:esp32c3"
@@ -65,32 +31,30 @@ jobs:
fqbn: "esp32:esp32:esp32c3"
- example: "OneOpenAir"
fqbn: "esp8266:esp8266:d1_mini"
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
submodules: 'true'
- uses: arduino/compile-sketches@v1.1.2
with:
fqbn: ${{ matrix.fqbn }}
sketch-paths: |
examples/${{ matrix.example }}
libraries: |
- source-path: ./
- name: NimBLE-Arduino
version: 2.3.7
cli-compile-flags: |
- --warnings
- none
- --board-options
- "${{ matrix.board_options }}"
platforms: |
- name: ${{ matrix.core }}
version: ${{ matrix.core_version}}
source-url: ${{ matrix.core_url }}
enable-deltas-report: true
- uses: actions/checkout@v4
- run:
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh |
sh -s 0.35.3
- run: bin/arduino-cli --verbose core install '${{ matrix.core }}'
--additional-urls '${{ matrix.core_url }}'
- run: bin/arduino-cli --verbose lib install
WiFiManager@2.0.16-rc.2
Arduino_JSON@0.2.0
U8g2@2.34.22
# In some cases, actions/checkout@v4 will check out a detached HEAD; for
# example, this happens on pull request events, where an hypothetical
# PR merge commit is checked out. This tends to confuse
# `arduino-cli lib install --git-url`, making it fail with errors such as:
# Error installing Git Library: Library install failed: object not found
# Create and check out a dummy branch to work around this issue.
- run: git checkout -b check
- run: bin/arduino-cli --verbose lib install --git-url .
env:
ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL: "true"
- run: bin/arduino-cli --verbose compile 'examples/${{ matrix.example }}'
--fqbn '${{ matrix.fqbn }}' --board-options '${{ matrix.board_options }}'
# TODO: at this point it would be a good idea to run some smoke tests on
# the resulting image (e.g. that it boots successfully and sends metrics)
# but that would either require a high fidelity device emulator, or a

5
.gitignore vendored
View File

@@ -3,8 +3,3 @@ build
.vscode
/.idea/
.pio
.cache
.clangd
logs
gen_compile_commands.py
compile_commands.json

6
.gitmodules vendored
View File

@@ -1,6 +0,0 @@
[submodule "src/Libraries/airgradient-client"]
path = src/Libraries/airgradient-client
url = ../../airgradienthq/airgradient-client.git
[submodule "src/Libraries/airgradient-ota"]
path = src/Libraries/airgradient-ota
url = ../../airgradienthq/airgradient-ota.git

View File

@@ -20,32 +20,13 @@ Make sure you have exactly the versions of libraries and boards installed as des
If you have an older version of the AirGradient PCB not mentioned in the example files, please downgrade this library to version 2.4.15 to support these legacy boards.
### Release Process
Releases published on GitHub are **not immediately deployed to all devices in the market**. Each release first goes through internal testing, including limited deployments in select locations to verify stability and functionality.
If the tests pass, the firmware is then made available for:
- **FOTA (Firmware Over-The-Air) updates** from AirGradient dashboard
- **Manual flashing** via [Airgradient](https://www.airgradient.com/documentation/firmwares/) website
Each GitHub release note will also include the planned rollout date for wider availability.
## 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
## Documentation
* See [compilation instructions](/docs/howto-compile.md) for details about how to customize AirGradient's firmware and flash it to your device.
## Over the air (OTA) updates
* See the [OTA Updates documentation](/docs/ota-updates.md) for details about how AirGradient monitors receive over the air updates.
## API documentation
* [Local server API documentation](/docs/local-server.md)
* [AirGradient Cloud server API documentation](https://api.airgradient.com/public/docs/api/v1/).
Local server API documentation is available in [/docs/local-server.md](/docs/local-server.md) and AirGradient server API on [https://api.airgradient.com/public/docs/api/v1/](https://api.airgradient.com/public/docs/api/v1/).
## The following libraries have been integrated into this library for ease of use

BIN
docs/epoch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

View File

@@ -1,107 +0,0 @@
# How to compile AirGradient firmware on Arduino IDE
## Prequisite
Arduino IDE version 2.x ([download](https://www.arduino.cc/en/software))
> For AirGradient model ONE and Open Air, the codebase **WILL NOT** work on the latest major version of arduino-esp32 which is *3.x* . This related to when installing "esp32 by Espressif Systems" in board manager. Instead use version **2.0.17**, please follow the first step carefully.
## Steps for ESP32C3 based board (ONE and Open Air Model)
1. Install "esp32 by Espressif Systems" in board manager with version **2.0.17** (Tools ➝ Board ➝ Boards Manager ➝ search for `"espressif"`)
![board manager](images/esp32-board.png)
2. Install AirGradient library
#### Version < 3.2.0
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
#### Version >= 3.6.0
- Ensure `NimBLE-Arduino` by h2zero library version `2.3.7` is installed using Arduino library manager
- Follow steps of ">= 3.3.0"
3. On tools tab, follow settings below
```
Board ➝ ESP32C3 Dev Module
USB CDC On Boot ➝ Enabled
CPU Frequency ➝ 160MHz (WiFi)
Core Debug Level ➝ Info
Erase All Flash Before Sketch Upload ➝ Enabled (or choose as needed)
Flash Frequency ➝ 80MHz
Flash Mode ➝ QIO
Flash Size ➝ 4MB (32Mb)
JTAG Adapter ➝ Disabled
Partition Scheme ➝ Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)
Upload Speed ➝ 921600
```
4. Open sketch to compile (File ➝ Examples ➝ AirGradient Air Quality Sensor ➝ OneOpenAir). This sketch for AirGradient ONE and Open Air monitor model
5. Compile
![compiled esp32](images/compiled.png)
## Steps for ESP8266 based board (DIY model)
1. Add esp8266 board by adding http://arduino.esp8266.com/stable/package_esp8266com_index.json into Additional Board Manager URLs field (File ➝ Preferences ➝ Additional boards manager URLs)
![additional-board](images/additional-board.png)
2. Install esp8266 board on board manager with version **3.1.2** (Tools ➝ Board ➝ Boards Manager ➝ search for `"esp8266"`)
![board manager](images/esp8266-board.png)
3. Install AirGradient library on library manager using the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
![Aigradient Library](images/ag-lib.png)
4. On tools tab, set board to `LOLIN(WEMOS) D1 R2 & mini`, and let other settings to default
![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`
6. Compile
![compiled esp8266](images/compiled-esp8266.png)
## Possible Issues
### Linux (Debian)
ModuleNotFoundError: No module named serial
![Linux Failed](images/linux-failed.png)
Make sure python pyserial module installed globally in the environment by executing:
`$ sudo apt install -y python3-pyserial`
or
`$ pip install pyserial`
Choose based on how python installed on your machine. But most user, using `apt` is better.
## How to contribute
The instructions above are the instructions for how to build an official release of the AirGradient firmware using the Arduino IDE. If you intend to make changes which you plan to contribute back to the main project, instead of installing the AirGradient library, check out the repository at Documents/Arduino/libraries (for Windows and Mac) or ~/Arduino/libraries (for Linux). If you installed the library, you can remove it from the library manager in the Arduino IDE, or just delete the directory.
**NOTE:** When cloning the repository, for version >= 3.3.0 it has submodule, please use `--recursive` flag like this: `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
Please follow github [contributing to a project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) tutorial to contribute to this project.
There are 2 environment options to compile this project, PlatformIO and ArduinoIDE.
- For PlatformIO, it should work out of the box
- For arduino, files in `src` folder and also from `Examples` can be modified at `Documents/Arduino/libraries` for Windows and Mac, and `~/Arduino/Libraries` for Linux

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -1,8 +1,8 @@
## 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
#### Discovery
The monitors run a mDNS discovery. So within the same network, the monitor can be accessed through:
@@ -11,7 +11,7 @@ http://airgradient_{{serialnumber}}.local
The following requests are possible:
### Get Current Air Quality (GET)
#### Get Current Air Quality (GET)
With the path "/measures/current" you can get the current air quality data.
@@ -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",
@@ -80,11 +80,11 @@ You get the following response:
Compensated values apply correction algorithms to make the sensor values more accurate. Temperature and relative humidity correction is only applied on the outdoor monitor Open Air but the properties _compensated will still be send also for the indoor monitor AirGradient ONE.
### Get Configuration Parameters (GET)
#### Get Configuration Parameters (GET)
"/config" path returns the current configuration of the monitor.
```json
```json
{
"country": "TH",
"pmStandard": "ugm3",
@@ -93,7 +93,6 @@ Compensated values apply correction algorithms to make the sensor values more ac
"tvocLearningOffset": 12,
"noxLearningOffset": 12,
"mqttBrokerUrl": "",
"httpDomain": "",
"temperatureUnit": "c",
"configurationControl": "local",
"postDataToAirGradient": true,
@@ -106,36 +105,37 @@ Compensated values apply correction algorithms to make the sensor values more ac
"pm02": {
"correctionAlgorithm": "epa_2021",
"slr": {}
}
}
}
}
```
### Set Configuration Parameters (PUT)
#### Set Configuration Parameters (PUT)
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
#### 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)
#### Configuration Parameters (GET/PUT)
| Properties | Description | Type | Accepted Values | Example |
|-----------------------------------|:-----------------------------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
@@ -146,8 +146,7 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `displayBrightness` | Brightness of the Display. | Number | 0-100 | `{"displayBrightness": 50}` |
| `ledBarBrightness` | Brightness of the LEDBar. | Number | 0-100 | `{"ledBarBrightness": 40}` |
| `abcDays` | Number of days for CO2 automatic baseline calibration. | Number | Maximum 200 days. Default 8 days. | `{"abcDays": 8}` |
| `mqttBrokerUrl` | MQTT broker URL. | String | Maximum 255 characters. Set value to empty string to disable mqtt connection. | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` |
| `httpDomain` | Domain name for http request. (version > 3.3.2) | String | Maximum 255 characters. Set value to empty string to set http domain to default airgradient | `{"httpDomain": "sub.domain.com"}` |
| `mqttBrokerUrl` | MQTT broker URL. | String | | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` |
| `temperatureUnit` | Temperature unit shown on the display. | String | `c` or `C`: Degree Celsius °C <br>`f` or `F`: Degree Fahrenheit °F | `{"temperatureUnit": "c"}` |
| `configurationControl` | The configuration source of the device. | String | `both`: Accept local and cloud configuration <br>`local`: Accept only local configuration <br>`cloud`: Accept only cloud configuration | `{"configurationControl": "both"}` |
| `postDataToAirGradient` | Send data to AirGradient cloud. | Boolean | `true`: Enabled <br>`false`: Disabled | `{"postDataToAirGradient": true}` |
@@ -155,18 +154,15 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `ledBarTestRequested` | Can be set to trigger a test. | Boolean | `true` : LEDs will run test sequence | `{"ledBarTestRequested": true}` |
| `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 12}` |
| `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on 3.1.9) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `corrections` | Sets correction options to display and measurement values on local server response. (version >= 3.1.11) | Object | _see corrections section_ | _see corrections section_ |
| `offlineMode` | Set monitor to run without WiFi. | Boolean | `false`: Disabled (default) <br> `true`: Enabled | `{"offlineMode": true}` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on [3.1.9]()) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `corrections` | Sets correction options to display and measurement values on local server response. (version >= [3.1.11]()) | Object | _see corrections section_ | _see corrections section_ |
**Notes**
- `offlineMode` : the device will disable all network operation, and only show measurements on the display and ledbar; Read-Only; Change can be apply using reset button on boot.
- `disableCloudConnection` : disable every request to AirGradient server, means features like post data to AirGradient server, configuration from AirGradient server and automatic firmware updates are disabled. This configuration overrides `configurationControl` and `postDataToAirGradient`; Read-Only; Change can be apply from wifi setup webpage.
#### Corrections
### Corrections
The `corrections` object allows configuring PM2.5, Temperature and Humidity correction algorithms and parameters locally. This affects both the display, local server response and open metrics values.
The `corrections` object allows configuring PM2.5 correction algorithms and parameters locally. This affects both the display and local server response values.
Example correction configuration:
@@ -180,79 +176,41 @@ Example correction configuration:
"scalingFactor": 0,
"useEpa2021": false
}
},
"atmp": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0
}
},
"rhum": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0
}
},
}
}
}
```
#### PM 2.5
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}}}}'
```
#### Temperature & Humidity
Field Name:
- Temperature: `atmp`
- 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 |
*Table above apply for both Temperature and Humidity*
**Example**
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"atmp":{"correctionAlgorithm":"custom","slr":{"intercept":0.2,"scalingFactor":1.1}}}}'
```

View File

@@ -0,0 +1,56 @@
*This document to explain local storage mode - experimental*
## How it works?
1. Monitor directly goes to local storage mode
2. On boot, monitor will attempt to connect to default wifi. And if connected, mdns and local server will be enabled, otherwise it will ignore and continues the measurements
3. On display, when boot it will show the mode ("local storage mode") and wifi related scenario. After that, monitor will show the measurements dashboard
4. Measurement records to the local storage every two minutes that saved on CSV file in SPIFFs partition
5. Every successful writes, monitor will blink the most left led bar to *blue* twice, but if failed it will blink *red* twice. There are two possibilities for failed write, SPIFFs partition already full or out of heap memory when load the file.
6. There are 2 endpoinds added for this mode, download measurements from local storage and reset measurement (delete old measurements file and create new one) with new timestamp. Timestamp here to set the monitor system time.
**Notes**
1. Default wifi
- ssid ➝ `airgradient`
- password ➝ `cleanair`
2. Maximum measurements file is around 113kb. If assume each measurements is 60 bytes, with write schedule 2 minutes, SPIFFS will be full in around 5 days
3. WiFi connection attempt on boot wait for 10s before considering timeout
4. Tips. If monitor not connected to wifi on boot, no need to restart the monitor for reconnection, it will automatically connect to AP once it is available
### Local Storage Endpoinds
*Make sure monitor is connected to AP, and client also connect to it. And change the serial number on the url*
**Download measurements file**
To download measurements file from local storage, just directly access following url on the browser `http://airgradient_aaaaaaaa.local/storage`, and browser should automatically download the file.
**Reset measurements**
Execute below command in terminal
```sh
curl -X PUT -H "Content-Type: text/plain" -d '1733431986' http://airgradient_aaaaaaa.local/storage/reset
```
`1733431986` this data is the time that we want to set monitor system time to. Its in epoch time format and expecting UTC+0 timezone.
To get epoch time, access this url [https://www.unixtimestamp.com/](https://www.unixtimestamp.com/), and click copy button.
![unixtimestamp website](epoch.png)
### Example measurements file content
```csv
datetime,pm0.3 count,pm1,pm2.5,pm10,temp,rhum,co2,tvoc,nox
05/12 21:10:59,869.67,11.17,20.33,21.83,26.69,72.93,417,40,1
05/12 21:11:30,834.83,11.50,19.33,20.33,26.68,73.08,413,79,1
05/12 21:12:01,829.67,10.33,19.33,22.00,26.64,73.09,412,90,1
05/12 21:12:32,831.50,10.33,18.33,20.83,26.62,73.21,411,97,1
05/12 21:13:02,887.50,12.00,20.33,21.67,26.59,73.33,412,95,1
05/12 21:13:33,785.17,8.67,18.50,19.50,26.56,73.43,414,92,1
05/12 21:14:04,827.50,10.50,18.50,19.50,26.54,73.43,415,98,1
05/12 21:14:35,815.83,10.50,19.50,19.83,26.49,73.47,413,99,1
```

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

@@ -12,8 +12,10 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
@@ -53,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_BASIC);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@@ -122,7 +124,6 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@@ -148,12 +149,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@@ -294,7 +292,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failure");
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
@@ -318,7 +316,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@@ -333,7 +331,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@@ -417,14 +415,6 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@@ -482,7 +472,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@@ -528,24 +518,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
}

View File

@@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
server.send(200, "application/json", toSend);
}

View File

@@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
@@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@@ -191,12 +191,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

@@ -12,8 +12,10 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
@@ -53,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_PRO_INDOOR_V3_3);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@@ -122,7 +124,6 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@@ -148,12 +149,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@@ -351,7 +349,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failure");
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
@@ -375,7 +373,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@@ -390,7 +388,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@@ -469,14 +467,6 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@@ -534,7 +524,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@@ -580,24 +570,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
}

View File

@@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
server.send(200, "application/json", toSend);
}

View File

@@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
@@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@@ -192,12 +192,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

@@ -12,8 +12,10 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
@@ -53,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_PRO_INDOOR_V4_2);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@@ -123,7 +125,6 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@@ -175,12 +176,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@@ -249,7 +247,7 @@ void loop() {
configUpdateHandle();
localServer._handle();
if (configuration.hasSensorSGP) {
ag.sgp41.handle();
}
@@ -374,7 +372,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failure");
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
@@ -398,7 +396,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@@ -413,7 +411,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@@ -509,14 +507,6 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@@ -574,7 +564,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@@ -621,24 +611,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
}

View File

@@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
server.send(200, "application/json", toSend);
}

View File

@@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
@@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@@ -191,12 +191,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

@@ -9,10 +9,16 @@ LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
LocalServer::~LocalServer() {}
bool LocalServer::begin(void) {
server.on("/", HTTP_GET, [this]() { _GET_root(); });
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
server.on("/dashboard", HTTP_GET, [this]() { _GET_dashboard(); });
server.on("/storage/download", HTTP_GET, [this]() { _GET_storage(); });
server.on("/storage/reset", HTTP_POST, [this]() { _POST_storage(); });
server.on("/timestamp", HTTP_POST, [this]() { _POST_time(); });
server.begin();
if (xTaskCreate(
@@ -38,6 +44,13 @@ String LocalServer::getHostname(void) {
void LocalServer::_handle(void) { server.handleClient(); }
void LocalServer::_GET_root(void) {
String body = "If you are not redirected automatically, go to <a "
"href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(302, "text/html", htmlResponse(body, true));
}
void LocalServer::_GET_config(void) {
if(ag->isOne()) {
server.send(200, "application/json", config.toString());
@@ -64,8 +77,178 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
server.send(200, "application/json", toSend);
}
void LocalServer::_GET_dashboard(void) {
String timestamp = ag->getCurrentTime();
server.send(200, "text/html", htmlDashboard(timestamp));
}
void LocalServer::_GET_storage(void) {
char *data = measure.getLocalStorage();
if (data != nullptr) {
String filename =
"measurements-" + ag->deviceId().substring(8) + ".csv"; // measurements-fdsa.csv
server.sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
server.send_P(200, "text/plain", data);
free(data);
} else {
server.send(204, "text/plain", "No data");
}
}
void LocalServer::_POST_storage(void) {
String body;
int statusCode = 200;
if (measure.resetLocalStorage()) {
body = "Success reset storage";
} else {
body = "Failed reset local storage, unknown error";
statusCode = 500;
}
body += ". Go to <a href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(statusCode, "text/html", htmlResponse(body, false));
}
void LocalServer::_POST_time(void) {
String epochTime = server.arg(0);
Serial.printf("Received epoch: %s \n", epochTime.c_str());
if (epochTime.isEmpty()) {
server.send(400, "text/plain", "Time query not provided");
return;
}
long _epochTime = epochTime.toInt();
if (_epochTime == 0) {
server.send(400, "text/plain", "Time format is not in epoch time");
return;
}
ag->setCurrentTime(_epochTime);
String body = "Success set new time. Go to <a href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(200, "text/html", htmlResponse(body, false));
}
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
String LocalServer::htmlDashboard(String timestamp) {
String page = "";
page += "<!DOCTYPE html>";
page += "<html lang=\"en\">";
page += "<head>";
page += " <meta charset=\"UTF-8\">";
page += " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
page += " <title>AirGradient Local Storage Mode</title>";
page += " <style>";
page += " body {";
page += " font-family: Arial, sans-serif;";
page += " display: flex;";
page += " flex-direction: column;";
page += " align-items: center;";
page += " margin-top: 50px;";
page += " }";
page += "";
page += " button {";
page += " display: block;";
page += " margin: 10px 0;";
page += " padding: 10px 20px;";
page += " font-size: 16px;";
page += " cursor: pointer;";
page += " }";
page += " .datetime-container {";
page += " display: flex;";
page += " align-items: center;";
page += " margin: 10px 0;";
page += " }";
page += " .datetime-container input[type=\"datetime-local\"] {";
page += " margin-left: 10px;";
page += " padding: 5px;";
page += " font-size: 16px;";
page += " }";
page += " button.reset-button {";
page += " background-color: red;";
page += " color: white;";
page += " border: none;";
page += " padding: 10px 20px;";
page += " font-size: 16px;";
page += " cursor: pointer;";
page += " }";
page += " .spacer {";
page += " height: 50px;";
page += " }";
page += " </style>";
page += "</head>";
page += "<body>";
page += " <h2>";
page += " Device Time: ";
page += timestamp;
page += " </h2>";
page += " <h2>";
page += " Serial Number: ";
page += ag->deviceId();
page += " </h2>";
page += " <form action=\"/storage/download\" method=\"GET\">";
page += " <button type=\"submit\">Download Measurements</button>";
page += " </form>";
page += " <form id=\"timestampForm\" method=\"POST\" action=\"/timestamp\">";
page += " <input type=\"datetime-local\" id=\"timestampInput\" required>";
page += " <button type=\"submit\">Set Timestamp</button>";
page += " <input type=\"hidden\" name=\"timestamp\" id=\"epochInput\">";
page += " </form>";
page += " <div class=\"spacer\"></div>";
page += " <form action=\"/storage/reset\" method=\"POST\"";
page += " onsubmit=\"return confirm('Are you sure you want to reset the measurements? "
"This action will permanently delete the existing measurement files!');\">";
page += " <button class=\"reset-button\" type=\"submit\">Reset Measurements</button>";
page += " </form>";
page += "</body>";
page += "<script>";
page += " document.querySelector('#timestampForm').onsubmit = function (event) {";
page += " const datetimeInput = document.querySelector('#timestampInput').value;";
page += " const localDate = new Date(datetimeInput);";
page += " const epochTimeUTC = Math.floor(Date.UTC(";
page += " localDate.getFullYear(),";
page += " localDate.getMonth(),";
page += " localDate.getDate(),";
page += " localDate.getHours(),";
page += " localDate.getMinutes()";
page += " ) / 1000);";
page += " document.querySelector('#epochInput').value = epochTimeUTC;";
page += " return true;";
page += " };";
page += "</script>";
page += "</html>";
return page;
}
String LocalServer::htmlResponse(String body, bool redirect) {
String page = "";
page += "<!DOCTYPE HTML>";
page += "<html lang=\"en-US\">";
page += " <head>";
page += "<style>";
page += "p { font-size: 22px; }";
page += "</style>";
page += " <meta charset=\"UTF-8\">";
if (redirect) {
page += " <meta http-equiv=\"refresh\" content=\"0;url=/dashboard\">";
}
page += " <title>Page Redirection</title>";
page += " </head>";
page += " <body>";
page += " <p>";
page += body;
page += " </p>";
page += " </body>";
page += "</html>";
return page;
}

View File

@@ -19,6 +19,9 @@ private:
WebServer server;
AgFirmwareMode fwMode;
String htmlDashboard(String timestamp);
String htmlResponse(String body, bool redirect);
public:
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
Configuration &config, WifiConnector& wifiConnector);
@@ -29,10 +32,15 @@ public:
String getHostname(void);
void setFwMode(AgFirmwareMode fwMode);
void _handle(void);
void _GET_root(void);
void _GET_config(void);
void _PUT_config(void);
void _GET_metrics(void);
void _GET_measure(void);
void _GET_dashboard(void);
void _GET_storage(void);
void _POST_storage(void);
void _POST_time(void);
};
#endif /** _LOCAL_SERVER_H_ */

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,13 @@
#include "OpenMetrics.h"
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector)
: measure(measure), config(config), wifiConnector(wifiConnector) {}
WifiConnector &wifiConnector, AgApiClient &apiClient)
: measure(measure), config(config), wifiConnector(wifiConnector),
apiClient(apiClient) {}
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) {
this->ag = ag;
}
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
this->agClient = client;
}
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
const char *OpenMetrics::getApiContentType(void) {
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
@@ -48,13 +43,13 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", agClient->isLastFetchConfigSucceed() ? "1" : "0");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
"1 if the AirGradient device was able to successfully send to the server",
"gauge");
add_metric_point("", agClient->isLastPostMeasureSucceed() ? "1" : "0");
add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1");
add_metric(
"wifi_rssi",
@@ -71,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@@ -86,8 +81,8 @@ String OpenMetrics::getPayload(void) {
measure.getFloat(Measurements::Humidity, 2)) /
2.0f;
pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f;
float correctedPm25_1 = measure.getCorrectedPM25(false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(false, 2);
float correctedPm25_1 = measure.getCorrectedPM25(*ag, config, false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(*ag, config, false, 2);
float correctedPm25 = (correctedPm25_1 + correctedPm25_2) / 2.0f;
pm25 = round(correctedPm25);
pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f;
@@ -102,7 +97,7 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@@ -112,7 +107,7 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature, 1);
_hum = measure.getFloat(Measurements::Humidity, 1);
pm01 = measure.get(Measurements::PM01, 1);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 1);
pm03PCount = measure.get(Measurements::PM03_PC, 1);
@@ -121,7 +116,7 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature, 2);
_hum = measure.getFloat(Measurements::Humidity, 2);
pm01 = measure.get(Measurements::PM01, 2);
float correctedPm = measure.getCorrectedPM25(false, 2);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 2);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 2);
pm03PCount = measure.get(Measurements::PM03_PC, 2);
@@ -142,15 +137,11 @@ String OpenMetrics::getPayload(void) {
/** Get temperature and humidity compensated */
if (ag->isOne()) {
atmpCompensated = round(measure.getCorrectedTempHum(Measurements::Temperature));
rhumCompensated = round(measure.getCorrectedTempHum(Measurements::Humidity));
atmpCompensated = _temp;
ahumCompensated = _hum;
} else {
atmpCompensated = round((measure.getCorrectedTempHum(Measurements::Temperature, 1) +
measure.getCorrectedTempHum(Measurements::Temperature, 2)) /
2.0f);
rhumCompensated = round((measure.getCorrectedTempHum(Measurements::Humidity, 1) +
measure.getCorrectedTempHum(Measurements::Humidity, 2)) /
2.0f);
atmpCompensated = ag->pms5003t_1.compensateTemp(_temp);
ahumCompensated = ag->pms5003t_1.compensateHum(_hum);
}
// Add measurements that valid to the metrics
@@ -202,14 +193,14 @@ String OpenMetrics::getPayload(void) {
}
if (utils::isValidNOx(nox)) {
add_metric("nox_index",
"The processed Nitrogen Oxide (NOx) index as measured by the "
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(nox));
}
if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrogen Oxide (NOx) index as "
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(noxRaw));
@@ -243,11 +234,11 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

@@ -5,22 +5,21 @@
#include "AgValue.h"
#include "AgWiFiConnector.h"
#include "AirGradient.h"
#include "Libraries/airgradient-client/src/airgradientClient.h"
#include "AgApiClient.h"
class OpenMetrics {
private:
AirGradient *ag;
AirgradientClient *agClient;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
AgApiClient &apiClient;
public:
OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector);
OpenMetrics(Measurements &measure, Configuration &conig,
WifiConnector &wifiConnector, AgApiClient& apiClient);
~OpenMetrics();
void setAirGradient(AirGradient *ag);
void setAirgradientClient(AirgradientClient *client);
const char *getApiContentType(void);
const char* getApi(void);
String getPayload(void);

View File

@@ -0,0 +1,206 @@
#ifndef _OTA_HANDLER_H_
#define _OTA_HANDLER_H_
#include <Arduino.h>
#include <esp_err.h>
#include <esp_http_client.h>
#include <esp_ota_ops.h>
#define OTA_BUF_SIZE 1024
#define URL_BUF_SIZE 256
enum OtaUpdateOutcome {
UPDATE_PERFORMED,
ALREADY_UP_TO_DATE,
UPDATE_FAILED,
UDPATE_SKIPPED
};
enum OtaState {
OTA_STATE_BEGIN,
OTA_STATE_FAIL,
OTA_STATE_SKIP,
OTA_STATE_UP_TO_DATE,
OTA_STATE_PROCESSING,
OTA_STATE_SUCCESS
};
typedef void(*OtaHandlerCallback_t)(OtaState state,
String message);
class OtaHandler {
public:
void updateFirmwareIfOutdated(String deviceId) {
String url = "http://hw.airgradient.com/sensors/airgradient:" + deviceId +
"/generic/os/firmware.bin";
url += "?current_firmware=";
url += GIT_VERSION;
char urlAsChar[URL_BUF_SIZE];
url.toCharArray(urlAsChar, URL_BUF_SIZE);
Serial.printf("checking for new OTA update @ %s\n", urlAsChar);
esp_http_client_config_t config = {};
config.url = urlAsChar;
OtaUpdateOutcome ret = attemptToPerformOta(&config);
Serial.println(ret);
if (this->callback) {
switch (ret) {
case OtaUpdateOutcome::UPDATE_PERFORMED:
this->callback(OtaState::OTA_STATE_SUCCESS, "");
break;
case OtaUpdateOutcome::UDPATE_SKIPPED:
this->callback(OtaState::OTA_STATE_SKIP, "");
break;
case OtaUpdateOutcome::ALREADY_UP_TO_DATE:
this->callback(OtaState::OTA_STATE_UP_TO_DATE, "");
break;
case OtaUpdateOutcome::UPDATE_FAILED:
this->callback(OtaState::OTA_STATE_FAIL, "");
break;
default:
break;
}
}
}
void setHandlerCallback(OtaHandlerCallback_t callback) {
this->callback = callback;
}
private:
OtaHandlerCallback_t callback;
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config) {
esp_http_client_handle_t client = esp_http_client_init(config);
if (client == NULL) {
Serial.println("Failed to initialize HTTP connection");
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
esp_http_client_cleanup(client);
Serial.printf("Failed to open HTTP connection: %s\n",
esp_err_to_name(err));
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_http_client_fetch_headers(client);
int httpStatusCode = esp_http_client_get_status_code(client);
if (httpStatusCode == 304) {
Serial.println("Firmware is already up to date");
cleanupHttp(client);
return OtaUpdateOutcome::ALREADY_UP_TO_DATE;
} else if (httpStatusCode != 200) {
Serial.printf("Firmware update skipped, the server returned %d\n",
httpStatusCode);
cleanupHttp(client);
return OtaUpdateOutcome::UDPATE_SKIPPED;
}
esp_ota_handle_t update_handle = 0;
const esp_partition_t *update_partition = NULL;
Serial.println("Starting OTA update ...");
update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
Serial.println("Passive OTA partition not found");
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_FAILED;
}
Serial.printf("Writing to partition subtype %d at offset 0x%x\n",
update_partition->subtype, update_partition->address);
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
if (err != ESP_OK) {
Serial.printf("esp_ota_begin failed, error=%d\n", err);
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_err_t ota_write_err = ESP_OK;
char *upgrade_data_buf = (char *)malloc(OTA_BUF_SIZE);
if (!upgrade_data_buf) {
Serial.println("Couldn't allocate memory for data buffer");
return OtaUpdateOutcome::UPDATE_FAILED;
}
int binary_file_len = 0;
int totalSize = esp_http_client_get_content_length(client);
Serial.println("File size: " + String(totalSize) + String(" bytes"));
// Show display start update new firmware.
if (this->callback) {
this->callback(OtaState::OTA_STATE_BEGIN, "");
}
// Download file and write new firmware to OTA partition
uint32_t lastUpdate = millis();
while (1) {
int data_read =
esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
if (data_read == 0) {
if (this->callback) {
this->callback(OtaState::OTA_STATE_PROCESSING, String(100));
}
Serial.println("Connection closed, all data received");
break;
}
if (data_read < 0) {
Serial.println("Data read error");
if (this->callback) {
this->callback(OtaState::OTA_STATE_FAIL, "");
}
break;
}
if (data_read > 0) {
ota_write_err = esp_ota_write(
update_handle, (const void *)upgrade_data_buf, data_read);
if (ota_write_err != ESP_OK) {
if (this->callback) {
this->callback(OtaState::OTA_STATE_FAIL, "");
}
break;
}
binary_file_len += data_read;
int percent = (binary_file_len * 100) / totalSize;
uint32_t ms = (uint32_t)(millis() - lastUpdate);
if (ms >= 250) {
// sm.executeOTA(StateMachine::OtaState::OTA_STATE_PROCESSING, "",
// percent);
if (this->callback) {
this->callback(OtaState::OTA_STATE_PROCESSING,
String(percent));
}
lastUpdate = millis();
}
}
}
free(upgrade_data_buf);
cleanupHttp(client);
Serial.printf("# of bytes written: %d\n", binary_file_len);
esp_err_t ota_end_err = esp_ota_end(update_handle);
if (ota_write_err != ESP_OK) {
Serial.printf("Error: esp_ota_write failed! err=0x%d\n", err);
return OtaUpdateOutcome::UPDATE_FAILED;
} else if (ota_end_err != ESP_OK) {
Serial.printf("Error: esp_ota_end failed! err=0x%d. Image is invalid",
ota_end_err);
return OtaUpdateOutcome::UPDATE_FAILED;
}
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
Serial.printf("esp_ota_set_boot_partition failed! err=0x%d\n", err);
return OtaUpdateOutcome::UPDATE_FAILED;
}
return OtaUpdateOutcome::UPDATE_PERFORMED;
}
void cleanupHttp(esp_http_client_handle_t client) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
};
#endif

View File

@@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor
version=3.6.0
version=3.1.13
author=AirGradient <support@airgradient.com>
maintainer=AirGradient <support@airgradient.com>
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.

View File

@@ -12,10 +12,10 @@
platform = espressif32
board = esp32-c3-devkitm-1
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)'\\"'
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -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,20 +26,19 @@ 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
ESP8266WebServer
DNSServer
;
monitor_filters = time
[platformio]

View File

@@ -34,6 +34,17 @@ void AgApiClient::begin(void) {
* @return false Failure
*/
bool AgApiClient::fetchServerConfiguration(void) {
if (config.getConfigurationControl() ==
ConfigurationControl::ConfigurationControlLocal ||
config.isOfflineMode()) {
logWarning("Ignore fetch server configuration");
// Clear server configuration failed flag, cause it's ignore but not
// really failed
getConfigFailed = false;
return false;
}
String uri = apiRoot + "/sensors/airgradient:" +
ag->deviceId() + "/one/config";
@@ -47,22 +58,10 @@ bool AgApiClient::fetchServerConfiguration(void) {
}
#else
HTTPClient client;
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
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (GET)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (GET)");
getConfigFailed = true;
return false;
}
client.setTimeout(timeoutMs);
if (client.begin(uri) == false) {
getConfigFailed = true;
return false;
}
#endif
@@ -91,6 +90,8 @@ bool AgApiClient::fetchServerConfiguration(void) {
String respContent = client.getString();
client.end();
// logInfo("Get configuration: " + respContent);
/** Parse configuration and return result */
return config.parse(respContent, false);
}
@@ -104,39 +105,32 @@ bool AgApiClient::fetchServerConfiguration(void) {
* @return false Failure
*/
bool AgApiClient::postToServer(String data) {
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
#ifdef ESP8266
HTTPClient client;
WiFiClient wifiClient;
if (client.begin(wifiClient, uri) == false) {
getConfigFailed = true;
if (config.isPostDataToAirGradient() == false) {
logWarning("Ignore post data to server");
return true;
}
if (WiFi.isConnected() == false) {
return false;
}
#else
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
// logInfo("Post uri: " + uri);
// logInfo("Post data: " + data);
WiFiClient wifiClient;
HTTPClient client;
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
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (POST)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (POST)");
getConfigFailed = true;
return false;
}
client.setTimeout(timeoutMs);
if (client.begin(wifiClient, uri.c_str()) == false) {
logError("Init client failed");
return false;
}
#endif
client.addHeader("content-type", "application/json");
int retCode = client.POST(data);
client.end();
logInfo(String("POST: ") + uri);
// logInfo(String("DATA: ") + data);
logInfo(String("Return code: ") + String(retCode));
if ((retCode == 200) || (retCode == 429)) {
@@ -155,12 +149,7 @@ bool AgApiClient::postToServer(String data) {
* @return true Success
* @return false Failure
*/
bool AgApiClient::isFetchConfigurationFailed(void) { return getConfigFailed; }
/**
* @brief Reset status of get configuration from AirGradient cloud
*/
void AgApiClient::resetFetchConfigurationStatus(void) { getConfigFailed = false; }
bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
/**
* @brief Get failed status when post data to AirGradient cloud
@@ -185,13 +174,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;
@@ -200,10 +189,7 @@ bool AgApiClient::sendPing(int rssi, int bootCount) {
String AgApiClient::getApiRoot() const { return apiRoot; }
void AgApiClient::setApiRoot(const String &apiRoot) {
this->apiRootChanged = true;
this->apiRoot = apiRoot;
}
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; }
/**
* @brief Set http request timeout. (Default: 10s)

View File

@@ -20,18 +20,12 @@ class AgApiClient : public PrintLog {
private:
Configuration &config;
AirGradient *ag;
#ifdef ESP8266
// ESP8266 not support HTTPS
String apiRoot = "http://hw.airgradient.com";
#else
String apiRoot = "https://hw.airgradient.com";
#endif
bool apiRootChanged = false; // Indicate if setApiRoot() is called
bool getConfigFailed;
bool postToServerFailed;
bool notAvailableOnDashboard = false; // Device not setup on Airgradient cloud dashboard.
uint16_t timeoutMs = 15000; // Default set to 15s
uint16_t timeoutMs = 10000; // Default set to 10s
public:
AgApiClient(Stream &stream, Configuration &config);
@@ -40,8 +34,7 @@ public:
void begin(void);
bool fetchServerConfiguration(void);
bool postToServer(String data);
bool isFetchConfigurationFailed(void);
void resetFetchConfigurationStatus(void);
bool isFetchConfigureFailed(void);
bool isPostToServerFailed(void);
bool isNotAvailableOnDashboard(void);
void setAirGradient(AirGradient *ag);

View File

@@ -22,17 +22,15 @@ const char *LED_BAR_MODE_NAMES[] = {
};
const char *PM_CORRECTION_ALGORITHM_NAMES[] = {
[COR_ALGO_PM_UNKNOWN] = "-", // This is only to pass "non-trivial designated initializers" error
[COR_ALGO_PM_NONE] = "none",
[COR_ALGO_PM_EPA_2021] = "epa_2021",
[COR_ALGO_PM_SLR_CUSTOM] = "custom",
};
const char *TEMP_HUM_CORRECTION_ALGORITHM_NAMES[] = {
[COR_ALGO_TEMP_HUM_UNKNOWN] = "-", // This is only to pass "non-trivial designated initializers" error
[COR_ALGO_TEMP_HUM_NONE] = "none",
[COR_ALGO_TEMP_HUM_AG_PMS5003T_2024] = "ag_pms5003t_2024",
[COR_ALGO_TEMP_HUM_SLR_CUSTOM] = "custom",
[Unknown] = "-", // This is only to pass "non-trivial designated initializers" error
[None] = "none",
[EPA_2021] = "epa_2021",
[SLR_PMS5003_20220802] = "slr_PMS5003_20220802",
[SLR_PMS5003_20220803] = "slr_PMS5003_20220803",
[SLR_PMS5003_20220824] = "slr_PMS5003_20220824",
[SLR_PMS5003_20231030] = "slr_PMS5003_20231030",
[SLR_PMS5003_20231218] = "slr_PMS5003_20231218",
[SLR_PMS5003_20240104] = "slr_PMS5003_20240104",
};
#define JSON_PROP_NAME(name) jprop_##name
@@ -46,11 +44,9 @@ JSON_PROP_DEF(abcDays);
JSON_PROP_DEF(tvocLearningOffset);
JSON_PROP_DEF(noxLearningOffset);
JSON_PROP_DEF(mqttBrokerUrl);
JSON_PROP_DEF(httpDomain);
JSON_PROP_DEF(temperatureUnit);
JSON_PROP_DEF(configurationControl);
JSON_PROP_DEF(postDataToAirGradient);
JSON_PROP_DEF(disableCloudConnection);
JSON_PROP_DEF(ledBarBrightness);
JSON_PROP_DEF(displayBrightness);
JSON_PROP_DEF(co2CalibrationRequested);
@@ -58,9 +54,6 @@ JSON_PROP_DEF(ledBarTestRequested);
JSON_PROP_DEF(offlineMode);
JSON_PROP_DEF(monitorDisplayCompensatedValues);
JSON_PROP_DEF(corrections);
JSON_PROP_DEF(atmp);
JSON_PROP_DEF(rhum);
JSON_PROP_DEF(extendedPmMeasures);
#define jprop_model_default ""
#define jprop_country_default "TH"
@@ -70,16 +63,13 @@ JSON_PROP_DEF(extendedPmMeasures);
#define jprop_tvocLearningOffset_default 12
#define jprop_noxLearningOffset_default 12
#define jprop_mqttBrokerUrl_default ""
#define jprop_httpDomain_default ""
#define jprop_temperatureUnit_default "c"
#define jprop_configurationControl_default String(CONFIGURATION_CONTROL_NAME[ConfigurationControl::ConfigurationControlBoth])
#define jprop_postDataToAirGradient_default true
#define jprop_disableCloudConnection_default false
#define jprop_ledBarBrightness_default 100
#define jprop_displayBrightness_default 100
#define jprop_offlineMode_default false
#define jprop_monitorDisplayCompensatedValues_default false
#define jprop_extendedPmMeasures_default false
JSONVar jconfig;
@@ -114,9 +104,9 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
// If the input string matches an algorithm name, return the corresponding enum value
// Else return Unknown
const size_t enumSize = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;;
const size_t enumSize = SLR_PMS5003_20240104 + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = PMCorrectionAlgorithm::Unknown;
// Loop through enum values
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
@@ -124,63 +114,42 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
}
}
// 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") {
// If it is, then its a custom correction
result = COR_ALGO_PM_SLR_CUSTOM;
}
}
return result;
}
TempHumCorrectionAlgorithm Configuration::matchTempHumAlgorithm(String algorithm) {
// Get the actual size of the enum
const int enumSize = static_cast<int>(COR_ALGO_TEMP_HUM_SLR_CUSTOM);
TempHumCorrectionAlgorithm result = COR_ALGO_TEMP_HUM_UNKNOWN;
// Loop through enum values
for (size_t enumVal = 0; enumVal <= enumSize; enumVal++) {
if (algorithm == TEMP_HUM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
result = static_cast<TempHumCorrectionAlgorithm>(enumVal);
}
}
return result;
}
bool Configuration::updatePmCorrection(JSONVar &json) {
if (!json.hasOwnProperty("corrections")) {
logInfo("corrections not found");
// TODO: need to response message?
Serial.println("corrections not found");
return false;
}
JSONVar corrections = json["corrections"];
if (!corrections.hasOwnProperty("pm02")) {
logWarning("pm02 not found");
Serial.println("pm02 not found");
return false;
}
JSONVar pm02 = corrections["pm02"];
if (!pm02.hasOwnProperty("correctionAlgorithm")) {
logWarning("pm02 correctionAlgorithm not found");
Serial.println("correctionAlgorithm not found");
return false;
}
// TODO: Need to have data type check, with error message response if invalid
// Check algorithm
String algorithm = pm02["correctionAlgorithm"];
PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm);
if (algo == COR_ALGO_PM_UNKNOWN) {
logWarning("Unknown algorithm");
if (algo == Unknown) {
logInfo("Unknown algorithm");
return false;
}
logInfo("Correction algorithm: " + algorithm);
// If algo is None or EPA_2021, no need to check slr
// But first check if pmCorrection different from algo
if (algo == COR_ALGO_PM_NONE || algo == COR_ALGO_PM_EPA_2021) {
if (algo == None || algo == EPA_2021) {
if (pmCorrection.algorithm != algo) {
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections]["pm02"]["correctionAlgorithm"] = algorithm;
@@ -197,7 +166,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Check if pm02 has slr object
if (!pm02.hasOwnProperty("slr")) {
logWarning("slr not found");
Serial.println("slr not found");
return false;
}
@@ -206,7 +175,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor") ||
!slr.hasOwnProperty("useEpa2021")) {
logWarning("Missing required slr properties");
Serial.println("Missing required slr properties");
return false;
}
@@ -236,87 +205,6 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
return true;
}
bool Configuration::updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
const char *correctionName) {
if (!json.hasOwnProperty(jprop_corrections)) {
return false;
}
JSONVar corrections = json[jprop_corrections];
if (!corrections.hasOwnProperty(correctionName)) {
logInfo(String(correctionName) + " correction field not found on configuration");
return false;
}
JSONVar correctionTarget = corrections[correctionName];
if (!correctionTarget.hasOwnProperty("correctionAlgorithm")) {
Serial.println("correctionAlgorithm not found");
return false;
}
String algorithm = correctionTarget["correctionAlgorithm"];
TempHumCorrectionAlgorithm algo = matchTempHumAlgorithm(algorithm);
if (algo == COR_ALGO_TEMP_HUM_UNKNOWN) {
logInfo("Uknown temp/hum algorithm");
return false;
}
logInfo(String(correctionName) + " correction algorithm: " + algorithm);
// If algo is None or Standard, then no need to check slr
// But first check if target correction different from algo
if (algo == COR_ALGO_TEMP_HUM_NONE || algo == COR_ALGO_TEMP_HUM_AG_PMS5003T_2024) {
if (target.algorithm != algo) {
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections][correctionName]["correctionAlgorithm"] = algorithm;
jconfig[jprop_corrections][correctionName]["slr"] = JSON.parse("{}"); // Clear slr
// Update pmCorrection with new values
target.algorithm = algo;
target.changed = true;
logInfo(String(correctionName) + " correction updated");
return true;
}
return false;
}
// Check if correction.target (atmp or rhum) has slr object
if (!correctionTarget.hasOwnProperty("slr")) {
logWarning(String(correctionName) + " slr not found");
return false;
}
JSONVar slr = correctionTarget["slr"];
// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor")) {
Serial.println("Missing required slr properties");
return false;
}
// arduino_json doesn't support float type, need to cast to double first
float intercept = (float)((double)slr["intercept"]);
float scalingFactor = (float)((double)slr["scalingFactor"]);
// Compare with current target correciont
if (target.algorithm == algo && target.intercept == intercept &&
target.scalingFactor == scalingFactor) {
return false; // No changes needed
}
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections] = corrections;
// Update target with new values
target.algorithm = algo;
target.intercept = intercept;
target.scalingFactor = scalingFactor;
target.changed = true;
// Correction values were updated
logInfo(String(correctionName) + " correction updated");
return true;
}
/**
* @brief Save configure to device storage (EEPROM)
*
@@ -365,7 +253,7 @@ void Configuration::loadConfig(void) {
}
file.close();
} else {
SPIFFS.format();
// SPIFFS.format();
}
#endif
toConfig(buf);
@@ -381,11 +269,9 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_country] = jprop_country_default;
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
jconfig[jprop_httpDomain] = jprop_httpDomain_default;
jconfig[jprop_configurationControl] = jprop_configurationControl_default;
jconfig[jprop_pmStandard] = jprop_pmStandard_default;
jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default;
jconfig[jprop_disableCloudConnection] = jprop_disableCloudConnection_default;
jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default;
if (ag->isOne()) {
jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default;
@@ -402,10 +288,9 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_model] = jprop_model_default;
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default;
jconfig[jprop_extendedPmMeasures] = jprop_extendedPmMeasures_default;
// PM2.5 default correction
pmCorrection.algorithm = COR_ALGO_PM_NONE;
// PM2.5 correction
pmCorrection.algorithm = None;
pmCorrection.changed = false;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 1;
@@ -459,10 +344,6 @@ bool Configuration::begin(void) {
return true;
}
void Configuration::setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback) {
_callback = callback;
}
/**
* @brief Parse JSON configura string to local configure
*
@@ -745,17 +626,11 @@ bool Configuration::parse(String data, bool isLocal) {
jconfig[jprop_mqttBrokerUrl] = broker;
}
} else {
failedMessage = "\"mqttBrokerUrl\" length should less than 255 character";
failedMessage = "\"mqttBrokerUrl\" length should <= 255";
jsonInvalid();
return false;
}
}
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)
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
}
else {
} else {
if (jsonTypeInvalid(root[jprop_mqttBrokerUrl], "string")) {
failedMessage =
jsonTypeInvalidMessage(String(jprop_mqttBrokerUrl), "string");
@@ -764,32 +639,6 @@ bool Configuration::parse(String data, bool isLocal) {
}
}
if (isLocal) {
if (JSON.typeof_(root[jprop_httpDomain]) == "string") {
String httpDomain = root[jprop_httpDomain];
String oldHttpDomain = jconfig[jprop_httpDomain];
if (httpDomain.length() <= 255) {
if (httpDomain != oldHttpDomain) {
changed = true;
configLogInfo(String(jprop_httpDomain), oldHttpDomain, httpDomain);
jconfig[jprop_httpDomain] = httpDomain;
}
} else {
failedMessage = "\"httpDomain\" length should less than 255 character";
jsonInvalid();
return false;
}
}
else {
if (jsonTypeInvalid(root[jprop_httpDomain], "string")) {
failedMessage =
jsonTypeInvalidMessage(String(jprop_httpDomain), "string");
jsonInvalid();
return false;
}
}
}
if (JSON.typeof_(root[jprop_temperatureUnit]) == "string") {
String unit = root[jprop_temperatureUnit];
String oldUnit = jconfig[jprop_temperatureUnit];
@@ -943,59 +792,20 @@ bool Configuration::parse(String data, bool isLocal) {
}
}
if (JSON.typeof_(root[jprop_extendedPmMeasures]) == "boolean") {
bool value = root[jprop_extendedPmMeasures];
bool oldValue = jconfig[jprop_extendedPmMeasures];
if (value != oldValue) {
changed = true;
configLogInfo(String(jprop_extendedPmMeasures),
String(oldValue ? "true" : "false"),
String(value ? "true" : "false"));
jconfig[jprop_extendedPmMeasures] = value;
}
}
else if (JSON.typeof_(root[jprop_extendedPmMeasures]) == "null" and !isLocal) {
// So if its not available on the json and json comes from aigradient server
// then set its value to default (false)
jconfig[jprop_extendedPmMeasures] = jprop_extendedPmMeasures_default;
}
else {
if (jsonTypeInvalid(root[jprop_extendedPmMeasures], "boolean")) {
failedMessage = jsonTypeInvalidMessage(
String(jprop_extendedPmMeasures), "boolean");
jsonInvalid();
return false;
}
}
// PM2.5 Corrections
// Corrections
if (updatePmCorrection(root)) {
changed = true;
}
// Temperature correction
if (updateTempHumCorrection(root, tempCorrection, jprop_atmp)) {
changed = true;
}
// Relative humidity correction
if (updateTempHumCorrection(root, rhumCorrection, jprop_rhum)) {
changed = true;
}
if (ledBarTestRequested || co2CalibrationRequested) {
commandRequested = true;
updated = true;
}
if (changed) {
updated = true;
saveConfig();
printConfig();
_callback();
} else {
if (ledBarTestRequested || co2CalibrationRequested) {
updated = true;
}
}
return true;
}
@@ -1031,11 +841,6 @@ bool Configuration::isTemperatureUnitInF(void) {
return (unit == "f");
}
bool Configuration::isExtendedPmMeasuresEnabled(void) {
return jconfig[jprop_extendedPmMeasures];
}
/**
* @brief Country name, it's short name ex: TH = Thailand
*
@@ -1106,16 +911,6 @@ String Configuration::getMqttBrokerUri(void) {
return broker;
}
/**
* @brief Get HTTP domain for post measures and get configuration
*
* @return String http domain, might be empty string
*/
String Configuration::getHttpDomain(void) {
String httpDomain = jconfig[jprop_httpDomain];
return httpDomain;
}
/**
* @brief Get configuratoin post data to AirGradient cloud
*
@@ -1200,14 +995,8 @@ bool Configuration::isUpdated(void) {
return updated;
}
bool Configuration::isCommandRequested(void) {
bool oldState = this->commandRequested;
this->commandRequested = false;
return oldState;
}
String Configuration::jsonTypeInvalidMessage(String name, String type) {
return "'" + name + "' type is invalid, expecting '" + type + "'";
return "'" + name + "' type invalid, it's should '" + type + "'";
}
String Configuration::jsonValueInvalidMessage(String name, String value) {
@@ -1247,20 +1036,20 @@ void Configuration::toConfig(const char *buf) {
}
bool changed = false;
bool isConfigFieldInvalid = false;
bool isInvalid = false;
/** Validate country */
if (JSON.typeof_(jconfig[jprop_country]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String country = jconfig[jprop_country];
if (country.length() != 2) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_country] = jprop_country_default;
changed = true;
logInfo("toConfig: country changed");
@@ -1268,17 +1057,17 @@ void Configuration::toConfig(const char *buf) {
/** validate: PM standard */
if (JSON.typeof_(jconfig[jprop_pmStandard]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String standard = jconfig[jprop_pmStandard];
if (standard != getPMStandardString(true) &&
standard != getPMStandardString(false)) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_pmStandard] = jprop_pmStandard_default;
changed = true;
logInfo("toConfig: pmStandard changed");
@@ -1286,18 +1075,18 @@ void Configuration::toConfig(const char *buf) {
/** validate led bar mode */
if (JSON.typeof_(jconfig[jprop_ledBarMode]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String mode = jconfig[jprop_ledBarMode];
if (mode != getLedBarModeName(LedBarMode::LedBarModeCO2) &&
mode != getLedBarModeName(LedBarMode::LedBarModeOff) &&
mode != getLedBarModeName(LedBarMode::LedBarModePm)) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_ledBarMode] = jprop_ledBarMode_default;
changed = true;
logInfo("toConfig: ledBarMode changed");
@@ -1305,11 +1094,11 @@ void Configuration::toConfig(const char *buf) {
/** validate abcday */
if (JSON.typeof_(jconfig[jprop_abcDays]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_abcDays] = jprop_abcDays_default;
changed = true;
logInfo("toConfig: abcDays changed");
@@ -1317,16 +1106,16 @@ void Configuration::toConfig(const char *buf) {
/** validate tvoc learning offset */
if (JSON.typeof_(jconfig[jprop_tvocLearningOffset]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_tvocLearningOffset];
if (value < 0) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_tvocLearningOffset] = jprop_tvocLearningOffset_default;
changed = true;
logInfo("toConfig: tvocLearningOffset changed");
@@ -1334,16 +1123,16 @@ void Configuration::toConfig(const char *buf) {
/** validate nox learning offset */
if (JSON.typeof_(jconfig[jprop_noxLearningOffset]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_noxLearningOffset];
if (value < 0) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_noxLearningOffset] = jprop_noxLearningOffset_default;
changed = true;
logInfo("toConfig: noxLearningOffset changed");
@@ -1351,72 +1140,36 @@ void Configuration::toConfig(const char *buf) {
/** validate mqtt broker */
if (JSON.typeof_(jconfig[jprop_mqttBrokerUrl]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
changed = true;
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
logInfo("toConfig: mqttBroker changed");
}
/** validate http domain */
if (JSON.typeof_(jconfig[jprop_httpDomain]) != "string") {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
}
if (isConfigFieldInvalid) {
changed = true;
jconfig[jprop_httpDomain] = jprop_httpDomain_default;
logInfo("toConfig: httpDomain changed");
}
/** Validate temperature unit */
if (JSON.typeof_(jconfig[jprop_temperatureUnit]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String unit = jconfig[jprop_temperatureUnit];
if (unit != "c" && unit != "f") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default;
changed = true;
logInfo("toConfig: temperatureUnit changed");
}
/** validate disableCloudConnection configuration */
if (JSON.typeof_(jconfig[jprop_disableCloudConnection]) != "boolean") {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
}
if (isConfigFieldInvalid) {
jconfig[jprop_disableCloudConnection] = jprop_disableCloudConnection_default;
changed = true;
logInfo("toConfig: disableCloudConnection changed");
}
/** validate extendedPmMeasures configuration */
if (JSON.typeof_(jconfig[jprop_extendedPmMeasures]) != "boolean") {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
}
if (isConfigFieldInvalid) {
jconfig[jprop_extendedPmMeasures] = jprop_extendedPmMeasures_default;
changed = true;
logInfo("toConfig: extendedPmMeasures changed");
}
/** validate configuration control */
if (JSON.typeof_(jprop_configurationControl) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String ctrl = jconfig[jprop_configurationControl];
if (ctrl != String(CONFIGURATION_CONTROL_NAME
@@ -1425,12 +1178,12 @@ void Configuration::toConfig(const char *buf) {
[ConfigurationControl::ConfigurationControlLocal]) &&
ctrl != String(CONFIGURATION_CONTROL_NAME
[ConfigurationControl::ConfigurationControlCloud])) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_configurationControl] =jprop_configurationControl_default;
changed = true;
logInfo("toConfig: configurationControl changed");
@@ -1438,11 +1191,11 @@ void Configuration::toConfig(const char *buf) {
/** Validate post to airgradient cloud */
if (JSON.typeof_(jconfig[jprop_postDataToAirGradient]) != "boolean") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default;
changed = true;
logInfo("toConfig: postToAirGradient changed");
@@ -1450,16 +1203,16 @@ void Configuration::toConfig(const char *buf) {
/** validate led bar brightness */
if (JSON.typeof_(jconfig[jprop_ledBarBrightness]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_ledBarBrightness];
if (value < 0 || value > 100) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default;
changed = true;
logInfo("toConfig: ledBarBrightness changed");
@@ -1467,16 +1220,16 @@ void Configuration::toConfig(const char *buf) {
/** Validate display brightness */
if (JSON.typeof_(jconfig[jprop_displayBrightness]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_displayBrightness];
if (value < 0 || value > 100) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_displayBrightness] = jprop_displayBrightness_default;
changed = true;
logInfo("toConfig: displayBrightness changed");
@@ -1495,31 +1248,15 @@ void Configuration::toConfig(const char *buf) {
jprop_monitorDisplayCompensatedValues_default;
}
// PM2.5 correction
/// Set default first before parsing local config
pmCorrection.algorithm = COR_ALGO_PM_NONE;
// Set default first before parsing local config
pmCorrection.algorithm = PMCorrectionAlgorithm::None;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 0;
pmCorrection.useEPA = false;
/// Load correction from saved config
// Load correction from saved config
updatePmCorrection(jconfig);
// Temperature correction
/// Set default first before parsing local config
tempCorrection.algorithm = COR_ALGO_TEMP_HUM_NONE;
tempCorrection.intercept = 0;
tempCorrection.scalingFactor = 0;
/// Load correction from saved config
updateTempHumCorrection(jconfig, tempCorrection, jprop_atmp);
// Relative humidity correction
/// Set default first before parsing local config
rhumCorrection.algorithm = COR_ALGO_TEMP_HUM_NONE;
rhumCorrection.intercept = 0;
rhumCorrection.scalingFactor = 0;
/// Load correction from saved config
updateTempHumCorrection(jconfig, rhumCorrection, jprop_rhum);
if (changed) {
saveConfig();
}
@@ -1597,24 +1334,13 @@ void Configuration::setOfflineModeWithoutSave(bool offline) {
_offlineMode = offline;
}
bool Configuration::isCloudConnectionDisabled(void) {
bool disabled = jconfig[jprop_disableCloudConnection];
return disabled;
}
void Configuration::setDisableCloudConnection(bool disable) {
logInfo("Set DisableCloudConnection to " + String(disable ? "True" : "False"));
jconfig[jprop_disableCloudConnection] = 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];
}
@@ -1637,22 +1363,20 @@ 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) {
PMCorrection pmCorrection = getPMCorrection();
if (pmCorrection.algorithm == COR_ALGO_PM_NONE ||
pmCorrection.algorithm == COR_ALGO_PM_UNKNOWN) {
if (pmCorrection.algorithm == PMCorrectionAlgorithm::None ||
pmCorrection.algorithm == PMCorrectionAlgorithm::Unknown) {
return false;
}
return true;
}
Configuration::PMCorrection Configuration::getPMCorrection(void) { return pmCorrection; }
Configuration::TempHumCorrection Configuration::getTempCorrection(void) { return tempCorrection; }
Configuration::TempHumCorrection Configuration::getHumCorrection(void) { return rhumCorrection; }
Configuration::PMCorrection Configuration::getPMCorrection(void) {
return pmCorrection;
}

View File

@@ -17,18 +17,10 @@ public:
bool changed;
};
struct TempHumCorrection {
TempHumCorrectionAlgorithm algorithm;
float intercept;
float scalingFactor;
bool changed;
};
private:
bool co2CalibrationRequested;
bool ledBarTestRequested;
bool updated;
bool commandRequested = false;
String failedMessage;
bool _noxLearnOffsetChanged;
bool _tvocLearningOffsetChanged;
@@ -38,17 +30,12 @@ private:
bool _offlineMode = false;
bool _ledBarModeChanged = false;
PMCorrection pmCorrection;
TempHumCorrection tempCorrection;
TempHumCorrection rhumCorrection;
AirGradient* ag;
String getLedBarModeName(LedBarMode mode);
PMCorrectionAlgorithm matchPmAlgorithm(String algorithm);
TempHumCorrectionAlgorithm matchTempHumAlgorithm(String algorithm);
bool updatePmCorrection(JSONVar &json);
bool updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
const char *correctionName);
void saveConfig(void);
void loadConfig(void);
void defaultConfig(void);
@@ -59,7 +46,7 @@ private:
void configLogInfo(String name, String fromValue, String toValue);
String getPMStandardString(bool usaqi);
String getAbcDayString(int value);
void toConfig(const char *buf);
void toConfig(const char* buf);
public:
Configuration(Stream &debugLog);
@@ -71,15 +58,11 @@ public:
bool hasSensorSGP = true;
bool hasSensorSHT = true;
typedef void (*ConfigurationUpdatedCallback_t)();
void setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback);
bool begin(void);
bool parse(String data, bool isLocal);
String toString(void);
String toString(AgFirmwareMode fwMode);
bool isTemperatureUnitInF(void);
bool isExtendedPmMeasuresEnabled(void);
String getCountry(void);
bool isPmStandardInUSAQI(void);
int getCO2CalibrationAbcDays(void);
@@ -87,7 +70,6 @@ public:
String getLedBarModeName(void);
bool getDisplayMode(void);
String getMqttBrokerUri(void);
String getHttpDomain(void);
bool isPostDataToAirGradient(void);
ConfigurationControl getConfigurationControl(void);
bool isCo2CalibrationRequested(void);
@@ -95,7 +77,6 @@ public:
void reset(void);
String getModel(void);
bool isUpdated(void);
bool isCommandRequested(void);
String getFailedMesage(void);
void setPostToAirGradient(bool enable);
bool noxLearnOffsetChanged(void);
@@ -113,17 +94,11 @@ public:
bool isOfflineMode(void);
void setOfflineMode(bool offline);
void setOfflineModeWithoutSave(bool offline);
bool isCloudConnectionDisabled(void);
void setDisableCloudConnection(bool disable);
bool isLedBarModeChanged(void);
bool isMonitorDisplayCompensatedValues(void);
bool isPMCorrectionChanged(void);
bool isPMCorrectionEnabled(void);
PMCorrection getPMCorrection(void);
TempHumCorrection getTempCorrection(void);
TempHumCorrection getHumCorrection(void);
private:
ConfigurationUpdatedCallback_t _callback;
};
#endif /** _AG_CONFIG_H_ */

View File

@@ -5,30 +5,14 @@
/** Cast U8G2 */
#define DISP() ((U8G2_SH1106_128X64_NONAME_F_HW_I2C *)(this->u8g2))
static const unsigned char WIFI_ISSUE_BITS[] = {
0xd8, 0xc6, 0xde, 0xde, 0xc7, 0xf8, 0xd1, 0xe2, 0xdc, 0xce, 0xcc,
0xcc, 0xc0, 0xc0, 0xd0, 0xc2, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0};
static const unsigned char CLOUD_ISSUE_BITS[] = {
0x70, 0xc0, 0x88, 0xc0, 0x04, 0xc1, 0x04, 0xcf, 0x02, 0xd0, 0x01,
0xe0, 0x01, 0xe0, 0x01, 0xe0, 0xa2, 0xd0, 0x4c, 0xce, 0xa0, 0xc0};
// Offline mode icon
static unsigned char OFFLINE_BITS[] = {
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x62, 0x00,
0xE6, 0x00, 0xFE, 0x1F, 0xFE, 0x1F, 0xE6, 0x00, 0x62, 0x00,
0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
};
/**
* @brief Show dashboard temperature and humdity
*
* @param hasStatus
*/
void OledDisplay::showTempHum(bool hasStatus) {
char buf[10];
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
/** Temperature */
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
float temp = value.getAverage(Measurements::Temperature);
if (utils::isValidTemperature(temp)) {
float t = 0.0f;
if (config.isTemperatureUnitInF()) {
@@ -39,32 +23,32 @@ void OledDisplay::showTempHum(bool hasStatus) {
if (config.isTemperatureUnitInF()) {
if (hasStatus) {
snprintf(buf, sizeof(buf), "%0.1f", t);
snprintf(buf, buf_size, "%0.1f", t);
} else {
snprintf(buf, sizeof(buf), "%0.1f°F", t);
snprintf(buf, buf_size, "%0.1f°F", t);
}
} else {
if (hasStatus) {
snprintf(buf, sizeof(buf), "%.1f", t);
snprintf(buf, buf_size, "%.1f", t);
} else {
snprintf(buf, sizeof(buf), "%.1f°C", t);
snprintf(buf, buf_size, "%.1f°C", t);
}
}
} else { /** Show invalid value */
if (config.isTemperatureUnitInF()) {
snprintf(buf, sizeof(buf), "-°F");
snprintf(buf, buf_size, "-°F");
} else {
snprintf(buf, sizeof(buf), "-°C");
snprintf(buf, buf_size, "-°C");
}
}
DISP()->drawUTF8(1, 10, buf);
/** Show humidity */
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
int rhum = round(value.getAverage(Measurements::Humidity));
if (utils::isValidHumidity(rhum)) {
snprintf(buf, sizeof(buf), "%d%%", rhum);
snprintf(buf, buf_size, "%d%%", rhum);
} else {
snprintf(buf, sizeof(buf), "-%%");
snprintf(buf, buf_size, "-%%");
}
if (rhum > 99.0) {
@@ -83,9 +67,6 @@ void OledDisplay::setCentralText(int y, const char *text) {
DISP()->drawStr(x, y, text);
}
void OledDisplay::showIcon(int x, int y, xbm_icon_t *icon) {
DISP()->drawXBM(x, y, icon->width, icon->height, icon->icon);
}
/**
* @brief Construct a new Ag Oled Display:: Ag Oled Display object
*
@@ -267,95 +248,40 @@ 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
*
*/
void OledDisplay::showDashboard(void) { showDashboard(DashBoardStatusNone); }
void OledDisplay::showDashboard(void) { showDashboard(NULL); }
/**
* @brief Update dashboard content and error status
*
*/
void OledDisplay::showDashboard(DashboardStatus status) {
void OledDisplay::showDashboard(const char *status) {
if (isDisplayOff) {
return;
}
char strBuf[16];
const int icon_pos_x = 64;
xbm_icon_t xbm_icon = {
.width = 0,
.height = 0,
.icon = nullptr,
};
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
switch (status) {
case DashBoardStatusNone: {
// Maybe show signal strength?
showTempHum(false);
break;
}
case DashBoardStatusWiFiIssue: {
DISP()->drawXBM(icon_pos_x, 0, 14, 11, WIFI_ISSUE_BITS);
showTempHum(false);
break;
}
case DashBoardStatusServerIssue: {
DISP()->drawXBM(icon_pos_x, 0, 14, 11, CLOUD_ISSUE_BITS);
showTempHum(false);
break;
}
case DashBoardStatusAddToDashboard: {
setCentralText(10, "Add To Dashboard");
break;
}
case DashBoardStatusDeviceId: {
setCentralText(10, ag->deviceId().c_str());
break;
}
case DashBoardStatusOfflineMode: {
DISP()->drawXBM(icon_pos_x, 0, 14, 14, OFFLINE_BITS);
showTempHum(false); // First true
break;
}
default:
break;
if ((status == NULL) || (strlen(status) == 0)) {
showTempHum(false, strBuf, sizeof(strBuf));
} else {
String strStatus = "Show status: " + String(status);
logInfo(strStatus);
int strWidth = DISP()->getStrWidth(status);
DISP()->drawStr((DISP()->getWidth() - strWidth) / 2, 10, status);
/** Show WiFi NA*/
if (strcmp(status, "WiFi N/A") == 0) {
DISP()->setFont(u8g2_font_t0_12_tf);
showTempHum(true, strBuf, sizeof(strBuf));
}
}
/** Draw horizonal line */
@@ -390,7 +316,7 @@ void OledDisplay::showDashboard(DashboardStatus status) {
int pm25 = round(value.getAverage(Measurements::PM25));
if (utils::isValidPm(pm25)) {
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = round(value.getCorrectedPM25(true));
pm25 = round(value.getCorrectedPM25(*ag, config, true));
}
if (config.isPmStandardInUSAQI()) {
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
@@ -451,7 +377,7 @@ void OledDisplay::showDashboard(DashboardStatus status) {
/** Set PM */
int pm25 = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = round(value.getCorrectedPM25(true));
pm25 = round(value.getCorrectedPM25(*ag, config, true));
}
ag->display.setCursor(0, 12);
@@ -463,11 +389,10 @@ void OledDisplay::showDashboard(DashboardStatus status) {
ag->display.setText(strBuf);
/** Set temperature and humidity */
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
float temp = value.getAverage(Measurements::Temperature);
if (utils::isValidTemperature(temp)) {
if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F",
utils::degreeC_To_F(temp));
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
} else {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp);
}
@@ -482,7 +407,7 @@ void OledDisplay::showDashboard(DashboardStatus status) {
ag->display.setCursor(0, 24);
ag->display.setText(strBuf);
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
int rhum = round(value.getAverage(Measurements::Humidity));
if (utils::isValidHumidity(rhum)) {
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
} else {
@@ -517,7 +442,8 @@ void OledDisplay::setBrightness(int percent) {
// Clear display.
ag->display.clear();
ag->display.show();
} else {
}
else {
isDisplayOff = false;
ag->display.setContrast((255 * percent) / 100);
}

View File

@@ -16,41 +16,25 @@ private:
Measurements &value;
bool isDisplayOff = false;
typedef struct {
int width;
int height;
unsigned char *icon;
} xbm_icon_t;
void showTempHum(bool hasStatus);
void showTempHum(bool hasStatus, char* buf, int buf_size);
void setCentralText(int y, String text);
void setCentralText(int y, const char *text);
void showIcon(int x, int y, xbm_icon_t *icon);
public:
OledDisplay(Configuration &config, Measurements &value, Stream &log);
OledDisplay(Configuration &config, Measurements &value,
Stream &log);
~OledDisplay();
enum DashboardStatus {
DashBoardStatusNone,
DashBoardStatusWiFiIssue,
DashBoardStatusServerIssue,
DashBoardStatusAddToDashboard,
DashBoardStatusDeviceId,
DashBoardStatusOfflineMode,
};
void setAirGradient(AirGradient *ag);
bool begin(void);
bool begin(void);
void end(void);
void setText(String &line1, String &line2, String &line3);
void setText(const char *line1, const char *line2, const char *line3);
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 showDashboard(const char *status);
void setBrightness(int percent);
#ifdef ESP32
void showFirmwareUpdateVersion(String version);

View File

@@ -8,8 +8,8 @@ AgSchedule::~AgSchedule() {}
void AgSchedule::run(void) {
uint32_t ms = (uint32_t)(millis() - count);
if (ms >= period) {
count = millis();
handler();
count = millis();
}
}

View File

@@ -1,7 +1,6 @@
#include "AgStateMachine.h"
#include "AgOledDisplay.h"
#define LED_TEST_BLINK_DELAY 50 /** ms */
#define LED_TEST_BLINK_DELAY 50 /** ms */
#define LED_FAST_BLINK_DELAY 250 /** ms */
#define LED_SLOW_BLINK_DELAY 1000 /** ms */
#define LED_SHORT_BLINK_DELAY 500 /** ms */
@@ -9,10 +8,10 @@
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define RGB_COLOR_R 255, 0, 0 /** Red */
#define RGB_COLOR_G 0, 255, 0 /** Green */
#define RGB_COLOR_Y 255, 255, 0 /** Yellow */
#define RGB_COLOR_O 255, 128, 0 /** Orange */
#define RGB_COLOR_R 255, 0, 0 /** Red */
#define RGB_COLOR_G 0, 255, 0 /** Green */
#define RGB_COLOR_Y 255, 150, 0 /** Yellow */
#define RGB_COLOR_O 255, 40, 0 /** Orange */
#define RGB_COLOR_P 180, 0, 255 /** Purple */
#define RGB_COLOR_CLEAR 0, 0, 0 /** No color */
@@ -51,7 +50,7 @@ void StateMachine::ledStatusBlinkDelay(uint32_t ms) {
/**
* @brief Led bar show PM or CO2 led color status
*
* @return true if all led bar are used, false othwerwise
* @return true if all led bar are used, false othwerwise
*/
bool StateMachine::sensorhandleLeds(void) {
int totalLedUsed = 0;
@@ -83,7 +82,7 @@ bool StateMachine::sensorhandleLeds(void) {
/**
* @brief Show CO2 LED status
*
* @return return total number of led that are used on the monitor
* @return return total number of led that are used on the monitor
*/
int StateMachine::co2handleLeds(void) {
int totalUsed = ag->ledBar.getNumberOfLeds();
@@ -167,15 +166,15 @@ int StateMachine::co2handleLeds(void) {
/**
* @brief Show PM2.5 LED status
*
* @return return total number of led that are used on the monitor
*
* @return return total number of led that are used on the monitor
*/
int StateMachine::pm25handleLeds(void) {
int totalUsed = ag->ledBar.getNumberOfLeds();
int pm25Value = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25Value = round(value.getCorrectedPM25(true));
pm25Value = round(value.getCorrectedPM25(*ag, config, true));
}
if (pm25Value <= 5) {
@@ -370,17 +369,18 @@ void StateMachine::ledBarTest(void) {
} else {
ledBarRunTest();
}
} else if (ag->isOpenAir()) {
}
else if(ag->isOpenAir()) {
ledBarRunTest();
}
}
}
void StateMachine::ledBarPowerUpTest(void) {
void StateMachine::ledBarPowerUpTest(void) {
if (ag->isOne()) {
ag->ledBar.clear();
}
ledBarRunTest();
ledBarRunTest();
}
void StateMachine::ledBarRunTest(void) {
@@ -494,10 +494,13 @@ void StateMachine::displayHandle(AgStateMachineState state) {
if (ag->isBasic()) {
String ssid = "\"airgradient-" + ag->deviceId() + "\" " +
String(wifiConnectCountDown) + String("s");
disp.setText("Connect to hotspot:", ssid.c_str(), "");
disp.setText("Connect tohotspot:", ssid.c_str(), "");
} else {
// NOTE: This bool is hardcoded!
disp.showWiFiProvisioning((wifiConnectCountDown == 180), wifiConnectCountDown);
String line1 = String(wifiConnectCountDown) + "s to connect";
String line2 = "to WiFi hotspot:";
String line3 = "\"airgradient-";
String line4 = ag->deviceId() + "\"";
disp.setText(line1, line2, line3, line4);
}
wifiConnectCountDown--;
}
@@ -541,11 +544,11 @@ void StateMachine::displayHandle(AgStateMachineState state) {
break;
}
case AgStateMachineWiFiLost: {
disp.showDashboard(OledDisplay::DashBoardStatusWiFiIssue);
disp.showDashboard("WiFi N/A");
break;
}
case AgStateMachineServerLost: {
disp.showDashboard(OledDisplay::DashBoardStatusServerIssue);
disp.showDashboard("AG Server N/A");
break;
}
case AgStateMachineSensorConfigFailed: {
@@ -554,24 +557,19 @@ void StateMachine::displayHandle(AgStateMachineState state) {
if (ms >= 5000) {
addToDashboardTime = millis();
if (addToDashBoardToggle) {
disp.showDashboard(OledDisplay::DashBoardStatusAddToDashboard);
disp.showDashboard("Add to AG Dashb.");
} else {
disp.showDashboard(OledDisplay::DashBoardStatusDeviceId);
disp.showDashboard(ag->deviceId().c_str());
}
addToDashBoardToggle = !addToDashBoardToggle;
}
} else {
disp.showDashboard();
disp.showDashboard("");
}
break;
}
case AgStateMachineNormal: {
if (config.isOfflineMode()) {
disp.showDashboard(
OledDisplay::DashBoardStatusOfflineMode);
} else {
disp.showDashboard();
}
disp.showDashboard();
break;
}
case AgStateMachineCo2Calibration:
@@ -645,7 +643,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
ag->ledBar.clear();
ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2);
} else {
ag->statusLed.setStep();
ag->statusLed.setToggle();
}
break;
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,7 @@
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#include "Main/utils.h"
#include <Arduino.h>
#include <cstdint>
#include <vector>
#include <string>
class Measurements {
private:
@@ -36,36 +34,9 @@ private:
};
public:
Measurements(Configuration &config);
Measurements() {}
~Measurements() {}
struct Measures {
float temperature[2];
float humidity[2];
float co2;
float tvoc; // Index value
float tvoc_raw;
float nox; // Index value
float nox_raw;
float pm_01[2]; // pm 1.0 atmospheric environment
float pm_25[2]; // pm 2.5 atmospheric environment
float pm_10[2]; // pm 10 atmospheric environment
float pm_01_sp[2]; // pm 1.0 standard particle
float pm_25_sp[2]; // pm 2.5 standard particle
float pm_10_sp[2]; // pm 10 standard particle
float pm_03_pc[2]; // particle count 0.3
float pm_05_pc[2]; // particle count 0.5
float pm_01_pc[2]; // particle count 1.0
float pm_25_pc[2]; // particle count 2.5
float pm_5_pc[2]; // particle count 5.0
float pm_10_pc[2]; // particle count 10
int bootCount;
int signal;
uint32_t freeHeap;
};
void setAirGradient(AirGradient *ag);
// Enumeration for every AG measurements
enum MeasurementType {
Temperature,
@@ -89,8 +60,6 @@ public:
PM10_PC, // Particle 10 count
};
void printCurrentAverage();
/**
* @brief Set each MeasurementType maximum period length for moving average
*
@@ -150,59 +119,42 @@ 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);
/**
* @brief Get Temperature or Humidity correction value
* Only if correction is applied from configuration or forceCorrection is True
*
* @param type measurement type either Temperature or Humidity
* @param ch target type value channel
* @param forceCorrection force using correction even though config correction is not applied, but
* not for CUSTOM
* @return correction value
*/
float getCorrectedTempHum(MeasurementType type, int ch = 1, bool forceCorrection = false);
/**
* @brief Get the Corrected PM25 object based on the correction algorithm from configuration
*
*
* If correction is not enabled, then will return the raw value (either average or last value)
*
* @param ag AirGradient instance
* @param config Configuration instance
* @param useAvg Use moving average value if true, otherwise use latest value
* @param ch MeasurementType channel
* @param forceCorrection force using correction even though config correction is not applied, default to EPA
* @return float Corrected PM2.5 value
*/
float getCorrectedPM25(bool useAvg = false, int ch = 1, bool forceCorrection = false);
float getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg = false, int ch = 1);
/**
* build json payload for every measurements
*/
String toString(bool localServer, AgFirmwareMode fwMode, int rssi);
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
Configuration &config);
Measures getMeasures();
std::string buildMeasuresPayload(Measures &mc, bool extendedPmMeasures);
bool resetLocalStorage();
bool saveLocalStorage(AirGradient &ag, Configuration &config);
char *getLocalStorage();
/**
* Set to true if want to debug every update value
*/
void setDebug(bool debug);
int bootCount();
void setBootCount(int bootCount);
#ifndef ESP8266
void setResetReason(esp_reset_reason_t reason);
#endif
// TODO: update this to use setter
int bootCount;
private:
Configuration &config;
AirGradient *ag;
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
FloatValue _temperature[2];
FloatValue _humidity[2];
@@ -223,9 +175,9 @@ private:
IntegerValue _pm_25_pc[2]; // particle count 2.5
IntegerValue _pm_5_pc[2]; // particle count 5.0
IntegerValue _pm_10_pc[2]; // particle count 10
int _bootCount;
int _resetReason;
bool _debug = false;
const char *FILE_PATH = "/measurements.csv"; // Local storage file path
/**
* @brief Get PMS5003 firmware version string
@@ -261,11 +213,10 @@ private:
*/
void validateChannel(int ch);
void printCurrentPMAverage(int ch);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode);
JSONVar buildIndoor(bool localServer);
JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode, AirGradient &ag,
Configuration &config);
JSONVar buildIndoor(bool localServer, AirGradient &ag, Configuration &config);
JSONVar buildPMS(AirGradient &ag, int ch, bool allCh, bool withTempHum, bool compensate);
};
#endif /** _AG_VALUE_H_ */

View File

@@ -1,19 +1,5 @@
#include "AgWiFiConnector.h"
#include "Arduino.h"
#include "Libraries/WiFiManager/WiFiManager.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#ifdef ESP32
#include "WiFiType.h"
#include "esp32-hal.h"
#define BLE_SERVICE_UUID "acbcfea8-e541-4c40-9bfd-17820f16c95c"
#define BLE_CRED_CHAR_UUID "703fa252-3d2a-4da9-a05c-83b0d9cacb8e"
#define BLE_SCAN_CHAR_UUID "467a080f-e50f-42c9-b9b2-a2ab14d82725"
#define BLE_CRED_BIT (1 << 0)
#define BLE_SCAN_BIT (1 << 1)
#endif // ESP32
#define WIFI_CONNECT_COUNTDOWN_MAX 180
#define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair"
@@ -46,7 +32,7 @@ WifiConnector::~WifiConnector() {}
* @return true Success
* @return false Failure
*/
bool WifiConnector::connect(String modelName) {
bool WifiConnector::connect(void) {
if (wifi == NULL) {
wifi = new WiFiManager();
if (wifi == NULL) {
@@ -75,89 +61,62 @@ bool WifiConnector::connect(String modelName) {
break;
}
}
}
if (!WiFi.isConnected()) {
// Erase already saved default credentials
WiFi.disconnect(false, true);
}
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", "...");
} else {
Serial.printf("Attempt connect to configured ssid: %d\n", wifiSSID.c_str());
// WiFi.begin() already called before, it will attempt connect when wifi creds already persist
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);
}
logInfo("Connecting to WiFi...");
}
ssid = "airgradient-" + ag->deviceId();
if (WiFi.isConnected()) {
sm.handleLeds(AgStateMachineWiFiManagerStaConnected);
return true;
}
// ssid = "AG-" + String(ESP.getChipId(), HEX);
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
// Enable provision by both BLE and WiFi portal
WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WiFiManagerParameter disableCloudInfo(
WiFiManagerParameter postToAg("chbPostToAg",
"Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&postToAg);
WiFiManagerParameter postToAgInfo(
"<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>");
setupProvisionByPortal(&disableCloud, &disableCloudInfo);
"features. As a result you will not receive automatic firmware updates "
"and your data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&postToAgInfo);
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
logInfo("Wait for configure portal");
#ifdef ESP32
// Provision by BLE only for ESP32
setupProvisionByBLE(modelName.c_str());
// Task handling WiFi portal
// Task handle WiFi connection.
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;
[](void *obj) {
WifiConnector *connector = (WifiConnector *)obj;
while (connector->_wifiConfigPortalActive()) {
connector->_wifiProcess();
vTaskDelay(1);
}
connector->_wifiProcess();
vTaskDelay(1);
}
vTaskDelete(NULL);
},
"wifi_cfg", 4096, this, 10, NULL);
vTaskDelete(NULL);
},
"wifi_cfg", 4096, this, 10, NULL);
// Wait for WiFi connect and show LED, display status
/** Wait for WiFi connect and show LED, display status */
uint32_t dispPeriod = millis();
uint32_t ledPeriod = millis();
bool clientConnectChanged = false;
// By default wifi portal loops run first
// Provision method defined when either wifi or ble client connected first
// If wifi client connect, then ble server will be stopped
// If ble client connect, then wifi portal will be stopped (see wifi_cfg task)
AgStateMachineState stateOld = sm.getDisplayState();
while (WIFI()->getConfigPortalActive()) {
/** LED animation and display update content */
/** LED animatoin and display update content */
if (WiFi.isConnected() == false) {
/** Display countdown */
uint32_t ms;
@@ -187,11 +146,6 @@ bool WifiConnector::connect(String modelName) {
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);
@@ -204,74 +158,6 @@ bool WifiConnector::connect(String modelName) {
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
@@ -288,14 +174,14 @@ bool WifiConnector::connect(String modelName) {
logInfo("WiFi Connected: " + WiFi.SSID() + " IP: " + localIpStr());
if (hasPortalConfig) {
String result = String(disableCloud.getValue());
logInfo("Setting disableCloudConnection set from " +
String(config.isCloudConnectionDisabled() ? "True" : "False") + String(" to ") +
String(result == "T" ? "True" : "False") + String(" successful"));
config.setDisableCloudConnection(result == "T");
String result = String(postToAg.getValue());
logInfo("Setting postToAirGradient set from " +
String(config.isPostDataToAirGradient() ? "True" : "False") +
String(" to ") + String(result != "T" ? "True" : "False") +
String(" successful"));
config.setPostToAirGradient(result != "T");
}
hasPortalConfig = false;
bleNotifyStatus(PROV_WIFI_CONNECT);
}
return true;
@@ -322,11 +208,6 @@ bool WifiConnector::wifiClientConnected(void) {
return WiFi.softAPgetStationNum() ? true : false;
}
bool WifiConnector::isBleClientConnected() {
return bleClientConnected;
}
/**
* @brief Handle WiFiManage softAP setup completed callback
*
@@ -369,10 +250,6 @@ bool WifiConnector::_wifiConfigPortalActive(void) {
}
void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; }
void WifiConnector::_wifiStop() {
WIFI()->stopConfigPortal();
}
/**
* @brief Process WiFiManager connection
*
@@ -483,7 +360,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){
@@ -491,7 +368,7 @@ void WifiConnector::reset(void) {
return;
}
}
WIFI()->resetSettings();
WIFI()->resetSettings();
}
/**
@@ -531,338 +408,8 @@ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; }
/**
* @brief Set wifi connect to default WiFi
*
*
*/
void WifiConnector::setDefault(void) {
WiFi.begin("airgradient", "cleanair");
}
void WifiConnector::setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo) {
WIFI()->setConfigPortalBlocking(false);
WIFI()->setConnectTimeout(15);
WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WIFI()->setBreakAfterConfig(true);
WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); });
WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); });
WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); });
WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();});
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
disp.setText("Connecting to", "WiFi", "...");
} else {
logInfo("Connecting to WiFi...");
}
ssid = "airgradient-" + ag->deviceId();
// ssid = "AG-" + String(ESP.getChipId(), HEX);
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WIFI()->addParameter(disableCloudParam);
WIFI()->addParameter(disableCloudInfo);
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
logInfo("Wait for configure portal");
}
void WifiConnector::bleNotifyStatus(int status) {
#ifdef ESP32
if (!bleServerRunning) {
return;
}
if (pServer->getConnectedCount()) {
NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID);
if (pSvc) {
NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_CRED_CHAR_UUID);
if (pChr) {
char tosend[50];
memset(tosend, 0, 50);
sprintf(tosend, "{\"status\":%d}", status);
Serial.printf("BLE Notify >> %s \n", tosend);
pChr->setValue(String(tosend));
pChr->notify();
}
}
}
#endif // ESP32
}
#ifdef ESP32
int WifiConnector::scanAndFilterWiFi(WiFiNetwork networks[], int maxResults) {
Serial.println("Scanning for Wi-Fi networks...");
int n = WiFi.scanNetworks(false, true); // async=false, show_hidden=true
Serial.printf("Found %d networks\n", n);
const int MAX_NETWORKS = 50;
if (n <= 0) {
Serial.println("No networks found");
return 0;
}
WiFiNetwork allNetworks[MAX_NETWORKS];
int allCount = 0;
// Collect valid networks (filter weak or empty SSID)
for (int i = 0; i < n && allCount < MAX_NETWORKS; ++i) {
String ssid = WiFi.SSID(i);
int32_t rssi = WiFi.RSSI(i);
bool open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN);
if (ssid.length() == 0 || rssi < -75) continue;
allNetworks[allCount++] = {ssid, rssi, open};
}
// Remove duplicates (keep the strongest)
WiFiNetwork uniqueNetworks[MAX_NETWORKS];
int uniqueCount = 0;
for (int i = 0; i < allCount; i++) {
bool exists = false;
for (int j = 0; j < uniqueCount; j++) {
if (uniqueNetworks[j].ssid == allNetworks[i].ssid) {
exists = true;
if (allNetworks[i].rssi > uniqueNetworks[j].rssi)
uniqueNetworks[j] = allNetworks[i]; // keep stronger one
break;
}
}
if (!exists && uniqueCount < MAX_NETWORKS) {
uniqueNetworks[uniqueCount++] = allNetworks[i];
}
}
// Sort by RSSI descending (simple bubble sort for small lists)
for (int i = 0; i < uniqueCount - 1; i++) {
for (int j = i + 1; j < uniqueCount; j++) {
if (uniqueNetworks[j].rssi > uniqueNetworks[i].rssi) {
WiFiNetwork temp = uniqueNetworks[i];
uniqueNetworks[i] = uniqueNetworks[j];
uniqueNetworks[j] = temp;
}
}
}
// Copy to output array
int resultCount = (uniqueCount > maxResults) ? maxResults : uniqueCount;
for (int i = 0; i < resultCount; i++) {
networks[i] = uniqueNetworks[i];
}
Serial.printf("Returning %d filtered networks\n", resultCount);
return resultCount;
}
String WifiConnector::buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalFound,
int page, int batchSize, int totalPages) {
// Calculate start and end indices for this page
int startIdx = (page - 1) * batchSize;
int endIdx = startIdx + batchSize;
if (endIdx > totalFound) {
endIdx = totalFound;
}
// Build JSON object with pagination
JSONVar jsonRoot;
JSONVar jsonArray;
for (int i = startIdx; i < endIdx; i++) {
JSONVar obj;
obj["s"] = networks[i].ssid;
obj["r"] = networks[i].rssi;
obj["o"] = networks[i].open ? 1 : 0;
jsonArray[i - startIdx] = obj;
}
jsonRoot["wifi"] = jsonArray;
jsonRoot["page"] = page;
jsonRoot["tpage"] = totalPages;
jsonRoot["found"] = totalFound;
String jsonString = JSON.stringify(jsonRoot);
Serial.printf("Page %d/%d JSON: %s\n", page, totalPages, jsonString.c_str());
return jsonString;
}
void WifiConnector::handleBleScanRequest() {
const int BATCH_SIZE = 3;
const int MAX_RESULTS = 30;
WiFiNetwork networks[MAX_RESULTS];
// Scan and filter networks once
int networkCount = scanAndFilterWiFi(networks, MAX_RESULTS);
// Calculate total pages
int totalFound = (networkCount + BATCH_SIZE - 1) / BATCH_SIZE;
NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID);
if (!pSvc) {
Serial.println("BLE service not found");
return;
}
NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_SCAN_CHAR_UUID);
if (!pChr) {
Serial.println("BLE scan characteristic not found");
return;
}
if (networkCount == 0) {
Serial.println("No networks found to send");
String tosend = "{\"found\":0}";
pChr->setValue(tosend);
pChr->notify();
return;
}
// Send results in batches
for (int page = 1; page <= totalFound; page++) {
String batchJson = buildPaginatedWiFiJSON(networks, networkCount,
page, BATCH_SIZE, totalFound);
pChr->setValue(batchJson);
pChr->notify();
Serial.printf("Sent WiFi scan page %d/%d through BLE notify\n", page, totalFound);
// Delay between batches (except last one)
if (page < totalFound) {
delay(100);
}
}
Serial.println("All WiFi scan pages sent successfully");
}
void WifiConnector::setupProvisionByBLE(const char *modelName) {
NimBLEDevice::init("AirGradient");
NimBLEDevice::setPower(3); /** +3db */
/** bonding, MITM, don't need BLE secure connections as we are using passkey pairing */
NimBLEDevice::setSecurityAuth(false, false, true);
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT);
pServer = NimBLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks(this));
// Service and characteristics for device information
NimBLEService *pServDeviceInfo = pServer->createService("180A");
NimBLECharacteristic *pModelCharacteristic = pServDeviceInfo->createCharacteristic("2A24", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
pModelCharacteristic->setValue(modelName);
NimBLECharacteristic *pSerialCharacteristic = pServDeviceInfo->createCharacteristic("2A25", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
pSerialCharacteristic->setValue(ag->deviceId().c_str());
NimBLECharacteristic *pFwCharacteristic = pServDeviceInfo->createCharacteristic("2A26", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
pFwCharacteristic->setValue(ag->getVersion().c_str());
NimBLECharacteristic *pManufCharacteristic = pServDeviceInfo->createCharacteristic("2A29", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
pManufCharacteristic->setValue("AirGradient");
// Service and characteristics for wifi provisioning
NimBLEService *pServProvisioning = pServer->createService(BLE_SERVICE_UUID);
auto characteristicCallback = new CharacteristicCallbacks(this);
NimBLECharacteristic *pCredentialCharacteristic =
pServProvisioning->createCharacteristic(BLE_CRED_CHAR_UUID,
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC |
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY);
pCredentialCharacteristic->setCallbacks(characteristicCallback);
NimBLECharacteristic *pScanCharacteristic =
pServProvisioning->createCharacteristic(BLE_SCAN_CHAR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY);
pScanCharacteristic->setCallbacks(characteristicCallback);
// Start services
pServProvisioning->start();
pServDeviceInfo->start();
// Advertise
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
// Format advertising data
String mdata;
mdata += (char)0xFF;
mdata += (char)0xFF;
mdata += modelName;
mdata += '#';
mdata += ag->deviceId();
pAdvertising->setManufacturerData(mdata.c_str());
// Start advertise
pAdvertising->start();
bleServerRunning = true;
// Create event group
bleEventGroup = xEventGroupCreate();
if (bleEventGroup == NULL) {
Serial.println("Failed to create BLE event group!");
// This case is very unlikely
}
Serial.println("Provision by BLE ready");
}
void WifiConnector::stopBLE() {
if (bleServerRunning) {
Serial.println("Stopping BLE");
NimBLEDevice::deinit();
}
bleServerRunning = false;
}
//
// BLE innerclass implementation
//
WifiConnector::ServerCallbacks::ServerCallbacks(WifiConnector* parent)
: parent(parent) {}
void WifiConnector::ServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) {
Serial.printf("Client address: %s\n", connInfo.getAddress().toString().c_str());
parent->bleClientConnected = true;
NimBLEDevice::stopAdvertising();
}
void WifiConnector::ServerCallbacks::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) {
Serial.printf("Client disconnected - start advertising\n");
NimBLEDevice::startAdvertising();
parent->bleClientConnected = false;
}
void WifiConnector::ServerCallbacks::onAuthenticationComplete(NimBLEConnInfo& connInfo) {
Serial.println("\n========== PAIRING COMPLETE ==========");
Serial.printf("Peer Address: %s\n", connInfo.getAddress().toString().c_str());
Serial.printf("Encrypted: %s\n", connInfo.isEncrypted() ? "YES" : "NO");
Serial.printf("Authenticated: %s\n", connInfo.isAuthenticated() ? "YES" : "NO");
Serial.printf("Key Size: %d bits\n", connInfo.getSecKeySize() * 8);
Serial.println("======================================\n");
}
WifiConnector::CharacteristicCallbacks::CharacteristicCallbacks(WifiConnector* parent)
: parent(parent) {}
void WifiConnector::CharacteristicCallbacks::onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) {
Serial.printf("%s : onRead(), value: %s\n", pCharacteristic->getUUID().toString().c_str(),
pCharacteristic->getValue().c_str());
}
void WifiConnector::CharacteristicCallbacks::onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) {
Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(),
pCharacteristic->getValue().c_str());
auto bleCred = NimBLEUUID(BLE_CRED_CHAR_UUID);
if (pCharacteristic->getUUID().equals(bleCred)) {
if (!parent->wifiConnecting) {
JSONVar root = JSON.parse(pCharacteristic->getValue().c_str());
String ssid = root["ssid"];
String pass = root["password"];
WiFi.begin(ssid.c_str(), pass.c_str());
xEventGroupSetBits(parent->bleEventGroup, BLE_CRED_BIT);
}
} else {
xEventGroupSetBits(parent->bleEventGroup, BLE_SCAN_BIT);
}
}
#endif // ESP32

View File

@@ -1,57 +1,20 @@
#ifndef _AG_WIFI_CONNECTOR_H_
#define _AG_WIFI_CONNECTOR_H_
#include <Arduino.h>
#include "AgOledDisplay.h"
#include "AgStateMachine.h"
#include "AirGradient.h"
#include "AgConfigure.h"
#include "Libraries/WiFiManager/WiFiManager.h"
#include "Main/PrintLog.h"
#ifdef ESP32
#include "esp32-hal.h"
#include <NimBLEDevice.h>
#include "NimBLECharacteristic.h"
#include "NimBLEService.h"
#endif
// Provisioning Status Codes
#define PROV_WIFI_CONNECT 0 // WiFi Connect
#define PROV_CONNECTING_TO_SERVER 1 // Connecting to server
#define PROV_SERVER_REACHABLE 2 // Server reachable
#define PROV_MONITOR_CONFIGURED 3 // Monitor configured properly on dashboard
// Provisioning Error Codes
#define PROV_ERR_WIFI_CONNECT_FAILED 10 // Failed to connect to WiFi
#define PROV_ERR_SERVER_UNREACHABLE 11 // Server unreachable
#define PROV_ERR_GET_MONITOR_CONFIG_FAILED 12 // Failed to get monitor configuration from dashboard
#define PROV_ERR_MONITOR_NOT_REGISTERED 13 // Monitor is not registered on dashboard
#include <Arduino.h>
class WifiConnector : public PrintLog {
public:
enum class ProvisionMethod {
Unknown = 0,
WiFi,
BLE
};
struct WiFiNetwork {
String ssid;
int32_t rssi;
bool open;
};
private:
AirGradient *ag;
OledDisplay &disp;
StateMachine &sm;
Configuration &config;
#ifdef ESP32
NimBLEServer *pServer;
EventGroupHandle_t bleEventGroup;
#endif // ESP32
String ssid;
void *wifi = NULL;
@@ -59,55 +22,16 @@ 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();
#ifdef ESP32
int scanAndFilterWiFi(WiFiNetwork networks[], int maxResults);
String buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalCount,
int page, int batchSize, int totalPages);
void handleBleScanRequest();
// BLE server handler
class ServerCallbacks : public NimBLEServerCallbacks {
public:
explicit ServerCallbacks(WifiConnector *parent);
void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override;
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override;
void onAuthenticationComplete(NimBLEConnInfo &connInfo) override;
private:
WifiConnector *parent;
};
// BLE Characteristics handler
class CharacteristicCallbacks : public NimBLECharacteristicCallbacks {
public:
explicit CharacteristicCallbacks(WifiConnector *parent);
void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
private:
WifiConnector *parent;
};
#endif // ESP32
public:
void setAirGradient(AirGradient *ag);
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config);
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config);
~WifiConnector();
#ifdef ESP32
void setupProvisionByBLE(const char *modelName);
void stopBLE();
#endif // ESP32
void setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo);
bool connect(String modelName = "");
bool connect(void);
void disconnect(void);
void handle(void);
void _wifiApCallback(void);
@@ -115,7 +39,6 @@ public:
void _wifiSaveParamCallback(void);
bool _wifiConfigPortalActive(void);
void _wifiTimeoutCallback(void);
void _wifiStop();
void _wifiProcess();
bool isConnected(void);
void reset(void);
@@ -124,10 +47,8 @@ public:
bool hasConfigurated(void);
bool isConfigurePorttalTimeout(void);
void bleNotifyStatus(int status);
const char *defaultSsid = "airgradient";
const char *defaultPassword = "cleanair";
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;
}
@@ -85,3 +85,25 @@ String AirGradient::deviceId(void) {
mac.toLowerCase();
return mac;
}
void AirGradient::setCurrentTime(long epochTime) {
// set current day/time
struct timeval tv;
tv.tv_sec = epochTime; // - 1020; // 17 minutes // don't know why it always off by 17 minutes
settimeofday(&tv, NULL);
Serial.println(epochTime);
Serial.printf("Set current time to %s\n", getCurrentTime().c_str());
}
String AirGradient::getCurrentTime() {
// Get time
time_t now;
char strftime_buf[64];
struct tm timeinfo;
time(&now);
// Format
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%d/%m %H:%M:%S", &timeinfo);
return String(strftime_buf);
}

View File

@@ -15,47 +15,7 @@
#include "Main/utils.h"
#ifndef GIT_VERSION
#define GIT_VERSION "3.6.0-snap"
#endif
#ifndef ESP8266
// Airgradient server root ca certificate
const char *const AG_SERVER_ROOT_CA =
"-----BEGIN CERTIFICATE-----\n"
"MIIF4jCCA8oCCQD7MgvcaVWxkTANBgkqhkiG9w0BAQsFADCBsjELMAkGA1UEBhMC\n"
"VEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAOBgNVBAcMB01hZSBSaW0xGTAXBgNV\n"
"BAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNVBAsMC1NlbnNvciBMYWJzMSgwJgYD\n"
"VQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFicyBSb290IENBMSEwHwYJKoZIhvcN\n"
"AQkBFhJjYUBhaXJncmFkaWVudC5jb20wHhcNMjEwOTE3MTE0NDE3WhcNNDEwOTEy\n"
"MTE0NDE3WjCBsjELMAkGA1UEBhMCVEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAO\n"
"BgNVBAcMB01hZSBSaW0xGTAXBgNVBAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNV\n"
"BAsMC1NlbnNvciBMYWJzMSgwJgYDVQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFi\n"
"cyBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJjYUBhaXJncmFkaWVudC5jb20wggIi\n"
"MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6XkVQ4O9d5GcUjPYRgF/uaY6O\n"
"5ry1xCGvotxkEeKkBk99lB1oNUUfNsP5bwuDci4XKfY9Ro6/jmkfHSVcPAwUnjAt\n"
"BcHqZtA/cMXykaynf9yXPxPQN7XLu/Rk32RIfb90sIGS318xgNziCYvzWZmlxpxc\n"
"3gUcAgGtamlgZ6wD3yOHVo8B9aFNvmP16QwkUm8fKDHunJG+iX2Bxa4ka5FJovhG\n"
"TnUwtso6Vrn0JaWF9qWcPZE0JZMjFW8PYRriyJmHwr/nAXfPPKphD1oRO+oA7/jq\n"
"dYkrJw6+OHfFXnPB1xkeh4OPBzcCZHT5XWNfwBYazYpjcJa9ngGFSmg8lX1ac23C\n"
"zea1XJmSrPwbZbWxoQznnf7Y78mRjruYKgSP8rf74KYvBe/HGPL5NQyXQ3l6kwmu\n"
"CCUqfcC0wCWEtWESxwSdFE2qQii8CZ12kQExzvR2PrOIyKQYSdkGx9/RBZtAVPXP\n"
"hmLuRBQYHrF5Cxf1oIbBK8OMoNVgBm6ftt15t9Sq9dH5Aup2YR6WEJkVaYkYzZzK\n"
"X7M+SQcdbXp+hAO8PFpABJxkaDAO2kiB5Ov7pDYPAcmNFqnJT48AY0TZJeVeCa5W\n"
"sIv3lPvB/XcFjP0+aZxxNSEEwpGPUYgvKUYUUmb0NammlYQwZHKaShPEmZ3UZ0bp\n"
"VNt4p6374nzO376sSwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQB/LfBPgTx7xKQB\n"
"JNMUhah17AFAn050NiviGJOHdPQely6u3DmJGg+ijEVlPWO1FEW3it+LOuNP5zOu\n"
"bhq8paTYIxPxtALIxw5ksykX9woDuX3H6FF9mPdQIbL7ft+3ZtZ4FWPui9dUtaPe\n"
"ZBmDFDi4U29nhWZK68JSp5QkWjfaYLV/vtag7120eVyGEPFZ0UAuTUNqpw+stOt9\n"
"gJ2ZxNx13xJ8ZnLK7qz1crPe8/8IVAdxbVLoY7JaWPLc//+VF+ceKicy8+4gV7zN\n"
"Gnq2IyM+CHFz8VYMLbW+3eVp4iJjTa72vae116kozboEIUVN9rgLqIKyVqQXiuoN\n"
"g3xY+yfncPB2+H/+lfyy6mepPIfgksd3+KeNxFADSc5EVY2JKEdorRodnAh7a8K6\n"
"WjTYgq+GjWXU2uQW2SyPt6Tu33OT8nBnu3NB80eT8WXgdVCkgsuyCuLvNRf1Xmze\n"
"igvurpU6JmQ1GlLgLJo8omJHTh1zIbkR9injPYne2v9ciHCoP6+LDEqe+rOsvPCB\n"
"C/o/iZ4svmYX4fWGuU7GgqZE8hhrC3+GdOTf2ADC752cYCZxBidXGtkrGNoHQKmQ\n"
"KCOMFBxZIvWteB3tUo3BKYz1D2CvKWz1wV4moc5JHkOgS+jqxhvOkQ/vfQBQ1pUY\n"
"TMui9BSwU7B1G2XjdLbfF3Dc67zaSg==\n"
"-----END CERTIFICATE-----\n";
#define GIT_VERSION "3.1.13-snap"
#endif
/**
@@ -177,9 +137,9 @@ public:
/**
* @brief Check that Airgradient object is OPEN_AIR
*
* @return true
* @return false
*
* @return true
* @return false
*/
bool isOpenAir(void);
@@ -213,6 +173,9 @@ public:
*/
String deviceId(void);
void setCurrentTime(long epochTime);
String getCurrentTime();
private:
BoardType boardType;
};

View File

@@ -95,29 +95,26 @@ enum ConfigurationControl {
};
enum PMCorrectionAlgorithm {
COR_ALGO_PM_UNKNOWN, // Unknown algorithm
COR_ALGO_PM_NONE, // No PM correction
COR_ALGO_PM_EPA_2021,
COR_ALGO_PM_SLR_CUSTOM,
};
// Don't change the order of the enum
enum TempHumCorrectionAlgorithm {
COR_ALGO_TEMP_HUM_UNKNOWN, // Unknown algorithm
COR_ALGO_TEMP_HUM_NONE, // No PM correction
COR_ALGO_TEMP_HUM_AG_PMS5003T_2024,
COR_ALGO_TEMP_HUM_SLR_CUSTOM
Unknown, // Unknown algorithm
None, // No PM correction
EPA_2021,
SLR_PMS5003_20220802,
SLR_PMS5003_20220803,
SLR_PMS5003_20220824,
SLR_PMS5003_20231030,
SLR_PMS5003_20231218,
SLR_PMS5003_20240104,
};
enum AgFirmwareMode {
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
FW_MODE_I_BASIC_40PS, /** DIY_BASIC 4.0 */
};
const char *AgFirmwareModeName(AgFirmwareMode mode);

View File

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

View File

@@ -72,36 +72,6 @@ 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,7 +25,6 @@ public:
void setOn(void);
void setOff(void);
void setToggle(void);
void setStep(void);
State getState(void);
String toString(StatusLed::State state);

View File

@@ -260,14 +260,13 @@ bool MqttClient::connect(String id) {
connected = false;
if (user.isEmpty()) {
logInfo("Connect without auth");
connected = CLIENT()->connect(id.c_str());
} else {
logInfo("Connect with auth");
connected = CLIENT()->connect(id.c_str(), user.c_str(), password.c_str());
if(CLIENT()->connect(id.c_str())) {
connected = true;
}
return connected;
}
return connected;
return CLIENT()->connect(id.c_str(), user.c_str(), password.c_str());
}
void MqttClient::handle(void) {
if (isBegin == false) {
return;

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

@@ -835,13 +835,3 @@ bool S8::setAbcPeriod(int hours) {
* @return int Hour
*/
int S8::getAbcPeriod(void) { return getCalibPeriodABC(); }
void S8::printInformation(void) {
Serial.print("S8 type ID: 0x");
Serial.println(getSensorTypeId(), HEX);
Serial.print("S8 serial number: 0x");
Serial.println(getSensorId(), HEX);
Serial.print("S8 memory map version: 0x");
Serial.println(getMemoryMapVersion(), HEX);
}

View File

@@ -80,7 +80,6 @@ public:
bool isBaseLineCalibrationDone(void);
bool setAbcPeriod(int hours);
int getAbcPeriod(void);
void printInformation(void);
private:
/** Variables */

View File

@@ -131,22 +131,6 @@ void Sgp41::handle(void) {
}
#else
void Sgp41::pause() {
onPause = true;
Serial.println("Pausing SGP41 handler task");
// Set latest value to invalid
tvocRaw = utils::getInvalidVOC();
tvoc = utils::getInvalidVOC();
noxRaw = utils::getInvalidNOx();
nox = utils::getInvalidNOx();
}
void Sgp41::resume() {
onPause = false;
Serial.println("Resuming SGP41 handler task");
}
/**
* @brief Handle the sensor conditioning and run time udpate value, This method
* must not call, it's called on private task
@@ -168,11 +152,6 @@ void Sgp41::_handle(void) {
uint16_t srawVoc, srawNox;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
if (onPause) {
continue;
}
if (getRawSignal(srawVoc, srawNox)) {
tvocRaw = srawVoc;
noxRaw = srawNox;

View File

@@ -18,10 +18,6 @@ public:
bool begin(TwoWire &wire, Stream &stream);
void handle(void);
#else
/* pause _handle task to read sensor */
void pause();
/* resume _handle task to read sensor */
void resume();
void _handle(void);
#endif
void end(void);
@@ -36,7 +32,6 @@ public:
int getTvocLearningOffset(void);
private:
bool onPause = false;
bool onConditioning = true;
bool ready = false;
bool _isBegin = false;