mirror of
https://github.com/airgradienthq/arduino.git
synced 2025-12-16 18:48:26 +01:00
Compare commits
127 Commits
3.3.0
...
feat/provi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f1846f040 | ||
|
|
b5d89cf118 | ||
|
|
b178ad12a5 | ||
|
|
a55677422b | ||
|
|
da694edcab | ||
|
|
ae247ead48 | ||
|
|
49eed70863 | ||
|
|
b8768e0ed6 | ||
|
|
94bd0adf8b | ||
|
|
38bcf4c05e | ||
|
|
f1110905ca | ||
|
|
e196e6c6b8 | ||
|
|
3a56ee448d | ||
|
|
2f13c8fa66 | ||
|
|
449817a384 | ||
|
|
19df574130 | ||
|
|
16339acb97 | ||
|
|
d075d12011 | ||
|
|
75be7d9fc5 | ||
|
|
309d322100 | ||
|
|
89ebe6c39f | ||
|
|
29db5469f5 | ||
|
|
5802cf17f6 | ||
|
|
4a4ce89f00 | ||
|
|
8985e08a00 | ||
|
|
e984aced18 | ||
|
|
73edf56c97 | ||
|
|
e54c62a2ef | ||
|
|
5c95f011e4 | ||
|
|
bed448e7d6 | ||
|
|
eb8378adfa | ||
|
|
f94ed5c5f5 | ||
|
|
9471f3e323 | ||
|
|
e6d90372c2 | ||
|
|
fabf0550fc | ||
|
|
ce0af5bf60 | ||
|
|
93bdbd85d5 | ||
|
|
29f583f20d | ||
|
|
0f6a2fe908 | ||
|
|
e3ce2c41be | ||
|
|
92b3c69b98 | ||
|
|
f4d518aa87 | ||
|
|
831c844c24 | ||
|
|
060a7f6815 | ||
|
|
d8eb6b3c1a | ||
|
|
77859bea22 | ||
|
|
969858b5cb | ||
|
|
09b5805686 | ||
|
|
b09b753339 | ||
|
|
ddb3dba131 | ||
|
|
e780b0ace6 | ||
|
|
e82da5401e | ||
|
|
50a98acde4 | ||
|
|
7049d21a41 | ||
|
|
d5cdeaa9f3 | ||
|
|
09207c6923 | ||
|
|
0a64424196 | ||
|
|
5b38ca222b | ||
|
|
9ee35341a5 | ||
|
|
cec0514444 | ||
|
|
21b9ddb2ed | ||
|
|
1ee05da5d1 | ||
|
|
626a2240fa | ||
|
|
174ec6568f | ||
|
|
6b55719399 | ||
|
|
e2084f0738 | ||
|
|
5e07923690 | ||
|
|
04049439b1 | ||
|
|
c148d256d7 | ||
|
|
02849a1938 | ||
|
|
074337a96d | ||
|
|
4daa817a0b | ||
|
|
81a4502952 | ||
|
|
764e2eae38 | ||
|
|
79bf9811be | ||
|
|
9475724d0c | ||
|
|
e7603a7659 | ||
|
|
9bba89722e | ||
|
|
81945a358e | ||
|
|
3d26a54d69 | ||
|
|
b70ee75d50 | ||
|
|
c6846c818a | ||
|
|
0b1c901a76 | ||
|
|
83504c8628 | ||
|
|
4487992748 | ||
|
|
3c8a65a329 | ||
|
|
673d564ddb | ||
|
|
423eb4808f | ||
|
|
18a710ffc2 | ||
|
|
040cb79a4d | ||
|
|
52d3dc03f1 | ||
|
|
1c6bc3ec55 | ||
|
|
34d7c93e14 | ||
|
|
fee1dc25d6 | ||
|
|
9fb01d42f4 | ||
|
|
7bb013939c | ||
|
|
0da21155e7 | ||
|
|
7a153cc0ea | ||
|
|
b079c35e6b | ||
|
|
6051e183b8 | ||
|
|
c95379b957 | ||
|
|
0cae8bc185 | ||
|
|
5902a4c8e4 | ||
|
|
66818cd075 | ||
|
|
c1a6ddc68f | ||
|
|
20a32dd22c | ||
|
|
263dc9934e | ||
|
|
61b863b7f1 | ||
|
|
e01c1029fe | ||
|
|
ba5d817739 | ||
|
|
a91747e379 | ||
|
|
029457c3fa | ||
|
|
55710dd4d9 | ||
|
|
4886163cda | ||
|
|
7c57477238 | ||
|
|
9ed58d1853 | ||
|
|
6c52b038e9 | ||
|
|
2f69932ef7 | ||
|
|
1d96a274a6 | ||
|
|
df9f6dfc95 | ||
|
|
3fc02b3f54 | ||
|
|
958ed0bd80 | ||
|
|
e9be9dcc83 | ||
|
|
7fbab82088 | ||
|
|
decdecdf22 | ||
|
|
145c612867 | ||
|
|
37de127887 |
32
.github/workflows/check.yml
vendored
32
.github/workflows/check.yml
vendored
@@ -1,5 +1,36 @@
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
trailing-whitespace:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for trailing whitespace
|
||||
run: |
|
||||
set -u
|
||||
|
||||
# Don't enforce checks on vendored libraries.
|
||||
readonly EXCLUDED_DIR='src/Libraries'
|
||||
|
||||
has_trailing_whitespace=false
|
||||
|
||||
while read -r line; do
|
||||
if grep \
|
||||
"\s$" \
|
||||
--line-number \
|
||||
--with-filename \
|
||||
--binary-files=without-match \
|
||||
"${line}"; then
|
||||
has_trailing_whitespace=true
|
||||
fi
|
||||
done < <(git ls-files | grep --invert-match "^${EXCLUDED_DIR}/")
|
||||
|
||||
if [ "$has_trailing_whitespace" = true ]; then
|
||||
echo "ERROR: Found trailing whitespace"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
compile:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -38,6 +69,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'true'
|
||||
- uses: arduino/compile-sketches@v1.1.2
|
||||
with:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,5 +4,7 @@ build
|
||||
/.idea/
|
||||
.pio
|
||||
.cache
|
||||
.clangd
|
||||
logs
|
||||
gen_compile_commands.py
|
||||
compile_commands.json
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,6 +1,6 @@
|
||||
[submodule "src/Libraries/airgradient-client"]
|
||||
path = src/Libraries/airgradient-client
|
||||
url = git@github.com:airgradienthq/airgradient-client.git
|
||||
url = ../../airgradienthq/airgradient-client.git
|
||||
[submodule "src/Libraries/airgradient-ota"]
|
||||
path = src/Libraries/airgradient-ota
|
||||
url = git@github.com:airgradienthq/airgradient-ota.git
|
||||
url = ../../airgradienthq/airgradient-ota.git
|
||||
|
||||
25
README.md
25
README.md
@@ -20,13 +20,32 @@ 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/).
|
||||
|
||||
## Documentation
|
||||
## Development
|
||||
|
||||
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/).
|
||||
* 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/).
|
||||
|
||||
## The following libraries have been integrated into this library for ease of use
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@ Arduino IDE version 2.x ([download](https://www.arduino.cc/en/software))
|
||||
|
||||
#### Version < 3.2.0
|
||||
|
||||
Using library manager install the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
|
||||
Using library manager install the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
|
||||
|
||||

|
||||
|
||||
#### Version >= 3.3.0
|
||||
|
||||
- From your terminal, go to Arduino libraries folder (windows and mac: `Documents/Arduino/libraries` or linux: `~/Arduino/Libraries`).
|
||||
- With **git** cli, execute this command `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
|
||||
- Restart Arduino IDE
|
||||
- With **git** cli, execute this command `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
|
||||
- Restart Arduino IDE
|
||||
|
||||
3. On tools tab, follow settings below
|
||||
|
||||
@@ -57,7 +57,7 @@ Upload Speed ➝ 921600
|
||||
|
||||

|
||||
|
||||
3. Install AirGradient library on library manager using the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
|
||||
3. Install AirGradient library on library manager using the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
|
||||
|
||||

|
||||
|
||||
@@ -65,7 +65,7 @@ Upload Speed ➝ 921600
|
||||
|
||||

|
||||
|
||||
5. Open sketch to compile (File ➝ Examples ➝ AirGradient Air Quality Sensor ➝ `<Model Option>`). Depends on the DIY model, either `BASIC`, `DiyProIndoorV3_3` and `DiyProIndoorV4_2`
|
||||
5. Open sketch to compile (File ➝ Examples ➝ AirGradient Air Quality Sensor ➝ `<Model Option>`). Depends on the DIY model, either `BASIC`, `DiyProIndoorV3_3` and `DiyProIndoorV4_2`
|
||||
6. Compile
|
||||
|
||||

|
||||
@@ -78,19 +78,19 @@ ModuleNotFoundError: No module named ‘serial’
|
||||
|
||||

|
||||
|
||||
Make sure python pyserial module installed globally in the environment by executing:
|
||||
Make sure python pyserial module installed globally in the environment by executing:
|
||||
|
||||
`$ sudo apt install -y python3-pyserial`
|
||||
|
||||
or
|
||||
or
|
||||
|
||||
`$ pip install pyserial`
|
||||
`$ pip install pyserial`
|
||||
|
||||
Choose based on how python installed on your machine. But most user, using `apt` is better.
|
||||
|
||||
## 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 that will you intent to contribute back to the main project, instead of installing the AirGradient library, check out the repo at `Documents/Arduino/libraries` (for Windows and Mac), or `~/Arduino/Libraries` (Linux). If you installed the library, you can remove it from the library manager in the Arduino IDE, or just delete the directory.
|
||||
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`
|
||||
|
||||
@@ -100,6 +100,3 @@ There are 2 environment options to compile this project, PlatformIO and ArduinoI
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Local Server API
|
||||
|
||||
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
|
||||
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
|
||||
|
||||
### Discovery
|
||||
|
||||
@@ -20,7 +20,7 @@ http://airgradient_ecda3b1eaaaf.local/measures/current
|
||||
“ecda3b1eaaaf” being the serial number of your monitor.
|
||||
|
||||
You get the following response:
|
||||
```json
|
||||
```json
|
||||
{
|
||||
"wifi": -46,
|
||||
"serialno": "ecda3b1eaaaf",
|
||||
@@ -84,7 +84,7 @@ Compensated values apply correction algorithms to make the sensor values more ac
|
||||
|
||||
"/config" path returns the current configuration of the monitor.
|
||||
|
||||
```json
|
||||
```json
|
||||
{
|
||||
"country": "TH",
|
||||
"pmStandard": "ugm3",
|
||||
@@ -93,6 +93,7 @@ Compensated values apply correction algorithms to make the sensor values more ac
|
||||
"tvocLearningOffset": 12,
|
||||
"noxLearningOffset": 12,
|
||||
"mqttBrokerUrl": "",
|
||||
"httpDomain": "",
|
||||
"temperatureUnit": "c",
|
||||
"configurationControl": "local",
|
||||
"postDataToAirGradient": true,
|
||||
@@ -105,7 +106,6 @@ Compensated values apply correction algorithms to make the sensor values more ac
|
||||
"pm02": {
|
||||
"correctionAlgorithm": "epa_2021",
|
||||
"slr": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,22 +118,22 @@ Configuration parameters can be changed with a PUT request to the monitor, e.g.
|
||||
Example to force CO2 calibration
|
||||
|
||||
```bash
|
||||
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
|
||||
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
|
||||
```
|
||||
|
||||
Example to set monitor to Celsius
|
||||
|
||||
```bash
|
||||
curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config
|
||||
curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config
|
||||
```
|
||||
|
||||
If you use command prompt on Windows, you need to escape the quotes:
|
||||
|
||||
|
||||
``` -d "{\"param\":\"value\"}" ```
|
||||
|
||||
### Avoiding Conflicts with Configuration on AirGradient Server
|
||||
|
||||
If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
|
||||
If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
|
||||
|
||||
### Configuration Parameters (GET/PUT)
|
||||
|
||||
@@ -146,7 +146,8 @@ 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 | | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` |
|
||||
| `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"}` |
|
||||
| `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}` |
|
||||
@@ -154,8 +155,8 @@ 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_ |
|
||||
| `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**
|
||||
@@ -203,34 +204,34 @@ Example correction configuration:
|
||||
Field Name: `pm02`
|
||||
|
||||
| Algorithm | Value | Description | SLR required |
|
||||
|------------|-------------|------|---------|
|
||||
|------------|-------------|------|---------|
|
||||
| Raw | `"none"` | No correction (default) | No |
|
||||
| EPA 2021 | `"epa_2021"` | Use EPA 2021 correction factors on top of raw value | No |
|
||||
| PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes |
|
||||
| PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes |
|
||||
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
|
||||
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
|
||||
|
||||
**NOTES**:
|
||||
**NOTES**:
|
||||
|
||||
- Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false`
|
||||
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)
|
||||
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
|
||||
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
|
||||
|
||||
**Examples**:
|
||||
|
||||
- PMS5003_20231030
|
||||
- PMS5003_20231030
|
||||
|
||||
```bash
|
||||
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":true}}}}'
|
||||
```
|
||||
|
||||
- PMS5003_20231218
|
||||
- PMS5003_20231218
|
||||
|
||||
```bash
|
||||
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0.03525,"useEpa2021":true}}}}'
|
||||
```
|
||||
|
||||
- PMS5003_20240104
|
||||
- PMS5003_20240104
|
||||
|
||||
```bash
|
||||
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}'
|
||||
@@ -243,10 +244,10 @@ Field Name:
|
||||
- Humidity: `rhum`
|
||||
|
||||
| Algorithm | Value | Description | SLR required |
|
||||
|------------|-------------|------|---------|
|
||||
|------------|-------------|------|---------|
|
||||
| Raw | `"none"` | No correction (default) | No |
|
||||
| AirGradient Standard Correction | `"ag_pms5003t_2024"` | Using standard airgradient correction (for outdoor monitor)| No |
|
||||
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
|
||||
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
|
||||
|
||||
*Table above apply for both Temperature and Humidity*
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -294,7 +294,7 @@ static bool sgp41Init(void) {
|
||||
configuration.hasSensorSGP = true;
|
||||
return true;
|
||||
} else {
|
||||
Serial.println("Init SGP41 failuire");
|
||||
Serial.println("Init SGP41 failure");
|
||||
configuration.hasSensorSGP = false;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -351,7 +351,7 @@ static bool sgp41Init(void) {
|
||||
configuration.hasSensorSGP = true;
|
||||
return true;
|
||||
} else {
|
||||
Serial.println("Init SGP41 failuire");
|
||||
Serial.println("Init SGP41 failure");
|
||||
configuration.hasSensorSGP = false;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -249,7 +249,7 @@ void loop() {
|
||||
configUpdateHandle();
|
||||
|
||||
localServer._handle();
|
||||
|
||||
|
||||
if (configuration.hasSensorSGP) {
|
||||
ag.sgp41.handle();
|
||||
}
|
||||
@@ -374,7 +374,7 @@ static bool sgp41Init(void) {
|
||||
configuration.hasSensorSGP = true;
|
||||
return true;
|
||||
} else {
|
||||
Serial.println("Init SGP41 failuire");
|
||||
Serial.println("Init SGP41 failure");
|
||||
configuration.hasSensorSGP = false;
|
||||
}
|
||||
return false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,11 @@ OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
|
||||
|
||||
OpenMetrics::~OpenMetrics() {}
|
||||
|
||||
void OpenMetrics::setAirGradient(AirGradient *ag, AirgradientClient *client) {
|
||||
this->ag = ag;
|
||||
void OpenMetrics::setAirGradient(AirGradient *ag) {
|
||||
this->ag = ag;
|
||||
}
|
||||
|
||||
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
|
||||
this->agClient = client;
|
||||
}
|
||||
|
||||
@@ -199,14 +202,14 @@ String OpenMetrics::getPayload(void) {
|
||||
}
|
||||
if (utils::isValidNOx(nox)) {
|
||||
add_metric("nox_index",
|
||||
"The processed Nitrous Oxide (NOx) index as measured by the "
|
||||
"The processed Nitrogen 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 Nitrous Oxide (NOx) index as "
|
||||
"The raw input value to the Nitrogen Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(noxRaw));
|
||||
|
||||
@@ -19,7 +19,8 @@ public:
|
||||
OpenMetrics(Measurements &measure, Configuration &config,
|
||||
WifiConnector &wifiConnector);
|
||||
~OpenMetrics();
|
||||
void setAirGradient(AirGradient *ag, AirgradientClient *client);
|
||||
void setAirGradient(AirGradient *ag);
|
||||
void setAirgradientClient(AirgradientClient *client);
|
||||
const char *getApiContentType(void);
|
||||
const char* getApi(void);
|
||||
String getPayload(void);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name=AirGradient Air Quality Sensor
|
||||
version=3.3.0
|
||||
version=3.3.9
|
||||
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.
|
||||
|
||||
@@ -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 CORE_DEBUG_LEVEL=3 -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
|
||||
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D AG_LOG_LEVEL=AG_LOG_LEVEL_INFO -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
|
||||
board_build.partitions = partitions.csv
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
lib_deps =
|
||||
aglib=symlink://../arduino
|
||||
EEPROM
|
||||
WebServer
|
||||
@@ -26,13 +26,14 @@ lib_deps =
|
||||
WiFiClientSecure
|
||||
Update
|
||||
DNSServer
|
||||
h2zero/NimBLE-Arduino@^2.1.0
|
||||
|
||||
[env:esp8266]
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
lib_deps =
|
||||
aglib=symlink://../arduino
|
||||
EEPROM
|
||||
ESP8266HTTPClient
|
||||
|
||||
@@ -47,7 +47,7 @@ bool AgApiClient::fetchServerConfiguration(void) {
|
||||
}
|
||||
#else
|
||||
HTTPClient client;
|
||||
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
|
||||
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
|
||||
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
|
||||
if (apiRootChanged) {
|
||||
// If apiRoot is changed, assume not using https
|
||||
@@ -114,7 +114,7 @@ bool AgApiClient::postToServer(String data) {
|
||||
}
|
||||
#else
|
||||
HTTPClient client;
|
||||
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
|
||||
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
|
||||
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
|
||||
if (apiRootChanged) {
|
||||
// If apiRoot is changed, assume not using https
|
||||
@@ -185,13 +185,13 @@ void AgApiClient::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||
|
||||
/**
|
||||
* @brief Send the package to check the connection with cloud
|
||||
*
|
||||
*
|
||||
* @param rssi WiFi RSSI
|
||||
* @param bootCount Boot count
|
||||
* @return true Success
|
||||
* @return false Failure
|
||||
*/
|
||||
bool AgApiClient::sendPing(int rssi, int bootCount) {
|
||||
bool AgApiClient::sendPing(int rssi, int bootCount) {
|
||||
JSONVar root;
|
||||
root["wifi"] = rssi;
|
||||
root["boot"] = bootCount;
|
||||
|
||||
@@ -46,6 +46,7 @@ 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);
|
||||
@@ -68,6 +69,7 @@ JSON_PROP_DEF(rhum);
|
||||
#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
|
||||
@@ -112,7 +114,7 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
|
||||
|
||||
const size_t enumSize = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum
|
||||
PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;;
|
||||
|
||||
|
||||
// Loop through enum values
|
||||
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
|
||||
if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
|
||||
@@ -120,7 +122,7 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
|
||||
}
|
||||
}
|
||||
|
||||
// If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections
|
||||
// If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections
|
||||
if (result == COR_ALGO_PM_UNKNOWN) {
|
||||
// Check the substring "slr_PMS5003_xxxxxxxx"
|
||||
if (algorithm.substring(0, 11) == "slr_PMS5003") {
|
||||
@@ -240,7 +242,7 @@ bool Configuration::updateTempHumCorrection(JSONVar &json, TempHumCorrection &ta
|
||||
|
||||
JSONVar corrections = json[jprop_corrections];
|
||||
if (!corrections.hasOwnProperty(correctionName)) {
|
||||
logWarning(String(correctionName) + " correction field not found on configuration");
|
||||
logInfo(String(correctionName) + " correction field not found on configuration");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -377,6 +379,7 @@ 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;
|
||||
@@ -453,6 +456,10 @@ bool Configuration::begin(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Configuration::setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback) {
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse JSON configura string to local configure
|
||||
*
|
||||
@@ -735,11 +742,17 @@ bool Configuration::parse(String data, bool isLocal) {
|
||||
jconfig[jprop_mqttBrokerUrl] = broker;
|
||||
}
|
||||
} else {
|
||||
failedMessage = "\"mqttBrokerUrl\" length should <= 255";
|
||||
failedMessage = "\"mqttBrokerUrl\" length should less than 255 character";
|
||||
jsonInvalid();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
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 {
|
||||
if (jsonTypeInvalid(root[jprop_mqttBrokerUrl], "string")) {
|
||||
failedMessage =
|
||||
jsonTypeInvalidMessage(String(jprop_mqttBrokerUrl), "string");
|
||||
@@ -748,6 +761,32 @@ 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];
|
||||
@@ -916,15 +955,18 @@ bool Configuration::parse(String data, bool isLocal) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (ledBarTestRequested || co2CalibrationRequested) {
|
||||
commandRequested = true;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
updated = true;
|
||||
saveConfig();
|
||||
printConfig();
|
||||
} else {
|
||||
if (ledBarTestRequested || co2CalibrationRequested) {
|
||||
updated = true;
|
||||
}
|
||||
_callback();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1030,6 +1072,16 @@ 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
|
||||
*
|
||||
@@ -1114,8 +1166,14 @@ 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 invalid, it's should '" + type + "'";
|
||||
return "'" + name + "' type is invalid, expecting '" + type + "'";
|
||||
}
|
||||
|
||||
String Configuration::jsonValueInvalidMessage(String name, String value) {
|
||||
@@ -1269,6 +1327,18 @@ void Configuration::toConfig(const char *buf) {
|
||||
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;
|
||||
@@ -1492,13 +1562,13 @@ void Configuration::setDisableCloudConnection(bool disable) {
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
bool Configuration::isLedBarModeChanged(void) {
|
||||
bool Configuration::isLedBarModeChanged(void) {
|
||||
bool changed = _ledBarModeChanged;
|
||||
_ledBarModeChanged = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool Configuration::isMonitorDisplayCompensatedValues(void) {
|
||||
bool Configuration::isMonitorDisplayCompensatedValues(void) {
|
||||
return jconfig[jprop_monitorDisplayCompensatedValues];
|
||||
}
|
||||
|
||||
@@ -1521,8 +1591,8 @@ bool Configuration::isPMCorrectionChanged(void) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if PM correction is enabled
|
||||
*
|
||||
* @brief Check if PM correction is enabled
|
||||
*
|
||||
* @return true if PM correction algorithm is not None, otherwise false
|
||||
*/
|
||||
bool Configuration::isPMCorrectionEnabled(void) {
|
||||
|
||||
@@ -28,6 +28,7 @@ private:
|
||||
bool co2CalibrationRequested;
|
||||
bool ledBarTestRequested;
|
||||
bool updated;
|
||||
bool commandRequested = false;
|
||||
String failedMessage;
|
||||
bool _noxLearnOffsetChanged;
|
||||
bool _tvocLearningOffsetChanged;
|
||||
@@ -70,6 +71,9 @@ 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);
|
||||
@@ -82,6 +86,7 @@ public:
|
||||
String getLedBarModeName(void);
|
||||
bool getDisplayMode(void);
|
||||
String getMqttBrokerUri(void);
|
||||
String getHttpDomain(void);
|
||||
bool isPostDataToAirGradient(void);
|
||||
ConfigurationControl getConfigurationControl(void);
|
||||
bool isCo2CalibrationRequested(void);
|
||||
@@ -89,6 +94,7 @@ public:
|
||||
void reset(void);
|
||||
String getModel(void);
|
||||
bool isUpdated(void);
|
||||
bool isCommandRequested(void);
|
||||
String getFailedMesage(void);
|
||||
void setPostToAirGradient(bool enable);
|
||||
bool noxLearnOffsetChanged(void);
|
||||
@@ -115,6 +121,8 @@ public:
|
||||
PMCorrection getPMCorrection(void);
|
||||
TempHumCorrection getTempCorrection(void);
|
||||
TempHumCorrection getHumCorrection(void);
|
||||
private:
|
||||
ConfigurationUpdatedCallback_t _callback;
|
||||
};
|
||||
|
||||
#endif /** _AG_CONFIG_H_ */
|
||||
|
||||
@@ -19,10 +19,7 @@ static unsigned char OFFLINE_BITS[] = {
|
||||
0xE6, 0x00, 0xFE, 0x1F, 0xFE, 0x1F, 0xE6, 0x00, 0x62, 0x00,
|
||||
0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
// {
|
||||
// 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x62, 0x00, 0xE2, 0x00,
|
||||
// 0xFE, 0x1F, 0xFE, 0x1F, 0xE2, 0x00, 0x62, 0x00, 0x60, 0x00, 0x30, 0x00,
|
||||
// 0x00, 0x00, 0x00, 0x00, };
|
||||
|
||||
/**
|
||||
* @brief Show dashboard temperature and humdity
|
||||
*
|
||||
@@ -270,6 +267,37 @@ void OledDisplay::setText(const char *line1, const char *line2,
|
||||
}
|
||||
}
|
||||
|
||||
void OledDisplay::showWiFiProvisioning(bool firstRun, int countdown) {
|
||||
if (firstRun) {
|
||||
DISP()->clearBuffer();
|
||||
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||
DISP()->drawStr(1, 25, "to WiFi hotspot:");
|
||||
DISP()->drawStr(1, 40, "\"airgradient-");
|
||||
DISP()->drawStr(1, 55, (ag->deviceId() + "\"").c_str());
|
||||
}
|
||||
|
||||
// Now just update countdown area
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%ds to connect", countdown);
|
||||
DISP()->setDrawColor(0); // erase previous text
|
||||
DISP()->drawBox(0, 0, 128, 14); // clear top region
|
||||
DISP()->setDrawColor(1); // draw new text in white
|
||||
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||
DISP()->drawStr(1, 10, buf);
|
||||
|
||||
// Blink the BLE mark section
|
||||
if (countdown % 2 == 0) {
|
||||
DISP()->setFont(u8g2_font_t0_12b_tf);
|
||||
DISP()->drawStr(108, 60, "BLE");
|
||||
} else {
|
||||
DISP()->setDrawColor(0);
|
||||
DISP()->drawBox(108, 48, 20, 16);
|
||||
DISP()->setDrawColor(1);
|
||||
}
|
||||
|
||||
DISP()->sendBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update dashboard content
|
||||
*
|
||||
|
||||
@@ -48,6 +48,7 @@ public:
|
||||
void setText(String &line1, String &line2, String &line3, String &line4);
|
||||
void setText(const char *line1, const char *line2, const char *line3,
|
||||
const char *line4);
|
||||
void showWiFiProvisioning(bool firstRun, int countdown);
|
||||
void showDashboard(void);
|
||||
void showDashboard(DashboardStatus status);
|
||||
void setBrightness(int percent);
|
||||
|
||||
@@ -8,8 +8,8 @@ AgSchedule::~AgSchedule() {}
|
||||
void AgSchedule::run(void) {
|
||||
uint32_t ms = (uint32_t)(millis() - count);
|
||||
if (ms >= period) {
|
||||
handler();
|
||||
count = millis();
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
#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_Y 255, 255, 0 /** Yellow */
|
||||
#define RGB_COLOR_O 255, 128, 0 /** Orange */
|
||||
#define RGB_COLOR_P 180, 0, 255 /** Purple */
|
||||
#define RGB_COLOR_CLEAR 0, 0, 0 /** No color */
|
||||
|
||||
@@ -494,13 +494,10 @@ void StateMachine::displayHandle(AgStateMachineState state) {
|
||||
if (ag->isBasic()) {
|
||||
String ssid = "\"airgradient-" + ag->deviceId() + "\" " +
|
||||
String(wifiConnectCountDown) + String("s");
|
||||
disp.setText("Connect tohotspot:", ssid.c_str(), "");
|
||||
disp.setText("Connect to hotspot:", ssid.c_str(), "");
|
||||
} else {
|
||||
String line1 = String(wifiConnectCountDown) + "s to connect";
|
||||
String line2 = "to WiFi hotspot:";
|
||||
String line3 = "\"airgradient-";
|
||||
String line4 = ag->deviceId() + "\"";
|
||||
disp.setText(line1, line2, line3, line4);
|
||||
// NOTE: This bool is hardcoded!
|
||||
disp.showWiFiProvisioning((wifiConnectCountDown == 180), wifiConnectCountDown);
|
||||
}
|
||||
wifiConnectCountDown--;
|
||||
}
|
||||
@@ -648,7 +645,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2);
|
||||
} else {
|
||||
ag->statusLed.setToggle();
|
||||
ag->statusLed.setStep();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
181
src/AgValue.cpp
181
src/AgValue.cpp
@@ -5,7 +5,7 @@
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
#define json_prop_pmFirmware "firmware"
|
||||
#define json_prop_pmFirmware "firmware"
|
||||
#define json_prop_pm01Ae "pm01"
|
||||
#define json_prop_pm25Ae "pm02"
|
||||
#define json_prop_pm10Ae "pm10"
|
||||
@@ -33,7 +33,7 @@ Measurements::Measurements(Configuration &config) : config(config) {
|
||||
#ifndef ESP8266
|
||||
_resetReason = (int)ESP_RST_UNKNOWN;
|
||||
#endif
|
||||
|
||||
|
||||
/* Set invalid value for each measurements as default value when initialized*/
|
||||
_temperature[0].update.avg = utils::getInvalidTemperature();
|
||||
_temperature[1].update.avg = utils::getInvalidTemperature();
|
||||
@@ -51,7 +51,7 @@ Measurements::Measurements(Configuration &config) : config(config) {
|
||||
_pm_05_pc[1].update.avg = utils::getInvalidPmValue();
|
||||
_pm_5_pc[0].update.avg = utils::getInvalidPmValue();
|
||||
_pm_5_pc[1].update.avg = utils::getInvalidPmValue();
|
||||
|
||||
|
||||
_pm_01[0].update.avg = utils::getInvalidPmValue();
|
||||
_pm_01_sp[0].update.avg = utils::getInvalidPmValue();
|
||||
_pm_01_pc[0].update.avg = utils::getInvalidPmValue();
|
||||
@@ -76,6 +76,86 @@ Measurements::Measurements(Configuration &config) : config(config) {
|
||||
|
||||
void Measurements::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||
|
||||
void Measurements::printCurrentAverage() {
|
||||
Serial.println();
|
||||
if (config.hasSensorS8) {
|
||||
if (utils::isValidCO2(_co2.update.avg)) {
|
||||
Serial.printf("CO2 = %.2f ppm\n", _co2.update.avg);
|
||||
} else {
|
||||
Serial.printf("CO2 = -\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.hasSensorSHT) {
|
||||
if (utils::isValidTemperature(_temperature[0].update.avg)) {
|
||||
Serial.printf("Temperature = %.2f C\n", _temperature[0].update.avg);
|
||||
} else {
|
||||
Serial.printf("Temperature = -\n");
|
||||
}
|
||||
if (utils::isValidHumidity(_humidity[0].update.avg)) {
|
||||
Serial.printf("Relative Humidity = %.2f\n", _humidity[0].update.avg);
|
||||
} else {
|
||||
Serial.printf("Relative Humidity = -\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
if (utils::isValidVOC(_tvoc.update.avg)) {
|
||||
Serial.printf("TVOC Index = %.1f\n", _tvoc.update.avg);
|
||||
} else {
|
||||
Serial.printf("TVOC Index = -\n");
|
||||
}
|
||||
if (utils::isValidVOC(_tvoc_raw.update.avg)) {
|
||||
Serial.printf("TVOC Raw = %.1f\n", _tvoc_raw.update.avg);
|
||||
} else {
|
||||
Serial.printf("TVOC Raw = -\n");
|
||||
}
|
||||
if (utils::isValidNOx(_nox.update.avg)) {
|
||||
Serial.printf("NOx Index = %.1f\n", _nox.update.avg);
|
||||
} else {
|
||||
Serial.printf("NOx Index = -\n");
|
||||
}
|
||||
if (utils::isValidNOx(_nox_raw.update.avg)) {
|
||||
Serial.printf("NOx Raw = %.1f\n", _nox_raw.update.avg);
|
||||
} else {
|
||||
Serial.printf("NOx Raw = -\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
printCurrentPMAverage(1);
|
||||
if (!config.hasSensorSHT) {
|
||||
if (utils::isValidTemperature(_temperature[0].update.avg)) {
|
||||
Serial.printf("[1] Temperature = %.2f C\n", _temperature[0].update.avg);
|
||||
} else {
|
||||
Serial.printf("[1] Temperature = -\n");
|
||||
}
|
||||
if (utils::isValidHumidity(_humidity[0].update.avg)) {
|
||||
Serial.printf("[1] Relative Humidity = %.2f\n", _humidity[0].update.avg);
|
||||
} else {
|
||||
Serial.printf("[1] Relative Humidity = -\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.hasSensorPMS2) {
|
||||
printCurrentPMAverage(2);
|
||||
if (!config.hasSensorSHT) {
|
||||
if (utils::isValidTemperature(_temperature[1].update.avg)) {
|
||||
Serial.printf("[2] Temperature = %.2f C\n", _temperature[1].update.avg);
|
||||
} else {
|
||||
Serial.printf("[2] Temperature = -\n");
|
||||
}
|
||||
if (utils::isValidHumidity(_humidity[1].update.avg)) {
|
||||
Serial.printf("[2] Relative Humidity = %.2f\n", _humidity[1].update.avg);
|
||||
} else {
|
||||
Serial.printf("[2] Relative Humidity = -\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
void Measurements::maxPeriod(MeasurementType type, int max) {
|
||||
switch (type) {
|
||||
case Temperature:
|
||||
@@ -488,7 +568,7 @@ float Measurements::getAverage(MeasurementType type, int ch) {
|
||||
assert(0);
|
||||
}
|
||||
|
||||
return measurementAverage;
|
||||
return measurementAverage;
|
||||
}
|
||||
|
||||
String Measurements::pms5003FirmwareVersion(int fwCode) {
|
||||
@@ -570,6 +650,77 @@ String Measurements::measurementTypeStr(MeasurementType type) {
|
||||
return str;
|
||||
}
|
||||
|
||||
void Measurements::printCurrentPMAverage(int ch) {
|
||||
int idx = ch - 1;
|
||||
|
||||
if (utils::isValidPm(_pm_01[idx].update.avg)) {
|
||||
Serial.printf("[%d] Atmospheric PM 1.0 = %.2f ug/m3\n", ch, _pm_01[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Atmospheric PM 1.0 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm(_pm_25[idx].update.avg)) {
|
||||
Serial.printf("[%d] Atmospheric PM 2.5 = %.2f ug/m3\n", ch, _pm_25[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Atmospheric PM 2.5 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm(_pm_10[idx].update.avg)) {
|
||||
Serial.printf("[%d] Atmospheric PM 10 = %.2f ug/m3\n", ch, _pm_10[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Atmospheric PM 10 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm(_pm_01_sp[idx].update.avg)) {
|
||||
Serial.printf("[%d] Standard Particle PM 1.0 = %.2f ug/m3\n", ch, _pm_01_sp[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Standard Particle PM 1.0 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm(_pm_25_sp[idx].update.avg)) {
|
||||
Serial.printf("[%d] Standard Particle PM 2.5 = %.2f ug/m3\n", ch, _pm_25_sp[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Standard Particle PM 2.5 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm(_pm_10_sp[idx].update.avg)) {
|
||||
Serial.printf("[%d] Standard Particle PM 10 = %.2f ug/m3\n", ch, _pm_10_sp[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Standard Particle PM 10 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm03Count(_pm_03_pc[idx].update.avg)) {
|
||||
Serial.printf("[%d] Particle Count 0.3 = %.1f\n", ch, _pm_03_pc[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Particle Count 0.3 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm03Count(_pm_05_pc[idx].update.avg)) {
|
||||
Serial.printf("[%d] Particle Count 0.5 = %.1f\n", ch, _pm_05_pc[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Particle Count 0.5 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm03Count(_pm_01_pc[idx].update.avg)) {
|
||||
Serial.printf("[%d] Particle Count 1.0 = %.1f\n", ch, _pm_01_pc[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Particle Count 1.0 = -\n", ch);
|
||||
}
|
||||
if (utils::isValidPm03Count(_pm_25_pc[idx].update.avg)) {
|
||||
Serial.printf("[%d] Particle Count 2.5 = %.1f\n", ch, _pm_25_pc[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Particle Count 2.5 = -\n", ch);
|
||||
}
|
||||
|
||||
if (_pm_5_pc[idx].listValues.empty() == false) {
|
||||
if (utils::isValidPm03Count(_pm_5_pc[idx].update.avg)) {
|
||||
Serial.printf("[%d] Particle Count 5.0 = %.1f\n", ch, _pm_5_pc[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Particle Count 5.0 = -\n", ch);
|
||||
}
|
||||
}
|
||||
|
||||
if (_pm_10_pc[idx].listValues.empty() == false) {
|
||||
if (utils::isValidPm03Count(_pm_10_pc[idx].update.avg)) {
|
||||
Serial.printf("[%d] Particle Count 10 = %.1f\n", ch, _pm_10_pc[idx].update.avg);
|
||||
} else {
|
||||
Serial.printf("[%d] Particle Count 10 = -\n", ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Measurements::validateChannel(int ch) {
|
||||
if (ch != 1 && ch != 2) {
|
||||
Serial.printf("ERROR! Channel %d is undefined. Only channel 1 or 2 is the optional value!", ch);
|
||||
@@ -744,7 +895,7 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
|
||||
|
||||
oss << ",";
|
||||
|
||||
// Temperature
|
||||
// Temperature
|
||||
if (utils::isValidTemperature(mc.temperature[0]) && utils::isValidTemperature(mc.temperature[1])) {
|
||||
float temp = (mc.temperature[0] + mc.temperature[1]) / 2.0f;
|
||||
oss << std::round(temp * 10);
|
||||
@@ -804,16 +955,16 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
|
||||
|
||||
oss << ",";
|
||||
|
||||
// NOx
|
||||
if (utils::isValidNOx(mc.nox)) {
|
||||
oss << std::round(mc.nox);
|
||||
// TVOC
|
||||
if (utils::isValidVOC(mc.tvoc)) {
|
||||
oss << std::round(mc.tvoc);
|
||||
}
|
||||
|
||||
oss << ",";
|
||||
|
||||
// TVOC
|
||||
if (utils::isValidVOC(mc.tvoc)) {
|
||||
oss << std::round(mc.tvoc);
|
||||
// NOx
|
||||
if (utils::isValidNOx(mc.nox)) {
|
||||
oss << std::round(mc.nox);
|
||||
}
|
||||
|
||||
oss << ",";
|
||||
@@ -827,9 +978,11 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
|
||||
oss << std::round(mc.pm_03_pc[1]);
|
||||
}
|
||||
|
||||
// char datapoint[128] = {0};
|
||||
// snprintf(datapoint, 128, "%d,%.0f,%.0f,%.0f,%.0f,%.0f,%d,%d,%d", co2, temp * 10,
|
||||
// hum * 10, pm01 * 10, pm25 * 10, pm10 * 10, tvoc, nox, pm003Count);
|
||||
oss << ",";
|
||||
|
||||
if (mc.signal < 0) {
|
||||
oss << mc.signal;
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ public:
|
||||
PM10_PC, // Particle 10 count
|
||||
};
|
||||
|
||||
void printCurrentAverage();
|
||||
|
||||
/**
|
||||
* @brief Set each MeasurementType maximum period length for moving average
|
||||
*
|
||||
@@ -147,7 +149,7 @@ public:
|
||||
*
|
||||
* @param type measurement type that will be retrieve
|
||||
* @param ch target type value channel
|
||||
* @return moving average value of target measurements type
|
||||
* @return moving average value of target measurements type
|
||||
*/
|
||||
float getAverage(MeasurementType type, int ch = 1);
|
||||
|
||||
@@ -258,6 +260,8 @@ 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);
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
#include "AgWiFiConnector.h"
|
||||
#include "Arduino.h"
|
||||
#include "Libraries/WiFiManager/WiFiManager.h"
|
||||
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
|
||||
#include "WiFiType.h"
|
||||
#include "esp32-hal.h"
|
||||
|
||||
#define WIFI_CONNECT_COUNTDOWN_MAX 180
|
||||
#define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair"
|
||||
|
||||
|
||||
#define BLE_SERVICE_UUID "acbcfea8-e541-4c40-9bfd-17820f16c95c"
|
||||
#define BLE_CRED_CHAR_UUID "703fa252-3d2a-4da9-a05c-83b0d9cacb8e"
|
||||
#define BLE_SCAN_CHAR_UUID "467a080f-e50f-42c9-b9b2-a2ab14d82725"
|
||||
|
||||
#define BLE_CRED_BIT (1 << 0)
|
||||
#define BLE_SCAN_BIT (1 << 1)
|
||||
|
||||
#define WIFI() ((WiFiManager *)(this->wifi))
|
||||
|
||||
/**
|
||||
@@ -32,7 +44,7 @@ WifiConnector::~WifiConnector() {}
|
||||
* @return true Success
|
||||
* @return false Failure
|
||||
*/
|
||||
bool WifiConnector::connect(void) {
|
||||
bool WifiConnector::connect(String modelName) {
|
||||
if (wifi == NULL) {
|
||||
wifi = new WiFiManager();
|
||||
if (wifi == NULL) {
|
||||
@@ -61,61 +73,89 @@ bool WifiConnector::connect(void) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WIFI()->setConfigPortalBlocking(false);
|
||||
WIFI()->setConnectTimeout(15);
|
||||
WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
||||
|
||||
WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); });
|
||||
WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); });
|
||||
WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); });
|
||||
WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();});
|
||||
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
|
||||
disp.setText("Connecting to", "WiFi", "...");
|
||||
if (!WiFi.isConnected()) {
|
||||
// Erase already saved default credentials
|
||||
WiFi.disconnect(false, true);
|
||||
}
|
||||
} else {
|
||||
logInfo("Connecting to WiFi...");
|
||||
Serial.printf("Attempt connect to configured ssid: %d\n", wifiSSID.c_str());
|
||||
// WiFi.begin() already called before, it will attempt connect when wifi creds already persist
|
||||
|
||||
sm.ledAnimationInit();
|
||||
sm.handleLeds(AgStateMachineWiFiManagerStaConnecting);
|
||||
sm.displayHandle(AgStateMachineWiFiManagerStaConnecting);
|
||||
|
||||
uint32_t ledPeriod = millis();
|
||||
uint32_t startTime = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 15000) {
|
||||
/** LED animations */
|
||||
if ((millis() - ledPeriod) >= 100) {
|
||||
ledPeriod = millis();
|
||||
sm.handleLeds();
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
|
||||
if (!WiFi.isConnected()) {
|
||||
// WiFi not connect, show indicator.
|
||||
sm.ledAnimationInit();
|
||||
sm.handleLeds(AgStateMachineWiFiManagerConnectFailed);
|
||||
sm.displayHandle(AgStateMachineWiFiManagerConnectFailed);
|
||||
delay(3000);
|
||||
}
|
||||
}
|
||||
ssid = "airgradient-" + ag->deviceId();
|
||||
|
||||
// ssid = "AG-" + String(ESP.getChipId(), HEX);
|
||||
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
||||
if (WiFi.isConnected()) {
|
||||
sm.handleLeds(AgStateMachineWiFiManagerStaConnected);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Enable provision by both BLE and WiFi portal
|
||||
WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T",
|
||||
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
|
||||
WIFI()->addParameter(&disableCloud);
|
||||
WiFiManagerParameter disableCloudInfo(
|
||||
"<p>Prevent connection to the AirGradient Server. Important: Only enable "
|
||||
"it if you are sure you don't want to use any AirGradient cloud "
|
||||
"features. As a result you will not receive automatic firmware updates, "
|
||||
"configuration settings from cloud and the measure data will not reach the AirGradient dashboard.</p>");
|
||||
WIFI()->addParameter(&disableCloudInfo);
|
||||
|
||||
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
|
||||
|
||||
logInfo("Wait for configure portal");
|
||||
setupProvisionByPortal(&disableCloud, &disableCloudInfo);
|
||||
|
||||
#ifdef ESP32
|
||||
// Task handle WiFi connection.
|
||||
xTaskCreate(
|
||||
[](void *obj) {
|
||||
WifiConnector *connector = (WifiConnector *)obj;
|
||||
while (connector->_wifiConfigPortalActive()) {
|
||||
connector->_wifiProcess();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
},
|
||||
"wifi_cfg", 4096, this, 10, NULL);
|
||||
// Provision by BLE only for ESP32
|
||||
setupProvisionByBLE(modelName.c_str());
|
||||
|
||||
/** Wait for WiFi connect and show LED, display status */
|
||||
// Task handling WiFi portal
|
||||
xTaskCreate(
|
||||
[](void *obj) {
|
||||
WifiConnector *connector = (WifiConnector *)obj;
|
||||
while (connector->_wifiConfigPortalActive()) {
|
||||
if (connector->isBleClientConnected()) {
|
||||
Serial.println("Stopping portal because BLE connected");
|
||||
connector->_wifiStop();
|
||||
connector->provisionMethod = ProvisionMethod::BLE;
|
||||
break;
|
||||
}
|
||||
connector->_wifiProcess();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
},
|
||||
"wifi_cfg", 4096, this, 10, NULL);
|
||||
|
||||
|
||||
// Wait for WiFi connect and show LED, display status
|
||||
uint32_t dispPeriod = millis();
|
||||
uint32_t ledPeriod = millis();
|
||||
bool clientConnectChanged = false;
|
||||
|
||||
// By default wifi portal loops run first
|
||||
// Provision method defined when either wifi or ble client connected first
|
||||
// If wifi client connect, then ble server will be stopped
|
||||
// If ble client connect, then wifi portal will be stopped (see wifi_cfg task)
|
||||
AgStateMachineState stateOld = sm.getDisplayState();
|
||||
while (WIFI()->getConfigPortalActive()) {
|
||||
/** LED animatoin and display update content */
|
||||
/** LED animation and display update content */
|
||||
if (WiFi.isConnected() == false) {
|
||||
/** Display countdown */
|
||||
uint32_t ms;
|
||||
@@ -145,6 +185,11 @@ bool WifiConnector::connect(void) {
|
||||
clientConnectChanged = clientConnected;
|
||||
if (clientConnectChanged) {
|
||||
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
|
||||
if (bleServerRunning) {
|
||||
Serial.println("Stopping BLE since wifi is connected");
|
||||
stopBLE();
|
||||
provisionMethod = ProvisionMethod::WiFi;
|
||||
}
|
||||
} else {
|
||||
sm.ledAnimationInit();
|
||||
sm.handleLeds(AgStateMachineWiFiManagerMode);
|
||||
@@ -157,6 +202,74 @@ bool WifiConnector::connect(void) {
|
||||
|
||||
delay(1); // avoid watchdog timer reset.
|
||||
}
|
||||
|
||||
if (provisionMethod == ProvisionMethod::BLE) {
|
||||
disp.setText("Provision by", "BLE", "");
|
||||
sm.ledAnimationInit();
|
||||
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
|
||||
|
||||
uint32_t wdMillis = 0;
|
||||
|
||||
// Loop until the BLE client disconnected or WiFi connected
|
||||
while (isBleClientConnected() && !WiFi.isConnected()) {
|
||||
EventBits_t bits = xEventGroupWaitBits(
|
||||
bleEventGroup,
|
||||
BLE_SCAN_BIT | BLE_CRED_BIT,
|
||||
pdTRUE,
|
||||
pdFALSE,
|
||||
10 / portTICK_PERIOD_MS
|
||||
);
|
||||
|
||||
if (bits & BLE_CRED_BIT) {
|
||||
Serial.printf("Connecting to %s...\n", ssid.c_str());
|
||||
wifiConnecting = true;
|
||||
|
||||
sm.ledAnimationInit();
|
||||
sm.handleLeds(AgStateMachineWiFiManagerStaConnecting);
|
||||
sm.displayHandle(AgStateMachineWiFiManagerStaConnecting);
|
||||
|
||||
uint32_t startTime = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 15000) {
|
||||
// Led animations
|
||||
if ((millis() - ledPeriod) >= 100) {
|
||||
ledPeriod = millis();
|
||||
sm.handleLeds();
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("Failed connect to WiFi");
|
||||
// If not connect send status through BLE while also turn led and display indicator
|
||||
WiFi.disconnect();
|
||||
wifiConnecting = false;
|
||||
bleNotifyStatus(PROV_ERR_WIFI_CONNECT_FAILED);
|
||||
|
||||
// Show failed inficator then revert back to provision mode
|
||||
sm.ledAnimationInit();
|
||||
sm.handleLeds(AgStateMachineWiFiManagerConnectFailed);
|
||||
sm.displayHandle(AgStateMachineWiFiManagerConnectFailed);
|
||||
delay(3000);
|
||||
sm.ledAnimationInit();
|
||||
disp.setText("Provision by", "BLE", "");
|
||||
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
|
||||
}
|
||||
}
|
||||
else if (bits & BLE_SCAN_BIT) {
|
||||
handleBleScanRequest();
|
||||
}
|
||||
|
||||
// Ensure watchdog fed every minute
|
||||
if ((millis() - wdMillis) >= 60000) {
|
||||
wdMillis = millis();
|
||||
ag->watchdog.reset();
|
||||
}
|
||||
|
||||
delay(1);
|
||||
}
|
||||
|
||||
Serial.println("Exit provision by BLE");
|
||||
}
|
||||
#else
|
||||
_wifiProcess();
|
||||
#endif
|
||||
@@ -180,6 +293,7 @@ bool WifiConnector::connect(void) {
|
||||
config.setDisableCloudConnection(result == "T");
|
||||
}
|
||||
hasPortalConfig = false;
|
||||
bleNotifyStatus(PROV_WIFI_CONNECT);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -206,6 +320,11 @@ bool WifiConnector::wifiClientConnected(void) {
|
||||
return WiFi.softAPgetStationNum() ? true : false;
|
||||
}
|
||||
|
||||
|
||||
bool WifiConnector::isBleClientConnected() {
|
||||
return bleClientConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle WiFiManage softAP setup completed callback
|
||||
*
|
||||
@@ -248,6 +367,10 @@ bool WifiConnector::_wifiConfigPortalActive(void) {
|
||||
}
|
||||
void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; }
|
||||
|
||||
void WifiConnector::_wifiStop() {
|
||||
WIFI()->stopConfigPortal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process WiFiManager connection
|
||||
*
|
||||
@@ -358,7 +481,7 @@ bool WifiConnector::isConnected(void) { return WiFi.isConnected(); }
|
||||
* this method
|
||||
*
|
||||
*/
|
||||
void WifiConnector::reset(void) {
|
||||
void WifiConnector::reset(void) {
|
||||
if(this->wifi == NULL) {
|
||||
this->wifi = new WiFiManager();
|
||||
if(this->wifi == NULL){
|
||||
@@ -366,7 +489,7 @@ void WifiConnector::reset(void) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
WIFI()->resetSettings();
|
||||
WIFI()->resetSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,10 +527,336 @@ bool WifiConnector::hasConfigurated(void) {
|
||||
*/
|
||||
bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; }
|
||||
|
||||
|
||||
void WifiConnector::bleNotifyStatus(int status) {
|
||||
if (!bleServerRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pServer->getConnectedCount()) {
|
||||
NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID);
|
||||
if (pSvc) {
|
||||
NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_CRED_CHAR_UUID);
|
||||
if (pChr) {
|
||||
char tosend[50];
|
||||
memset(tosend, 0, 50);
|
||||
sprintf(tosend, "{\"status\":%d}", status);
|
||||
Serial.printf("BLE Notify >> %s \n", tosend);
|
||||
pChr->setValue(String(tosend));
|
||||
pChr->notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set wifi connect to default WiFi
|
||||
*
|
||||
*
|
||||
*/
|
||||
void WifiConnector::setDefault(void) {
|
||||
WiFi.begin("airgradient", "cleanair");
|
||||
}
|
||||
|
||||
int WifiConnector::scanAndFilterWiFi(WiFiNetwork networks[], int maxResults) {
|
||||
Serial.println("Scanning for Wi-Fi networks...");
|
||||
int n = WiFi.scanNetworks(false, true); // async=false, show_hidden=true
|
||||
Serial.printf("Found %d networks\n", n);
|
||||
|
||||
const int MAX_NETWORKS = 50;
|
||||
|
||||
if (n <= 0) {
|
||||
Serial.println("No networks found");
|
||||
return 0;
|
||||
}
|
||||
|
||||
WiFiNetwork allNetworks[MAX_NETWORKS];
|
||||
int allCount = 0;
|
||||
|
||||
// Collect valid networks (filter weak or empty SSID)
|
||||
for (int i = 0; i < n && allCount < MAX_NETWORKS; ++i) {
|
||||
String ssid = WiFi.SSID(i);
|
||||
int32_t rssi = WiFi.RSSI(i);
|
||||
bool open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN);
|
||||
|
||||
if (ssid.length() == 0 || rssi < -75) continue;
|
||||
|
||||
allNetworks[allCount++] = {ssid, rssi, open};
|
||||
}
|
||||
|
||||
// Remove duplicates (keep the strongest)
|
||||
WiFiNetwork uniqueNetworks[MAX_NETWORKS];
|
||||
int uniqueCount = 0;
|
||||
|
||||
for (int i = 0; i < allCount; i++) {
|
||||
bool exists = false;
|
||||
for (int j = 0; j < uniqueCount; j++) {
|
||||
if (uniqueNetworks[j].ssid == allNetworks[i].ssid) {
|
||||
exists = true;
|
||||
if (allNetworks[i].rssi > uniqueNetworks[j].rssi)
|
||||
uniqueNetworks[j] = allNetworks[i]; // keep stronger one
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists && uniqueCount < MAX_NETWORKS) {
|
||||
uniqueNetworks[uniqueCount++] = allNetworks[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by RSSI descending (simple bubble sort for small lists)
|
||||
for (int i = 0; i < uniqueCount - 1; i++) {
|
||||
for (int j = i + 1; j < uniqueCount; j++) {
|
||||
if (uniqueNetworks[j].rssi > uniqueNetworks[i].rssi) {
|
||||
WiFiNetwork temp = uniqueNetworks[i];
|
||||
uniqueNetworks[i] = uniqueNetworks[j];
|
||||
uniqueNetworks[j] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy to output array
|
||||
int resultCount = (uniqueCount > maxResults) ? maxResults : uniqueCount;
|
||||
for (int i = 0; i < resultCount; i++) {
|
||||
networks[i] = uniqueNetworks[i];
|
||||
}
|
||||
|
||||
Serial.printf("Returning %d filtered networks\n", resultCount);
|
||||
return resultCount;
|
||||
}
|
||||
|
||||
String WifiConnector::buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalFound,
|
||||
int page, int batchSize, int totalPages) {
|
||||
// Calculate start and end indices for this page
|
||||
int startIdx = (page - 1) * batchSize;
|
||||
int endIdx = startIdx + batchSize;
|
||||
if (endIdx > totalFound) {
|
||||
endIdx = totalFound;
|
||||
}
|
||||
|
||||
// Build JSON object with pagination
|
||||
JSONVar jsonRoot;
|
||||
JSONVar jsonArray;
|
||||
|
||||
for (int i = startIdx; i < endIdx; i++) {
|
||||
JSONVar obj;
|
||||
obj["s"] = networks[i].ssid;
|
||||
obj["r"] = networks[i].rssi;
|
||||
obj["o"] = networks[i].open ? 1 : 0;
|
||||
jsonArray[i - startIdx] = obj;
|
||||
}
|
||||
|
||||
jsonRoot["wifi"] = jsonArray;
|
||||
jsonRoot["page"] = page;
|
||||
jsonRoot["tpage"] = totalPages;
|
||||
jsonRoot["found"] = totalFound;
|
||||
|
||||
String jsonString = JSON.stringify(jsonRoot);
|
||||
|
||||
Serial.printf("Page %d/%d JSON: %s\n", page, totalPages, jsonString.c_str());
|
||||
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
void WifiConnector::handleBleScanRequest() {
|
||||
const int BATCH_SIZE = 3;
|
||||
const int MAX_RESULTS = 30;
|
||||
WiFiNetwork networks[MAX_RESULTS];
|
||||
|
||||
// Scan and filter networks once
|
||||
int networkCount = scanAndFilterWiFi(networks, MAX_RESULTS);
|
||||
|
||||
// Calculate total pages
|
||||
int totalFound = (networkCount + BATCH_SIZE - 1) / BATCH_SIZE;
|
||||
|
||||
NimBLEService* pSvc = pServer->getServiceByUUID(BLE_SERVICE_UUID);
|
||||
if (!pSvc) {
|
||||
Serial.println("BLE service not found");
|
||||
return;
|
||||
}
|
||||
|
||||
NimBLECharacteristic* pChr = pSvc->getCharacteristic(BLE_SCAN_CHAR_UUID);
|
||||
if (!pChr) {
|
||||
Serial.println("BLE scan characteristic not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (networkCount == 0) {
|
||||
Serial.println("No networks found to send");
|
||||
String tosend = "{\"found\":0}";
|
||||
pChr->setValue(tosend);
|
||||
pChr->notify();
|
||||
return;
|
||||
}
|
||||
|
||||
// Send results in batches
|
||||
for (int page = 1; page <= totalFound; page++) {
|
||||
String batchJson = buildPaginatedWiFiJSON(networks, networkCount,
|
||||
page, BATCH_SIZE, totalFound);
|
||||
pChr->setValue(batchJson);
|
||||
pChr->notify();
|
||||
|
||||
Serial.printf("Sent WiFi scan page %d/%d through BLE notify\n", page, totalFound);
|
||||
|
||||
// Delay between batches (except last one)
|
||||
if (page < totalFound) {
|
||||
delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("All WiFi scan pages sent successfully");
|
||||
}
|
||||
|
||||
void WifiConnector::setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo) {
|
||||
WIFI()->setConfigPortalBlocking(false);
|
||||
WIFI()->setConnectTimeout(15);
|
||||
WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
||||
WIFI()->setBreakAfterConfig(true);
|
||||
|
||||
WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); });
|
||||
WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); });
|
||||
WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); });
|
||||
WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();});
|
||||
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
|
||||
disp.setText("Connecting to", "WiFi", "...");
|
||||
} else {
|
||||
logInfo("Connecting to WiFi...");
|
||||
}
|
||||
ssid = "airgradient-" + ag->deviceId();
|
||||
|
||||
// ssid = "AG-" + String(ESP.getChipId(), HEX);
|
||||
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
||||
|
||||
WIFI()->addParameter(disableCloudParam);
|
||||
WIFI()->addParameter(disableCloudInfo);
|
||||
|
||||
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
|
||||
|
||||
logInfo("Wait for configure portal");
|
||||
}
|
||||
|
||||
void WifiConnector::setupProvisionByBLE(const char *modelName) {
|
||||
NimBLEDevice::init("AirGradient");
|
||||
NimBLEDevice::setPower(3); /** +3db */
|
||||
|
||||
/** bonding, MITM, don't need BLE secure connections as we are using passkey pairing */
|
||||
NimBLEDevice::setSecurityAuth(false, false, true);
|
||||
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT);
|
||||
|
||||
pServer = NimBLEDevice::createServer();
|
||||
pServer->setCallbacks(new ServerCallbacks(this));
|
||||
|
||||
// Service and characteristics for device information
|
||||
NimBLEService *pServDeviceInfo = pServer->createService("180A");
|
||||
NimBLECharacteristic *pModelCharacteristic = pServDeviceInfo->createCharacteristic("2A24", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
|
||||
pModelCharacteristic->setValue(modelName);
|
||||
NimBLECharacteristic *pSerialCharacteristic = pServDeviceInfo->createCharacteristic("2A25", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
|
||||
pSerialCharacteristic->setValue(ag->deviceId().c_str());
|
||||
NimBLECharacteristic *pFwCharacteristic = pServDeviceInfo->createCharacteristic("2A26", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
|
||||
pFwCharacteristic->setValue(ag->getVersion().c_str());
|
||||
NimBLECharacteristic *pManufCharacteristic = pServDeviceInfo->createCharacteristic("2A29", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC);
|
||||
pManufCharacteristic->setValue("AirGradient");
|
||||
|
||||
// Service and characteristics for wifi provisioning
|
||||
NimBLEService *pServProvisioning = pServer->createService(BLE_SERVICE_UUID);
|
||||
auto characteristicCallback = new CharacteristicCallbacks(this);
|
||||
NimBLECharacteristic *pCredentialCharacteristic =
|
||||
pServProvisioning->createCharacteristic(BLE_CRED_CHAR_UUID,
|
||||
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC |
|
||||
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY);
|
||||
pCredentialCharacteristic->setCallbacks(characteristicCallback);
|
||||
NimBLECharacteristic *pScanCharacteristic =
|
||||
pServProvisioning->createCharacteristic(BLE_SCAN_CHAR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::NOTIFY);
|
||||
pScanCharacteristic->setCallbacks(characteristicCallback);
|
||||
|
||||
// Start services
|
||||
pServProvisioning->start();
|
||||
pServDeviceInfo->start();
|
||||
|
||||
// Advertise
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
// Format advertising data
|
||||
String mdata;
|
||||
mdata += (char)0xFF;
|
||||
mdata += (char)0xFF;
|
||||
mdata += modelName;
|
||||
mdata += '#';
|
||||
mdata += ag->deviceId();
|
||||
pAdvertising->setManufacturerData(mdata.c_str());
|
||||
// Start advertise
|
||||
pAdvertising->start();
|
||||
bleServerRunning = true;
|
||||
|
||||
// Create event group
|
||||
bleEventGroup = xEventGroupCreate();
|
||||
if (bleEventGroup == NULL) {
|
||||
Serial.println("Failed to create BLE event group!");
|
||||
// This case is very unlikely
|
||||
}
|
||||
|
||||
Serial.println("Provision by BLE ready");
|
||||
}
|
||||
|
||||
void WifiConnector::stopBLE() {
|
||||
if (bleServerRunning) {
|
||||
Serial.println("Stopping BLE");
|
||||
NimBLEDevice::deinit();
|
||||
}
|
||||
bleServerRunning = false;
|
||||
}
|
||||
|
||||
//
|
||||
// BLE innerclass implementation
|
||||
//
|
||||
|
||||
WifiConnector::ServerCallbacks::ServerCallbacks(WifiConnector* parent)
|
||||
: parent(parent) {}
|
||||
|
||||
void WifiConnector::ServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) {
|
||||
Serial.printf("Client address: %s\n", connInfo.getAddress().toString().c_str());
|
||||
parent->bleClientConnected = true;
|
||||
NimBLEDevice::stopAdvertising();
|
||||
}
|
||||
|
||||
void WifiConnector::ServerCallbacks::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) {
|
||||
Serial.printf("Client disconnected - start advertising\n");
|
||||
NimBLEDevice::startAdvertising();
|
||||
parent->bleClientConnected = false;
|
||||
}
|
||||
|
||||
void WifiConnector::ServerCallbacks::onAuthenticationComplete(NimBLEConnInfo& connInfo) {
|
||||
Serial.println("\n========== PAIRING COMPLETE ==========");
|
||||
Serial.printf("Peer Address: %s\n", connInfo.getAddress().toString().c_str());
|
||||
Serial.printf("Encrypted: %s\n", connInfo.isEncrypted() ? "YES" : "NO");
|
||||
Serial.printf("Authenticated: %s\n", connInfo.isAuthenticated() ? "YES" : "NO");
|
||||
Serial.printf("Key Size: %d bits\n", connInfo.getSecKeySize() * 8);
|
||||
Serial.println("======================================\n");
|
||||
}
|
||||
|
||||
WifiConnector::CharacteristicCallbacks::CharacteristicCallbacks(WifiConnector* parent)
|
||||
: parent(parent) {}
|
||||
|
||||
void WifiConnector::CharacteristicCallbacks::onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) {
|
||||
Serial.printf("%s : onRead(), value: %s\n", pCharacteristic->getUUID().toString().c_str(),
|
||||
pCharacteristic->getValue().c_str());
|
||||
}
|
||||
|
||||
void WifiConnector::CharacteristicCallbacks::onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) {
|
||||
Serial.printf("%s : onWrite(), value: %s\n", pCharacteristic->getUUID().toString().c_str(),
|
||||
pCharacteristic->getValue().c_str());
|
||||
|
||||
auto bleCred = NimBLEUUID(BLE_CRED_CHAR_UUID);
|
||||
if (pCharacteristic->getUUID().equals(bleCred)) {
|
||||
if (!parent->wifiConnecting) {
|
||||
JSONVar root = JSON.parse(pCharacteristic->getValue().c_str());
|
||||
|
||||
String ssid = root["ssid"];
|
||||
String pass = root["password"];
|
||||
|
||||
WiFi.begin(ssid.c_str(), pass.c_str());
|
||||
xEventGroupSetBits(parent->bleEventGroup, BLE_CRED_BIT);
|
||||
}
|
||||
} else {
|
||||
xEventGroupSetBits(parent->bleEventGroup, BLE_SCAN_BIT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,49 @@
|
||||
#include "AgStateMachine.h"
|
||||
#include "AirGradient.h"
|
||||
#include "AgConfigure.h"
|
||||
#include "Libraries/WiFiManager/WiFiManager.h"
|
||||
#include "Main/PrintLog.h"
|
||||
#include "NimBLECharacteristic.h"
|
||||
#include "NimBLEService.h"
|
||||
#include "esp32-hal.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <NimBLEDevice.h>
|
||||
|
||||
// Provisioning Status Codes
|
||||
#define PROV_WIFI_CONNECT 0 // WiFi Connect
|
||||
#define PROV_CONNECTING_TO_SERVER 1 // Connecting to server
|
||||
#define PROV_SERVER_REACHABLE 2 // Server reachable
|
||||
#define PROV_MONITOR_CONFIGURED 3 // Monitor configured properly on dashboard
|
||||
|
||||
// Provisioning Error Codes
|
||||
#define PROV_ERR_WIFI_CONNECT_FAILED 10 // Failed to connect to WiFi
|
||||
#define PROV_ERR_SERVER_UNREACHABLE 11 // Server unreachable
|
||||
#define PROV_ERR_GET_MONITOR_CONFIG_FAILED 12 // Failed to get monitor configuration from dashboard
|
||||
#define PROV_ERR_MONITOR_NOT_REGISTERED 13 // Monitor is not registered on dashboard
|
||||
|
||||
class WifiConnector : public PrintLog {
|
||||
public:
|
||||
enum class ProvisionMethod {
|
||||
Unknown = 0,
|
||||
WiFi,
|
||||
BLE
|
||||
};
|
||||
|
||||
struct WiFiNetwork {
|
||||
String ssid;
|
||||
int32_t rssi;
|
||||
bool open;
|
||||
};
|
||||
|
||||
private:
|
||||
AirGradient *ag;
|
||||
OledDisplay &disp;
|
||||
StateMachine &sm;
|
||||
Configuration &config;
|
||||
NimBLEServer *pServer;
|
||||
|
||||
EventGroupHandle_t bleEventGroup;
|
||||
|
||||
String ssid;
|
||||
void *wifi = NULL;
|
||||
@@ -22,16 +55,51 @@ private:
|
||||
uint32_t lastRetry;
|
||||
bool hasPortalConfig = false;
|
||||
bool connectorTimeout = false;
|
||||
bool bleServerRunning = false;
|
||||
bool bleClientConnected = false;
|
||||
bool wifiConnecting = false;
|
||||
ProvisionMethod provisionMethod = ProvisionMethod::Unknown;
|
||||
|
||||
bool wifiClientConnected(void);
|
||||
bool isBleClientConnected();
|
||||
int scanAndFilterWiFi(WiFiNetwork networks[], int maxResults);
|
||||
String buildPaginatedWiFiJSON(WiFiNetwork networks[], int totalCount,
|
||||
int page, int batchSize, int totalPages);
|
||||
void handleBleScanRequest();
|
||||
|
||||
// BLE server handler
|
||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
public:
|
||||
explicit ServerCallbacks(WifiConnector *parent);
|
||||
void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override;
|
||||
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override;
|
||||
void onAuthenticationComplete(NimBLEConnInfo &connInfo) override;
|
||||
|
||||
private:
|
||||
WifiConnector *parent;
|
||||
};
|
||||
|
||||
// BLE Characteristics handler
|
||||
class CharacteristicCallbacks : public NimBLECharacteristicCallbacks {
|
||||
public:
|
||||
explicit CharacteristicCallbacks(WifiConnector *parent);
|
||||
void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
|
||||
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
|
||||
private:
|
||||
WifiConnector *parent;
|
||||
};
|
||||
|
||||
|
||||
public:
|
||||
void setAirGradient(AirGradient *ag);
|
||||
|
||||
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config);
|
||||
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration &config);
|
||||
~WifiConnector();
|
||||
|
||||
bool connect(void);
|
||||
void setupProvisionByPortal(WiFiManagerParameter *disableCloudParam, WiFiManagerParameter *disableCloudInfo);
|
||||
void setupProvisionByBLE(const char *modelName);
|
||||
void stopBLE();
|
||||
bool connect(String modelName = "");
|
||||
void disconnect(void);
|
||||
void handle(void);
|
||||
void _wifiApCallback(void);
|
||||
@@ -39,6 +107,7 @@ public:
|
||||
void _wifiSaveParamCallback(void);
|
||||
bool _wifiConfigPortalActive(void);
|
||||
void _wifiTimeoutCallback(void);
|
||||
void _wifiStop();
|
||||
void _wifiProcess();
|
||||
bool isConnected(void);
|
||||
void reset(void);
|
||||
@@ -47,8 +116,11 @@ public:
|
||||
bool hasConfigurated(void);
|
||||
bool isConfigurePorttalTimeout(void);
|
||||
|
||||
const char* defaultSsid = "airgradient";
|
||||
const char* defaultPassword = "cleanair";
|
||||
|
||||
void bleNotifyStatus(int status);
|
||||
|
||||
const char *defaultSsid = "airgradient";
|
||||
const char *defaultPassword = "cleanair";
|
||||
void setDefault(void);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#include "Main/utils.h"
|
||||
|
||||
#ifndef GIT_VERSION
|
||||
#define GIT_VERSION "3.3.0-snap"
|
||||
#define GIT_VERSION "3.3.9-snap"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -177,9 +177,9 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Check that Airgradient object is OPEN_AIR
|
||||
*
|
||||
* @return true
|
||||
* @return false
|
||||
*
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
bool isOpenAir(void);
|
||||
|
||||
|
||||
Submodule src/Libraries/airgradient-client updated: 774f8b70e6...c23bb2ceac
Submodule src/Libraries/airgradient-ota updated: 24d2dc537c...7b103e9073
@@ -27,7 +27,7 @@ enum BoardType {
|
||||
|
||||
/**
|
||||
* @brief Board definitions
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct BoardDef {
|
||||
/** Board Support CO2 SenseS8 */
|
||||
|
||||
@@ -72,6 +72,36 @@ void StatusLed::setToggle(void) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void StatusLed::setStep(void) {
|
||||
static uint8_t step = 0;
|
||||
|
||||
// Pattern definition
|
||||
const bool pattern[] = {
|
||||
true, // 0: ON
|
||||
false, // 1: OFF
|
||||
true, // 2: ON
|
||||
false, // 3: OFF
|
||||
false, // 4: OFF
|
||||
false, // 5: OFF
|
||||
false, // 6: OFF
|
||||
false, // 7: OFF
|
||||
false, // 8: OFF
|
||||
false // 9: OFF
|
||||
};
|
||||
|
||||
if (pattern[step]) {
|
||||
this->setOn();
|
||||
} else {
|
||||
this->setOff();
|
||||
}
|
||||
|
||||
step++;
|
||||
if (step >= sizeof(pattern)) {
|
||||
step = 0; // restart pattern
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current LED state
|
||||
*
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/**
|
||||
* @brief The class define how to handle the LED
|
||||
*
|
||||
*
|
||||
*/
|
||||
class StatusLed {
|
||||
public:
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
void setOn(void);
|
||||
void setOff(void);
|
||||
void setToggle(void);
|
||||
void setStep(void);
|
||||
State getState(void);
|
||||
String toString(StatusLed::State state);
|
||||
|
||||
|
||||
@@ -260,13 +260,14 @@ bool MqttClient::connect(String id) {
|
||||
connected = false;
|
||||
if (user.isEmpty()) {
|
||||
logInfo("Connect without auth");
|
||||
if(CLIENT()->connect(id.c_str())) {
|
||||
connected = true;
|
||||
}
|
||||
return connected;
|
||||
connected = CLIENT()->connect(id.c_str());
|
||||
} else {
|
||||
logInfo("Connect with auth");
|
||||
connected = CLIENT()->connect(id.c_str(), user.c_str(), password.c_str());
|
||||
}
|
||||
return CLIENT()->connect(id.c_str(), user.c_str(), password.c_str());
|
||||
return connected;
|
||||
}
|
||||
|
||||
void MqttClient::handle(void) {
|
||||
if (isBegin == false) {
|
||||
return;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class PMS5003TBase
|
||||
{
|
||||
private:
|
||||
|
||||
|
||||
public:
|
||||
PMS5003TBase();
|
||||
~PMS5003TBase();
|
||||
|
||||
@@ -131,6 +131,22 @@ 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
|
||||
@@ -152,6 +168,11 @@ 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;
|
||||
|
||||
@@ -18,6 +18,10 @@ 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);
|
||||
@@ -32,6 +36,7 @@ public:
|
||||
int getTvocLearningOffset(void);
|
||||
|
||||
private:
|
||||
bool onPause = false;
|
||||
bool onConditioning = true;
|
||||
bool ready = false;
|
||||
bool _isBegin = false;
|
||||
|
||||
Reference in New Issue
Block a user