Compare commits
394 Commits
Author | SHA1 | Date | |
---|---|---|---|
831c844c24 | |||
060a7f6815 | |||
d8eb6b3c1a | |||
969858b5cb | |||
09b5805686 | |||
b09b753339 | |||
ddb3dba131 | |||
e780b0ace6 | |||
e82da5401e | |||
50a98acde4 | |||
7049d21a41 | |||
d5cdeaa9f3 | |||
09207c6923 | |||
0a64424196 | |||
5b38ca222b | |||
9ee35341a5 | |||
cec0514444 | |||
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 | |||
baf80ce250 | |||
80100e2475 | |||
d9c3fc6ec4 | |||
67d377a514 | |||
fff982f35f | |||
86cd90b94a | |||
656509c74d | |||
01f83cb02e | |||
5c9c25c6b5 | |||
9291598209 | |||
429adb5e5e | |||
4e651afc8c | |||
859abbe177 | |||
f079bb30d2 | |||
070a103234 | |||
ef87cde9d6 | |||
ea5e23b307 | |||
c2a26e78a0 | |||
0297059e91 | |||
30622fca99 | |||
7c2aa35e4f | |||
e93009f31c | |||
26db6372cd | |||
d94ebbc570 | |||
299234ac40 | |||
76b2b3f940 | |||
bf09b746c7 | |||
b5c67cb0b1 | |||
5f40a327b3 | |||
66b0c63de5 | |||
cc3228f49a | |||
8728589ca1 | |||
4b356920c2 | |||
c94b886360 | |||
e056e44917 | |||
3b00fa69b8 | |||
e0720ac580 | |||
0861c2dcaa | |||
59fc0c409b | |||
6d63fdf643 | |||
033358e2c2 | |||
47034f62b4 | |||
71a21ce7e6 | |||
3f5e5eebbb | |||
f9be400a5d | |||
54808ac076 | |||
063bb2a227 | |||
93f79173b2 | |||
615c2389e7 | |||
f972637cca | |||
4c7e72b8e7 | |||
d4b4f51c3c | |||
1c42ff083d | |||
17d2e62b15 | |||
38aebeb50a | |||
b0f5263829 | |||
3226c14b6d | |||
0e26aa1b5d | |||
830f652bf9 | |||
bd9dbec663 | |||
29d701780a | |||
15869be234 | |||
4b09b98524 | |||
afd498074b | |||
e851d0781c | |||
2c27c6904c | |||
03f1b969c2 | |||
85ba13de12 | |||
6ec545b00e | |||
05dbe60db2 | |||
d2ee3a5d24 | |||
1839664137 | |||
154f3ecf8a | |||
b75e40b800 | |||
84a358291b | |||
0e41b2d630 | |||
3e48a562e7 | |||
f0c4df42b7 | |||
a50e1e2472 | |||
c8f0e6a0d2 | |||
1537d5d480 | |||
32c78f6018 | |||
c5c0dae4bb | |||
4bb97fc8be | |||
a28931493a | |||
af16c1c060 | |||
1666923ab3 | |||
89475ddf95 | |||
20db9d699b | |||
88c2437907 | |||
e9b27185b4 | |||
4534f7319a | |||
c842346724 | |||
92e74feabd | |||
cc0fd88068 | |||
56809a412c | |||
6a83743e2a | |||
faaf051e39 | |||
5bc1821ef9 | |||
280ea5e997 | |||
e95627ece6 | |||
80b9ae11d8 | |||
1937e3d59e | |||
107fb21331 | |||
ccc1ab463a | |||
38e792b88d | |||
aeee0cad01 | |||
401326d00d | |||
fb0dcad54d | |||
3556e4a96a | |||
283646a699 | |||
6312612ada | |||
c1f22674e2 | |||
40d38a75d8 | |||
39ef69cbdf | |||
3473e30e2e | |||
566f8a63b4 | |||
9e4d52454b | |||
5f5e985309 | |||
d638573ca7 | |||
4c165b31f5 | |||
2be91b3968 | |||
3ca2d1d208 | |||
aad12fc868 | |||
79fbd901bd | |||
3644dc43fe | |||
03fa62d8f0 | |||
902a768f28 | |||
1de9344f43 | |||
46f6309b77 | |||
a6b48acb41 | |||
1b4d89e1a1 | |||
0d2b0fb657 | |||
9f08af44b0 | |||
6b661cdeb7 | |||
dc299c4b54 | |||
2f595b4e41 | |||
a30535f75f | |||
a513943cba | |||
96bb6952fb | |||
10653bfe26 | |||
c7f89fa7b7 | |||
b11c461b60 | |||
404c14aad2 | |||
bfbae680fd | |||
3ae5982380 | |||
db2c2ef052 | |||
593547cdbe | |||
673c46950d | |||
ae0b4038d4 | |||
cac0bd5355 | |||
3d6203dabf | |||
1db8fbefe9 | |||
d850d27dc1 | |||
f49e4a4b37 | |||
75f88b0009 | |||
c6961b3ca8 | |||
ade72ff3b8 | |||
9fbbea22ff | |||
7b0381dea3 | |||
5867d0f1d5 | |||
a98d77e0c3 | |||
641003f9d1 | |||
0275aee370 | |||
ea46b812c1 | |||
16c932962a | |||
f90b2e1a07 | |||
3a9bb16c09 | |||
bb754edc51 | |||
1d991b1004 | |||
3ebcc584a4 | |||
4d40ae421c | |||
3004a82e7e | |||
4af5ca2665 | |||
e6696f3d41 | |||
2b33823162 | |||
bf0768c7da | |||
33e2977eb4 | |||
85e779cfc2 | |||
4783684443 | |||
3b0c77ca4d | |||
eeba41f497 | |||
fd1f35f6d8 | |||
eb76eff403 | |||
4673999dda | |||
83aa6a4502 | |||
8a87b865e6 | |||
c3068be6e9 | |||
63bb5f8ddb | |||
8548d3e9f4 | |||
f7e1363da9 | |||
2ffe0a62aa | |||
2cda36ed0d | |||
7de2d0cc30 | |||
f478dd16c8 | |||
43ca0a2c2e | |||
84884d0c15 | |||
f36f860c2e | |||
e47a9057ea | |||
399b4ca1dc | |||
2e4f4643fa | |||
0ccf46c219 | |||
76a2f332d7 | |||
ed344d3e1a | |||
2082a2fa93 | |||
e145d32714 | |||
a2c19438c0 | |||
ac838efdb5 | |||
751d4e8380 | |||
6925b1ac9a | |||
77a23b4202 | |||
ea91cf9b6c | |||
467b3e8637 | |||
2a5cf78b68 | |||
9c09b82efd | |||
60d01c0d94 | |||
e7a91c53bc | |||
4e41fd5d71 | |||
fe4389bff4 | |||
9325830fad | |||
b86f0d45e3 | |||
210f0a5ff9 | |||
c841476ca4 | |||
359394af53 | |||
b8e10f473e | |||
cb511903ef | |||
ebb3f01dcd | |||
2e0ba26c97 | |||
c1a4758c6c | |||
0370a8aa15 | |||
863a37132a | |||
612317d976 | |||
8873bacf55 | |||
bf2388b121 | |||
b3918bd1fb | |||
2a6fce674e | |||
2f0663ced0 | |||
3adf58537a | |||
e10c9ff854 | |||
12c6ec9910 | |||
d108b63a57 | |||
6e212714fc | |||
866684eb30 | |||
9d01479406 | |||
20245f2110 | |||
3890919f54 | |||
76e40fea8c | |||
c4024f49fb | |||
ca5fc8d65b | |||
fd2cef153e | |||
507b958fdf | |||
335c29ebb1 | |||
2907d6f14e | |||
c8d5b546ed | |||
b7cfdc4c4d | |||
994d281e02 | |||
39470384e4 | |||
c25ba764bf | |||
826ff00f42 | |||
520550037d | |||
90f336dee7 | |||
0d39643e76 | |||
21232ec49d | |||
b7339de31f | |||
013fb94307 | |||
e16373a64d | |||
f929623443 | |||
59587ce2b7 | |||
9ec74450a5 | |||
28096e9faf | |||
682378a47c | |||
a1861be7b7 | |||
99ddd24432 | |||
29491e4cbe | |||
87cc3fc45f | |||
7471d8079a | |||
8b0fe967f1 | |||
6f1cef4e67 | |||
02b63ff816 | |||
228bf83e92 | |||
d3534cda52 | |||
aafaa42a68 | |||
2e9ff0d7dd | |||
244b7814a6 | |||
28d27ee8fd | |||
753f22923c | |||
c45901706f | |||
663836e277 | |||
d39e10908d | |||
c52962d628 | |||
6b65efd3d6 | |||
8bb87a75ef | |||
1afcca25a1 | |||
17238cff86 | |||
03e2afbf54 | |||
104d58a8c0 | |||
7a988ea6c1 | |||
54ed83cb89 | |||
e461b92c9f | |||
db21648e91 | |||
a9654506f5 | |||
63f653d5cd | |||
b049a23657 | |||
d6766ef68b | |||
6c3259b94b | |||
2df78e9066 | |||
81b13134d2 | |||
6d01366887 | |||
040bd28038 | |||
01943f594d | |||
01a69668cc | |||
812c2ab803 |
53
.github/workflows/check.yml
vendored
@ -17,11 +17,14 @@ jobs:
|
||||
- "esp32:esp32:esp32c3"
|
||||
include:
|
||||
- fqbn: "esp8266:esp8266:d1_mini"
|
||||
core: "esp8266:esp8266@3.1.2"
|
||||
core: "esp8266:esp8266"
|
||||
core_version: "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"
|
||||
@ -31,30 +34,30 @@ jobs:
|
||||
fqbn: "esp32:esp32:esp32c3"
|
||||
- example: "OneOpenAir"
|
||||
fqbn: "esp8266:esp8266:d1_mini"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- 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 }}'
|
||||
- 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: ./
|
||||
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
|
||||
|
||||
# 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
@ -3,3 +3,8 @@ build
|
||||
.vscode
|
||||
/.idea/
|
||||
.pio
|
||||
.cache
|
||||
.clangd
|
||||
logs
|
||||
gen_compile_commands.py
|
||||
compile_commands.json
|
||||
|
6
.gitmodules
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
[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
|
105
docs/howto-compile.md
Normal file
@ -0,0 +1,105 @@
|
||||
# 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"`)
|
||||
|
||||

|
||||
|
||||
2. Install AirGradient library
|
||||
|
||||
#### Version < 3.2.0
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
## 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)
|
||||
|
||||

|
||||
|
||||
2. Install esp8266 board on board manager with version **3.1.2** (Tools ➝ Board ➝ Boards Manager ➝ search for `"esp8266"`)
|
||||
|
||||

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

|
||||
|
||||
4. On tools tab, set board to `LOLIN(WEMOS) D1 R2 & mini`, and let other settings to default
|
||||
|
||||

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

|
||||
|
||||
## Possible Issues
|
||||
|
||||
### Linux (Debian)
|
||||
|
||||
ModuleNotFoundError: No module named ‘serial’
|
||||
|
||||

|
||||
|
||||
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 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.
|
||||
|
||||
**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
|
||||
|
||||
|
||||
|
BIN
docs/images/additional-board.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/images/ag-lib.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
docs/images/compiled-esp8266.png
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
docs/images/compiled.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
docs/images/esp32-board.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
docs/images/esp8266-board.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
docs/images/linux-failed.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
docs/images/settings-esp8266.png
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
docs/images/settings.png
Normal file
After Width: | Height: | Size: 106 KiB |
@ -2,7 +2,7 @@
|
||||
|
||||
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.
|
||||
|
||||
@ -41,90 +41,219 @@ You get the following response:
|
||||
"bootCount": 6,
|
||||
"ledMode": "pm",
|
||||
"firmware": "3.1.3",
|
||||
"model": "I-9PSL"
|
||||
"model": "I-9PSL",
|
||||
"monitorDisplayCompensatedValues": true
|
||||
}
|
||||
```
|
||||
|
||||
| Properties | Type | Explanation |
|
||||
|------------------|--------|--------------------------------------------------------------------|
|
||||
| `serialno` | String | Serial Number of the monitor |
|
||||
| `wifi` | Number | WiFi signal strength |
|
||||
| `pm01` | Number | PM1 in ug/m3 |
|
||||
| `pm02` | Number | PM2.5 in ug/m3 |
|
||||
| `pm10` | Number | PM10 in ug/m3 |
|
||||
| `pm02Compensated` | Number | PM2.5 in ug/m3 with correction applied (from fw version 3.1.4 onwards) |
|
||||
| `rco2` | Number | CO2 in ppm |
|
||||
| `pm003Count` | Number | Particle count per dL |
|
||||
| `atmp` | Number | Temperature in Degrees Celsius |
|
||||
| `atmpCompensated` | Number | Temperature in Degrees Celsius with correction applied |
|
||||
| `rhum` | Number | Relative Humidity |
|
||||
| `rhumCompensated` | Number | Relative Humidity with correction applied |
|
||||
| `tvocIndex` | Number | Senisiron VOC Index |
|
||||
| `tvocRaw` | Number | VOC raw value |
|
||||
| `noxIndex` | Number | Senisirion NOx Index |
|
||||
| `noxRaw` | Number | NOx raw value |
|
||||
| `boot` | Number | Counts every measurement cycle. Low boot counts indicate restarts. |
|
||||
| `bootCount` | Number | Same as boot property. Required for Home Assistant compatability. Will be depreciated. |
|
||||
| `ledMode` | String | Current configuration of the LED mode |
|
||||
| `firmware` | String | Current firmware version |
|
||||
| `model` | String | Current model name |
|
||||
| Properties | Type | Explanation |
|
||||
|-----------------------------------|---------|----------------------------------------------------------------------------------------|
|
||||
| `serialno` | String | Serial Number of the monitor |
|
||||
| `wifi` | Number | WiFi signal strength |
|
||||
| `pm01` | Number | PM1.0 in ug/m3 (atmospheric environment) |
|
||||
| `pm02` | Number | PM2.5 in ug/m3 (atmospheric environment) |
|
||||
| `pm10` | Number | PM10 in ug/m3 (atmospheric environment) |
|
||||
| `pm02Compensated` | Number | PM2.5 in ug/m3 with correction applied (from fw version 3.1.4 onwards) |
|
||||
| `pm01Standard` | Number | PM1.0 in ug/m3 (standard particle) |
|
||||
| `pm02Standard` | Number | PM2.5 in ug/m3 (standard particle) |
|
||||
| `pm10Standard` | Number | PM10 in ug/m3 (standard particle) |
|
||||
| `rco2` | Number | CO2 in ppm |
|
||||
| `pm003Count` | Number | Particle count 0.3um per dL |
|
||||
| `pm005Count` | Number | Particle count 0.5um per dL |
|
||||
| `pm01Count` | Number | Particle count 1.0um per dL |
|
||||
| `pm02Count` | Number | Particle count 2.5um per dL |
|
||||
| `pm50Count` | Number | Particle count 5.0um per dL (only for indoor monitor) |
|
||||
| `pm10Count` | Number | Particle count 10um per dL (only for indoor monitor) |
|
||||
| `atmp` | Number | Temperature in Degrees Celsius |
|
||||
| `atmpCompensated` | Number | Temperature in Degrees Celsius with correction applied |
|
||||
| `rhum` | Number | Relative Humidity |
|
||||
| `rhumCompensated` | Number | Relative Humidity with correction applied |
|
||||
| `tvocIndex` | Number | Senisiron VOC Index |
|
||||
| `tvocRaw` | Number | VOC raw value |
|
||||
| `noxIndex` | Number | Senisirion NOx Index |
|
||||
| `noxRaw` | Number | NOx raw value |
|
||||
| `boot` | Number | Counts every measurement cycle. Low boot counts indicate restarts. |
|
||||
| `bootCount` | Number | Same as boot property. Required for Home Assistant compatability. (deprecated soon!) |
|
||||
| `ledMode` | String | Current configuration of the LED mode |
|
||||
| `firmware` | String | Current firmware version |
|
||||
| `model` | String | Current model name |
|
||||
|
||||
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)
|
||||
With the path "/config" you can get the current configuration.
|
||||
### Get Configuration Parameters (GET)
|
||||
|
||||
"/config" path returns the current configuration of the monitor.
|
||||
|
||||
```json
|
||||
{
|
||||
"country": "US",
|
||||
"country": "TH",
|
||||
"pmStandard": "ugm3",
|
||||
"ledBarMode": "pm",
|
||||
"displayMode": "on",
|
||||
"abcDays": 30,
|
||||
"abcDays": 7,
|
||||
"tvocLearningOffset": 12,
|
||||
"noxLearningOffset": 12,
|
||||
"mqttBrokerUrl": "",
|
||||
"temperatureUnit": "f",
|
||||
"configurationControl": "both",
|
||||
"postDataToAirGradient": true
|
||||
"httpDomain": "",
|
||||
"temperatureUnit": "c",
|
||||
"configurationControl": "local",
|
||||
"postDataToAirGradient": true,
|
||||
"ledBarBrightness": 100,
|
||||
"displayBrightness": 100,
|
||||
"offlineMode": false,
|
||||
"model": "I-9PSL",
|
||||
"monitorDisplayCompensatedValues": true,
|
||||
"corrections": {
|
||||
"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.
|
||||
Configuration parameters can be changed with a PUT request to the monitor, e.g.
|
||||
|
||||
Example to force CO2 calibration
|
||||
|
||||
```curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config ```
|
||||
```bash
|
||||
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
|
||||
```
|
||||
|
||||
Example to set monitor to Celsius
|
||||
|
||||
```curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config ```
|
||||
```bash
|
||||
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 configurations 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.
|
||||
### Avoiding Conflicts with Configuration on AirGradient Server
|
||||
|
||||
#### Configuration Parameters (GET/PUT)
|
||||
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.
|
||||
|
||||
| Properties | Description | Type | Accepted Values | Example |
|
||||
|-------------------------|:-------------------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|
|
||||
| `country` | Country where the device is. | String | Country code as [ALPHA-2 notation](https://www.iban.com/country-codes) | {"country": "TH"} |
|
||||
| `model` | Hardware identifier (only GET). | String | I-9PSL-DE | {"model": "I-9PSL-DE"} |
|
||||
| `pmStandard` | Particle matter standard used on the display. | String | `ugm3`: ug/m3 <br> `us-aqi`: USAQI | {"pmStandard": "ugm3"} |
|
||||
| `ledBarMode` | Mode in which the led bar can be set. | String | `co2`: LED bar displays CO2 <br>`pm`: LED bar displays PM <br>`off`: Turn off LED bar | {"ledBarMode": "off"} |
|
||||
| `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"} |
|
||||
| `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} |
|
||||
| `co2CalibrationRequested` | Can be set to trigger a calibration. | Boolean | `true`: CO2 calibration (400ppm) will be triggered | {"co2CalibrationRequested": true} |
|
||||
| `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} |
|
||||
| `offlineMode` | Set monitor to run without WiFi. | Boolean | `false`: Disabled (default) <br> `true`: Enabled | {"offlineMode": true} |
|
||||
### Configuration Parameters (GET/PUT)
|
||||
|
||||
| Properties | Description | Type | Accepted Values | Example |
|
||||
|-----------------------------------|:-----------------------------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
|
||||
| `country` | Country where the device is. | String | Country code as [ALPHA-2 notation](https://www.iban.com/country-codes) | `{"country": "TH"}` |
|
||||
| `model` | Hardware identifier (only GET). | String | I-9PSL-DE | `{"model": "I-9PSL-DE"}` |
|
||||
| `pmStandard` | Particle matter standard used on the display. | String | `ugm3`: ug/m3 <br> `us-aqi`: USAQI | `{"pmStandard": "ugm3"}` |
|
||||
| `ledBarMode` | Mode in which the led bar can be set. | String | `co2`: LED bar displays CO2 <br>`pm`: LED bar displays PM <br>`off`: Turn off LED bar | `{"ledBarMode": "off"}` |
|
||||
| `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"}` |
|
||||
| `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}` |
|
||||
| `co2CalibrationRequested` | Can be set to trigger a calibration. | Boolean | `true`: CO2 calibration (400ppm) will be triggered | `{"co2CalibrationRequested": true}` |
|
||||
| `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_ |
|
||||
|
||||
|
||||
**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
|
||||
|
||||
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.
|
||||
|
||||
Example correction configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"corrections": {
|
||||
"pm02": {
|
||||
"correctionAlgorithm": "<Option In String>",
|
||||
"slr": {
|
||||
"intercept": 0,
|
||||
"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_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
|
||||
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
|
||||
|
||||
**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
|
||||
|
||||
**Examples**:
|
||||
|
||||
- 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
|
||||
|
||||
```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
|
||||
|
||||
```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}}}}'
|
||||
```
|
@ -12,10 +12,8 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
|
||||
Build Instructions:
|
||||
https://www.airgradient.com/documentation/diy-v4/
|
||||
|
||||
Please make sure you have esp8266 board manager installed. Tested with
|
||||
version 3.1.2.
|
||||
|
||||
Set board to "LOLIN(WEMOS) D1 R2 & mini"
|
||||
Compile Instructions:
|
||||
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
|
||||
|
||||
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||
can be set through the AirGradient dashboard.
|
||||
@ -49,14 +47,13 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
||||
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
|
||||
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
|
||||
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 2000 /** ms */
|
||||
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
|
||||
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
||||
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
|
||||
|
||||
static AirGradient ag(DIY_BASIC);
|
||||
static Configuration configuration(Serial);
|
||||
static AgApiClient apiClient(Serial, configuration);
|
||||
static Measurements measurements;
|
||||
static Measurements measurements(configuration);
|
||||
static OledDisplay oledDisplay(configuration, measurements, Serial);
|
||||
static StateMachine stateMachine(oledDisplay, Serial, measurements,
|
||||
configuration);
|
||||
@ -68,8 +65,6 @@ static LocalServer localServer(Serial, openMetrics, measurements, configuration,
|
||||
wifiConnector);
|
||||
static MqttClient mqttClient(Serial);
|
||||
|
||||
static int pmFailCount = 0;
|
||||
static int getCO2FailCount = 0;
|
||||
static AgFirmwareMode fwMode = FW_MODE_I_BASIC_40PS;
|
||||
|
||||
static String fwNewVersion;
|
||||
@ -91,6 +86,8 @@ static void wdgFeedUpdate(void);
|
||||
static bool sgp41Init(void);
|
||||
static void wifiFactoryConfigure(void);
|
||||
static void mqttHandle(void);
|
||||
static int calculateMaxPeriod(int updateInterval);
|
||||
static void setMeasurementMaxPeriod();
|
||||
|
||||
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
|
||||
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
|
||||
@ -125,12 +122,17 @@ 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");
|
||||
|
||||
/** Init sensor */
|
||||
boardInit();
|
||||
setMeasurementMaxPeriod();
|
||||
|
||||
// Uncomment below line to print every measurements reading update
|
||||
// measurements.setDebug(true);
|
||||
|
||||
/** Connecting wifi */
|
||||
bool connectToWifi = false;
|
||||
@ -146,9 +148,12 @@ void setup() {
|
||||
initMqtt();
|
||||
sendDataToAg();
|
||||
|
||||
apiClient.fetchServerConfiguration();
|
||||
if (configuration.getConfigurationControl() !=
|
||||
ConfigurationControl::ConfigurationControlLocal) {
|
||||
apiClient.fetchServerConfiguration();
|
||||
}
|
||||
configSchedule.update();
|
||||
if (apiClient.isFetchConfigureFailed()) {
|
||||
if (apiClient.isFetchConfigurationFailed()) {
|
||||
if (apiClient.isNotAvailableOnDashboard()) {
|
||||
stateMachine.displaySetAddToDashBoard();
|
||||
stateMachine.displayHandle(
|
||||
@ -207,11 +212,7 @@ void loop() {
|
||||
tvocSchedule.run();
|
||||
}
|
||||
|
||||
/** Auto reset watchdog timer if offline mode or postDataToAirGradient */
|
||||
if (configuration.isOfflineMode() ||
|
||||
(configuration.isPostDataToAirGradient() == false)) {
|
||||
watchdogFeedSchedule.run();
|
||||
}
|
||||
watchdogFeedSchedule.run();
|
||||
|
||||
/** Check for handle WiFi reconnect */
|
||||
wifiConnector.handle();
|
||||
@ -235,17 +236,16 @@ void loop() {
|
||||
}
|
||||
|
||||
static void co2Update(void) {
|
||||
if (!configuration.hasSensorS8) {
|
||||
// Device don't have S8 sensor
|
||||
return;
|
||||
}
|
||||
|
||||
int value = ag.s8.getCo2();
|
||||
if (utils::isValidCO2(value)) {
|
||||
measurements.CO2 = value;
|
||||
getCO2FailCount = 0;
|
||||
Serial.printf("CO2 (ppm): %d\r\n", measurements.CO2);
|
||||
measurements.update(Measurements::CO2, value);
|
||||
} else {
|
||||
getCO2FailCount++;
|
||||
Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount);
|
||||
if (getCO2FailCount >= 3) {
|
||||
measurements.CO2 = utils::getInvalidCO2();
|
||||
}
|
||||
measurements.update(Measurements::CO2, utils::getInvalidCO2());
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,18 +267,23 @@ static void mdnsInit(void) {
|
||||
}
|
||||
|
||||
static void initMqtt(void) {
|
||||
if (mqttClient.begin(configuration.getMqttBrokerUri())) {
|
||||
Serial.println("Setup connect to MQTT broker successful");
|
||||
String mqttUri = configuration.getMqttBrokerUri();
|
||||
if (mqttUri.isEmpty()) {
|
||||
Serial.println(
|
||||
"MQTT is not configured, skipping initialization of MQTT client");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mqttClient.begin(mqttUri)) {
|
||||
Serial.println("Successfully connected to MQTT broker");
|
||||
} else {
|
||||
Serial.println("setup Connect to MQTT broker failed");
|
||||
Serial.println("Connection to MQTT broker failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void wdgFeedUpdate(void) {
|
||||
ag.watchdog.reset();
|
||||
Serial.println();
|
||||
Serial.println("Offline mode or isPostToAirGradient = false: watchdog reset");
|
||||
Serial.println();
|
||||
Serial.println("External watchdog feed!");
|
||||
}
|
||||
|
||||
static bool sgp41Init(void) {
|
||||
@ -313,8 +318,7 @@ static void mqttHandle(void) {
|
||||
}
|
||||
|
||||
if (mqttClient.isConnected()) {
|
||||
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(),
|
||||
&ag, &configuration);
|
||||
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
|
||||
String topic = "airgradient/readings/" + ag.deviceId();
|
||||
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
|
||||
Serial.println("MQTT sync success");
|
||||
@ -329,7 +333,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);
|
||||
@ -413,6 +417,14 @@ 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();
|
||||
}
|
||||
@ -470,7 +482,7 @@ static void appDispHandler(void) {
|
||||
if (configuration.isOfflineMode() == false) {
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
state = AgStateMachineWiFiLost;
|
||||
} else if (apiClient.isFetchConfigureFailed()) {
|
||||
} else if (apiClient.isFetchConfigurationFailed()) {
|
||||
state = AgStateMachineSensorConfigFailed;
|
||||
if (apiClient.isNotAvailableOnDashboard()) {
|
||||
stateMachine.displaySetAddToDashBoard();
|
||||
@ -490,84 +502,103 @@ static void oledDisplaySchedule(void) {
|
||||
}
|
||||
|
||||
static void updateTvoc(void) {
|
||||
measurements.TVOC = ag.sgp41.getTvocIndex();
|
||||
measurements.TVOCRaw = ag.sgp41.getTvocRaw();
|
||||
measurements.NOx = ag.sgp41.getNoxIndex();
|
||||
measurements.NOxRaw = ag.sgp41.getNoxRaw();
|
||||
if (!configuration.hasSensorSGP) {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
Serial.printf("TVOC index: %d\r\n", measurements.TVOC);
|
||||
Serial.printf("TVOC raw: %d\r\n", measurements.TVOCRaw);
|
||||
Serial.printf("NOx index: %d\r\n", measurements.NOx);
|
||||
Serial.printf("NOx raw: %d\r\n", measurements.NOxRaw);
|
||||
measurements.update(Measurements::TVOC, ag.sgp41.getTvocIndex());
|
||||
measurements.update(Measurements::TVOCRaw, ag.sgp41.getTvocRaw());
|
||||
measurements.update(Measurements::NOx, ag.sgp41.getNoxIndex());
|
||||
measurements.update(Measurements::NOxRaw, ag.sgp41.getNoxRaw());
|
||||
}
|
||||
|
||||
static void updatePm(void) {
|
||||
if (ag.pms5003.isFailed() == false) {
|
||||
measurements.pm01_1 = ag.pms5003.getPm01Ae();
|
||||
measurements.pm25_1 = ag.pms5003.getPm25Ae();
|
||||
measurements.pm10_1 = ag.pms5003.getPm10Ae();
|
||||
measurements.pm03PCount_1 = ag.pms5003.getPm03ParticleCount();
|
||||
|
||||
Serial.println();
|
||||
Serial.printf("PM1 ug/m3: %d\r\n", measurements.pm01_1);
|
||||
Serial.printf("PM2.5 ug/m3: %d\r\n", measurements.pm25_1);
|
||||
Serial.printf("PM10 ug/m3: %d\r\n", measurements.pm10_1);
|
||||
Serial.printf("PM0.3 Count: %d\r\n", measurements.pm03PCount_1);
|
||||
pmFailCount = 0;
|
||||
if (ag.pms5003.connected()) {
|
||||
measurements.update(Measurements::PM01, ag.pms5003.getPm01Ae());
|
||||
measurements.update(Measurements::PM25, ag.pms5003.getPm25Ae());
|
||||
measurements.update(Measurements::PM10, ag.pms5003.getPm10Ae());
|
||||
measurements.update(Measurements::PM03_PC, ag.pms5003.getPm03ParticleCount());
|
||||
} else {
|
||||
pmFailCount++;
|
||||
Serial.printf("PMS read failed: %d\r\n", pmFailCount);
|
||||
if (pmFailCount >= 3) {
|
||||
measurements.pm01_1 = utils::getInvalidPMS();
|
||||
measurements.pm25_1 = utils::getInvalidPMS();
|
||||
measurements.pm10_1 = utils::getInvalidPMS();
|
||||
measurements.pm03PCount_1 = utils::getInvalidPMS();
|
||||
}
|
||||
measurements.update(Measurements::PM01, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM25, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM10, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue());
|
||||
}
|
||||
}
|
||||
|
||||
static void sendDataToServer(void) {
|
||||
/** Ignore send data to server if postToAirGradient disabled */
|
||||
if (configuration.isPostDataToAirGradient() == false ||
|
||||
configuration.isOfflineMode()) {
|
||||
/** Increment bootcount when send measurements data is scheduled */
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(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");
|
||||
return;
|
||||
}
|
||||
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(),
|
||||
&ag, &configuration);
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
ag.watchdog.reset();
|
||||
Serial.println();
|
||||
Serial.println(
|
||||
"Online mode and isPostToAirGradient = true: watchdog reset");
|
||||
Serial.println();
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
Serial.println("WiFi not connected, skipping data transmission to AG server");
|
||||
return;
|
||||
}
|
||||
|
||||
measurements.bootCount++;
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
Serial.println();
|
||||
Serial.println("Online mode and isPostToAirGradient = true");
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
|
||||
static void tempHumUpdate(void) {
|
||||
delay(100);
|
||||
if (ag.sht.measure()) {
|
||||
measurements.Temperature = ag.sht.getTemperature();
|
||||
measurements.Humidity = ag.sht.getRelativeHumidity();
|
||||
float temp = ag.sht.getTemperature();
|
||||
float rhum = ag.sht.getRelativeHumidity();
|
||||
|
||||
Serial.printf("Temperature in C: %0.2f\r\n", measurements.Temperature);
|
||||
Serial.printf("Relative Humidity: %d\r\n", measurements.Humidity);
|
||||
Serial.printf("Temperature compensated in C: %0.2f\r\n",
|
||||
measurements.Temperature);
|
||||
Serial.printf("Relative Humidity compensated: %d\r\n",
|
||||
measurements.Humidity);
|
||||
measurements.update(Measurements::Temperature, temp);
|
||||
measurements.update(Measurements::Humidity, rhum);
|
||||
|
||||
// Update compensation temperature and humidity for SGP41
|
||||
if (configuration.hasSensorSGP) {
|
||||
ag.sgp41.setCompensationTemperatureHumidity(measurements.Temperature,
|
||||
measurements.Humidity);
|
||||
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
|
||||
}
|
||||
} else {
|
||||
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
|
||||
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
|
||||
Serial.println("SHT read failed");
|
||||
measurements.Temperature = utils::getInvalidTemperature();
|
||||
measurements.Humidity = utils::getInvalidHumidity();
|
||||
}
|
||||
}
|
||||
|
||||
/* Set max period for each measurement type based on sensor update interval*/
|
||||
void setMeasurementMaxPeriod() {
|
||||
/// Max period for S8 sensors measurements
|
||||
measurements.maxPeriod(Measurements::CO2, calculateMaxPeriod(SENSOR_CO2_UPDATE_INTERVAL));
|
||||
/// Max period for SGP sensors measurements
|
||||
measurements.maxPeriod(Measurements::TVOC, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::TVOCRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::NOx, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::NOxRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
/// Max period for PMS sensors measurements
|
||||
measurements.maxPeriod(Measurements::PM25, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM01, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM10, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM03_PC, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
// Temperature and Humidity
|
||||
if (configuration.hasSensorSHT) {
|
||||
/// Max period for SHT sensors measurements
|
||||
measurements.maxPeriod(Measurements::Temperature,
|
||||
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::Humidity,
|
||||
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
|
||||
} else {
|
||||
/// Temp and hum data retrieved from PMS5003T sensor
|
||||
measurements.maxPeriod(Measurements::Temperature,
|
||||
calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::Humidity, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
}
|
||||
}
|
||||
|
||||
int calculateMaxPeriod(int updateInterval) {
|
||||
// 0.5 is 50% reduced interval for max period
|
||||
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval;
|
||||
}
|
@ -53,9 +53,8 @@ void LocalServer::_GET_metrics(void) {
|
||||
}
|
||||
|
||||
void LocalServer::_GET_measure(void) {
|
||||
server.send(
|
||||
200, "application/json",
|
||||
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
|
||||
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
||||
|
@ -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.isFetchConfigureFailed() ? "0" : "1");
|
||||
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
|
||||
|
||||
add_metric(
|
||||
"post_ok",
|
||||
@ -57,60 +57,70 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "dbm");
|
||||
add_metric_point("", String(wifiConnector.RSSI()));
|
||||
|
||||
if (config.hasSensorS8 && measure.CO2 >= 0) {
|
||||
add_metric("co2",
|
||||
"Carbon dioxide concentration as measured by the AirGradient S8 "
|
||||
"sensor, in parts per million",
|
||||
"gauge", "ppm");
|
||||
add_metric_point("", String(measure.CO2));
|
||||
}
|
||||
|
||||
// Initialize default invalid value for each measurements
|
||||
float _temp = utils::getInvalidTemperature();
|
||||
float _hum = utils::getInvalidHumidity();
|
||||
int pm01 = utils::getInvalidPMS();
|
||||
int pm25 = utils::getInvalidPMS();
|
||||
int pm10 = utils::getInvalidPMS();
|
||||
int pm03PCount = utils::getInvalidPMS();
|
||||
int pm01 = utils::getInvalidPmValue();
|
||||
int pm25 = utils::getInvalidPmValue();
|
||||
int pm10 = utils::getInvalidPmValue();
|
||||
int pm03PCount = utils::getInvalidPmValue();
|
||||
int co2 = utils::getInvalidCO2();
|
||||
int atmpCompensated = utils::getInvalidTemperature();
|
||||
int ahumCompensated = utils::getInvalidHumidity();
|
||||
int rhumCompensated = utils::getInvalidHumidity();
|
||||
int tvoc = utils::getInvalidVOC();
|
||||
int tvocRaw = utils::getInvalidVOC();
|
||||
int nox = utils::getInvalidNOx();
|
||||
int noxRaw = utils::getInvalidNOx();
|
||||
|
||||
if (config.hasSensorSHT) {
|
||||
_temp = measure.Temperature;
|
||||
_hum = measure.Humidity;
|
||||
_temp = measure.getFloat(Measurements::Temperature);
|
||||
_hum = measure.getFloat(Measurements::Humidity);
|
||||
atmpCompensated = _temp;
|
||||
ahumCompensated = _hum;
|
||||
rhumCompensated = _hum;
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
pm01 = measure.pm01_1;
|
||||
pm25 = measure.pm25_1;
|
||||
pm10 = measure.pm10_1;
|
||||
pm03PCount = measure.pm03PCount_1;
|
||||
pm01 = measure.get(Measurements::PM01);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
nox = measure.get(Measurements::NOx);
|
||||
noxRaw = measure.get(Measurements::NOxRaw);
|
||||
}
|
||||
|
||||
if (config.hasSensorS8) {
|
||||
co2 = measure.get(Measurements::CO2);
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
if (utils::isValidPMS(pm01)) {
|
||||
if (utils::isValidPm(pm01)) {
|
||||
add_metric("pm1",
|
||||
"PM1.0 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm01));
|
||||
}
|
||||
if (utils::isValidPMS(pm25)) {
|
||||
if (utils::isValidPm(pm25)) {
|
||||
add_metric("pm2d5",
|
||||
"PM2.5 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm25));
|
||||
}
|
||||
if (utils::isValidPMS(pm10)) {
|
||||
if (utils::isValidPm(pm10)) {
|
||||
add_metric("pm10",
|
||||
"PM10 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm10));
|
||||
}
|
||||
if (utils::isValidPMS03Count(pm03PCount)) {
|
||||
if (utils::isValidPm03Count(pm03PCount)) {
|
||||
add_metric("pm0d3",
|
||||
"PM0.3 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in number of particules per 100 milliliters",
|
||||
@ -120,36 +130,44 @@ String OpenMetrics::getPayload(void) {
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
if (utils::isValidVOC(measure.TVOC)) {
|
||||
if (utils::isValidVOC(tvoc)) {
|
||||
add_metric("tvoc_index",
|
||||
"The processed Total Volatile Organic Compounds (TVOC) index "
|
||||
"as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.TVOC));
|
||||
add_metric_point("", String(tvoc));
|
||||
}
|
||||
if (utils::isValidVOC(measure.TVOCRaw)) {
|
||||
if (utils::isValidVOC(tvocRaw)) {
|
||||
add_metric("tvoc_raw",
|
||||
"The raw input value to the Total Volatile Organic Compounds "
|
||||
"(TVOC) index as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.TVOCRaw));
|
||||
add_metric_point("", String(tvocRaw));
|
||||
}
|
||||
if (utils::isValidNOx(measure.NOx)) {
|
||||
if (utils::isValidNOx(nox)) {
|
||||
add_metric("nox_index",
|
||||
"The processed Nitrous Oxide (NOx) index as measured by the "
|
||||
"AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.NOx));
|
||||
add_metric_point("", String(nox));
|
||||
}
|
||||
if (utils::isValidNOx(measure.NOxRaw)) {
|
||||
if (utils::isValidNOx(noxRaw)) {
|
||||
add_metric("nox_raw",
|
||||
"The raw input value to the Nitrous Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.NOxRaw));
|
||||
add_metric_point("", String(noxRaw));
|
||||
}
|
||||
}
|
||||
|
||||
if (utils::isValidCO2(co2)) {
|
||||
add_metric("co2",
|
||||
"Carbon dioxide concentration as measured by the AirGradient S8 "
|
||||
"sensor, in parts per million",
|
||||
"gauge", "ppm");
|
||||
add_metric_point("", String(co2));
|
||||
}
|
||||
|
||||
if (utils::isValidTemperature(_temp)) {
|
||||
add_metric(
|
||||
"temperature",
|
||||
@ -173,12 +191,12 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(_hum));
|
||||
}
|
||||
if (utils::isValidHumidity(ahumCompensated)) {
|
||||
if (utils::isValidHumidity(rhumCompensated)) {
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the "
|
||||
"AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(ahumCompensated));
|
||||
add_metric_point("", String(rhumCompensated));
|
||||
}
|
||||
|
||||
response += "# EOF\n";
|
||||
|
@ -12,10 +12,8 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
|
||||
Build Instructions:
|
||||
https://www.airgradient.com/documentation/diy-v4/
|
||||
|
||||
Please make sure you have esp8266 board manager installed. Tested with
|
||||
version 3.1.2.
|
||||
|
||||
Set board to "LOLIN(WEMOS) D1 R2 & mini"
|
||||
Compile Instructions:
|
||||
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
|
||||
|
||||
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||
can be set through the AirGradient dashboard.
|
||||
@ -49,14 +47,13 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
||||
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
|
||||
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
|
||||
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 2000 /** ms */
|
||||
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
|
||||
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
||||
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
|
||||
|
||||
static AirGradient ag(DIY_PRO_INDOOR_V3_3);
|
||||
static Configuration configuration(Serial);
|
||||
static AgApiClient apiClient(Serial, configuration);
|
||||
static Measurements measurements;
|
||||
static Measurements measurements(configuration);
|
||||
static OledDisplay oledDisplay(configuration, measurements, Serial);
|
||||
static StateMachine stateMachine(oledDisplay, Serial, measurements,
|
||||
configuration);
|
||||
@ -68,8 +65,6 @@ static LocalServer localServer(Serial, openMetrics, measurements, configuration,
|
||||
wifiConnector);
|
||||
static MqttClient mqttClient(Serial);
|
||||
|
||||
static int pmFailCount = 0;
|
||||
static int getCO2FailCount = 0;
|
||||
static AgFirmwareMode fwMode = FW_MODE_I_33PS;
|
||||
|
||||
static String fwNewVersion;
|
||||
@ -91,6 +86,8 @@ static void wdgFeedUpdate(void);
|
||||
static bool sgp41Init(void);
|
||||
static void wifiFactoryConfigure(void);
|
||||
static void mqttHandle(void);
|
||||
static int calculateMaxPeriod(int updateInterval);
|
||||
static void setMeasurementMaxPeriod();
|
||||
|
||||
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
|
||||
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
|
||||
@ -125,12 +122,17 @@ 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");
|
||||
|
||||
/** Init sensor */
|
||||
boardInit();
|
||||
setMeasurementMaxPeriod();
|
||||
|
||||
// Uncomment below line to print every measurements reading update
|
||||
// measurements.setDebug(true);
|
||||
|
||||
/** Connecting wifi */
|
||||
bool connectToWifi = false;
|
||||
@ -146,9 +148,12 @@ void setup() {
|
||||
initMqtt();
|
||||
sendDataToAg();
|
||||
|
||||
apiClient.fetchServerConfiguration();
|
||||
if (configuration.getConfigurationControl() !=
|
||||
ConfigurationControl::ConfigurationControlLocal) {
|
||||
apiClient.fetchServerConfiguration();
|
||||
}
|
||||
configSchedule.update();
|
||||
if (apiClient.isFetchConfigureFailed()) {
|
||||
if (apiClient.isFetchConfigurationFailed()) {
|
||||
if (apiClient.isNotAvailableOnDashboard()) {
|
||||
stateMachine.displaySetAddToDashBoard();
|
||||
stateMachine.displayHandle(
|
||||
@ -205,11 +210,7 @@ void loop() {
|
||||
tvocSchedule.run();
|
||||
}
|
||||
|
||||
/** Auto reset watchdog timer if offline mode or postDataToAirGradient */
|
||||
if (configuration.isOfflineMode() ||
|
||||
(configuration.isPostDataToAirGradient() == false)) {
|
||||
watchdogFeedSchedule.run();
|
||||
}
|
||||
watchdogFeedSchedule.run();
|
||||
|
||||
/** Check for handle WiFi reconnect */
|
||||
wifiConnector.handle();
|
||||
@ -233,17 +234,16 @@ void loop() {
|
||||
}
|
||||
|
||||
static void co2Update(void) {
|
||||
if (!configuration.hasSensorS8) {
|
||||
// Device don't have S8 sensor
|
||||
return;
|
||||
}
|
||||
|
||||
int value = ag.s8.getCo2();
|
||||
if (utils::isValidCO2(value)) {
|
||||
measurements.CO2 = value;
|
||||
getCO2FailCount = 0;
|
||||
Serial.printf("CO2 (ppm): %d\r\n", measurements.CO2);
|
||||
measurements.update(Measurements::CO2, value);
|
||||
} else {
|
||||
getCO2FailCount++;
|
||||
Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount);
|
||||
if (getCO2FailCount >= 3) {
|
||||
measurements.CO2 = utils::getInvalidCO2();
|
||||
}
|
||||
measurements.update(Measurements::CO2, utils::getInvalidCO2());
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,10 +265,17 @@ static void mdnsInit(void) {
|
||||
}
|
||||
|
||||
static void initMqtt(void) {
|
||||
if (mqttClient.begin(configuration.getMqttBrokerUri())) {
|
||||
Serial.println("Setup connect to MQTT broker successful");
|
||||
String mqttUri = configuration.getMqttBrokerUri();
|
||||
if (mqttUri.isEmpty()) {
|
||||
Serial.println(
|
||||
"MQTT is not configured, skipping initialization of MQTT client");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mqttClient.begin(mqttUri)) {
|
||||
Serial.println("Successfully connected to MQTT broker");
|
||||
} else {
|
||||
Serial.println("setup Connect to MQTT broker failed");
|
||||
Serial.println("Connection to MQTT broker failed");
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,9 +340,7 @@ static void factoryConfigReset(void) {
|
||||
|
||||
static void wdgFeedUpdate(void) {
|
||||
ag.watchdog.reset();
|
||||
Serial.println();
|
||||
Serial.println("Offline mode or isPostToAirGradient = false: watchdog reset");
|
||||
Serial.println();
|
||||
Serial.println("External watchdog feed!");
|
||||
}
|
||||
|
||||
static bool sgp41Init(void) {
|
||||
@ -370,8 +375,7 @@ static void mqttHandle(void) {
|
||||
}
|
||||
|
||||
if (mqttClient.isConnected()) {
|
||||
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(),
|
||||
&ag, &configuration);
|
||||
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
|
||||
String topic = "airgradient/readings/" + ag.deviceId();
|
||||
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
|
||||
Serial.println("MQTT sync success");
|
||||
@ -386,7 +390,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);
|
||||
@ -465,6 +469,14 @@ 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();
|
||||
}
|
||||
@ -522,7 +534,7 @@ static void appDispHandler(void) {
|
||||
if (configuration.isOfflineMode() == false) {
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
state = AgStateMachineWiFiLost;
|
||||
} else if (apiClient.isFetchConfigureFailed()) {
|
||||
} else if (apiClient.isFetchConfigurationFailed()) {
|
||||
state = AgStateMachineSensorConfigFailed;
|
||||
if (apiClient.isNotAvailableOnDashboard()) {
|
||||
stateMachine.displaySetAddToDashBoard();
|
||||
@ -542,84 +554,103 @@ static void oledDisplaySchedule(void) {
|
||||
}
|
||||
|
||||
static void updateTvoc(void) {
|
||||
measurements.TVOC = ag.sgp41.getTvocIndex();
|
||||
measurements.TVOCRaw = ag.sgp41.getTvocRaw();
|
||||
measurements.NOx = ag.sgp41.getNoxIndex();
|
||||
measurements.NOxRaw = ag.sgp41.getNoxRaw();
|
||||
if (!configuration.hasSensorSGP) {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
Serial.printf("TVOC index: %d\r\n", measurements.TVOC);
|
||||
Serial.printf("TVOC raw: %d\r\n", measurements.TVOCRaw);
|
||||
Serial.printf("NOx index: %d\r\n", measurements.NOx);
|
||||
Serial.printf("NOx raw: %d\r\n", measurements.NOxRaw);
|
||||
measurements.update(Measurements::TVOC, ag.sgp41.getTvocIndex());
|
||||
measurements.update(Measurements::TVOCRaw, ag.sgp41.getTvocRaw());
|
||||
measurements.update(Measurements::NOx, ag.sgp41.getNoxIndex());
|
||||
measurements.update(Measurements::NOxRaw, ag.sgp41.getNoxRaw());
|
||||
}
|
||||
|
||||
static void updatePm(void) {
|
||||
if (ag.pms5003.isFailed() == false) {
|
||||
measurements.pm01_1 = ag.pms5003.getPm01Ae();
|
||||
measurements.pm25_1 = ag.pms5003.getPm25Ae();
|
||||
measurements.pm10_1 = ag.pms5003.getPm10Ae();
|
||||
measurements.pm03PCount_1 = ag.pms5003.getPm03ParticleCount();
|
||||
|
||||
Serial.println();
|
||||
Serial.printf("PM1 ug/m3: %d\r\n", measurements.pm01_1);
|
||||
Serial.printf("PM2.5 ug/m3: %d\r\n", measurements.pm25_1);
|
||||
Serial.printf("PM10 ug/m3: %d\r\n", measurements.pm10_1);
|
||||
Serial.printf("PM0.3 Count: %d\r\n", measurements.pm03PCount_1);
|
||||
pmFailCount = 0;
|
||||
if (ag.pms5003.connected()) {
|
||||
measurements.update(Measurements::PM01, ag.pms5003.getPm01Ae());
|
||||
measurements.update(Measurements::PM25, ag.pms5003.getPm25Ae());
|
||||
measurements.update(Measurements::PM10, ag.pms5003.getPm10Ae());
|
||||
measurements.update(Measurements::PM03_PC, ag.pms5003.getPm03ParticleCount());
|
||||
} else {
|
||||
pmFailCount++;
|
||||
Serial.printf("PMS read failed: %d\r\n", pmFailCount);
|
||||
if (pmFailCount >= 3) {
|
||||
measurements.pm01_1 = utils::getInvalidPMS();
|
||||
measurements.pm25_1 = utils::getInvalidPMS();
|
||||
measurements.pm10_1 = utils::getInvalidPMS();
|
||||
measurements.pm03PCount_1 = utils::getInvalidPMS();
|
||||
}
|
||||
measurements.update(Measurements::PM01, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM25, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM10, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue());
|
||||
}
|
||||
}
|
||||
|
||||
static void sendDataToServer(void) {
|
||||
/** Ignore send data to server if postToAirGradient disabled */
|
||||
if (configuration.isPostDataToAirGradient() == false ||
|
||||
configuration.isOfflineMode()) {
|
||||
/** Increment bootcount when send measurements data is scheduled */
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(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");
|
||||
return;
|
||||
}
|
||||
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(),
|
||||
&ag, &configuration);
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
ag.watchdog.reset();
|
||||
Serial.println();
|
||||
Serial.println(
|
||||
"Online mode and isPostToAirGradient = true: watchdog reset");
|
||||
Serial.println();
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
Serial.println("WiFi not connected, skipping data transmission to AG server");
|
||||
return;
|
||||
}
|
||||
|
||||
measurements.bootCount++;
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
Serial.println();
|
||||
Serial.println("Online mode and isPostToAirGradient = true");
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
|
||||
static void tempHumUpdate(void) {
|
||||
delay(100);
|
||||
if (ag.sht.measure()) {
|
||||
measurements.Temperature = ag.sht.getTemperature();
|
||||
measurements.Humidity = ag.sht.getRelativeHumidity();
|
||||
float temp = ag.sht.getTemperature();
|
||||
float rhum = ag.sht.getRelativeHumidity();
|
||||
|
||||
Serial.printf("Temperature in C: %0.2f\r\n", measurements.Temperature);
|
||||
Serial.printf("Relative Humidity: %d\r\n", measurements.Humidity);
|
||||
Serial.printf("Temperature compensated in C: %0.2f\r\n",
|
||||
measurements.Temperature);
|
||||
Serial.printf("Relative Humidity compensated: %d\r\n",
|
||||
measurements.Humidity);
|
||||
measurements.update(Measurements::Temperature, temp);
|
||||
measurements.update(Measurements::Humidity, rhum);
|
||||
|
||||
// Update compensation temperature and humidity for SGP41
|
||||
if (configuration.hasSensorSGP) {
|
||||
ag.sgp41.setCompensationTemperatureHumidity(measurements.Temperature,
|
||||
measurements.Humidity);
|
||||
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
|
||||
}
|
||||
} else {
|
||||
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
|
||||
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
|
||||
Serial.println("SHT read failed");
|
||||
measurements.Temperature = utils::getInvalidTemperature();
|
||||
measurements.Humidity = utils::getInvalidHumidity();
|
||||
}
|
||||
}
|
||||
|
||||
/* Set max period for each measurement type based on sensor update interval*/
|
||||
void setMeasurementMaxPeriod() {
|
||||
/// Max period for S8 sensors measurements
|
||||
measurements.maxPeriod(Measurements::CO2, calculateMaxPeriod(SENSOR_CO2_UPDATE_INTERVAL));
|
||||
/// Max period for SGP sensors measurements
|
||||
measurements.maxPeriod(Measurements::TVOC, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::TVOCRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::NOx, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::NOxRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
/// Max period for PMS sensors measurements
|
||||
measurements.maxPeriod(Measurements::PM25, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM01, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM10, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM03_PC, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
// Temperature and Humidity
|
||||
if (configuration.hasSensorSHT) {
|
||||
/// Max period for SHT sensors measurements
|
||||
measurements.maxPeriod(Measurements::Temperature,
|
||||
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::Humidity,
|
||||
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
|
||||
} else {
|
||||
/// Temp and hum data retrieved from PMS5003T sensor
|
||||
measurements.maxPeriod(Measurements::Temperature,
|
||||
calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::Humidity, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
}
|
||||
}
|
||||
|
||||
int calculateMaxPeriod(int updateInterval) {
|
||||
// 0.5 is 50% reduced interval for max period
|
||||
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval;
|
||||
}
|
@ -53,9 +53,8 @@ void LocalServer::_GET_metrics(void) {
|
||||
}
|
||||
|
||||
void LocalServer::_GET_measure(void) {
|
||||
server.send(
|
||||
200, "application/json",
|
||||
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
|
||||
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
||||
|
@ -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.isFetchConfigureFailed() ? "0" : "1");
|
||||
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
|
||||
|
||||
add_metric(
|
||||
"post_ok",
|
||||
@ -57,60 +57,70 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "dbm");
|
||||
add_metric_point("", String(wifiConnector.RSSI()));
|
||||
|
||||
if (config.hasSensorS8 && measure.CO2 >= 0) {
|
||||
add_metric("co2",
|
||||
"Carbon dioxide concentration as measured by the AirGradient S8 "
|
||||
"sensor, in parts per million",
|
||||
"gauge", "ppm");
|
||||
add_metric_point("", String(measure.CO2));
|
||||
}
|
||||
|
||||
// Initialize default invalid value for each measurements
|
||||
float _temp = utils::getInvalidTemperature();
|
||||
float _hum = utils::getInvalidHumidity();
|
||||
int pm01 = utils::getInvalidPMS();
|
||||
int pm25 = utils::getInvalidPMS();
|
||||
int pm10 = utils::getInvalidPMS();
|
||||
int pm03PCount = utils::getInvalidPMS();
|
||||
int pm01 = utils::getInvalidPmValue();
|
||||
int pm25 = utils::getInvalidPmValue();
|
||||
int pm10 = utils::getInvalidPmValue();
|
||||
int pm03PCount = utils::getInvalidPmValue();
|
||||
int co2 = utils::getInvalidCO2();
|
||||
int atmpCompensated = utils::getInvalidTemperature();
|
||||
int ahumCompensated = utils::getInvalidHumidity();
|
||||
int rhumCompensated = utils::getInvalidHumidity();
|
||||
int tvoc = utils::getInvalidVOC();
|
||||
int tvocRaw = utils::getInvalidVOC();
|
||||
int nox = utils::getInvalidNOx();
|
||||
int noxRaw = utils::getInvalidNOx();
|
||||
|
||||
if (config.hasSensorSHT) {
|
||||
_temp = measure.Temperature;
|
||||
_hum = measure.Humidity;
|
||||
_temp = measure.getFloat(Measurements::Temperature);
|
||||
_hum = measure.getFloat(Measurements::Humidity);
|
||||
atmpCompensated = _temp;
|
||||
ahumCompensated = _hum;
|
||||
rhumCompensated = _hum;
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
pm01 = measure.pm01_1;
|
||||
pm25 = measure.pm25_1;
|
||||
pm10 = measure.pm10_1;
|
||||
pm03PCount = measure.pm03PCount_1;
|
||||
pm01 = measure.get(Measurements::PM01);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
nox = measure.get(Measurements::NOx);
|
||||
noxRaw = measure.get(Measurements::NOxRaw);
|
||||
}
|
||||
|
||||
if (config.hasSensorS8) {
|
||||
co2 = measure.get(Measurements::CO2);
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
if (utils::isValidPMS(pm01)) {
|
||||
if (utils::isValidPm(pm01)) {
|
||||
add_metric("pm1",
|
||||
"PM1.0 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm01));
|
||||
}
|
||||
if (utils::isValidPMS(pm25)) {
|
||||
if (utils::isValidPm(pm25)) {
|
||||
add_metric("pm2d5",
|
||||
"PM2.5 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm25));
|
||||
}
|
||||
if (utils::isValidPMS(pm10)) {
|
||||
if (utils::isValidPm(pm10)) {
|
||||
add_metric("pm10",
|
||||
"PM10 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm10));
|
||||
}
|
||||
if (utils::isValidPMS03Count(pm03PCount)) {
|
||||
if (utils::isValidPm03Count(pm03PCount)) {
|
||||
add_metric("pm0d3",
|
||||
"PM0.3 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in number of particules per 100 milliliters",
|
||||
@ -120,36 +130,45 @@ String OpenMetrics::getPayload(void) {
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
if (utils::isValidVOC(measure.TVOC)) {
|
||||
if (utils::isValidVOC(tvoc)) {
|
||||
add_metric("tvoc_index",
|
||||
"The processed Total Volatile Organic Compounds (TVOC) index "
|
||||
"as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.TVOC));
|
||||
add_metric_point("", String(tvoc));
|
||||
}
|
||||
if (utils::isValidVOC(measure.TVOCRaw)) {
|
||||
|
||||
if (utils::isValidVOC(tvocRaw)) {
|
||||
add_metric("tvoc_raw",
|
||||
"The raw input value to the Total Volatile Organic Compounds "
|
||||
"(TVOC) index as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.TVOCRaw));
|
||||
add_metric_point("", String(tvocRaw));
|
||||
}
|
||||
if (utils::isValidNOx(measure.NOx)) {
|
||||
if (utils::isValidNOx(nox)) {
|
||||
add_metric("nox_index",
|
||||
"The processed Nitrous Oxide (NOx) index as measured by the "
|
||||
"AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.NOx));
|
||||
add_metric_point("", String(nox));
|
||||
}
|
||||
if (utils::isValidNOx(measure.NOxRaw)) {
|
||||
if (utils::isValidNOx(noxRaw)) {
|
||||
add_metric("nox_raw",
|
||||
"The raw input value to the Nitrous Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.NOxRaw));
|
||||
add_metric_point("", String(noxRaw));
|
||||
}
|
||||
}
|
||||
|
||||
if (utils::isValidCO2(co2)) {
|
||||
add_metric("co2",
|
||||
"Carbon dioxide concentration as measured by the AirGradient S8 "
|
||||
"sensor, in parts per million",
|
||||
"gauge", "ppm");
|
||||
add_metric_point("", String(co2));
|
||||
}
|
||||
|
||||
if (utils::isValidTemperature(_temp)) {
|
||||
add_metric(
|
||||
"temperature",
|
||||
@ -173,12 +192,12 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(_hum));
|
||||
}
|
||||
if (utils::isValidHumidity(ahumCompensated)) {
|
||||
if (utils::isValidHumidity(rhumCompensated)) {
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the "
|
||||
"AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(ahumCompensated));
|
||||
add_metric_point("", String(rhumCompensated));
|
||||
}
|
||||
|
||||
response += "# EOF\n";
|
||||
|
@ -12,10 +12,8 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
|
||||
Build Instructions:
|
||||
https://www.airgradient.com/documentation/diy-v4/
|
||||
|
||||
Please make sure you have esp8266 board manager installed. Tested with
|
||||
version 3.1.2.
|
||||
|
||||
Set board to "LOLIN(WEMOS) D1 R2 & mini"
|
||||
Compile Instructions:
|
||||
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
|
||||
|
||||
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||
can be set through the AirGradient dashboard.
|
||||
@ -49,14 +47,13 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
||||
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
|
||||
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
|
||||
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 2000 /** ms */
|
||||
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
|
||||
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
||||
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
|
||||
|
||||
static AirGradient ag(DIY_PRO_INDOOR_V4_2);
|
||||
static Configuration configuration(Serial);
|
||||
static AgApiClient apiClient(Serial, configuration);
|
||||
static Measurements measurements;
|
||||
static Measurements measurements(configuration);
|
||||
static OledDisplay oledDisplay(configuration, measurements, Serial);
|
||||
static StateMachine stateMachine(oledDisplay, Serial, measurements,
|
||||
configuration);
|
||||
@ -68,9 +65,7 @@ static LocalServer localServer(Serial, openMetrics, measurements, configuration,
|
||||
wifiConnector);
|
||||
static MqttClient mqttClient(Serial);
|
||||
|
||||
static int pmFailCount = 0;
|
||||
static uint32_t factoryBtnPressTime = 0;
|
||||
static int getCO2FailCount = 0;
|
||||
static AgFirmwareMode fwMode = FW_MODE_I_42PS;
|
||||
|
||||
static String fwNewVersion;
|
||||
@ -92,6 +87,8 @@ static void wdgFeedUpdate(void);
|
||||
static bool sgp41Init(void);
|
||||
static void wifiFactoryConfigure(void);
|
||||
static void mqttHandle(void);
|
||||
static int calculateMaxPeriod(int updateInterval);
|
||||
static void setMeasurementMaxPeriod();
|
||||
|
||||
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
|
||||
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
|
||||
@ -126,12 +123,17 @@ 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");
|
||||
|
||||
/** Init sensor */
|
||||
boardInit();
|
||||
setMeasurementMaxPeriod();
|
||||
|
||||
// Uncomment below line to print every measurements reading update
|
||||
// measurements.setDebug(true);
|
||||
|
||||
/** Connecting wifi */
|
||||
bool connectToWifi = false;
|
||||
@ -173,9 +175,12 @@ void setup() {
|
||||
initMqtt();
|
||||
sendDataToAg();
|
||||
|
||||
apiClient.fetchServerConfiguration();
|
||||
if (configuration.getConfigurationControl() !=
|
||||
ConfigurationControl::ConfigurationControlLocal) {
|
||||
apiClient.fetchServerConfiguration();
|
||||
}
|
||||
configSchedule.update();
|
||||
if (apiClient.isFetchConfigureFailed()) {
|
||||
if (apiClient.isFetchConfigurationFailed()) {
|
||||
if (apiClient.isNotAvailableOnDashboard()) {
|
||||
stateMachine.displaySetAddToDashBoard();
|
||||
stateMachine.displayHandle(
|
||||
@ -232,11 +237,7 @@ void loop() {
|
||||
tvocSchedule.run();
|
||||
}
|
||||
|
||||
/** Auto reset watchdog timer if offline mode or postDataToAirGradient */
|
||||
if (configuration.isOfflineMode() ||
|
||||
(configuration.isPostDataToAirGradient() == false)) {
|
||||
watchdogFeedSchedule.run();
|
||||
}
|
||||
watchdogFeedSchedule.run();
|
||||
|
||||
/** Check for handle WiFi reconnect */
|
||||
wifiConnector.handle();
|
||||
@ -260,17 +261,16 @@ void loop() {
|
||||
}
|
||||
|
||||
static void co2Update(void) {
|
||||
if (!configuration.hasSensorS8) {
|
||||
// Device don't have S8 sensor
|
||||
return;
|
||||
}
|
||||
|
||||
int value = ag.s8.getCo2();
|
||||
if (utils::isValidCO2(value)) {
|
||||
measurements.CO2 = value;
|
||||
getCO2FailCount = 0;
|
||||
Serial.printf("CO2 (ppm): %d\r\n", measurements.CO2);
|
||||
measurements.update(Measurements::CO2, value);
|
||||
} else {
|
||||
getCO2FailCount++;
|
||||
Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount);
|
||||
if (getCO2FailCount >= 3) {
|
||||
measurements.CO2 = utils::getInvalidCO2();
|
||||
}
|
||||
measurements.update(Measurements::CO2, utils::getInvalidCO2());
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,10 +292,17 @@ static void mdnsInit(void) {
|
||||
}
|
||||
|
||||
static void initMqtt(void) {
|
||||
if (mqttClient.begin(configuration.getMqttBrokerUri())) {
|
||||
Serial.println("Setup connect to MQTT broker successful");
|
||||
String mqttUri = configuration.getMqttBrokerUri();
|
||||
if (mqttUri.isEmpty()) {
|
||||
Serial.println(
|
||||
"MQTT is not configured, skipping initialization of MQTT client");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mqttClient.begin(mqttUri)) {
|
||||
Serial.println("Successfully connected to MQTT broker");
|
||||
} else {
|
||||
Serial.println("setup Connect to MQTT broker failed");
|
||||
Serial.println("Connection to MQTT broker failed");
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,8 +335,6 @@ static void factoryConfigReset(void) {
|
||||
// }
|
||||
|
||||
/** Reset WIFI */
|
||||
// WiFi.enableSTA(true); // Incase offline mode
|
||||
// WiFi.disconnect(true, true);
|
||||
wifiConnector.reset();
|
||||
|
||||
/** Reset local config */
|
||||
@ -358,9 +363,7 @@ static void factoryConfigReset(void) {
|
||||
|
||||
static void wdgFeedUpdate(void) {
|
||||
ag.watchdog.reset();
|
||||
Serial.println();
|
||||
Serial.println("Offline mode or isPostToAirGradient = false: watchdog reset");
|
||||
Serial.println();
|
||||
Serial.println("External watchdog feed!");
|
||||
}
|
||||
|
||||
static bool sgp41Init(void) {
|
||||
@ -395,8 +398,7 @@ static void mqttHandle(void) {
|
||||
}
|
||||
|
||||
if (mqttClient.isConnected()) {
|
||||
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(),
|
||||
&ag, &configuration);
|
||||
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
|
||||
String topic = "airgradient/readings/" + ag.deviceId();
|
||||
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
|
||||
Serial.println("MQTT sync success");
|
||||
@ -411,7 +413,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);
|
||||
@ -507,6 +509,14 @@ 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();
|
||||
}
|
||||
@ -564,7 +574,7 @@ static void appDispHandler(void) {
|
||||
if (configuration.isOfflineMode() == false) {
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
state = AgStateMachineWiFiLost;
|
||||
} else if (apiClient.isFetchConfigureFailed()) {
|
||||
} else if (apiClient.isFetchConfigurationFailed()) {
|
||||
state = AgStateMachineSensorConfigFailed;
|
||||
if (apiClient.isNotAvailableOnDashboard()) {
|
||||
stateMachine.displaySetAddToDashBoard();
|
||||
@ -585,84 +595,103 @@ static void oledDisplaySchedule(void) {
|
||||
}
|
||||
|
||||
static void updateTvoc(void) {
|
||||
measurements.TVOC = ag.sgp41.getTvocIndex();
|
||||
measurements.TVOCRaw = ag.sgp41.getTvocRaw();
|
||||
measurements.NOx = ag.sgp41.getNoxIndex();
|
||||
measurements.NOxRaw = ag.sgp41.getNoxRaw();
|
||||
if (!configuration.hasSensorSGP) {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
Serial.printf("TVOC index: %d\r\n", measurements.TVOC);
|
||||
Serial.printf("TVOC raw: %d\r\n", measurements.TVOCRaw);
|
||||
Serial.printf("NOx index: %d\r\n", measurements.NOx);
|
||||
Serial.printf("NOx raw: %d\r\n", measurements.NOxRaw);
|
||||
measurements.update(Measurements::TVOC, ag.sgp41.getTvocIndex());
|
||||
measurements.update(Measurements::TVOCRaw, ag.sgp41.getTvocRaw());
|
||||
measurements.update(Measurements::NOx, ag.sgp41.getNoxIndex());
|
||||
measurements.update(Measurements::NOxRaw, ag.sgp41.getNoxRaw());
|
||||
}
|
||||
|
||||
static void updatePm(void) {
|
||||
if (ag.pms5003.isFailed() == false) {
|
||||
measurements.pm01_1 = ag.pms5003.getPm01Ae();
|
||||
measurements.pm25_1 = ag.pms5003.getPm25Ae();
|
||||
measurements.pm10_1 = ag.pms5003.getPm10Ae();
|
||||
measurements.pm03PCount_1 = ag.pms5003.getPm03ParticleCount();
|
||||
|
||||
Serial.println();
|
||||
Serial.printf("PM1 ug/m3: %d\r\n", measurements.pm01_1);
|
||||
Serial.printf("PM2.5 ug/m3: %d\r\n", measurements.pm25_1);
|
||||
Serial.printf("PM10 ug/m3: %d\r\n", measurements.pm10_1);
|
||||
Serial.printf("PM0.3 Count: %d\r\n", measurements.pm03PCount_1);
|
||||
pmFailCount = 0;
|
||||
if (ag.pms5003.connected()) {
|
||||
measurements.update(Measurements::PM01, ag.pms5003.getPm01Ae());
|
||||
measurements.update(Measurements::PM25, ag.pms5003.getPm25Ae());
|
||||
measurements.update(Measurements::PM10, ag.pms5003.getPm10Ae());
|
||||
measurements.update(Measurements::PM03_PC, ag.pms5003.getPm03ParticleCount());
|
||||
} else {
|
||||
pmFailCount++;
|
||||
Serial.printf("PMS read failed: %d\r\n", pmFailCount);
|
||||
if (pmFailCount >= 3) {
|
||||
measurements.pm01_1 = utils::getInvalidPMS();
|
||||
measurements.pm25_1 = utils::getInvalidPMS();
|
||||
measurements.pm10_1 = utils::getInvalidPMS();
|
||||
measurements.pm03PCount_1 = utils::getInvalidPMS();
|
||||
}
|
||||
measurements.update(Measurements::PM01, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM25, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM10, utils::getInvalidPmValue());
|
||||
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue());
|
||||
}
|
||||
}
|
||||
|
||||
static void sendDataToServer(void) {
|
||||
/** Ignore send data to server if postToAirGradient disabled */
|
||||
if (configuration.isPostDataToAirGradient() == false ||
|
||||
configuration.isOfflineMode()) {
|
||||
/** Increment bootcount when send measurements data is scheduled */
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(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");
|
||||
return;
|
||||
}
|
||||
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(),
|
||||
&ag, &configuration);
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
ag.watchdog.reset();
|
||||
Serial.println();
|
||||
Serial.println(
|
||||
"Online mode and isPostToAirGradient = true: watchdog reset");
|
||||
Serial.println();
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
Serial.println("WiFi not connected, skipping data transmission to AG server");
|
||||
return;
|
||||
}
|
||||
|
||||
measurements.bootCount++;
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
Serial.println();
|
||||
Serial.println("Online mode and isPostToAirGradient = true");
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
|
||||
static void tempHumUpdate(void) {
|
||||
delay(100);
|
||||
if (ag.sht.measure()) {
|
||||
measurements.Temperature = ag.sht.getTemperature();
|
||||
measurements.Humidity = ag.sht.getRelativeHumidity();
|
||||
float temp = ag.sht.getTemperature();
|
||||
float rhum = ag.sht.getRelativeHumidity();
|
||||
|
||||
Serial.printf("Temperature in C: %0.2f\r\n", measurements.Temperature);
|
||||
Serial.printf("Relative Humidity: %d\r\n", measurements.Humidity);
|
||||
Serial.printf("Temperature compensated in C: %0.2f\r\n",
|
||||
measurements.Temperature);
|
||||
Serial.printf("Relative Humidity compensated: %d\r\n",
|
||||
measurements.Humidity);
|
||||
measurements.update(Measurements::Temperature, temp);
|
||||
measurements.update(Measurements::Humidity, rhum);
|
||||
|
||||
// Update compensation temperature and humidity for SGP41
|
||||
if (configuration.hasSensorSGP) {
|
||||
ag.sgp41.setCompensationTemperatureHumidity(measurements.Temperature,
|
||||
measurements.Humidity);
|
||||
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
|
||||
}
|
||||
} else {
|
||||
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
|
||||
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
|
||||
Serial.println("SHT read failed");
|
||||
measurements.Temperature = utils::getInvalidTemperature();
|
||||
measurements.Humidity = utils::getInvalidHumidity();
|
||||
}
|
||||
}
|
||||
|
||||
/* Set max period for each measurement type based on sensor update interval*/
|
||||
void setMeasurementMaxPeriod() {
|
||||
/// Max period for S8 sensors measurements
|
||||
measurements.maxPeriod(Measurements::CO2, calculateMaxPeriod(SENSOR_CO2_UPDATE_INTERVAL));
|
||||
/// Max period for SGP sensors measurements
|
||||
measurements.maxPeriod(Measurements::TVOC, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::TVOCRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::NOx, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::NOxRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
|
||||
/// Max period for PMS sensors measurements
|
||||
measurements.maxPeriod(Measurements::PM25, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM01, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM10, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::PM03_PC, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
// Temperature and Humidity
|
||||
if (configuration.hasSensorSHT) {
|
||||
/// Max period for SHT sensors measurements
|
||||
measurements.maxPeriod(Measurements::Temperature,
|
||||
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::Humidity,
|
||||
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
|
||||
} else {
|
||||
/// Temp and hum data retrieved from PMS5003T sensor
|
||||
measurements.maxPeriod(Measurements::Temperature,
|
||||
calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
measurements.maxPeriod(Measurements::Humidity, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
|
||||
}
|
||||
}
|
||||
|
||||
int calculateMaxPeriod(int updateInterval) {
|
||||
// 0.5 is 50% reduced interval for max period
|
||||
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval;
|
||||
}
|
@ -53,9 +53,8 @@ void LocalServer::_GET_metrics(void) {
|
||||
}
|
||||
|
||||
void LocalServer::_GET_measure(void) {
|
||||
server.send(
|
||||
200, "application/json",
|
||||
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
|
||||
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
||||
|
@ -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.isFetchConfigureFailed() ? "0" : "1");
|
||||
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
|
||||
|
||||
add_metric(
|
||||
"post_ok",
|
||||
@ -57,60 +57,70 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "dbm");
|
||||
add_metric_point("", String(wifiConnector.RSSI()));
|
||||
|
||||
if (config.hasSensorS8 && measure.CO2 >= 0) {
|
||||
add_metric("co2",
|
||||
"Carbon dioxide concentration as measured by the AirGradient S8 "
|
||||
"sensor, in parts per million",
|
||||
"gauge", "ppm");
|
||||
add_metric_point("", String(measure.CO2));
|
||||
}
|
||||
|
||||
// Initialize default invalid value for each measurements
|
||||
float _temp = utils::getInvalidTemperature();
|
||||
float _hum = utils::getInvalidHumidity();
|
||||
int pm01 = utils::getInvalidPMS();
|
||||
int pm25 = utils::getInvalidPMS();
|
||||
int pm10 = utils::getInvalidPMS();
|
||||
int pm03PCount = utils::getInvalidPMS();
|
||||
int pm01 = utils::getInvalidPmValue();
|
||||
int pm25 = utils::getInvalidPmValue();
|
||||
int pm10 = utils::getInvalidPmValue();
|
||||
int pm03PCount = utils::getInvalidPmValue();
|
||||
int co2 = utils::getInvalidCO2();
|
||||
int atmpCompensated = utils::getInvalidTemperature();
|
||||
int ahumCompensated = utils::getInvalidHumidity();
|
||||
int rhumCompensated = utils::getInvalidHumidity();
|
||||
int tvoc = utils::getInvalidVOC();
|
||||
int tvocRaw = utils::getInvalidVOC();
|
||||
int nox = utils::getInvalidNOx();
|
||||
int noxRaw = utils::getInvalidNOx();
|
||||
|
||||
if (config.hasSensorSHT) {
|
||||
_temp = measure.Temperature;
|
||||
_hum = measure.Humidity;
|
||||
_temp = measure.getFloat(Measurements::Temperature);
|
||||
_hum = measure.getFloat(Measurements::Humidity);
|
||||
atmpCompensated = _temp;
|
||||
ahumCompensated = _hum;
|
||||
rhumCompensated = _hum;
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
pm01 = measure.pm01_1;
|
||||
pm25 = measure.pm25_1;
|
||||
pm10 = measure.pm10_1;
|
||||
pm03PCount = measure.pm03PCount_1;
|
||||
pm01 = measure.get(Measurements::PM01);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
nox = measure.get(Measurements::NOx);
|
||||
noxRaw = measure.get(Measurements::NOxRaw);
|
||||
}
|
||||
|
||||
if (config.hasSensorS8) {
|
||||
co2 = measure.get(Measurements::CO2);
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
if (utils::isValidPMS(pm01)) {
|
||||
if (utils::isValidPm(pm01)) {
|
||||
add_metric("pm1",
|
||||
"PM1.0 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm01));
|
||||
}
|
||||
if (utils::isValidPMS(pm25)) {
|
||||
if (utils::isValidPm(pm25)) {
|
||||
add_metric("pm2d5",
|
||||
"PM2.5 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm25));
|
||||
}
|
||||
if (utils::isValidPMS(pm10)) {
|
||||
if (utils::isValidPm(pm10)) {
|
||||
add_metric("pm10",
|
||||
"PM10 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm10));
|
||||
}
|
||||
if (utils::isValidPMS03Count(pm03PCount)) {
|
||||
if (utils::isValidPm03Count(pm03PCount)) {
|
||||
add_metric("pm0d3",
|
||||
"PM0.3 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in number of particules per 100 milliliters",
|
||||
@ -120,36 +130,44 @@ String OpenMetrics::getPayload(void) {
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
if (utils::isValidVOC(measure.TVOC)) {
|
||||
if (utils::isValidVOC(tvoc)) {
|
||||
add_metric("tvoc_index",
|
||||
"The processed Total Volatile Organic Compounds (TVOC) index "
|
||||
"as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.TVOC));
|
||||
add_metric_point("", String(tvoc));
|
||||
}
|
||||
if (utils::isValidVOC(measure.TVOCRaw)) {
|
||||
if (utils::isValidVOC(tvocRaw)) {
|
||||
add_metric("tvoc_raw",
|
||||
"The raw input value to the Total Volatile Organic Compounds "
|
||||
"(TVOC) index as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.TVOCRaw));
|
||||
add_metric_point("", String(tvocRaw));
|
||||
}
|
||||
if (utils::isValidNOx(measure.NOx)) {
|
||||
if (utils::isValidNOx(nox)) {
|
||||
add_metric("nox_index",
|
||||
"The processed Nitrous Oxide (NOx) index as measured by the "
|
||||
"AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.NOx));
|
||||
add_metric_point("", String(nox));
|
||||
}
|
||||
if (utils::isValidNOx(measure.NOxRaw)) {
|
||||
if (utils::isValidNOx(noxRaw)) {
|
||||
add_metric("nox_raw",
|
||||
"The raw input value to the Nitrous Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.NOxRaw));
|
||||
add_metric_point("", String(noxRaw));
|
||||
}
|
||||
}
|
||||
|
||||
if (utils::isValidCO2(co2)) {
|
||||
add_metric("co2",
|
||||
"Carbon dioxide concentration as measured by the AirGradient S8 "
|
||||
"sensor, in parts per million",
|
||||
"gauge", "ppm");
|
||||
add_metric_point("", String(co2));
|
||||
}
|
||||
|
||||
if (utils::isValidTemperature(_temp)) {
|
||||
add_metric(
|
||||
"temperature",
|
||||
@ -173,12 +191,12 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(_hum));
|
||||
}
|
||||
if (utils::isValidHumidity(ahumCompensated)) {
|
||||
if (utils::isValidHumidity(rhumCompensated)) {
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the "
|
||||
"AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(ahumCompensated));
|
||||
add_metric_point("", String(rhumCompensated));
|
||||
}
|
||||
|
||||
response += "# EOF\n";
|
||||
|
@ -64,9 +64,8 @@ void LocalServer::_GET_metrics(void) {
|
||||
}
|
||||
|
||||
void LocalServer::_GET_measure(void) {
|
||||
server.send(
|
||||
200, "application/json",
|
||||
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
|
||||
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
||||
|
@ -1,13 +1,18 @@
|
||||
#include "OpenMetrics.h"
|
||||
|
||||
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
|
||||
WifiConnector &wifiConnector, AgApiClient &apiClient)
|
||||
: measure(measure), config(config), wifiConnector(wifiConnector),
|
||||
apiClient(apiClient) {}
|
||||
WifiConnector &wifiConnector)
|
||||
: measure(measure), config(config), wifiConnector(wifiConnector) {}
|
||||
|
||||
OpenMetrics::~OpenMetrics() {}
|
||||
|
||||
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||
void OpenMetrics::setAirGradient(AirGradient *ag) {
|
||||
this->ag = ag;
|
||||
}
|
||||
|
||||
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
|
||||
this->agClient = client;
|
||||
}
|
||||
|
||||
const char *OpenMetrics::getApiContentType(void) {
|
||||
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
|
||||
@ -43,13 +48,13 @@ String OpenMetrics::getPayload(void) {
|
||||
"1 if the AirGradient device was able to successfully fetch its "
|
||||
"configuration from the server",
|
||||
"gauge");
|
||||
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
|
||||
add_metric_point("", agClient->isLastFetchConfigSucceed() ? "1" : "0");
|
||||
|
||||
add_metric(
|
||||
"post_ok",
|
||||
"1 if the AirGradient device was able to successfully send to the server",
|
||||
"gauge");
|
||||
add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1");
|
||||
add_metric_point("", agClient->isLastPostMeasureSucceed() ? "1" : "0");
|
||||
|
||||
add_metric(
|
||||
"wifi_rssi",
|
||||
@ -57,94 +62,121 @@ String OpenMetrics::getPayload(void) {
|
||||
"gauge", "dbm");
|
||||
add_metric_point("", String(wifiConnector.RSSI()));
|
||||
|
||||
if (config.hasSensorS8 && measure.CO2 >= 0) {
|
||||
add_metric("co2",
|
||||
"Carbon dioxide concentration as measured by the AirGradient S8 "
|
||||
"sensor, in parts per million",
|
||||
"gauge", "ppm");
|
||||
add_metric_point("", String(measure.CO2));
|
||||
}
|
||||
|
||||
// Initialize default invalid value for each measurements
|
||||
float _temp = utils::getInvalidTemperature();
|
||||
float _hum = utils::getInvalidHumidity();
|
||||
int pm01 = utils::getInvalidPMS();
|
||||
int pm25 = utils::getInvalidPMS();
|
||||
int pm10 = utils::getInvalidPMS();
|
||||
int pm03PCount = utils::getInvalidPMS();
|
||||
int pm01 = utils::getInvalidPmValue();
|
||||
int pm25 = utils::getInvalidPmValue();
|
||||
int pm10 = utils::getInvalidPmValue();
|
||||
int pm03PCount = utils::getInvalidPmValue();
|
||||
int co2 = utils::getInvalidCO2();
|
||||
int atmpCompensated = utils::getInvalidTemperature();
|
||||
int ahumCompensated = utils::getInvalidHumidity();
|
||||
int rhumCompensated = utils::getInvalidHumidity();
|
||||
int tvoc = utils::getInvalidVOC();
|
||||
int tvocRaw = utils::getInvalidVOC();
|
||||
int nox = utils::getInvalidNOx();
|
||||
int noxRaw = utils::getInvalidNOx();
|
||||
|
||||
// Get values
|
||||
if (config.hasSensorPMS1 && config.hasSensorPMS2) {
|
||||
_temp = (measure.temp_1 + measure.temp_2) / 2.0f;
|
||||
_hum = (measure.hum_1 + measure.hum_2) / 2.0f;
|
||||
pm01 = (measure.pm01_1 + measure.pm01_2) / 2;
|
||||
pm25 = (measure.pm25_1 + measure.pm25_2) / 2;
|
||||
pm10 = (measure.pm10_1 + measure.pm10_2) / 2;
|
||||
pm03PCount = (measure.pm03PCount_1 + measure.pm03PCount_2) / 2;
|
||||
_temp = (measure.getFloat(Measurements::Temperature, 1) +
|
||||
measure.getFloat(Measurements::Temperature, 2)) /
|
||||
2.0f;
|
||||
_hum = (measure.getFloat(Measurements::Humidity, 1) +
|
||||
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 = (correctedPm25_1 + correctedPm25_2) / 2.0f;
|
||||
pm25 = round(correctedPm25);
|
||||
pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f;
|
||||
pm03PCount =
|
||||
(measure.get(Measurements::PM03_PC, 1) + measure.get(Measurements::PM03_PC, 2)) / 2.0f;
|
||||
} else {
|
||||
if (ag->isOne()) {
|
||||
if (config.hasSensorSHT) {
|
||||
_temp = measure.Temperature;
|
||||
_hum = measure.Humidity;
|
||||
_temp = measure.getFloat(Measurements::Temperature);
|
||||
_hum = measure.getFloat(Measurements::Humidity);
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
pm01 = measure.pm01_1;
|
||||
pm25 = measure.pm25_1;
|
||||
pm10 = measure.pm10_1;
|
||||
pm03PCount = measure.pm03PCount_1;
|
||||
pm01 = measure.get(Measurements::PM01);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
} else {
|
||||
if (config.hasSensorPMS1) {
|
||||
_temp = measure.temp_1;
|
||||
_hum = measure.hum_1;
|
||||
pm01 = measure.pm01_1;
|
||||
pm25 = measure.pm25_1;
|
||||
pm10 = measure.pm10_1;
|
||||
pm03PCount = measure.pm03PCount_1;
|
||||
_temp = measure.getFloat(Measurements::Temperature, 1);
|
||||
_hum = measure.getFloat(Measurements::Humidity, 1);
|
||||
pm01 = measure.get(Measurements::PM01, 1);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10, 1);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC, 1);
|
||||
}
|
||||
if (config.hasSensorPMS2) {
|
||||
_temp = measure.temp_2;
|
||||
_hum = measure.hum_2;
|
||||
pm01 = measure.pm01_2;
|
||||
pm25 = measure.pm25_2;
|
||||
pm10 = measure.pm10_2;
|
||||
pm03PCount = measure.pm03PCount_2;
|
||||
_temp = measure.getFloat(Measurements::Temperature, 2);
|
||||
_hum = measure.getFloat(Measurements::Humidity, 2);
|
||||
pm01 = measure.get(Measurements::PM01, 2);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 2);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10, 2);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get temperature and humidity compensated */
|
||||
if (ag->isOne()) {
|
||||
atmpCompensated = _temp;
|
||||
ahumCompensated = _hum;
|
||||
} else {
|
||||
atmpCompensated = ag->pms5003t_1.temperatureCompensated(_temp);
|
||||
ahumCompensated = ag->pms5003t_1.humidityCompensated(_hum);
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
nox = measure.get(Measurements::NOx);
|
||||
noxRaw = measure.get(Measurements::NOxRaw);
|
||||
}
|
||||
|
||||
if (config.hasSensorS8) {
|
||||
co2 = measure.get(Measurements::CO2);
|
||||
}
|
||||
|
||||
/** Get temperature and humidity compensated */
|
||||
if (ag->isOne()) {
|
||||
atmpCompensated = round(measure.getCorrectedTempHum(Measurements::Temperature));
|
||||
rhumCompensated = round(measure.getCorrectedTempHum(Measurements::Humidity));
|
||||
} 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);
|
||||
}
|
||||
|
||||
// Add measurements that valid to the metrics
|
||||
if (config.hasSensorPMS1 || config.hasSensorPMS2) {
|
||||
if (utils::isValidPMS(pm01)) {
|
||||
if (utils::isValidPm(pm01)) {
|
||||
add_metric("pm1",
|
||||
"PM1.0 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm01));
|
||||
}
|
||||
if (utils::isValidPMS(pm25)) {
|
||||
if (utils::isValidPm(pm25)) {
|
||||
add_metric("pm2d5",
|
||||
"PM2.5 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm25));
|
||||
}
|
||||
if (utils::isValidPMS(pm10)) {
|
||||
if (utils::isValidPm(pm10)) {
|
||||
add_metric("pm10",
|
||||
"PM10 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in micrograms per cubic meter",
|
||||
"gauge", "ugm3");
|
||||
add_metric_point("", String(pm10));
|
||||
}
|
||||
if (utils::isValidPMS03Count(pm03PCount)) {
|
||||
if (utils::isValidPm03Count(pm03PCount)) {
|
||||
add_metric("pm0d3",
|
||||
"PM0.3 concentration as measured by the AirGradient PMS "
|
||||
"sensor, in number of particules per 100 milliliters",
|
||||
@ -154,36 +186,44 @@ String OpenMetrics::getPayload(void) {
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
if (utils::isValidVOC(measure.TVOC)) {
|
||||
if (utils::isValidVOC(tvoc)) {
|
||||
add_metric("tvoc_index",
|
||||
"The processed Total Volatile Organic Compounds (TVOC) index "
|
||||
"as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.TVOC));
|
||||
add_metric_point("", String(tvoc));
|
||||
}
|
||||
if (utils::isValidVOC(measure.TVOCRaw)) {
|
||||
if (utils::isValidVOC(tvocRaw)) {
|
||||
add_metric("tvoc_raw",
|
||||
"The raw input value to the Total Volatile Organic Compounds "
|
||||
"(TVOC) index as measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.TVOCRaw));
|
||||
add_metric_point("", String(tvocRaw));
|
||||
}
|
||||
if (utils::isValidNOx(measure.NOx)) {
|
||||
if (utils::isValidNOx(nox)) {
|
||||
add_metric("nox_index",
|
||||
"The processed Nitrous Oxide (NOx) index as measured by the "
|
||||
"AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.NOx));
|
||||
add_metric_point("", String(nox));
|
||||
}
|
||||
if (utils::isValidNOx(measure.NOxRaw)) {
|
||||
if (utils::isValidNOx(noxRaw)) {
|
||||
add_metric("nox_raw",
|
||||
"The raw input value to the Nitrous Oxide (NOx) index as "
|
||||
"measured by the AirGradient SGP sensor",
|
||||
"gauge");
|
||||
add_metric_point("", String(measure.NOxRaw));
|
||||
add_metric_point("", String(noxRaw));
|
||||
}
|
||||
}
|
||||
|
||||
if (utils::isValidCO2(co2)) {
|
||||
add_metric("co2",
|
||||
"Carbon dioxide concentration as measured by the AirGradient S8 "
|
||||
"sensor, in parts per million",
|
||||
"gauge", "ppm");
|
||||
add_metric_point("", String(co2));
|
||||
}
|
||||
|
||||
if (utils::isValidTemperature(_temp)) {
|
||||
add_metric("temperature",
|
||||
"The ambient temperature as measured by the AirGradient SHT / PMS "
|
||||
@ -192,26 +232,22 @@ String OpenMetrics::getPayload(void) {
|
||||
add_metric_point("", String(_temp));
|
||||
}
|
||||
if (utils::isValidTemperature(atmpCompensated)) {
|
||||
add_metric(
|
||||
"temperature_compensated",
|
||||
"The compensated ambient temperature as measured by the AirGradient SHT / PMS "
|
||||
"sensor, in degrees Celsius",
|
||||
"gauge", "celsius");
|
||||
add_metric("temperature_compensated",
|
||||
"The compensated ambient temperature as measured by the AirGradient SHT / PMS "
|
||||
"sensor, in degrees Celsius",
|
||||
"gauge", "celsius");
|
||||
add_metric_point("", String(atmpCompensated));
|
||||
}
|
||||
if (utils::isValidHumidity(_hum)) {
|
||||
add_metric(
|
||||
"humidity",
|
||||
"The relative humidity as measured by the AirGradient SHT sensor",
|
||||
"gauge", "percent");
|
||||
add_metric("humidity", "The relative humidity as measured by the AirGradient SHT sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(_hum));
|
||||
}
|
||||
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(ahumCompensated));
|
||||
if (utils::isValidHumidity(rhumCompensated)) {
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(rhumCompensated));
|
||||
}
|
||||
|
||||
response += "# EOF\n";
|
||||
|
@ -5,21 +5,22 @@
|
||||
#include "AgValue.h"
|
||||
#include "AgWiFiConnector.h"
|
||||
#include "AirGradient.h"
|
||||
#include "AgApiClient.h"
|
||||
#include "Libraries/airgradient-client/src/airgradientClient.h"
|
||||
|
||||
class OpenMetrics {
|
||||
private:
|
||||
AirGradient *ag;
|
||||
AirgradientClient *agClient;
|
||||
Measurements &measure;
|
||||
Configuration &config;
|
||||
WifiConnector &wifiConnector;
|
||||
AgApiClient &apiClient;
|
||||
|
||||
public:
|
||||
OpenMetrics(Measurements &measure, Configuration &conig,
|
||||
WifiConnector &wifiConnector, AgApiClient& apiClient);
|
||||
OpenMetrics(Measurements &measure, Configuration &config,
|
||||
WifiConnector &wifiConnector);
|
||||
~OpenMetrics();
|
||||
void setAirGradient(AirGradient *ag);
|
||||
void setAirgradientClient(AirgradientClient *client);
|
||||
const char *getApiContentType(void);
|
||||
const char* getApi(void);
|
||||
String getPayload(void);
|
||||
|
@ -1,206 +0,0 @@
|
||||
#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
|
@ -44,7 +44,7 @@ void loop() {
|
||||
if (ms >= 5000) {
|
||||
lastRead = millis();
|
||||
#ifdef ESP8266
|
||||
if (ag.pms5003.isFailed() == false) {
|
||||
if (ag.pms5003.connected()) {
|
||||
PM2 = ag.pms5003.getPm25Ae();
|
||||
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
|
||||
Serial.printf("PM2.5 in US AQI: %d\r\n",
|
||||
@ -54,12 +54,12 @@ void loop() {
|
||||
}
|
||||
#else
|
||||
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
|
||||
if (ag.pms5003t_1.isFailed() == false) {
|
||||
if (ag.pms5003t_1.connected()) {
|
||||
PM2 = ag.pms5003t_1.getPm25Ae();
|
||||
readResul = true;
|
||||
}
|
||||
} else {
|
||||
if (ag.pms5003.isFailed() == false) {
|
||||
if (ag.pms5003.connected()) {
|
||||
PM2 = ag.pms5003.getPm25Ae();
|
||||
readResul = true;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
name=AirGradient Air Quality Sensor
|
||||
version=3.1.5
|
||||
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,7 +12,7 @@
|
||||
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 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 =
|
||||
|
@ -34,17 +34,6 @@ 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";
|
||||
|
||||
@ -58,9 +47,22 @@ bool AgApiClient::fetchServerConfiguration(void) {
|
||||
}
|
||||
#else
|
||||
HTTPClient client;
|
||||
if (client.begin(uri) == false) {
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -89,8 +91,6 @@ 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,33 +104,39 @@ bool AgApiClient::fetchServerConfiguration(void) {
|
||||
* @return false Failure
|
||||
*/
|
||||
bool AgApiClient::postToServer(String data) {
|
||||
if (config.isPostDataToAirGradient() == false) {
|
||||
logWarning("Ignore post data to server");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (WiFi.isConnected() == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String uri =
|
||||
"http://hw.airgradient.com/sensors/airgradient:" + ag->deviceId() +
|
||||
"/measures";
|
||||
// logInfo("Post uri: " + uri);
|
||||
// logInfo("Post data: " + data);
|
||||
|
||||
WiFiClient wifiClient;
|
||||
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
|
||||
#ifdef ESP8266
|
||||
HTTPClient client;
|
||||
if (client.begin(wifiClient, uri.c_str()) == false) {
|
||||
logError("Init client failed");
|
||||
WiFiClient wifiClient;
|
||||
if (client.begin(wifiClient, uri) == false) {
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
#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 (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;
|
||||
}
|
||||
}
|
||||
#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)) {
|
||||
@ -149,7 +155,12 @@ bool AgApiClient::postToServer(String data) {
|
||||
* @return true Success
|
||||
* @return false Failure
|
||||
*/
|
||||
bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
|
||||
bool AgApiClient::isFetchConfigurationFailed(void) { return getConfigFailed; }
|
||||
|
||||
/**
|
||||
* @brief Reset status of get configuration from AirGradient cloud
|
||||
*/
|
||||
void AgApiClient::resetFetchConfigurationStatus(void) { getConfigFailed = false; }
|
||||
|
||||
/**
|
||||
* @brief Get failed status when post data to AirGradient cloud
|
||||
@ -189,4 +200,16 @@ bool AgApiClient::sendPing(int rssi, int bootCount) {
|
||||
|
||||
String AgApiClient::getApiRoot() const { return apiRoot; }
|
||||
|
||||
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; }
|
||||
void AgApiClient::setApiRoot(const String &apiRoot) {
|
||||
this->apiRootChanged = true;
|
||||
this->apiRoot = apiRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set http request timeout. (Default: 10s)
|
||||
*
|
||||
* @param timeoutMs
|
||||
*/
|
||||
void AgApiClient::setTimeout(uint16_t timeoutMs) {
|
||||
this->timeoutMs = timeoutMs;
|
||||
}
|
@ -20,11 +20,18 @@ 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
|
||||
|
||||
public:
|
||||
AgApiClient(Stream &stream, Configuration &config);
|
||||
@ -33,13 +40,15 @@ public:
|
||||
void begin(void);
|
||||
bool fetchServerConfiguration(void);
|
||||
bool postToServer(String data);
|
||||
bool isFetchConfigureFailed(void);
|
||||
bool isFetchConfigurationFailed(void);
|
||||
void resetFetchConfigurationStatus(void);
|
||||
bool isPostToServerFailed(void);
|
||||
bool isNotAvailableOnDashboard(void);
|
||||
void setAirGradient(AirGradient *ag);
|
||||
bool sendPing(int rssi, int bootCount);
|
||||
String getApiRoot() const;
|
||||
void setApiRoot(const String &apiRoot);
|
||||
void setTimeout(uint16_t timeoutMs);
|
||||
};
|
||||
|
||||
#endif /** _AG_API_CLIENT_H_ */
|
||||
|
@ -1,5 +1,4 @@
|
||||
#include "AgConfigure.h"
|
||||
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
|
||||
#if ESP32
|
||||
#include "FS.h"
|
||||
#include "SPIFFS.h"
|
||||
@ -22,6 +21,20 @@ const char *LED_BAR_MODE_NAMES[] = {
|
||||
[LedBarModeCO2] = "co2",
|
||||
};
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
#define JSON_PROP_NAME(name) jprop_##name
|
||||
#define JSON_PROP_DEF(name) const char *JSON_PROP_NAME(name) = #name
|
||||
|
||||
@ -33,29 +46,38 @@ 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);
|
||||
JSON_PROP_DEF(ledBarTestRequested);
|
||||
JSON_PROP_DEF(offlineMode);
|
||||
JSON_PROP_DEF(monitorDisplayCompensatedValues);
|
||||
JSON_PROP_DEF(corrections);
|
||||
JSON_PROP_DEF(atmp);
|
||||
JSON_PROP_DEF(rhum);
|
||||
|
||||
#define jprop_model_default ""
|
||||
#define jprop_country_default "TH"
|
||||
#define jprop_pmStandard_default getPMStandardString(false)
|
||||
#define jprop_ledBarMode_default getLedBarModeName(LedBarMode::LedBarModeCO2)
|
||||
#define jprop_abcDays_default 8
|
||||
#define jprop_tvocLearningOffset_default 12
|
||||
#define jprop_noxLearningOffset_default 12
|
||||
#define jprop_mqttBrokerUrl_default ""
|
||||
#define jprop_temperatureUnit_default "c"
|
||||
#define jprop_configurationControl_default String(CONFIGURATION_CONTROL_NAME[ConfigurationControl::ConfigurationControlBoth])
|
||||
#define jprop_postDataToAirGradient_default true
|
||||
#define jprop_ledBarBrightness_default 100
|
||||
#define jprop_displayBrightness_default 100
|
||||
#define jprop_offlineMode_default false
|
||||
#define jprop_model_default ""
|
||||
#define jprop_country_default "TH"
|
||||
#define jprop_pmStandard_default getPMStandardString(false)
|
||||
#define jprop_ledBarMode_default getLedBarModeName(LedBarMode::LedBarModeCO2)
|
||||
#define jprop_abcDays_default 8
|
||||
#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
|
||||
|
||||
JSONVar jconfig;
|
||||
|
||||
@ -85,6 +107,214 @@ String Configuration::getLedBarModeName(LedBarMode mode) {
|
||||
return String("unknown");
|
||||
}
|
||||
|
||||
PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
|
||||
// Loop through all algorithm names in the PM_CORRECTION_ALGORITHM_NAMES array
|
||||
// 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;;
|
||||
|
||||
// Loop through enum values
|
||||
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
|
||||
if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
|
||||
result = static_cast<PMCorrectionAlgorithm>(enumVal);
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONVar corrections = json["corrections"];
|
||||
if (!corrections.hasOwnProperty("pm02")) {
|
||||
logWarning("pm02 not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONVar pm02 = corrections["pm02"];
|
||||
if (!pm02.hasOwnProperty("correctionAlgorithm")) {
|
||||
logWarning("pm02 correctionAlgorithm not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check algorithm
|
||||
String algorithm = pm02["correctionAlgorithm"];
|
||||
PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm);
|
||||
if (algo == COR_ALGO_PM_UNKNOWN) {
|
||||
logWarning("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 (pmCorrection.algorithm != algo) {
|
||||
// Deep copy corrections from root to jconfig, so it will be saved later
|
||||
jconfig[jprop_corrections]["pm02"]["correctionAlgorithm"] = algorithm;
|
||||
jconfig[jprop_corrections]["pm02"]["slr"] = JSON.parse("{}"); // Clear slr
|
||||
// Update pmCorrection with new values
|
||||
pmCorrection.algorithm = algo;
|
||||
pmCorrection.changed = true;
|
||||
logInfo("PM2.5 correction updated");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if pm02 has slr object
|
||||
if (!pm02.hasOwnProperty("slr")) {
|
||||
logWarning("slr not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONVar slr = pm02["slr"];
|
||||
|
||||
// Validate required slr properties exist
|
||||
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor") ||
|
||||
!slr.hasOwnProperty("useEpa2021")) {
|
||||
logWarning("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 pmCorrection
|
||||
if (pmCorrection.algorithm == algo && pmCorrection.intercept == intercept &&
|
||||
pmCorrection.scalingFactor == scalingFactor &&
|
||||
pmCorrection.useEPA == (bool)slr["useEpa2021"]) {
|
||||
return false; // No changes needed
|
||||
}
|
||||
|
||||
// Deep copy corrections from root to jconfig, so it will be saved later
|
||||
jconfig[jprop_corrections] = corrections;
|
||||
|
||||
// Update pmCorrection with new values
|
||||
pmCorrection.algorithm = algo;
|
||||
pmCorrection.intercept = intercept;
|
||||
pmCorrection.scalingFactor = scalingFactor;
|
||||
pmCorrection.useEPA = (bool)slr["useEpa2021"];
|
||||
pmCorrection.changed = true;
|
||||
|
||||
// Correction values were updated
|
||||
logInfo("PM2.5 correction updated");
|
||||
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)
|
||||
*
|
||||
@ -149,9 +379,11 @@ 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;
|
||||
@ -160,13 +392,21 @@ void Configuration::defaultConfig(void) {
|
||||
jconfig[jprop_displayBrightness] = jprop_displayBrightness_default;
|
||||
}
|
||||
if (ag->isOne()) {
|
||||
jconfig[jprop_ledBarMode] = jprop_ledBarBrightness_default;
|
||||
jconfig[jprop_ledBarMode] = jprop_ledBarMode_default;
|
||||
}
|
||||
jconfig[jprop_tvocLearningOffset] = jprop_tvocLearningOffset_default;
|
||||
jconfig[jprop_noxLearningOffset] = jprop_noxLearningOffset_default;
|
||||
jconfig[jprop_abcDays] = jprop_abcDays_default;
|
||||
jconfig[jprop_model] = jprop_model_default;
|
||||
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
|
||||
jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default;
|
||||
|
||||
// PM2.5 default correction
|
||||
pmCorrection.algorithm = COR_ALGO_PM_NONE;
|
||||
pmCorrection.changed = false;
|
||||
pmCorrection.intercept = 0;
|
||||
pmCorrection.scalingFactor = 1;
|
||||
pmCorrection.useEPA = false;
|
||||
|
||||
saveConfig();
|
||||
}
|
||||
@ -216,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
|
||||
*
|
||||
@ -226,16 +470,16 @@ bool Configuration::begin(void) {
|
||||
* @return false Failure
|
||||
*/
|
||||
bool Configuration::parse(String data, bool isLocal) {
|
||||
logInfo("Parse configure: " + data);
|
||||
logInfo("Parsing configuration: " + data);
|
||||
|
||||
JSONVar root = JSON.parse(data);
|
||||
failedMessage = "";
|
||||
if (root == undefined) {
|
||||
if (root == undefined || JSONVar::typeof_(root) != "object") {
|
||||
logError("Parse configuration failed, JSON invalid (" + JSONVar::typeof_(root) + ")");
|
||||
failedMessage = "JSON invalid";
|
||||
logError(failedMessage);
|
||||
return false;
|
||||
}
|
||||
logInfo("Parse configure success");
|
||||
logInfo("Parse configuration success");
|
||||
|
||||
/** Is configuration changed */
|
||||
bool changed = false;
|
||||
@ -498,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");
|
||||
@ -511,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];
|
||||
@ -628,6 +904,27 @@ bool Configuration::parse(String data, bool isLocal) {
|
||||
}
|
||||
}
|
||||
|
||||
if (JSON.typeof_(root[jprop_monitorDisplayCompensatedValues]) == "boolean") {
|
||||
bool value = root[jprop_monitorDisplayCompensatedValues];
|
||||
bool oldValue = jconfig[jprop_monitorDisplayCompensatedValues];
|
||||
if (value != oldValue) {
|
||||
changed = true;
|
||||
jconfig[jprop_monitorDisplayCompensatedValues] = value;
|
||||
|
||||
configLogInfo(String(jprop_monitorDisplayCompensatedValues),
|
||||
String(oldValue ? "true" : "false"),
|
||||
String(value ? "true" : "false"));
|
||||
}
|
||||
} else {
|
||||
if (jsonTypeInvalid(root[jprop_monitorDisplayCompensatedValues],
|
||||
"boolean")) {
|
||||
failedMessage = jsonTypeInvalidMessage(
|
||||
String(jprop_monitorDisplayCompensatedValues), "boolean");
|
||||
jsonInvalid();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ag->getBoardType() == ONE_INDOOR ||
|
||||
ag->getBoardType() == OPEN_AIR_OUTDOOR) {
|
||||
if (JSON.typeof_(root["targetFirmware"]) == "string") {
|
||||
@ -636,22 +933,40 @@ bool Configuration::parse(String data, bool isLocal) {
|
||||
if (curVer != newVer) {
|
||||
logInfo("Detected new firmware version: " + newVer);
|
||||
otaNewFirmwareVersion = newVer;
|
||||
udpated = true;
|
||||
updated = true;
|
||||
} else {
|
||||
otaNewFirmwareVersion = String("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PM2.5 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) {
|
||||
udpated = true;
|
||||
updated = true;
|
||||
saveConfig();
|
||||
printConfig();
|
||||
} else {
|
||||
if (ledBarTestRequested || co2CalibrationRequested) {
|
||||
udpated = true;
|
||||
}
|
||||
_callback();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -757,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
|
||||
*
|
||||
@ -836,13 +1161,19 @@ String Configuration::getModel(void) {
|
||||
}
|
||||
|
||||
bool Configuration::isUpdated(void) {
|
||||
bool updated = this->udpated;
|
||||
this->udpated = false;
|
||||
bool updated = this->updated;
|
||||
this->updated = false;
|
||||
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) {
|
||||
@ -882,20 +1213,20 @@ void Configuration::toConfig(const char *buf) {
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
bool isInvalid = false;
|
||||
bool isConfigFieldInvalid = false;
|
||||
|
||||
/** Validate country */
|
||||
if (JSON.typeof_(jconfig[jprop_country]) != "string") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
String country = jconfig[jprop_country];
|
||||
if (country.length() != 2) {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_country] = jprop_country_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: country changed");
|
||||
@ -903,17 +1234,17 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** validate: PM standard */
|
||||
if (JSON.typeof_(jconfig[jprop_pmStandard]) != "string") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
String standard = jconfig[jprop_pmStandard];
|
||||
if (standard != getPMStandardString(true) &&
|
||||
standard != getPMStandardString(false)) {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_pmStandard] = jprop_pmStandard_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: pmStandard changed");
|
||||
@ -921,18 +1252,18 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** validate led bar mode */
|
||||
if (JSON.typeof_(jconfig[jprop_ledBarMode]) != "string") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
String mode = jconfig[jprop_ledBarMode];
|
||||
if (mode != getLedBarModeName(LedBarMode::LedBarModeCO2) &&
|
||||
mode != getLedBarModeName(LedBarMode::LedBarModeOff) &&
|
||||
mode != getLedBarModeName(LedBarMode::LedBarModePm)) {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_ledBarMode] = jprop_ledBarMode_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: ledBarMode changed");
|
||||
@ -940,11 +1271,11 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** validate abcday */
|
||||
if (JSON.typeof_(jconfig[jprop_abcDays]) != "number") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_abcDays] = jprop_abcDays_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: abcDays changed");
|
||||
@ -952,16 +1283,16 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** validate tvoc learning offset */
|
||||
if (JSON.typeof_(jconfig[jprop_tvocLearningOffset]) != "number") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
int value = jconfig[jprop_tvocLearningOffset];
|
||||
if (value < 0) {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_tvocLearningOffset] = jprop_tvocLearningOffset_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: tvocLearningOffset changed");
|
||||
@ -969,16 +1300,16 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** validate nox learning offset */
|
||||
if (JSON.typeof_(jconfig[jprop_noxLearningOffset]) != "number") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
int value = jconfig[jprop_noxLearningOffset];
|
||||
if (value < 0) {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_noxLearningOffset] = jprop_noxLearningOffset_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: noxLearningOffset changed");
|
||||
@ -986,36 +1317,60 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** validate mqtt broker */
|
||||
if (JSON.typeof_(jconfig[jprop_mqttBrokerUrl]) != "string") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
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") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
String unit = jconfig[jprop_temperatureUnit];
|
||||
if (unit != "c" && unit != "f") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
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 configuration control */
|
||||
if (JSON.typeof_(jprop_configurationControl) != "string") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
String ctrl = jconfig[jprop_configurationControl];
|
||||
if (ctrl != String(CONFIGURATION_CONTROL_NAME
|
||||
@ -1024,12 +1379,12 @@ void Configuration::toConfig(const char *buf) {
|
||||
[ConfigurationControl::ConfigurationControlLocal]) &&
|
||||
ctrl != String(CONFIGURATION_CONTROL_NAME
|
||||
[ConfigurationControl::ConfigurationControlCloud])) {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_configurationControl] =jprop_configurationControl_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: configurationControl changed");
|
||||
@ -1037,11 +1392,11 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** Validate post to airgradient cloud */
|
||||
if (JSON.typeof_(jconfig[jprop_postDataToAirGradient]) != "boolean") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: postToAirGradient changed");
|
||||
@ -1049,16 +1404,16 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** validate led bar brightness */
|
||||
if (JSON.typeof_(jconfig[jprop_ledBarBrightness]) != "number") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
int value = jconfig[jprop_ledBarBrightness];
|
||||
if (value < 0 || value > 100) {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: ledBarBrightness changed");
|
||||
@ -1066,30 +1421,59 @@ void Configuration::toConfig(const char *buf) {
|
||||
|
||||
/** Validate display brightness */
|
||||
if (JSON.typeof_(jconfig[jprop_displayBrightness]) != "number") {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
int value = jconfig[jprop_displayBrightness];
|
||||
if (value < 0 || value > 100) {
|
||||
isInvalid = true;
|
||||
isConfigFieldInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
isConfigFieldInvalid = false;
|
||||
}
|
||||
}
|
||||
if (isInvalid) {
|
||||
if (isConfigFieldInvalid) {
|
||||
jconfig[jprop_displayBrightness] = jprop_displayBrightness_default;
|
||||
changed = true;
|
||||
logInfo("toConfig: displayBrightness changed");
|
||||
}
|
||||
|
||||
if (JSON.typeof_(jconfig[jprop_offlineMode]) != "boolean") {
|
||||
isInvalid = true;
|
||||
} else {
|
||||
isInvalid = false;
|
||||
changed = true;
|
||||
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
|
||||
}
|
||||
if (isInvalid) {
|
||||
jconfig[jprop_offlineMode] = false;
|
||||
|
||||
/** Validate monitorDisplayCompensatedValues */
|
||||
if (JSON.typeof_(jconfig[jprop_monitorDisplayCompensatedValues]) !=
|
||||
"boolean") {
|
||||
changed = true;
|
||||
jconfig[jprop_monitorDisplayCompensatedValues] =
|
||||
jprop_monitorDisplayCompensatedValues_default;
|
||||
}
|
||||
|
||||
// PM2.5 correction
|
||||
/// Set default first before parsing local config
|
||||
pmCorrection.algorithm = COR_ALGO_PM_NONE;
|
||||
pmCorrection.intercept = 0;
|
||||
pmCorrection.scalingFactor = 0;
|
||||
pmCorrection.useEPA = false;
|
||||
/// 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();
|
||||
}
|
||||
@ -1167,12 +1551,27 @@ 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 changed = _ledBarModeChanged;
|
||||
_ledBarModeChanged = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool Configuration::isMonitorDisplayCompensatedValues(void) {
|
||||
return jconfig[jprop_monitorDisplayCompensatedValues];
|
||||
}
|
||||
|
||||
bool Configuration::isDisplayBrightnessChanged(void) {
|
||||
bool changed = displayBrightnessChanged;
|
||||
displayBrightnessChanged = false;
|
||||
@ -1184,3 +1583,30 @@ String Configuration::newFirmwareVersion(void) {
|
||||
otaNewFirmwareVersion = String("");
|
||||
return newFw;
|
||||
}
|
||||
|
||||
bool Configuration::isPMCorrectionChanged(void) {
|
||||
bool changed = pmCorrection.changed;
|
||||
pmCorrection.changed = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
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; }
|
||||
|
@ -5,12 +5,30 @@
|
||||
#include "Main/PrintLog.h"
|
||||
#include "AirGradient.h"
|
||||
#include <Arduino.h>
|
||||
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
|
||||
|
||||
class Configuration : public PrintLog {
|
||||
public:
|
||||
struct PMCorrection {
|
||||
PMCorrectionAlgorithm algorithm;
|
||||
float intercept;
|
||||
float scalingFactor;
|
||||
bool useEPA; // EPA 2021
|
||||
bool changed;
|
||||
};
|
||||
|
||||
struct TempHumCorrection {
|
||||
TempHumCorrectionAlgorithm algorithm;
|
||||
float intercept;
|
||||
float scalingFactor;
|
||||
bool changed;
|
||||
};
|
||||
|
||||
private:
|
||||
bool co2CalibrationRequested;
|
||||
bool ledBarTestRequested;
|
||||
bool udpated;
|
||||
bool updated;
|
||||
bool commandRequested = false;
|
||||
String failedMessage;
|
||||
bool _noxLearnOffsetChanged;
|
||||
bool _tvocLearningOffsetChanged;
|
||||
@ -19,10 +37,18 @@ private:
|
||||
String otaNewFirmwareVersion;
|
||||
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);
|
||||
@ -33,7 +59,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);
|
||||
@ -45,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);
|
||||
@ -57,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);
|
||||
@ -64,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);
|
||||
@ -81,7 +112,17 @@ 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_ */
|
||||
|
@ -5,29 +5,55 @@
|
||||
/** 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,
|
||||
};
|
||||
// {
|
||||
// 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
|
||||
*
|
||||
* @param hasStatus
|
||||
*/
|
||||
void OledDisplay::showTempHum(bool hasStatus) {
|
||||
char buf[16];
|
||||
if (utils::isValidTemperature(value.Temperature)) {
|
||||
char buf[10];
|
||||
/** Temperature */
|
||||
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
|
||||
if (utils::isValidTemperature(temp)) {
|
||||
float t = 0.0f;
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
t = utils::degreeC_To_F(temp);
|
||||
} else {
|
||||
t = temp;
|
||||
}
|
||||
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
float tempF = (value.Temperature * 9) / 5 + 32;
|
||||
if (hasStatus) {
|
||||
snprintf(buf, sizeof(buf), "%0.1f", tempF);
|
||||
snprintf(buf, sizeof(buf), "%0.1f", t);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%0.1f°F", tempF);
|
||||
snprintf(buf, sizeof(buf), "%0.1f°F", t);
|
||||
}
|
||||
} else {
|
||||
if (hasStatus) {
|
||||
snprintf(buf, sizeof(buf), "%.1f", value.Temperature);
|
||||
snprintf(buf, sizeof(buf), "%.1f", t);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%.1f°C", value.Temperature);
|
||||
snprintf(buf, sizeof(buf), "%.1f°C", t);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else { /** Show invalid value */
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
snprintf(buf, sizeof(buf), "-°F");
|
||||
} else {
|
||||
@ -36,14 +62,15 @@ void OledDisplay::showTempHum(bool hasStatus) {
|
||||
}
|
||||
DISP()->drawUTF8(1, 10, buf);
|
||||
|
||||
/** Show humidty */
|
||||
if (utils::isValidHumidity(value.Humidity)) {
|
||||
snprintf(buf, sizeof(buf), "%d%%", value.Humidity);
|
||||
/** Show humidity */
|
||||
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
|
||||
if (utils::isValidHumidity(rhum)) {
|
||||
snprintf(buf, sizeof(buf), "%d%%", rhum);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "-%%");
|
||||
}
|
||||
|
||||
if (value.Humidity > 99) {
|
||||
if (rhum > 99.0) {
|
||||
DISP()->drawStr(97, 10, buf);
|
||||
} else {
|
||||
DISP()->drawStr(105, 10, buf);
|
||||
@ -59,6 +86,9 @@ 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
|
||||
*
|
||||
@ -244,36 +274,60 @@ void OledDisplay::setText(const char *line1, const char *line2,
|
||||
* @brief Update dashboard content
|
||||
*
|
||||
*/
|
||||
void OledDisplay::showDashboard(void) { showDashboard(NULL); }
|
||||
void OledDisplay::showDashboard(void) { showDashboard(DashBoardStatusNone); }
|
||||
|
||||
/**
|
||||
* @brief Update dashboard content and error status
|
||||
*
|
||||
*/
|
||||
void OledDisplay::showDashboard(const char *status) {
|
||||
void OledDisplay::showDashboard(DashboardStatus 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);
|
||||
if ((status == NULL) || (strlen(status) == 0)) {
|
||||
switch (status) {
|
||||
case DashBoardStatusNone: {
|
||||
// Maybe show signal strength?
|
||||
showTempHum(false);
|
||||
} 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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/** Draw horizonal line */
|
||||
@ -284,8 +338,9 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
DISP()->drawUTF8(1, 27, "CO2");
|
||||
|
||||
DISP()->setFont(u8g2_font_t0_22b_tf);
|
||||
if (utils::isValidCO2(value.CO2)) {
|
||||
sprintf(strBuf, "%d", value.CO2);
|
||||
int co2 = round(value.getAverage(Measurements::CO2));
|
||||
if (utils::isValidCO2(co2)) {
|
||||
sprintf(strBuf, "%d", co2);
|
||||
} else {
|
||||
sprintf(strBuf, "%s", "-");
|
||||
}
|
||||
@ -304,28 +359,27 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
DISP()->drawStr(55, 27, "PM2.5");
|
||||
|
||||
/** Draw PM2.5 value */
|
||||
int pm25 = value.pm25_1;
|
||||
if (config.hasSensorSHT) {
|
||||
pm25 = ag->pms5003.compensated(pm25, value.Humidity);
|
||||
int pm25 = round(value.getAverage(Measurements::PM25));
|
||||
if (utils::isValidPm(pm25)) {
|
||||
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||
pm25 = round(value.getCorrectedPM25(true));
|
||||
}
|
||||
if (config.isPmStandardInUSAQI()) {
|
||||
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
|
||||
} else {
|
||||
sprintf(strBuf, "%d", pm25);
|
||||
}
|
||||
} else { /** Show invalid value. */
|
||||
sprintf(strBuf, "%s", "-");
|
||||
}
|
||||
DISP()->setFont(u8g2_font_t0_22b_tf);
|
||||
DISP()->drawStr(55, 48, strBuf);
|
||||
|
||||
/** Draw PM2.5 unit */
|
||||
DISP()->setFont(u8g2_font_t0_12_tf);
|
||||
if (config.isPmStandardInUSAQI()) {
|
||||
if (utils::isValidPMS(value.pm25_1)) {
|
||||
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(value.pm25_1));
|
||||
} else {
|
||||
sprintf(strBuf, "%s", "-");
|
||||
}
|
||||
DISP()->drawStr(55, 48, strBuf);
|
||||
DISP()->setFont(u8g2_font_t0_12_tf);
|
||||
DISP()->drawUTF8(55, 61, "AQI");
|
||||
} else {
|
||||
if (utils::isValidPMS(value.pm25_1)) {
|
||||
sprintf(strBuf, "%d", value.pm25_1);
|
||||
} else {
|
||||
sprintf(strBuf, "%s", "-");
|
||||
}
|
||||
DISP()->drawStr(55, 48, strBuf);
|
||||
DISP()->setFont(u8g2_font_t0_12_tf);
|
||||
DISP()->drawUTF8(55, 61, "ug/m³");
|
||||
}
|
||||
|
||||
@ -334,17 +388,19 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
DISP()->drawStr(100, 27, "VOC:");
|
||||
|
||||
/** Draw tvocIndexvalue */
|
||||
if (utils::isValidVOC(value.TVOC)) {
|
||||
sprintf(strBuf, "%d", value.TVOC);
|
||||
int tvoc = round(value.getAverage(Measurements::TVOC));
|
||||
if (utils::isValidVOC(tvoc)) {
|
||||
sprintf(strBuf, "%d", tvoc);
|
||||
} else {
|
||||
sprintf(strBuf, "%s", "-");
|
||||
}
|
||||
DISP()->drawStr(100, 39, strBuf);
|
||||
|
||||
/** Draw NOx label */
|
||||
int nox = round(value.getAverage(Measurements::NOx));
|
||||
DISP()->drawStr(100, 53, "NOx:");
|
||||
if (utils::isValidNOx(value.NOx)) {
|
||||
sprintf(strBuf, "%d", value.NOx);
|
||||
if (utils::isValidNOx(nox)) {
|
||||
sprintf(strBuf, "%d", nox);
|
||||
} else {
|
||||
sprintf(strBuf, "%s", "-");
|
||||
}
|
||||
@ -354,35 +410,38 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
ag->display.clear();
|
||||
|
||||
/** Set CO2 */
|
||||
if(utils::isValidCO2(value.CO2)) {
|
||||
snprintf(strBuf, sizeof(strBuf), "CO2:%d", value.CO2);
|
||||
int co2 = round(value.getAverage(Measurements::CO2));
|
||||
if (utils::isValidCO2(co2)) {
|
||||
snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2);
|
||||
} else {
|
||||
snprintf(strBuf, sizeof(strBuf), "CO2:-");
|
||||
}
|
||||
|
||||
|
||||
ag->display.setCursor(0, 0);
|
||||
ag->display.setText(strBuf);
|
||||
|
||||
/** Set PM */
|
||||
int pm25 = value.pm25_1;
|
||||
if(config.hasSensorSHT) {
|
||||
pm25 = (int)ag->pms5003.compensated(pm25, value.Humidity);
|
||||
int pm25 = round(value.getAverage(Measurements::PM25));
|
||||
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||
pm25 = round(value.getCorrectedPM25(true));
|
||||
}
|
||||
|
||||
ag->display.setCursor(0, 12);
|
||||
if (utils::isValidPMS(value.pm25_1)) {
|
||||
snprintf(strBuf, sizeof(strBuf), "PM2.5:%d", value.pm25_1);
|
||||
if (utils::isValidPm(pm25)) {
|
||||
snprintf(strBuf, sizeof(strBuf), "PM2.5:%d", pm25);
|
||||
} else {
|
||||
snprintf(strBuf, sizeof(strBuf), "PM2.5:-");
|
||||
}
|
||||
ag->display.setText(strBuf);
|
||||
|
||||
/** Set temperature and humidity */
|
||||
if (utils::isValidTemperature(value.Temperature)) {
|
||||
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
|
||||
if (utils::isValidTemperature(temp)) {
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
float tempF = (value.Temperature * 9) / 5 + 32;
|
||||
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", tempF);
|
||||
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F",
|
||||
utils::degreeC_To_F(temp));
|
||||
} else {
|
||||
snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", value.Temperature);
|
||||
snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp);
|
||||
}
|
||||
} else {
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
@ -395,8 +454,9 @@ void OledDisplay::showDashboard(const char *status) {
|
||||
ag->display.setCursor(0, 24);
|
||||
ag->display.setText(strBuf);
|
||||
|
||||
if (utils::isValidHumidity(value.Humidity)) {
|
||||
snprintf(strBuf, sizeof(strBuf), "H:%d %%", (int)value.Humidity);
|
||||
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
|
||||
if (utils::isValidHumidity(rhum)) {
|
||||
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
|
||||
} else {
|
||||
snprintf(strBuf, sizeof(strBuf), "H:- %%");
|
||||
}
|
||||
@ -423,7 +483,16 @@ void OledDisplay::setBrightness(int percent) {
|
||||
DISP()->setContrast((127 * percent) / 100);
|
||||
}
|
||||
} else if (ag->isBasic()) {
|
||||
ag->display.setContrast((255 * percent) / 100);
|
||||
if (percent == 0) {
|
||||
isDisplayOff = true;
|
||||
|
||||
// Clear display.
|
||||
ag->display.clear();
|
||||
ag->display.show();
|
||||
} else {
|
||||
isDisplayOff = false;
|
||||
ag->display.setContrast((255 * percent) / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,7 +587,7 @@ void OledDisplay::showRebooting(void) {
|
||||
do {
|
||||
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||
// setCentralText(20, "Firmware Update");
|
||||
setCentralText(40, "Reboot...");
|
||||
setCentralText(40, "Rebooting...");
|
||||
// setCentralText(60, String("Retry after 24h"));
|
||||
} while (DISP()->nextPage());
|
||||
} else if (ag->isBasic()) {
|
||||
|
@ -16,17 +16,32 @@ private:
|
||||
Measurements &value;
|
||||
bool isDisplayOff = false;
|
||||
|
||||
typedef struct {
|
||||
int width;
|
||||
int height;
|
||||
unsigned char *icon;
|
||||
} xbm_icon_t;
|
||||
|
||||
void showTempHum(bool hasStatus);
|
||||
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);
|
||||
@ -34,7 +49,7 @@ public:
|
||||
void setText(const char *line1, const char *line2, const char *line3,
|
||||
const char *line4);
|
||||
void showDashboard(void);
|
||||
void showDashboard(const char *status);
|
||||
void showDashboard(DashboardStatus status);
|
||||
void setBrightness(int percent);
|
||||
#ifdef ESP32
|
||||
void showFirmwareUpdateVersion(String version);
|
||||
|
@ -1,5 +1,7 @@
|
||||
#include "AgStateMachine.h"
|
||||
#include "AgOledDisplay.h"
|
||||
|
||||
#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 */
|
||||
@ -7,11 +9,12 @@
|
||||
|
||||
#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, 150, 0 /** Yellow */
|
||||
#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 */
|
||||
|
||||
/**
|
||||
* @brief Animation LED bar with color
|
||||
@ -46,47 +49,67 @@ void StateMachine::ledStatusBlinkDelay(uint32_t ms) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Led bar show led color status
|
||||
* @brief Led bar show PM or CO2 led color status
|
||||
*
|
||||
* @return true if all led bar are used, false othwerwise
|
||||
*/
|
||||
void StateMachine::sensorhandleLeds(void) {
|
||||
bool StateMachine::sensorhandleLeds(void) {
|
||||
int totalLedUsed = 0;
|
||||
switch (config.getLedBarMode()) {
|
||||
case LedBarMode::LedBarModeCO2:
|
||||
co2handleLeds();
|
||||
totalLedUsed = co2handleLeds();
|
||||
break;
|
||||
case LedBarMode::LedBarModePm:
|
||||
pm25handleLeds();
|
||||
totalLedUsed = pm25handleLeds();
|
||||
break;
|
||||
default:
|
||||
ag->ledBar.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
if (totalLedUsed == ag->ledBar.getNumberOfLeds()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clear the rest of unused led
|
||||
int startIndex = totalLedUsed + 1;
|
||||
for (int i = startIndex; i <= ag->ledBar.getNumberOfLeds(); i++) {
|
||||
ag->ledBar.setColor(RGB_COLOR_CLEAR, ag->ledBar.getNumberOfLeds() - i);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Show CO2 LED status
|
||||
*
|
||||
* @return return total number of led that are used on the monitor
|
||||
*/
|
||||
void StateMachine::co2handleLeds(void) {
|
||||
int co2Value = value.CO2;
|
||||
int StateMachine::co2handleLeds(void) {
|
||||
int totalUsed = ag->ledBar.getNumberOfLeds();
|
||||
int co2Value = round(value.getAverage(Measurements::CO2));
|
||||
if (co2Value <= 600) {
|
||||
/** G; 1 */
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||
totalUsed = 1;
|
||||
} else if (co2Value <= 800) {
|
||||
/** GG; 2 */
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
|
||||
totalUsed = 2;
|
||||
} else if (co2Value <= 1000) {
|
||||
/** YYY; 3 */
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
|
||||
totalUsed = 3;
|
||||
} else if (co2Value <= 1250) {
|
||||
/** OOOO; 4 */
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
|
||||
totalUsed = 4;
|
||||
} else if (co2Value <= 1500) {
|
||||
/** OOOOO; 5 */
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
|
||||
@ -94,6 +117,7 @@ void StateMachine::co2handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
|
||||
totalUsed = 5;
|
||||
} else if (co2Value <= 1750) {
|
||||
/** RRRRRR; 6 */
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
|
||||
@ -102,6 +126,7 @@ void StateMachine::co2handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
|
||||
totalUsed = 6;
|
||||
} else if (co2Value <= 2000) {
|
||||
/** RRRRRRR; 7 */
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
|
||||
@ -111,6 +136,7 @@ void StateMachine::co2handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
|
||||
totalUsed = 7;
|
||||
} else if (co2Value <= 3000) {
|
||||
/** PPPPPPPP; 8 */
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
|
||||
@ -121,6 +147,7 @@ void StateMachine::co2handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 6);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
|
||||
totalUsed = 8;
|
||||
} else { /** > 3000 */
|
||||
/* PRPRPRPRP; 9 */
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
|
||||
@ -132,41 +159,56 @@ void StateMachine::co2handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
|
||||
totalUsed = 9;
|
||||
}
|
||||
|
||||
return totalUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Show PM2.5 LED status
|
||||
*
|
||||
* @return return total number of led that are used on the monitor
|
||||
*/
|
||||
void StateMachine::pm25handleLeds(void) {
|
||||
int pm25Value = value.pm25_1;
|
||||
if (pm25Value < 5) {
|
||||
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));
|
||||
}
|
||||
|
||||
if (pm25Value <= 5) {
|
||||
/** G; 1 */
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||
} else if (pm25Value < 10) {
|
||||
totalUsed = 1;
|
||||
} else if (pm25Value <= 9) {
|
||||
/** GG; 2 */
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
|
||||
} else if (pm25Value < 20) {
|
||||
totalUsed = 2;
|
||||
} else if (pm25Value <= 20) {
|
||||
/** YYY; 3 */
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
|
||||
} else if (pm25Value < 35) {
|
||||
totalUsed = 3;
|
||||
} else if (pm25Value <= 35) {
|
||||
/** YYYY; 4 */
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4);
|
||||
} else if (pm25Value < 45) {
|
||||
totalUsed = 4;
|
||||
} else if (pm25Value <= 45) {
|
||||
/** OOOOO; 5 */
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
|
||||
} else if (pm25Value < 55) {
|
||||
totalUsed = 5;
|
||||
} else if (pm25Value <= 55) {
|
||||
/** OOOOOO; 6 */
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
|
||||
@ -174,7 +216,8 @@ void StateMachine::pm25handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
|
||||
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 6);
|
||||
} else if (pm25Value < 100) {
|
||||
totalUsed = 6;
|
||||
} else if (pm25Value <= 100) {
|
||||
/** RRRRRRR; 7 */
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
|
||||
@ -183,7 +226,8 @@ void StateMachine::pm25handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
|
||||
} else if (pm25Value < 200) {
|
||||
totalUsed = 7;
|
||||
} else if (pm25Value <= 125) {
|
||||
/** RRRRRRRR; 8 */
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
|
||||
@ -193,7 +237,8 @@ void StateMachine::pm25handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
|
||||
} else if (pm25Value < 250) {
|
||||
totalUsed = 8;
|
||||
} else if (pm25Value <= 225) {
|
||||
/** PPPPPPPPP; 9 */
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 2);
|
||||
@ -204,7 +249,8 @@ void StateMachine::pm25handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
|
||||
} else { /** > 250 */
|
||||
totalUsed = 9;
|
||||
} else { /** > 225 */
|
||||
/* PRPRPRPRP; 9 */
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
|
||||
@ -215,7 +261,10 @@ void StateMachine::pm25handleLeds(void) {
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
|
||||
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
|
||||
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
|
||||
totalUsed = 9;
|
||||
}
|
||||
|
||||
return totalUsed;
|
||||
}
|
||||
|
||||
void StateMachine::co2Calibration(void) {
|
||||
@ -305,42 +354,61 @@ void StateMachine::co2Calibration(void) {
|
||||
|
||||
void StateMachine::ledBarTest(void) {
|
||||
if (config.isLedBarTestRequested()) {
|
||||
if (config.getCountry() == "TH") {
|
||||
uint32_t tstart = millis();
|
||||
logInfo("Start run LED test for 2 min");
|
||||
while (1) {
|
||||
ledBarRunTest();
|
||||
uint32_t ms = (uint32_t)(millis() - tstart);
|
||||
if (ms >= (60 * 1000 * 2)) {
|
||||
logInfo("LED test after 2 min finish");
|
||||
break;
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
if (config.getCountry() == "TH") {
|
||||
uint32_t tstart = millis();
|
||||
logInfo("Start run LED test for 2 min");
|
||||
while (1) {
|
||||
ledBarRunTest();
|
||||
uint32_t ms = (uint32_t)(millis() - tstart);
|
||||
if (ms >= (60 * 1000 * 2)) {
|
||||
logInfo("LED test after 2 min finish");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ledBarRunTest();
|
||||
}
|
||||
} else {
|
||||
} else if (ag->isOpenAir()) {
|
||||
ledBarRunTest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StateMachine::ledBarPowerUpTest(void) { ledBarRunTest(); }
|
||||
void StateMachine::ledBarPowerUpTest(void) {
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
}
|
||||
ledBarRunTest();
|
||||
}
|
||||
|
||||
void StateMachine::ledBarRunTest(void) {
|
||||
disp.setText("LED Test", "running", ".....");
|
||||
runLedTest('r');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
runLedTest('g');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
runLedTest('b');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
runLedTest('w');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
runLedTest('n');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
if (ag->isOne()) {
|
||||
disp.setText("LED Test", "running", ".....");
|
||||
runLedTest('r');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
runLedTest('g');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
runLedTest('b');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
runLedTest('w');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
runLedTest('n');
|
||||
ag->ledBar.show();
|
||||
delay(1000);
|
||||
} else if (ag->isOpenAir()) {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ag->statusLed.setOn();
|
||||
delay(LED_TEST_BLINK_DELAY);
|
||||
ag->statusLed.setOff();
|
||||
delay(LED_TEST_BLINK_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StateMachine::runLedTest(char color) {
|
||||
@ -476,11 +544,11 @@ void StateMachine::displayHandle(AgStateMachineState state) {
|
||||
break;
|
||||
}
|
||||
case AgStateMachineWiFiLost: {
|
||||
disp.showDashboard("WiFi N/A");
|
||||
disp.showDashboard(OledDisplay::DashBoardStatusWiFiIssue);
|
||||
break;
|
||||
}
|
||||
case AgStateMachineServerLost: {
|
||||
disp.showDashboard("Server N/A");
|
||||
disp.showDashboard(OledDisplay::DashBoardStatusServerIssue);
|
||||
break;
|
||||
}
|
||||
case AgStateMachineSensorConfigFailed: {
|
||||
@ -489,19 +557,24 @@ void StateMachine::displayHandle(AgStateMachineState state) {
|
||||
if (ms >= 5000) {
|
||||
addToDashboardTime = millis();
|
||||
if (addToDashBoardToggle) {
|
||||
disp.showDashboard("Add to Dashboard");
|
||||
disp.showDashboard(OledDisplay::DashBoardStatusAddToDashboard);
|
||||
} else {
|
||||
disp.showDashboard(ag->deviceId().c_str());
|
||||
disp.showDashboard(OledDisplay::DashBoardStatusDeviceId);
|
||||
}
|
||||
addToDashBoardToggle = !addToDashBoardToggle;
|
||||
}
|
||||
} else {
|
||||
disp.showDashboard("");
|
||||
disp.showDashboard();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AgStateMachineNormal: {
|
||||
disp.showDashboard();
|
||||
if (config.isOfflineMode()) {
|
||||
disp.showDashboard(
|
||||
OledDisplay::DashBoardStatusOfflineMode);
|
||||
} else {
|
||||
disp.showDashboard();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AgStateMachineCo2Calibration:
|
||||
@ -566,15 +639,13 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
}
|
||||
|
||||
ledState = state;
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear(); // Set all LED OFF
|
||||
}
|
||||
switch (state) {
|
||||
case AgStateMachineWiFiManagerMode: {
|
||||
/** In WiFi Manager Mode */
|
||||
/** Turn LED OFF */
|
||||
/** Turn middle LED Color */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2);
|
||||
} else {
|
||||
ag->statusLed.setToggle();
|
||||
@ -584,6 +655,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiManagerPortalActive: {
|
||||
/** WiFi Manager has connected to mobile phone */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(0, 0, 255);
|
||||
} else {
|
||||
ag->statusLed.setOn();
|
||||
@ -594,6 +666,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
/** after SSID and PW entered and OK clicked, connection to WiFI network is
|
||||
* attempted */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ledBarSingleLedAnimation(255, 255, 255);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -603,6 +676,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiManagerStaConnected: {
|
||||
/** Connecting to WiFi worked */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(255, 255, 255);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -612,6 +686,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiOkServerConnecting: {
|
||||
/** once connected to WiFi an attempt to reach the server is performed */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ledBarSingleLedAnimation(0, 255, 0);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -621,6 +696,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiOkServerConnected: {
|
||||
/** Server is reachable, all fine */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(0, 255, 0);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -637,6 +713,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiManagerConnectFailed: {
|
||||
/** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(255, 0, 0);
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -655,6 +732,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
/** Connected to WiFi but server not reachable, e.g. firewall block/
|
||||
* whitelisting needed etc. */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(233, 183, 54); /** orange */
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -671,6 +749,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
case AgStateMachineWiFiOkServerOkSensorConfigFailed: {
|
||||
/** Server reachable but sensor not configured correctly */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.clear();
|
||||
ag->ledBar.setColor(139, 24, 248); /** violet */
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
@ -688,11 +767,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
/** Connection to WiFi network failed credentials incorrect encryption not
|
||||
* supported etc. */
|
||||
if (ag->isOne()) {
|
||||
/** WIFI failed status LED color */
|
||||
ag->ledBar.setColor(255, 0, 0, 0);
|
||||
/** Show CO2 or PM color status */
|
||||
// sensorLedColorHandler();
|
||||
sensorhandleLeds();
|
||||
bool allUsed = sensorhandleLeds();
|
||||
if (allUsed == false) {
|
||||
ag->ledBar.setColor(255, 0, 0, 0);
|
||||
}
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
}
|
||||
@ -702,11 +780,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
/** Connected to WiFi network but the server cannot be reached through the
|
||||
* internet, e.g. blocked by firewall */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.setColor(233, 183, 54, 0);
|
||||
|
||||
/** Show CO2 or PM color status */
|
||||
sensorhandleLeds();
|
||||
// sensorLedColorHandler();
|
||||
bool allUsed = sensorhandleLeds();
|
||||
if (allUsed == false) {
|
||||
ag->ledBar.setColor(233, 183, 54, 0);
|
||||
}
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
}
|
||||
@ -716,10 +793,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
|
||||
/** Server is reachable but there is some configuration issue to be fixed on
|
||||
* the server side */
|
||||
if (ag->isOne()) {
|
||||
ag->ledBar.setColor(139, 24, 248, 0);
|
||||
|
||||
/** Show CO2 or PM color status */
|
||||
sensorhandleLeds();
|
||||
bool allUsed = sensorhandleLeds();
|
||||
if (allUsed == false) {
|
||||
ag->ledBar.setColor(139, 24, 248, 0);
|
||||
}
|
||||
} else {
|
||||
ag->statusLed.setOff();
|
||||
}
|
||||
|
@ -24,9 +24,9 @@ private:
|
||||
|
||||
void ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b);
|
||||
void ledStatusBlinkDelay(uint32_t delay);
|
||||
void sensorhandleLeds(void);
|
||||
void co2handleLeds(void);
|
||||
void pm25handleLeds(void);
|
||||
bool sensorhandleLeds(void);
|
||||
int co2handleLeds(void);
|
||||
int pm25handleLeds(void);
|
||||
void co2Calibration(void);
|
||||
void ledBarTest(void);
|
||||
void ledBarPowerUpTest(void);
|
||||
|
1825
src/AgValue.cpp
312
src/AgValue.h
@ -1,76 +1,270 @@
|
||||
#ifndef _AG_VALUE_H_
|
||||
#define _AG_VALUE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "AgConfigure.h"
|
||||
#include "AirGradient.h"
|
||||
#include "App/AppDef.h"
|
||||
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
|
||||
#include "Main/utils.h"
|
||||
#include <Arduino.h>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class Measurements {
|
||||
private:
|
||||
// Generic struct for update indication for respective value
|
||||
struct Update {
|
||||
int invalidCounter; // Counting on how many invalid value that are passed to update function
|
||||
int max; // Maximum length of the period of the moving average
|
||||
float avg; // Moving average value, updated every update function called
|
||||
};
|
||||
|
||||
// Reading type for sensor value that outputs float
|
||||
struct FloatValue {
|
||||
float sumValues; // Total value from each update
|
||||
std::vector<float> listValues; // List of update value that are kept
|
||||
Update update;
|
||||
};
|
||||
|
||||
// Reading type for sensor value that outputs integer
|
||||
struct IntegerValue {
|
||||
unsigned long sumValues; // Total value from each update; unsigned long to accomodate TVOx and
|
||||
// NOx raw data
|
||||
std::vector<int> listValues; // List of update value that are kept
|
||||
Update update;
|
||||
};
|
||||
|
||||
public:
|
||||
Measurements() {
|
||||
pm25_1 = -1;
|
||||
pm01_1 = -1;
|
||||
pm10_1 = -1;
|
||||
pm03PCount_1 = -1;
|
||||
temp_1 = -1001;
|
||||
hum_1 = -1;
|
||||
|
||||
pm25_2 = -1;
|
||||
pm01_2 = -1;
|
||||
pm10_2 = -1;
|
||||
pm03PCount_2 = -1;
|
||||
temp_2 = -1001;
|
||||
hum_2 = -1;
|
||||
|
||||
Temperature = -1001;
|
||||
Humidity = -1;
|
||||
CO2 = -1;
|
||||
TVOC = -1;
|
||||
TVOCRaw = -1;
|
||||
NOx = -1;
|
||||
NOxRaw = -1;
|
||||
}
|
||||
Measurements(Configuration &config);
|
||||
~Measurements() {}
|
||||
|
||||
float Temperature;
|
||||
int Humidity;
|
||||
int CO2;
|
||||
int TVOC;
|
||||
int TVOCRaw;
|
||||
int NOx;
|
||||
int NOxRaw;
|
||||
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;
|
||||
};
|
||||
|
||||
int pm25_1;
|
||||
int pm01_1;
|
||||
int pm10_1;
|
||||
int pm03PCount_1;
|
||||
float temp_1;
|
||||
int hum_1;
|
||||
void setAirGradient(AirGradient *ag);
|
||||
|
||||
int pm25_2;
|
||||
int pm01_2;
|
||||
int pm10_2;
|
||||
int pm03PCount_2;
|
||||
float temp_2;
|
||||
int hum_2;
|
||||
// Enumeration for every AG measurements
|
||||
enum MeasurementType {
|
||||
Temperature,
|
||||
Humidity,
|
||||
CO2,
|
||||
TVOC, // index value
|
||||
TVOCRaw,
|
||||
NOx, // index value
|
||||
NOxRaw,
|
||||
PM01, // PM1.0 under atmospheric environment
|
||||
PM25, // PM2.5 under athompheric environment
|
||||
PM10, // PM10 under atmospheric environment
|
||||
PM01_SP, // PM1.0 standard particle
|
||||
PM25_SP, // PM2.5 standard particle
|
||||
PM10_SP, // PM10 standard particle
|
||||
PM03_PC, // Particle 0.3 count
|
||||
PM05_PC, // Particle 0.5 count
|
||||
PM01_PC, // Particle 1.0 count
|
||||
PM25_PC, // Particle 2.5 count
|
||||
PM5_PC, // Particle 5.0 count
|
||||
PM10_PC, // Particle 10 count
|
||||
};
|
||||
|
||||
int pm1Value01;
|
||||
int pm1Value25;
|
||||
int pm1Value10;
|
||||
int pm1PCount;
|
||||
int pm1temp;
|
||||
int pm1hum;
|
||||
int pm2Value01;
|
||||
int pm2Value25;
|
||||
int pm2Value10;
|
||||
int pm2PCount;
|
||||
int pm2temp;
|
||||
int pm2hum;
|
||||
int countPosition;
|
||||
const int targetCount = 20;
|
||||
int bootCount;
|
||||
void printCurrentAverage();
|
||||
|
||||
String toString(bool isLocal, AgFirmwareMode fwMode, int rssi, void* _ag, void* _config);
|
||||
/**
|
||||
* @brief Set each MeasurementType maximum period length for moving average
|
||||
*
|
||||
* @param type the target measurement type to set
|
||||
* @param max the maximum period length
|
||||
*/
|
||||
void maxPeriod(MeasurementType, int max);
|
||||
|
||||
/**
|
||||
* @brief update target measurement type with new value.
|
||||
* Each MeasurementType has last raw value and moving average value based on max period
|
||||
* This function is for MeasurementType that use INT as the data type
|
||||
*
|
||||
* @param type measurement type that will be updated
|
||||
* @param val (int) the new value
|
||||
* @param ch (int) the MeasurementType channel, not every MeasurementType has more than 1 channel.
|
||||
* Currently maximum channel is 2. Default: 1 (channel 1)
|
||||
* @return false if new value invalid consecutively reach threshold (max period)
|
||||
* @return true otherwise
|
||||
*/
|
||||
bool update(MeasurementType type, int val, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief update target measurement type with new value.
|
||||
* Each MeasurementType has last raw value and moving average value based on max period
|
||||
* This function is for MeasurementType that use FLOAT as the data type
|
||||
*
|
||||
* @param type measurement type that will be updated
|
||||
* @param val (float) the new value
|
||||
* @param ch (int) the MeasurementType channel, not every MeasurementType has more than 1 channel.
|
||||
* Currently maximum channel is 2. Default: 1 (channel 1)
|
||||
* @return false if new value invalid consecutively reach threshold (max period)
|
||||
* @return true otherwise
|
||||
*/
|
||||
bool update(MeasurementType type, float val, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief Get the target measurement latest value
|
||||
*
|
||||
* @param type measurement type that will be retrieve
|
||||
* @param ch target type value channel
|
||||
* @return int measurement type value
|
||||
*/
|
||||
int get(MeasurementType type, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief Get the target measurement latest value
|
||||
*
|
||||
* @param type measurement type that will be retrieve
|
||||
* @param ch target type value channel
|
||||
* @return float measurement type value
|
||||
*/
|
||||
float getFloat(MeasurementType type, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief Get the target measurement average value
|
||||
*
|
||||
* @param type measurement type that will be retrieve
|
||||
* @param ch target type value channel
|
||||
* @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 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);
|
||||
|
||||
/**
|
||||
* build json payload for every measurements
|
||||
*/
|
||||
String toString(bool localServer, AgFirmwareMode fwMode, int rssi);
|
||||
|
||||
Measures getMeasures();
|
||||
|
||||
std::string buildMeasuresPayload(Measures &measures);
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
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];
|
||||
IntegerValue _co2;
|
||||
IntegerValue _tvoc; // Index value
|
||||
IntegerValue _tvoc_raw;
|
||||
IntegerValue _nox; // Index value
|
||||
IntegerValue _nox_raw;
|
||||
IntegerValue _pm_01[2]; // pm 1.0 atmospheric environment
|
||||
IntegerValue _pm_25[2]; // pm 2.5 atmospheric environment
|
||||
IntegerValue _pm_10[2]; // pm 10 atmospheric environment
|
||||
IntegerValue _pm_01_sp[2]; // pm 1.0 standard particle
|
||||
IntegerValue _pm_25_sp[2]; // pm 2.5 standard particle
|
||||
IntegerValue _pm_10_sp[2]; // pm 10 standard particle
|
||||
IntegerValue _pm_03_pc[2]; // particle count 0.3
|
||||
IntegerValue _pm_05_pc[2]; // particle count 0.5
|
||||
IntegerValue _pm_01_pc[2]; // particle count 1.0
|
||||
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;
|
||||
|
||||
/**
|
||||
* @brief Get PMS5003 firmware version string
|
||||
*
|
||||
* @param fwCode
|
||||
* @return String
|
||||
*/
|
||||
String pms5003FirmwareVersion(int fwCode);
|
||||
/**
|
||||
* @brief Get PMS5003T firmware version string
|
||||
*
|
||||
* @param fwCode
|
||||
* @return String
|
||||
*/
|
||||
String pms5003TFirmwareVersion(int fwCode);
|
||||
/**
|
||||
* @brief Get firmware version string
|
||||
*
|
||||
* @param prefix Prefix firmware string
|
||||
* @param fwCode Version code
|
||||
* @return string
|
||||
*/
|
||||
String pms5003FirmwareVersionBase(String prefix, int fwCode);
|
||||
|
||||
/**
|
||||
* Convert AgValue Type to string representation of the value
|
||||
*/
|
||||
String measurementTypeStr(MeasurementType type);
|
||||
|
||||
/**
|
||||
* @brief check if provided channel is a valid channel or not
|
||||
* abort program if invalid
|
||||
*/
|
||||
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);
|
||||
};
|
||||
|
||||
#endif /** _AG_VALUE_H_ */
|
||||
|
@ -41,6 +41,28 @@ bool WifiConnector::connect(void) {
|
||||
}
|
||||
}
|
||||
|
||||
WiFi.begin();
|
||||
String wifiSSID = WIFI()->getWiFiSSID(true);
|
||||
if (wifiSSID.isEmpty()) {
|
||||
logInfo("Connected WiFi is empty, connect to default wifi \"" +
|
||||
String(this->defaultSsid) + String("\""));
|
||||
|
||||
/** Set wifi connect */
|
||||
WiFi.begin(this->defaultSsid, this->defaultPassword);
|
||||
|
||||
/** Wait for wifi connect to AP */
|
||||
int count = 0;
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(1000);
|
||||
count++;
|
||||
if (count >= 15) {
|
||||
logError("Try connect to default wifi \"" + String(this->defaultSsid) +
|
||||
String("\" failed"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WIFI()->setConfigPortalBlocking(false);
|
||||
WIFI()->setConnectTimeout(15);
|
||||
WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
||||
@ -50,7 +72,7 @@ bool WifiConnector::connect(void) {
|
||||
WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); });
|
||||
WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();});
|
||||
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
|
||||
disp.setText("Connect to", "WiFi", "...");
|
||||
disp.setText("Connecting to", "WiFi", "...");
|
||||
} else {
|
||||
logInfo("Connecting to WiFi...");
|
||||
}
|
||||
@ -59,16 +81,15 @@ bool WifiConnector::connect(void) {
|
||||
// ssid = "AG-" + String(ESP.getChipId(), HEX);
|
||||
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
||||
|
||||
WiFiManagerParameter postToAg("chbPostToAg",
|
||||
"Prevent Connection to AirGradient Server", "T",
|
||||
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
|
||||
WIFI()->addParameter(&postToAg);
|
||||
WiFiManagerParameter postToAgInfo(
|
||||
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 "
|
||||
"and your data will not reach the AirGradient dashboard.</p>");
|
||||
WIFI()->addParameter(&postToAgInfo);
|
||||
"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);
|
||||
|
||||
@ -152,12 +173,11 @@ bool WifiConnector::connect(void) {
|
||||
logInfo("WiFi Connected: " + WiFi.SSID() + " IP: " + localIpStr());
|
||||
|
||||
if (hasPortalConfig) {
|
||||
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");
|
||||
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");
|
||||
}
|
||||
hasPortalConfig = false;
|
||||
}
|
||||
@ -383,3 +403,11 @@ bool WifiConnector::hasConfigurated(void) {
|
||||
* @return false
|
||||
*/
|
||||
bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; }
|
||||
|
||||
/**
|
||||
* @brief Set wifi connect to default WiFi
|
||||
*
|
||||
*/
|
||||
void WifiConnector::setDefault(void) {
|
||||
WiFi.begin("airgradient", "cleanair");
|
||||
}
|
||||
|
@ -46,6 +46,10 @@ public:
|
||||
String localIpStr(void);
|
||||
bool hasConfigurated(void);
|
||||
bool isConfigurePorttalTimeout(void);
|
||||
|
||||
const char* defaultSsid = "airgradient";
|
||||
const char* defaultPassword = "cleanair";
|
||||
void setDefault(void);
|
||||
};
|
||||
|
||||
#endif /** _AG_WIFI_CONNECTOR_H_ */
|
||||
|
@ -65,6 +65,10 @@ bool AirGradient::isOne(void) {
|
||||
return boardType == BoardType::ONE_INDOOR;
|
||||
}
|
||||
|
||||
bool AirGradient::isOpenAir(void) {
|
||||
return boardType == BoardType::OPEN_AIR_OUTDOOR;
|
||||
}
|
||||
|
||||
bool AirGradient::isPro4_2(void) {
|
||||
return boardType == BoardType::DIY_PRO_INDOOR_V4_2;
|
||||
}
|
||||
|
@ -15,7 +15,47 @@
|
||||
#include "Main/utils.h"
|
||||
|
||||
#ifndef GIT_VERSION
|
||||
#define GIT_VERSION "3.1.5-snap"
|
||||
#define GIT_VERSION "3.3.9-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";
|
||||
#endif
|
||||
|
||||
/**
|
||||
@ -135,6 +175,14 @@ public:
|
||||
*/
|
||||
bool isOne(void);
|
||||
|
||||
/**
|
||||
* @brief Check that Airgradient object is OPEN_AIR
|
||||
*
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
bool isOpenAir(void);
|
||||
|
||||
/**
|
||||
* @brief Check that Airgradient object is DIY_PRO 4.2 indoor
|
||||
*
|
||||
|
@ -94,15 +94,30 @@ enum ConfigurationControl {
|
||||
ConfigurationControlBoth
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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);
|
||||
|
1
src/Libraries/airgradient-client
Submodule
1
src/Libraries/airgradient-ota
Submodule
@ -48,14 +48,14 @@ bool utils::isValidCO2(int16_t value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool utils::isValidPMS(int value) {
|
||||
bool utils::isValidPm(int value) {
|
||||
if ((value >= VALID_PMS_MIN) && (value <= VALID_PMS_MAX)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool utils::isValidPMS03Count(int value) {
|
||||
bool utils::isValidPm03Count(int value) {
|
||||
if (value >= VALID_PMS03COUNT_MIN) {
|
||||
return true;
|
||||
}
|
||||
@ -82,8 +82,13 @@ float utils::getInvalidHumidity(void) { return INVALID_HUMIDITY; }
|
||||
|
||||
int utils::getInvalidCO2(void) { return INVALID_CO2; }
|
||||
|
||||
int utils::getInvalidPMS(void) { return INVALID_PMS; }
|
||||
int utils::getInvalidPmValue(void) { return INVALID_PMS; }
|
||||
|
||||
int utils::getInvalidNOx(void) { return INVALID_NOX; }
|
||||
|
||||
int utils::getInvalidVOC(void) { return INVALID_VOC; }
|
||||
|
||||
float utils::degreeC_To_F(float t) {
|
||||
/** (t * 9)/5 + 32 */
|
||||
return t * 1.8f + 32.0f;
|
||||
}
|
||||
|
@ -14,16 +14,17 @@ public:
|
||||
static bool isValidTemperature(float value);
|
||||
static bool isValidHumidity(float value);
|
||||
static bool isValidCO2(int16_t value);
|
||||
static bool isValidPMS(int value);
|
||||
static bool isValidPMS03Count(int value);
|
||||
static bool isValidPm(int value);
|
||||
static bool isValidPm03Count(int value);
|
||||
static bool isValidNOx(int value);
|
||||
static bool isValidVOC(int value);
|
||||
static float getInvalidTemperature(void);
|
||||
static float getInvalidHumidity(void);
|
||||
static int getInvalidCO2(void);
|
||||
static int getInvalidPMS(void);
|
||||
static int getInvalidPmValue(void);
|
||||
static int getInvalidNOx(void);
|
||||
static int getInvalidVOC(void);
|
||||
static float degreeC_To_F(float t);
|
||||
};
|
||||
|
||||
|
||||
|
392
src/PMS/PMS.cpp
@ -2,248 +2,294 @@
|
||||
#include "../Main/BoardDef.h"
|
||||
|
||||
/**
|
||||
* @brief Init and check that sensor has connected
|
||||
* @brief Initializes the sensor and attempts to read data.
|
||||
*
|
||||
* @param stream UART stream
|
||||
* @return true Sucecss
|
||||
* @return false Failure
|
||||
*/
|
||||
bool PMSBase::begin(Stream *stream) {
|
||||
this->stream = stream;
|
||||
Serial.printf("initializing PM sensor\n");
|
||||
|
||||
failed = true;
|
||||
lastRead = 0; // To read buffer on handle without wait after 1.5sec
|
||||
failCount = 0;
|
||||
_connected = false;
|
||||
|
||||
this->stream->flush();
|
||||
// empty first
|
||||
int bytesCleared = 0;
|
||||
while (stream->read() != -1) {
|
||||
bytesCleared++;
|
||||
}
|
||||
Serial.printf("cleared %d byte(s)\n", bytesCleared);
|
||||
|
||||
// explicitly put the sensor into active mode, this seems to be be needed for the Cubic PM2009X
|
||||
Serial.printf("setting active mode\n");
|
||||
uint8_t activeModeCommand[] = { 0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x71 };
|
||||
size_t bytesWritten = stream->write(activeModeCommand, sizeof(activeModeCommand));
|
||||
Serial.printf("%d byte(s) written\n", bytesWritten);
|
||||
|
||||
// Run and check sensor data for 4sec
|
||||
while (1) {
|
||||
handle();
|
||||
if (failed == false) {
|
||||
return true;
|
||||
unsigned long lastInit = millis();
|
||||
while (true) {
|
||||
readPackage(stream);
|
||||
if (_connected) {
|
||||
break;
|
||||
}
|
||||
|
||||
delay(1);
|
||||
uint32_t ms = (uint32_t)(millis() - lastRead);
|
||||
unsigned long ms = (unsigned long)(millis() - lastInit);
|
||||
if (ms >= 4000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return _connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check and read sensor data then update variable.
|
||||
* Check result from method @isFailed before get value
|
||||
* @brief Read PMS package send to device each 1sec
|
||||
*
|
||||
* @param serial
|
||||
*/
|
||||
void PMSBase::handle() {
|
||||
uint32_t ms;
|
||||
if (lastRead == 0) {
|
||||
lastRead = millis();
|
||||
if (lastRead == 0) {
|
||||
lastRead = 1;
|
||||
void PMSBase::readPackage(Stream *serial) {
|
||||
/** If readPackage has process as period larger than READ_PACKAGE_TIMEOUT,
|
||||
* should be clear the lastPackage and readBufferIndex */
|
||||
if (lastReadPackage) {
|
||||
unsigned long ms = (unsigned long)(millis() - lastReadPackage);
|
||||
if (ms >= READ_PACKGE_TIMEOUT) {
|
||||
/** Clear buffer */
|
||||
readBufferIndex = 0;
|
||||
|
||||
/** Disable check read package timeout */
|
||||
lastPackage = 0;
|
||||
|
||||
Serial.println("Last process timeout, clear buffer and last handle package");
|
||||
}
|
||||
|
||||
lastReadPackage = millis();
|
||||
if (!lastReadPackage) {
|
||||
lastReadPackage = 1;
|
||||
}
|
||||
} else {
|
||||
ms = (uint32_t)(millis() - lastRead);
|
||||
/**
|
||||
* The PMS in Active mode sends an update data every 1 second. If we read
|
||||
* exactly every 1 sec then we may or may not get an update (depending on
|
||||
* timing tolerances). Hence we read every 2.5 seconds and expect 2 ..3
|
||||
* updates,
|
||||
*/
|
||||
if (ms < 2500) {
|
||||
return;
|
||||
lastReadPackage = millis();
|
||||
if (!lastReadPackage) {
|
||||
lastReadPackage = 1;
|
||||
}
|
||||
}
|
||||
bool result = false;
|
||||
char buf[32];
|
||||
int bufIndex;
|
||||
int step = 0;
|
||||
int len = 0;
|
||||
int bcount = 0;
|
||||
|
||||
while (stream->available()) {
|
||||
char value = stream->read();
|
||||
switch (step) {
|
||||
case 0: {
|
||||
/** Count to call delay() to release the while loop MCU resource for avoid the
|
||||
* watchdog time reset */
|
||||
uint8_t delayCount = 0;
|
||||
while (serial->available()) {
|
||||
/** Get value */
|
||||
uint8_t value = (uint8_t)serial->read();
|
||||
|
||||
/** Process receiving package... */
|
||||
switch (readBufferIndex) {
|
||||
case 0: /** Start byte 1 */
|
||||
if (value == 0x42) {
|
||||
step = 1;
|
||||
bufIndex = 0;
|
||||
buf[bufIndex++] = value;
|
||||
readBuffer[readBufferIndex++] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
case 1: /** Start byte 2 */
|
||||
if (value == 0x4d) {
|
||||
step = 2;
|
||||
buf[bufIndex++] = value;
|
||||
// Serial.println("Got 0x4d");
|
||||
readBuffer[readBufferIndex++] = value;
|
||||
} else {
|
||||
step = 0;
|
||||
readBufferIndex = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
buf[bufIndex++] = value;
|
||||
if (bufIndex >= 4) {
|
||||
len = toI16(&buf[2]);
|
||||
if (len != 28) {
|
||||
// Serial.printf("Got good bad len %d\r\n", len);
|
||||
len += 4;
|
||||
step = 3;
|
||||
} else {
|
||||
// Serial.println("Got good len");
|
||||
step = 4;
|
||||
case 2: /** Frame length */
|
||||
if (value == 0x00) {
|
||||
readBuffer[readBufferIndex++] = value;
|
||||
} else {
|
||||
readBufferIndex = 0;
|
||||
}
|
||||
break;
|
||||
case 3: /** Frame length */
|
||||
if (value == 0x1C) {
|
||||
readBuffer[readBufferIndex++] = value;
|
||||
} else {
|
||||
readBufferIndex = 0;
|
||||
}
|
||||
break;
|
||||
default: /** Data */
|
||||
{
|
||||
readBuffer[readBufferIndex++] = value;
|
||||
|
||||
/** Check that received full bufer */
|
||||
if (readBufferIndex >= sizeof(readBuffer)) {
|
||||
/** validata package */
|
||||
if (validate(readBuffer)) {
|
||||
_connected = true; /** Set connected status */
|
||||
|
||||
/** Parse data */
|
||||
parse(readBuffer);
|
||||
|
||||
/** Set last received package */
|
||||
lastPackage = millis();
|
||||
if (lastPackage == 0) {
|
||||
lastPackage = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear buffer index */
|
||||
readBufferIndex = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
bufIndex++;
|
||||
if (bufIndex >= len) {
|
||||
step = 0;
|
||||
// Serial.println("Bad lengh read all buffer");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
buf[bufIndex++] = value;
|
||||
if (bufIndex >= 32) {
|
||||
result |= validate(buf);
|
||||
step = 0;
|
||||
// Serial.println("Got data");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Reduce core panic: delay 1 ms each 32bytes data
|
||||
bcount++;
|
||||
if ((bcount % 32) == 0) {
|
||||
/** Avoid task watchdog timer reset... */
|
||||
delayCount++;
|
||||
if (delayCount >= 32) {
|
||||
delayCount = 0;
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
lastRead = millis();
|
||||
if (lastRead == 0) {
|
||||
lastRead = 1;
|
||||
}
|
||||
failed = false;
|
||||
} else {
|
||||
if (ms > 5000) {
|
||||
failed = true;
|
||||
/** Check that sensor removed */
|
||||
if (lastPackage) {
|
||||
unsigned long ms = (unsigned long)(millis() - lastPackage);
|
||||
if (ms >= READ_PACKGE_TIMEOUT) {
|
||||
lastPackage = 0;
|
||||
_connected = false;
|
||||
Serial.println("PMS disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check that PMS send is failed or disconnected
|
||||
* @brief Increate number of fail
|
||||
*
|
||||
* @return true Failed
|
||||
* @return false No problem
|
||||
*/
|
||||
bool PMSBase::isFailed(void) { return failed; }
|
||||
void PMSBase::updateFailCount(void) {
|
||||
if (failCount < failCountMax) {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
void PMSBase::resetFailCount(void) { failCount = 0; }
|
||||
|
||||
/**
|
||||
* @brief Get number of fail
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int PMSBase::getFailCount(void) { return failCount; }
|
||||
|
||||
int PMSBase::getFailCountMax(void) { return failCountMax; }
|
||||
|
||||
/**
|
||||
* @brief Read PMS 0.1 ug/m3 with CF = 1 PM estimates
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getRaw0_1(void) { return toU16(&package[4]); }
|
||||
uint16_t PMSBase::getRaw0_1(void) { return pms_raw0_1; }
|
||||
|
||||
/**
|
||||
* @brief Read PMS 2.5 ug/m3 with CF = 1 PM estimates
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getRaw2_5(void) { return toU16(&package[6]); }
|
||||
uint16_t PMSBase::getRaw2_5(void) { return pms_raw2_5; }
|
||||
|
||||
/**
|
||||
* @brief Read PMS 10 ug/m3 with CF = 1 PM estimates
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getRaw10(void) { return toU16(&package[8]); }
|
||||
uint16_t PMSBase::getRaw10(void) { return pms_raw10; }
|
||||
|
||||
/**
|
||||
* @brief Read PMS 0.1 ug/m3
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getPM0_1(void) { return toU16(&package[10]); }
|
||||
uint16_t PMSBase::getPM0_1(void) { return pms_pm0_1; }
|
||||
|
||||
/**
|
||||
* @brief Read PMS 2.5 ug/m3
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getPM2_5(void) { return toU16(&package[12]); }
|
||||
uint16_t PMSBase::getPM2_5(void) { return pms_pm2_5; }
|
||||
|
||||
/**
|
||||
* @brief Read PMS 10 ug/m3
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getPM10(void) { return toU16(&package[14]); }
|
||||
uint16_t PMSBase::getPM10(void) { return pms_pm10; }
|
||||
|
||||
/**
|
||||
* @brief Get numnber concentrations over 0.3 um/0.1L
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getCount0_3(void) { return toU16(&package[16]); }
|
||||
uint16_t PMSBase::getCount0_3(void) { return pms_count0_3; }
|
||||
|
||||
/**
|
||||
* @brief Get numnber concentrations over 0.5 um/0.1L
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getCount0_5(void) { return toU16(&package[18]); }
|
||||
uint16_t PMSBase::getCount0_5(void) { return pms_count0_5; }
|
||||
|
||||
/**
|
||||
* @brief Get numnber concentrations over 1.0 um/0.1L
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getCount1_0(void) { return toU16(&package[20]); }
|
||||
uint16_t PMSBase::getCount1_0(void) { return pms_count1_0; }
|
||||
|
||||
/**
|
||||
* @brief Get numnber concentrations over 2.5 um/0.1L
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getCount2_5(void) { return toU16(&package[22]); }
|
||||
uint16_t PMSBase::getCount2_5(void) { return pms_count2_5; }
|
||||
|
||||
bool PMSBase::connected(void) { return _connected; }
|
||||
|
||||
/**
|
||||
* @brief Get numnber concentrations over 5.0 um/0.1L (only PMS5003)
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getCount5_0(void) { return toU16(&package[24]); }
|
||||
uint16_t PMSBase::getCount5_0(void) { return pms_count5_0; }
|
||||
|
||||
/**
|
||||
* @brief Get numnber concentrations over 10.0 um/0.1L (only PMS5003)
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getCount10(void) { return toU16(&package[26]); }
|
||||
uint16_t PMSBase::getCount10(void) { return pms_count10; }
|
||||
|
||||
/**
|
||||
* @brief Get temperature (only PMS5003T)
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
int16_t PMSBase::getTemp(void) { return toI16(&package[24]); }
|
||||
int16_t PMSBase::getTemp(void) { return pms_temp; }
|
||||
|
||||
/**
|
||||
* @brief Get humidity (only PMS5003T)
|
||||
*
|
||||
* @return uint16_t
|
||||
*/
|
||||
uint16_t PMSBase::getHum(void) { return toU16(&package[26]); }
|
||||
uint16_t PMSBase::getHum(void) { return pms_hum; }
|
||||
|
||||
/**
|
||||
* @brief Get firmware version code
|
||||
*
|
||||
* @return uint8_t
|
||||
*/
|
||||
uint8_t PMSBase::getFirmwareVersion(void) { return pms_firmwareVersion; }
|
||||
|
||||
/**
|
||||
* @brief Ge PMS5003 error code
|
||||
*
|
||||
* @return uint8_t
|
||||
*/
|
||||
uint8_t PMSBase::getErrorCode(void) { return pms_errorCode; }
|
||||
|
||||
/**
|
||||
* @brief Convert PMS2.5 to US AQI unit
|
||||
@ -252,57 +298,99 @@ uint16_t PMSBase::getHum(void) { return toU16(&package[26]); }
|
||||
* @return int
|
||||
*/
|
||||
int PMSBase::pm25ToAQI(int pm02) {
|
||||
if (pm02 <= 12.0)
|
||||
return ((50 - 0) / (12.0 - .0) * (pm02 - .0) + 0);
|
||||
if (pm02 <= 9.0)
|
||||
return ((50 - 0) / (9.0 - .0) * (pm02 - .0) + 0);
|
||||
else if (pm02 <= 35.4)
|
||||
return ((100 - 50) / (35.4 - 12.0) * (pm02 - 12.0) + 50);
|
||||
return ((100 - 51) / (35.4 - 9.1) * (pm02 - 9.0) + 51);
|
||||
else if (pm02 <= 55.4)
|
||||
return ((150 - 100) / (55.4 - 35.4) * (pm02 - 35.4) + 100);
|
||||
else if (pm02 <= 150.4)
|
||||
return ((200 - 150) / (150.4 - 55.4) * (pm02 - 55.4) + 150);
|
||||
else if (pm02 <= 250.4)
|
||||
return ((300 - 200) / (250.4 - 150.4) * (pm02 - 150.4) + 200);
|
||||
else if (pm02 <= 350.4)
|
||||
return ((400 - 300) / (350.4 - 250.4) * (pm02 - 250.4) + 300);
|
||||
else if (pm02 <= 500.4)
|
||||
return ((500 - 400) / (500.4 - 350.4) * (pm02 - 350.4) + 400);
|
||||
return ((150 - 101) / (55.4 - 35.5) * (pm02 - 35.5) + 101);
|
||||
else if (pm02 <= 125.4)
|
||||
return ((200 - 151) / (125.4 - 55.5) * (pm02 - 55.5) + 151);
|
||||
else if (pm02 <= 225.4)
|
||||
return ((300 - 201) / (225.4 - 125.5) * (pm02 - 125.5) + 201);
|
||||
else if (pm02 <= 325.4)
|
||||
return ((500 - 301) / (325.4 - 225.5) * (pm02 - 225.5) + 301);
|
||||
else
|
||||
return 500;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @param intercept Intercept
|
||||
* @return float Calibrated PM2.5 value
|
||||
*/
|
||||
float PMSBase::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) {
|
||||
float calibrated;
|
||||
|
||||
float lowCalibrated = (scalingFactor * pm003Count) + intercept;
|
||||
if (lowCalibrated < 31) {
|
||||
calibrated = lowCalibrated;
|
||||
} else {
|
||||
calibrated = pm25;
|
||||
}
|
||||
|
||||
// No negative value for pm2.5
|
||||
if (calibrated < 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return calibrated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Correction PM2.5
|
||||
*
|
||||
*
|
||||
* Formula: https://www.airgradient.com/documentation/correction-algorithms/
|
||||
*
|
||||
* @param pm25 Raw PM2.5 value
|
||||
* @param humidity Humidity value (%)
|
||||
* @return int
|
||||
* @return compensated pm25 value
|
||||
*/
|
||||
int PMSBase::compensated(int pm25, float humidity) {
|
||||
float PMSBase::compensate(float pm25, float humidity) {
|
||||
float value;
|
||||
|
||||
// Correct invalid humidity value
|
||||
if (humidity < 0) {
|
||||
humidity = 0;
|
||||
}
|
||||
if (humidity > 100) {
|
||||
humidity = 100;
|
||||
humidity = 100.0f;
|
||||
}
|
||||
|
||||
if(pm25 < 30) {
|
||||
// If its already 0, do not proceed
|
||||
if (pm25 == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if (pm25 < 30) { /** pm2.5 < 30 */
|
||||
value = (pm25 * 0.524f) - (humidity * 0.0862f) + 5.75f;
|
||||
} else if(pm25 < 50) {
|
||||
value = (0.786f * (pm25 / 20 - 3 / 2) + 0.524f * (1 - (pm25 / 20 - 3 / 2))) * pm25 - (0.0862f * humidity) + 5.75f;
|
||||
} else if(pm25 < 210) {
|
||||
} else if (pm25 < 50) { /** 30 <= pm2.5 < 50 */
|
||||
value = (0.786f * (pm25 * 0.05f - 1.5f) + 0.524f * (1.0f - (pm25 * 0.05f - 1.5f))) * pm25 -
|
||||
(0.0862f * humidity) + 5.75f;
|
||||
} else if (pm25 < 210) { /** 50 <= pm2.5 < 210 */
|
||||
value = (0.786f * pm25) - (0.0862f * humidity) + 5.75f;
|
||||
} else if(pm25 < 260) {
|
||||
value = (0.69f * (pm25/50 - 21/5) + 0.786f * (1 - (pm25/50 - 21/5))) * pm25 - (0.0862f * humidity * (1 - (pm25/50 - 21/5))) + (2.966f * (pm25/50 -21/5)) + (5.75f * (1 - (pm25/50 - 21/5))) + (8.84f * (1.e-4) * pm25* (pm25/50 - 21/5));
|
||||
} else {
|
||||
value = 2.966f + (0.69f * pm25) + (8.84f * (1.e-4) * pm25);
|
||||
} else if (pm25 < 260) { /** 210 <= pm2.5 < 260 */
|
||||
value = (0.69f * (pm25 * 0.02f - 4.2f) + 0.786f * (1.0f - (pm25 * 0.02f - 4.2f))) * pm25 -
|
||||
(0.0862f * humidity * (1.0f - (pm25 * 0.02f - 4.2f))) +
|
||||
(2.966f * (pm25 * 0.02f - 4.2f)) + (5.75f * (1.0f - (pm25 * 0.02f - 4.2f))) +
|
||||
(8.84f * (1.e-4) * pm25 * pm25 * (pm25 * 0.02f - 4.2f));
|
||||
} else { /** 260 <= pm2.5 */
|
||||
value = 2.966f + (0.69f * pm25) + (8.84f * (1.e-4) * pm25 * pm25);
|
||||
}
|
||||
|
||||
if(value < 0) {
|
||||
value = 0;
|
||||
// No negative value for pm2.5
|
||||
if (value < 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (int)value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -311,13 +399,13 @@ int PMSBase::compensated(int pm25, float humidity) {
|
||||
* @param buf bytes array (must be >= 2)
|
||||
* @return int16_t
|
||||
*/
|
||||
int16_t PMSBase::toI16(char *buf) {
|
||||
int16_t PMSBase::toI16(const uint8_t *buf) {
|
||||
int16_t value = buf[0];
|
||||
value = (value << 8) | buf[1];
|
||||
return value;
|
||||
}
|
||||
|
||||
uint16_t PMSBase::toU16(char *buf) {
|
||||
uint16_t PMSBase::toU16(const uint8_t *buf) {
|
||||
uint16_t value = buf[0];
|
||||
value = (value << 8) | buf[1];
|
||||
return value;
|
||||
@ -330,16 +418,38 @@ uint16_t PMSBase::toU16(char *buf) {
|
||||
* @return true Success
|
||||
* @return false Failed
|
||||
*/
|
||||
bool PMSBase::validate(char *buf) {
|
||||
bool PMSBase::validate(const uint8_t *buf) {
|
||||
uint16_t sum = 0;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
sum += buf[i];
|
||||
}
|
||||
if (sum == toU16(&buf[30])) {
|
||||
for (int i = 0; i < 32; i++) {
|
||||
package[i] = buf[i];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PMSBase::parse(const uint8_t *buf) {
|
||||
// Standard particle
|
||||
pms_raw0_1 = toU16(&buf[4]);
|
||||
pms_raw2_5 = toU16(&buf[6]);
|
||||
pms_raw10 = toU16(&buf[8]);
|
||||
// atmospheric
|
||||
pms_pm0_1 = toU16(&buf[10]);
|
||||
pms_pm2_5 = toU16(&buf[12]);
|
||||
pms_pm10 = toU16(&buf[14]);
|
||||
|
||||
// particle count
|
||||
pms_count0_3 = toU16(&buf[16]);
|
||||
pms_count0_5 = toU16(&buf[18]);
|
||||
pms_count1_0 = toU16(&buf[20]);
|
||||
pms_count2_5 = toU16(&buf[22]);
|
||||
pms_count5_0 = toU16(&buf[24]); // PMS5003 only
|
||||
pms_count10 = toU16(&buf[26]); // PMS5003 only
|
||||
|
||||
// Others
|
||||
pms_temp = toU16(&buf[24]); // PMS5003T only
|
||||
pms_hum = toU16(&buf[26]); // PMS5003T only
|
||||
pms_firmwareVersion = buf[28];
|
||||
pms_errorCode = buf[29];
|
||||
}
|
||||
|
@ -3,11 +3,19 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define PMS_FAIL_COUNT_SET_INVALID 3
|
||||
|
||||
/**
|
||||
* Known to work with these sensors: Plantower PMS5003, Plantower PMS5003, Cubic PM2009X
|
||||
*/
|
||||
class PMSBase {
|
||||
public:
|
||||
bool begin(Stream *stream);
|
||||
void handle();
|
||||
bool isFailed(void);
|
||||
void readPackage(Stream *stream);
|
||||
void updateFailCount(void);
|
||||
void resetFailCount(void);
|
||||
int getFailCount(void);
|
||||
int getFailCountMax(void);
|
||||
uint16_t getRaw0_1(void);
|
||||
uint16_t getRaw2_5(void);
|
||||
uint16_t getRaw10(void);
|
||||
@ -18,6 +26,7 @@ public:
|
||||
uint16_t getCount0_5(void);
|
||||
uint16_t getCount1_0(void);
|
||||
uint16_t getCount2_5(void);
|
||||
bool connected(void);
|
||||
|
||||
/** For PMS5003 */
|
||||
uint16_t getCount5_0(void);
|
||||
@ -26,20 +35,56 @@ public:
|
||||
/** For PMS5003T*/
|
||||
int16_t getTemp(void);
|
||||
uint16_t getHum(void);
|
||||
uint8_t getFirmwareVersion(void);
|
||||
uint8_t getErrorCode(void);
|
||||
|
||||
int pm25ToAQI(int pm02);
|
||||
int compensated(int pm25, float humidity);
|
||||
float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept);
|
||||
float compensate(float pm25, float humidity);
|
||||
|
||||
private:
|
||||
Stream *stream;
|
||||
char package[32];
|
||||
int packageIndex;
|
||||
bool failed = false;
|
||||
uint32_t lastRead;
|
||||
static const uint8_t package_size = 32;
|
||||
|
||||
int16_t toI16(char *buf);
|
||||
uint16_t toU16(char* buf);
|
||||
bool validate(char *buf);
|
||||
/** In normal package interval is 200-800ms, In case small changed on sensor
|
||||
* it's will interval reach to 2.3sec
|
||||
*/
|
||||
const uint16_t READ_PACKGE_TIMEOUT = 3000; /** ms */
|
||||
const int failCountMax = 10;
|
||||
int failCount = 0;
|
||||
|
||||
uint8_t readBuffer[package_size];
|
||||
uint8_t readBufferIndex = 0;
|
||||
|
||||
/**
|
||||
* Save last time received package success. 0 to disable check package
|
||||
* timeout.
|
||||
*/
|
||||
unsigned long lastPackage = 0;
|
||||
bool _connected;
|
||||
|
||||
unsigned long lastReadPackage = 0;
|
||||
|
||||
uint16_t pms_raw0_1;
|
||||
uint16_t pms_raw2_5;
|
||||
uint16_t pms_raw10;
|
||||
uint16_t pms_pm0_1;
|
||||
uint16_t pms_pm2_5;
|
||||
uint16_t pms_pm10;
|
||||
uint16_t pms_count0_3;
|
||||
uint16_t pms_count0_5;
|
||||
uint16_t pms_count1_0;
|
||||
uint16_t pms_count2_5;
|
||||
uint16_t pms_count5_0;
|
||||
uint16_t pms_count10;
|
||||
int16_t pms_temp;
|
||||
uint16_t pms_hum;
|
||||
uint8_t pms_errorCode;
|
||||
uint8_t pms_firmwareVersion;
|
||||
|
||||
int16_t toI16(const uint8_t *buf);
|
||||
uint16_t toU16(const uint8_t *buf);
|
||||
bool validate(const uint8_t *buf);
|
||||
void parse(const uint8_t* buf);
|
||||
};
|
||||
|
||||
#endif /** _PMS5003_BASE_H_ */
|
||||
|
@ -3,7 +3,6 @@
|
||||
#include "../Main/utils.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include <SoftwareSerial.h>
|
||||
/**
|
||||
* @brief Init sensor
|
||||
*
|
||||
@ -38,14 +37,11 @@ bool PMS5003::begin(HardwareSerial &serial) {
|
||||
PMS5003::PMS5003(BoardType def) : _boardDef(def) {}
|
||||
|
||||
/**
|
||||
* @brief Init sensor
|
||||
*
|
||||
* @return true Success
|
||||
* @return false Failure
|
||||
* Initializes the sensor.
|
||||
*/
|
||||
bool PMS5003::begin(void) {
|
||||
if (this->_isBegin) {
|
||||
AgLog("Initialized, call end() then try again");
|
||||
AgLog("Already initialized, call end() then try again");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -63,11 +59,10 @@ bool PMS5003::begin(void) {
|
||||
}
|
||||
|
||||
#if defined(ESP8266)
|
||||
bsp->Pms5003.uart_tx_pin;
|
||||
SoftwareSerial *uart =
|
||||
this->_serial =
|
||||
new SoftwareSerial(bsp->Pms5003.uart_tx_pin, bsp->Pms5003.uart_rx_pin);
|
||||
uart->begin(9600);
|
||||
if (pms.begin(uart) == false) {
|
||||
this->_serial->begin(9600);
|
||||
if (pms.begin(this->_serial) == false) {
|
||||
AgLog("PMS failed");
|
||||
return false;
|
||||
}
|
||||
@ -78,34 +73,55 @@ bool PMS5003::begin(void) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
_ver = pms.getFirmwareVersion();
|
||||
this->_isBegin = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read PM1.0 must call this function after @ref readData success
|
||||
* @brief Read PM1.0
|
||||
*
|
||||
* @return int PM1.0 index
|
||||
* @return int PM1.0 index (atmospheric environment)
|
||||
*/
|
||||
int PMS5003::getPm01Ae(void) { return pms.getPM0_1(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM2.5 must call this function after @ref readData success
|
||||
* @brief Read PM2.5
|
||||
*
|
||||
* @return int PM2.5 index
|
||||
* @return int PM2.5 index (atmospheric environment)
|
||||
*/
|
||||
int PMS5003::getPm25Ae(void) { return pms.getPM2_5(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM10.0 must call this function after @ref readData success
|
||||
* @brief Read PM10.0
|
||||
*
|
||||
* @return int PM10.0 index
|
||||
* @return int PM10.0 index (atmospheric environment)
|
||||
*/
|
||||
int PMS5003::getPm10Ae(void) { return pms.getPM10(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM0.3 must call this function after @ref readData success
|
||||
* @brief Read PM1.0
|
||||
*
|
||||
* @return int PM1.0 index (standard particle)
|
||||
*/
|
||||
int PMS5003::getPm01Sp(void) { return pms.getRaw0_1(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM2.5
|
||||
*
|
||||
* @return int PM2.5 index (standard particle)
|
||||
*/
|
||||
int PMS5003::getPm25Sp(void) { return pms.getRaw2_5(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM10
|
||||
*
|
||||
* @return int PM10 index (standard particle)
|
||||
*/
|
||||
int PMS5003::getPm10Sp(void) { return pms.getRaw10(); }
|
||||
|
||||
/**
|
||||
* @brief Read particle 0.3 count
|
||||
*
|
||||
* @return int PM0.3 index
|
||||
*/
|
||||
@ -113,6 +129,41 @@ int PMS5003::getPm03ParticleCount(void) {
|
||||
return pms.getCount0_3();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read particle 1.0 count
|
||||
*
|
||||
* @return int particle 1.0 count index
|
||||
*/
|
||||
int PMS5003::getPm01ParticleCount(void) { return pms.getCount1_0(); }
|
||||
|
||||
/**
|
||||
* @brief Read particle 0.5 count
|
||||
*
|
||||
* @return int particle 0.5 count index
|
||||
*/
|
||||
int PMS5003::getPm05ParticleCount(void) { return pms.getCount0_5(); }
|
||||
|
||||
/**
|
||||
* @brief Read particle 2.5 count
|
||||
*
|
||||
* @return int particle 2.5 count index
|
||||
*/
|
||||
int PMS5003::getPm25ParticleCount(void) { return pms.getCount2_5(); }
|
||||
|
||||
/**
|
||||
* @brief Read particle 5.0 count
|
||||
*
|
||||
* @return int particle 5.0 count index
|
||||
*/
|
||||
int PMS5003::getPm5ParticleCount(void) { return pms.getCount5_0(); }
|
||||
|
||||
/**
|
||||
* @brief Read particle 10 count
|
||||
*
|
||||
* @return int particle 10 count index
|
||||
*/
|
||||
int PMS5003::getPm10ParticleCount(void) { return pms.getCount10(); }
|
||||
|
||||
/**
|
||||
* @brief Convert PM2.5 to US AQI
|
||||
*
|
||||
@ -121,16 +172,42 @@ int PMS5003::getPm03ParticleCount(void) {
|
||||
*/
|
||||
int PMS5003::convertPm25ToUsAqi(int pm25) { return pms.pm25ToAQI(pm25); }
|
||||
|
||||
float PMS5003::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) {
|
||||
return pms.slrCorrection(pm25, pm003Count, scalingFactor, intercept);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Correct PM2.5
|
||||
*
|
||||
*
|
||||
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
|
||||
*
|
||||
* @param pm25 PM2.5 raw value
|
||||
* @param humidity Humidity value
|
||||
* @return float
|
||||
* @return compensated value in float
|
||||
*/
|
||||
int PMS5003::compensated(int pm25, float humidity) {
|
||||
return pms.compensated(pm25, humidity);
|
||||
}
|
||||
float PMS5003::compensate(float pm25, float humidity) { return pms.compensate(pm25, humidity); }
|
||||
|
||||
/**
|
||||
* @brief Get sensor firmware version
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int PMS5003::getFirmwareVersion(void) { return _ver; }
|
||||
|
||||
/**
|
||||
* @brief Get sensor error code
|
||||
*
|
||||
* @return uint8_t
|
||||
*/
|
||||
uint8_t PMS5003::getErrorCode(void) { return pms.getErrorCode(); }
|
||||
|
||||
/**
|
||||
* @brief Is sensor connect with device
|
||||
*
|
||||
* @return true Connected
|
||||
* @return false Removed
|
||||
*/
|
||||
bool PMS5003::connected(void) { return pms.connected(); }
|
||||
|
||||
/**
|
||||
* @brief Check device initialized or not
|
||||
@ -166,12 +243,26 @@ void PMS5003::end(void) {
|
||||
* @brief Check and read PMS sensor data. This method should be callack from
|
||||
* loop process to continoue check sensor data if it's available
|
||||
*/
|
||||
void PMS5003::handle(void) { pms.handle(); }
|
||||
void PMS5003::handle(void) { pms.readPackage(this->_serial); }
|
||||
|
||||
void PMS5003::updateFailCount(void) {
|
||||
pms.updateFailCount();
|
||||
}
|
||||
|
||||
void PMS5003::resetFailCount(void) {
|
||||
pms.resetFailCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sensor status
|
||||
* @brief Get number of fail count
|
||||
*
|
||||
* @return true No problem
|
||||
* @return false Communication timeout or sensor has removed
|
||||
* @return int
|
||||
*/
|
||||
bool PMS5003::isFailed(void) { return pms.isFailed(); }
|
||||
int PMS5003::getFailCount(void) { return pms.getFailCount(); }
|
||||
|
||||
/**
|
||||
* @brief Get number of fail count max
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int PMS5003::getFailCountMax(void) { return pms.getFailCountMax(); }
|
||||
|
@ -4,6 +4,9 @@
|
||||
#include "../Main/BoardDef.h"
|
||||
#include "PMS.h"
|
||||
#include "Stream.h"
|
||||
#ifdef ESP8266
|
||||
#include <SoftwareSerial.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief The class define how to handle PMS5003 sensor bas on @ref PMS class
|
||||
@ -18,22 +21,43 @@ public:
|
||||
#endif
|
||||
void end(void);
|
||||
void handle(void);
|
||||
bool isFailed(void);
|
||||
void updateFailCount(void);
|
||||
void resetFailCount(void);
|
||||
int getFailCount(void);
|
||||
int getFailCountMax(void);
|
||||
// Atmospheric environment
|
||||
int getPm01Ae(void);
|
||||
int getPm25Ae(void);
|
||||
int getPm10Ae(void);
|
||||
// Standard particle
|
||||
int getPm01Sp(void);
|
||||
int getPm25Sp(void);
|
||||
int getPm10Sp(void);
|
||||
// Particle count
|
||||
int getPm03ParticleCount(void);
|
||||
int getPm05ParticleCount(void);
|
||||
int getPm01ParticleCount(void);
|
||||
int getPm25ParticleCount(void);
|
||||
int getPm5ParticleCount(void);
|
||||
int getPm10ParticleCount(void);
|
||||
|
||||
int convertPm25ToUsAqi(int pm25);
|
||||
int compensated(int pm25, float humidity);
|
||||
float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept);
|
||||
float compensate(float pm25, float humidity);
|
||||
int getFirmwareVersion(void);
|
||||
uint8_t getErrorCode(void);
|
||||
bool connected(void);
|
||||
|
||||
private:
|
||||
bool _isBegin = false;
|
||||
int _ver;
|
||||
BoardType _boardDef;
|
||||
PMSBase pms;
|
||||
const BoardDef *bsp;
|
||||
#if defined(ESP8266)
|
||||
Stream *_debugStream;
|
||||
const char *TAG = "PMS5003";
|
||||
SoftwareSerial *_serial;
|
||||
#else
|
||||
HardwareSerial *_serial;
|
||||
#endif
|
||||
|
@ -67,11 +67,10 @@ bool PMS5003T::begin(void) {
|
||||
}
|
||||
|
||||
#if defined(ESP8266)
|
||||
bsp->Pms5003.uart_tx_pin;
|
||||
SoftwareSerial *uart =
|
||||
this->_serial =
|
||||
new SoftwareSerial(bsp->Pms5003.uart_tx_pin, bsp->Pms5003.uart_rx_pin);
|
||||
uart->begin(9600);
|
||||
if (pms.begin(uart) == false) {
|
||||
this->_serial->begin(9600);
|
||||
if (pms.begin(this->_serial) == false) {
|
||||
AgLog("PMS failed");
|
||||
return false;
|
||||
}
|
||||
@ -103,41 +102,83 @@ bool PMS5003T::begin(void) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
_ver = pms.getFirmwareVersion();
|
||||
this->_isBegin = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read PM1.0 must call this function after @ref readData success
|
||||
* @brief Read PM1.0
|
||||
*
|
||||
* @return int PM1.0 index
|
||||
* @return int PM1.0 index (atmospheric environment)
|
||||
*/
|
||||
int PMS5003T::getPm01Ae(void) { return pms.getPM0_1(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM2.5 must call this function after @ref readData success
|
||||
* @brief Read PM2.5
|
||||
*
|
||||
* @return int PM2.5 index
|
||||
* @return int PM2.5 index (atmospheric environment)
|
||||
*/
|
||||
int PMS5003T::getPm25Ae(void) { return pms.getPM2_5(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM10.0 must call this function after @ref readData success
|
||||
* @brief Read PM10.0
|
||||
*
|
||||
* @return int PM10.0 index
|
||||
* @return int PM10.0 index (atmospheric environment)
|
||||
*/
|
||||
int PMS5003T::getPm10Ae(void) { return pms.getPM10(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM 0.3 Count must call this function after @ref readData success
|
||||
* @brief Read PM1.0
|
||||
*
|
||||
* @return int PM 0.3 Count index
|
||||
* @return int PM1.0 index (standard particle)
|
||||
*/
|
||||
int PMS5003T::getPm01Sp(void) { return pms.getRaw0_1(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM2.5
|
||||
*
|
||||
* @return int PM2.5 index (standard particle)
|
||||
*/
|
||||
int PMS5003T::getPm25Sp(void) { return pms.getRaw2_5(); }
|
||||
|
||||
/**
|
||||
* @brief Read PM10
|
||||
*
|
||||
* @return int PM10 index (standard particle)
|
||||
*/
|
||||
int PMS5003T::getPm10Sp(void) { return pms.getRaw10(); }
|
||||
|
||||
/**
|
||||
* @brief Read particle 0.3 count
|
||||
*
|
||||
* @return int particle 0.3 count index
|
||||
*/
|
||||
int PMS5003T::getPm03ParticleCount(void) {
|
||||
return pms.getCount0_3();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read particle 0.5 count
|
||||
*
|
||||
* @return int particle 0.5 count index
|
||||
*/
|
||||
int PMS5003T::getPm05ParticleCount(void) { return pms.getCount0_5(); }
|
||||
|
||||
/**
|
||||
* @brief Read particle 1.0 count
|
||||
*
|
||||
* @return int particle 1.0 count index
|
||||
*/
|
||||
int PMS5003T::getPm01ParticleCount(void) { return pms.getCount1_0(); }
|
||||
|
||||
/**
|
||||
* @brief Read particle 2.5 count
|
||||
*
|
||||
* @return int particle 2.5 count index
|
||||
*/
|
||||
int PMS5003T::getPm25ParticleCount(void) { return pms.getCount2_5(); }
|
||||
|
||||
/**
|
||||
* @brief Convert PM2.5 to US AQI
|
||||
*
|
||||
@ -166,14 +207,36 @@ float PMS5003T::getRelativeHumidity(void) {
|
||||
|
||||
/**
|
||||
* @brief Correct PM2.5
|
||||
*
|
||||
*
|
||||
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
|
||||
*
|
||||
* @param pm25 PM2.5 raw value
|
||||
* @param humidity Humidity value
|
||||
* @return float
|
||||
* @return compensated value
|
||||
*/
|
||||
float PMS5003T::compensated(int pm25, float humidity) {
|
||||
return pms.compensated(pm25, humidity);
|
||||
}
|
||||
float PMS5003T::compensate(float pm25, float humidity) { return pms.compensate(pm25, humidity); }
|
||||
|
||||
/**
|
||||
* @brief Get module(s) firmware version
|
||||
*
|
||||
* @return int Version code
|
||||
*/
|
||||
int PMS5003T::getFirmwareVersion(void) { return _ver; }
|
||||
|
||||
/**
|
||||
* @brief Get sensor error code
|
||||
*
|
||||
* @return uint8_t
|
||||
*/
|
||||
uint8_t PMS5003T::getErrorCode(void) { return pms.getErrorCode(); }
|
||||
|
||||
/**
|
||||
* @brief Is sensor connect to device
|
||||
*
|
||||
* @return true Connected
|
||||
* @return false Removed
|
||||
*/
|
||||
bool PMS5003T::connected(void) { return pms.connected(); }
|
||||
|
||||
/**
|
||||
* @brief Check device initialized or not
|
||||
@ -206,13 +269,26 @@ void PMS5003T::end(void) {
|
||||
* @brief Check and read PMS sensor data. This method should be callack from
|
||||
* loop process to continoue check sensor data if it's available
|
||||
*/
|
||||
void PMS5003T::handle(void) { pms.handle(); }
|
||||
void PMS5003T::handle(void) { pms.readPackage(this->_serial); }
|
||||
|
||||
void PMS5003T::updateFailCount(void) {
|
||||
pms.updateFailCount();
|
||||
}
|
||||
|
||||
void PMS5003T::resetFailCount(void) {
|
||||
pms.resetFailCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sensor status
|
||||
*
|
||||
* @return true No problem
|
||||
* @return false Communication timeout or sensor has removed
|
||||
* @brief Get fail count
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
bool PMS5003T::isFailed(void) { return pms.isFailed(); }
|
||||
int PMS5003T::getFailCount(void) { return pms.getFailCount(); }
|
||||
|
||||
/**
|
||||
* @brief Get fail count max
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int PMS5003T::getFailCountMax(void) { return pms.getFailCountMax(); }
|
||||
|
@ -6,6 +6,9 @@
|
||||
#include "PMS5003TBase.h"
|
||||
#include "Stream.h"
|
||||
#include <HardwareSerial.h>
|
||||
#ifdef ESP8266
|
||||
#include <SoftwareSerial.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief The class define how to handle PMS5003T sensor bas on @ref PMS class
|
||||
@ -21,25 +24,43 @@ public:
|
||||
void end(void);
|
||||
|
||||
void handle(void);
|
||||
bool isFailed(void);
|
||||
void updateFailCount(void);
|
||||
void resetFailCount(void);
|
||||
int getFailCount(void);
|
||||
int getFailCountMax(void);
|
||||
// Atmospheric environment
|
||||
int getPm01Ae(void);
|
||||
int getPm25Ae(void);
|
||||
int getPm10Ae(void);
|
||||
// Standard particle
|
||||
int getPm01Sp(void);
|
||||
int getPm25Sp(void);
|
||||
int getPm10Sp(void);
|
||||
// Particle count
|
||||
int getPm03ParticleCount(void);
|
||||
int getPm05ParticleCount(void);
|
||||
int getPm01ParticleCount(void);
|
||||
int getPm25ParticleCount(void);
|
||||
|
||||
int convertPm25ToUsAqi(int pm25);
|
||||
float getTemperature(void);
|
||||
float getRelativeHumidity(void);
|
||||
float compensated(int pm25, float humidity);
|
||||
float compensate(float pm25, float humidity);
|
||||
int getFirmwareVersion(void);
|
||||
uint8_t getErrorCode(void);
|
||||
bool connected(void);
|
||||
|
||||
private:
|
||||
bool _isBegin = false;
|
||||
bool _isSleep = false;
|
||||
int _ver; /** Firmware version code */
|
||||
|
||||
BoardType _boardDef;
|
||||
const BoardDef *bsp;
|
||||
#if defined(ESP8266)
|
||||
Stream *_debugStream;
|
||||
const char *TAG = "PMS5003T";
|
||||
SoftwareSerial *_serial;
|
||||
#else
|
||||
HardwareSerial *_serial;
|
||||
#endif
|
||||
|
@ -4,14 +4,30 @@ PMS5003TBase::PMS5003TBase() {}
|
||||
|
||||
PMS5003TBase::~PMS5003TBase() {}
|
||||
|
||||
float PMS5003TBase::temperatureCompensated(float temp) {
|
||||
/**
|
||||
* @brief Compensate the temperature
|
||||
*
|
||||
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
|
||||
*
|
||||
* @param temp
|
||||
* @return * float
|
||||
*/
|
||||
float PMS5003TBase::compensateTemp(float temp) {
|
||||
if (temp < 10.0f) {
|
||||
return temp * 1.327f - 6.738f;
|
||||
}
|
||||
return temp * 1.181f - 5.113f;
|
||||
}
|
||||
|
||||
float PMS5003TBase::humidityCompensated(float hum) {
|
||||
/**
|
||||
* @brief Compensate the humidity
|
||||
*
|
||||
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
|
||||
*
|
||||
* @param temp
|
||||
* @return * float
|
||||
*/
|
||||
float PMS5003TBase::compensateHum(float hum) {
|
||||
hum = hum * 1.259f + 7.34f;
|
||||
|
||||
if (hum > 100.0f) {
|
||||
|
@ -8,8 +8,8 @@ private:
|
||||
public:
|
||||
PMS5003TBase();
|
||||
~PMS5003TBase();
|
||||
float temperatureCompensated(float temp);
|
||||
float humidityCompensated(float hum);
|
||||
float compensateTemp(float temp);
|
||||
float compensateHum(float hum);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -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;
|
||||
|