mirror of
https://github.com/airgradienthq/arduino.git
synced 2025-06-26 16:21:33 +02:00
Compare commits
619 Commits
Author | SHA1 | Date | |
---|---|---|---|
280ea5e997 | |||
566f8a63b4 | |||
9e4d52454b | |||
5f5e985309 | |||
d638573ca7 | |||
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 | |||
b1aaa04421 | |||
2df78e9066 | |||
186f0d27ab | |||
e25aa87ecc | |||
1cc8941a5c | |||
9bf1495be7 | |||
73089b51f5 | |||
625e60a5bf | |||
88e3d0bd3f | |||
171821cfcf | |||
900a2da2ac | |||
fb57a112c9 | |||
ab69b686ec | |||
6746d25dc2 | |||
be150e105a | |||
ecadeeb156 | |||
219ff73132 | |||
0a9142204d | |||
81b13134d2 | |||
f3a9c722b2 | |||
3be3218115 | |||
5edb21cfe9 | |||
6cd587b008 | |||
6d01366887 | |||
1a347e9cfe | |||
6432e4451e | |||
97f0696002 | |||
e46e11c030 | |||
dc261f668d | |||
b5cced40d2 | |||
040bd28038 | |||
b0ae851427 | |||
01943f594d | |||
01a69668cc | |||
ed7b8df6fe | |||
6c1c914716 | |||
6a0d88ff10 | |||
9097eed137 | |||
c9b5e5f0d7 | |||
c12bac4ce3 | |||
9ae9b2ac9c | |||
7fb3e68b6d | |||
cf65a1f901 | |||
5fb27b6d1e | |||
7b9dac756b | |||
4b2a5f5540 | |||
4af77d532e | |||
812c2ab803 | |||
0ece16f434 | |||
df6cca3714 | |||
c8aa07ae20 | |||
a1d216ac77 | |||
a9d9c60dfa | |||
e58ce1cbea | |||
64827223ec | |||
bddd4fef25 | |||
0eba54fd28 | |||
a4176f966a | |||
2de54ca6a6 | |||
bfd20a73da | |||
3ebce4ac44 | |||
f08c8edd19 | |||
6b3e8e3096 | |||
beb17de7dc | |||
bccadd17d7 | |||
863b1f908e | |||
5a2c1bd1d0 | |||
dbc63194e6 | |||
57c33e4900 | |||
48f1a8042a | |||
fbd5779fe6 | |||
45a4d98267 | |||
0119a4d62a | |||
7560251deb | |||
31655b0a4f | |||
7d15c37ad9 | |||
411beda220 | |||
0e869d1c0c | |||
f25f816428 | |||
c8745e123b | |||
5ea01b8e9f | |||
a11eaea532 | |||
3b6859f483 | |||
bd237ed95d | |||
d24b20a734 | |||
227a4f76f7 | |||
adabb9baa4 | |||
935e7f365f | |||
d4ea03f39e | |||
89aefdda43 | |||
8a83e408d3 | |||
7a1a0337d1 | |||
6bdd4cb02f | |||
38bd758b69 | |||
6aa19ea3e6 | |||
da323b1a46 | |||
2ae90444bb | |||
21e802da33 | |||
e0869fbaf0 | |||
70662091ec | |||
960d2bad64 | |||
7abf7f5e6a | |||
6bad4fd04b | |||
a17f18b3db | |||
3da4900462 | |||
d2d81f6b4b | |||
0b12f56513 | |||
f7e811e34b | |||
037bb37184 | |||
fde510ba96 | |||
14b152a2a7 | |||
ef6b041529 | |||
11ecea1493 | |||
3e2e8b15eb | |||
4249a86fd5 | |||
4a58b0b1c7 | |||
5f93329e96 | |||
59d189e1d3 | |||
2e62abe2d7 | |||
7569b114bf | |||
3c1d0a862f | |||
4d883af77e | |||
e9224a5de0 | |||
af0fbadd80 | |||
79f6c040c7 | |||
f262866148 | |||
78a2a78020 | |||
65e759fb90 | |||
3fc7e4b55e | |||
5857388c2d | |||
5770b41fd4 | |||
d85d890878 | |||
9fbd31d0c8 | |||
c5b7c43293 | |||
7550ef7b0c | |||
805546b78e | |||
59880f4be5 | |||
ee7837a471 | |||
ebbf0adf2f | |||
d9551dc560 | |||
6ea0ab9272 | |||
6e54409512 | |||
f35bc4feaa | |||
c04ab90fd2 | |||
ed02f66ca2 | |||
4b94926651 | |||
f505b39247 | |||
9e44cd89d9 | |||
e7b95c0bde | |||
b72394b004 | |||
1544989fe6 | |||
d7b5e999c1 | |||
9b0210f7b5 | |||
3715266f8c | |||
a4eb607174 | |||
f55fa6a617 | |||
4f9f800cce | |||
cad5d1f0e7 | |||
55ede2b04d | |||
563bdfe4b2 | |||
e2154af85f | |||
9a2bbd7a57 | |||
a8c8246632 | |||
a71c038864 | |||
799217e724 | |||
b3f02f0a58 | |||
c640cf773e | |||
c145666fcb | |||
466bb0eb21 | |||
4612f4b793 | |||
45c7279866 | |||
5cb838af29 | |||
3201fd8d9c | |||
f23c7e9e31 | |||
5b18a8353d | |||
1e81c9b125 | |||
3dae4cb06d | |||
b4745ef55d | |||
348cb2663a | |||
1b69e8a599 | |||
d17ad3cbab | |||
a6d8936ea6 | |||
8b428855b0 | |||
22dc2136e4 | |||
f23f81f575 | |||
7a4255b2bb | |||
d844ab09fa | |||
31c60dcbec | |||
f0749783fe | |||
d34605a018 | |||
1bcb9bf5ee | |||
296bf49e5e | |||
e3dee42b4b | |||
279ccb8bfb | |||
a3c9727b02 | |||
1b886d9843 | |||
066e81b186 | |||
c98d078d4c | |||
cb98183e20 | |||
0e1734b35d | |||
da6326db0f | |||
ad1da129c0 | |||
8a90fe511b | |||
cca1ab69bb | |||
955172d3d3 | |||
4500f41ed3 | |||
7c2f8e5b9b | |||
fb84b53077 | |||
3359b1817b | |||
8eb8d4a1ec | |||
d2723de0f8 | |||
4493156739 | |||
0acb7d470d | |||
8428442dea | |||
f32d6b1bbe | |||
46600f59a3 | |||
01c42387ed | |||
f08438db46 | |||
221730160b | |||
d40b1d37a8 | |||
ecd3aa988f | |||
095787d1f2 | |||
d94d074abe | |||
cdef9822b3 | |||
29c1989e78 | |||
bc3872e631 | |||
7a182ebb12 | |||
5e6f801534 | |||
88808a2ad2 | |||
a4fc954712 | |||
1580cd51aa | |||
3efba24e24 | |||
940dd0167c | |||
6b4f86e7e4 | |||
e5a0c6bc7b | |||
b8fdb38db8 | |||
b2c55c38dc | |||
4fac3fddb8 | |||
c025bae3df | |||
e82b5f8369 | |||
1ec5d84043 | |||
26546a6079 | |||
5078b35341 | |||
a3cbca61ee | |||
6d7750d917 | |||
1c8d7b04e9 | |||
e12a154235 | |||
12f03aff30 | |||
5275f5a810 | |||
442f0fd942 | |||
9feac035eb | |||
21d984c95a | |||
b91a3058fc | |||
2d96fc28c5 | |||
7561017b3d | |||
2b891c0194 | |||
d5fc35df2f | |||
a91f6c1fa0 | |||
89802551a0 | |||
556a6fbd6d | |||
302bec9d37 | |||
e61cd9ba6a | |||
71b45fcdc1 | |||
a2b30a5467 | |||
4063536078 | |||
563fc062cb | |||
5534a2cf7e | |||
cb4ae82b1b | |||
8adca3a9ee | |||
5f15e29c76 | |||
6e1ac26187 | |||
ccee987d05 | |||
09cbbed856 | |||
e1115659e2 | |||
25ef1ced9e | |||
2ccddf0e19 | |||
9a1e0f4cdd | |||
86c6095362 | |||
1d6a0a06c0 | |||
bd1197971f | |||
c28a937384 | |||
b2195219ab | |||
cb7a6a2dfd | |||
51ff8f8df4 | |||
5b271f4ed9 | |||
4577082731 | |||
9a03fb2bd7 | |||
dfba4fa4b1 | |||
f681d4b2e8 | |||
dba385f5bb | |||
027ffeaa92 | |||
c1ab99ba8d | |||
8e032927c6 | |||
f52eab87d2 | |||
954a7751cc | |||
7d68b02f76 | |||
3788aa2746 | |||
260e904326 | |||
9055bbb690 | |||
5e20b9e8ec | |||
adce439ce7 | |||
dc875dd5a8 | |||
378688d2fa | |||
a2200795d7 | |||
3889aa660e | |||
efe68a54a4 | |||
a960d086e1 | |||
3537a3012c | |||
d255e6ad04 | |||
e47096feac | |||
063612e08f | |||
7cfa722684 | |||
53285ab4ff | |||
f46c66a77f | |||
9c8ae315a0 | |||
3ef438412f | |||
ce1373141a | |||
aceecde7b6 | |||
6926abd6f7 | |||
15dec40dfc | |||
4a36cf0c13 | |||
ecc92a6824 | |||
3d243cb8ca | |||
471448a0f1 | |||
ea3e976232 | |||
87f2463233 | |||
49c7877ec3 | |||
be1a9778e6 | |||
ed1d45cea1 | |||
db31b39ce2 | |||
d92d312b0c | |||
6837529096 | |||
b94ae9eff0 | |||
1810c0f355 | |||
eb0f45750d | |||
9ae8fb2355 | |||
512509c2e2 | |||
66815f590c | |||
f60e9bbe3e | |||
f361e3c9a9 | |||
e76dcf07c8 | |||
e6fe489be7 | |||
9ddb606a00 | |||
cd5ee2da18 | |||
4c42a9ddc8 | |||
78b1b0975c | |||
d99881aa46 | |||
df937fe65f | |||
dc742d3c92 | |||
a7b2ad526f | |||
bb804b9f6a | |||
1a00073cf6 | |||
469d07a2d6 | |||
6cf5e31843 | |||
3f1da6387b | |||
99b4858f1d | |||
4374c980ec | |||
ded7637b06 | |||
6a79ab6b5b | |||
7baff75524 | |||
d421c94647 | |||
d78205aa20 | |||
c1228bbd06 | |||
1eb43f684b | |||
4798e44cb7 | |||
a867e9af38 | |||
8fcf257726 | |||
a59d5a1bb8 | |||
b4d6006678 | |||
236c5bab84 | |||
852fdc4360 | |||
f7e85a92e8 | |||
e99fc2ecdc | |||
67785ed99b | |||
45ac4f116b | |||
173e3caf2f | |||
351af57591 | |||
0bda7a1c4b | |||
9a31c107fa | |||
5449fa15ea | |||
3e4e2affa8 | |||
d3a242a0b7 | |||
e5e2887c4d | |||
4eda2e4cb5 | |||
fcee721d58 | |||
7d12e63e34 | |||
8ff8b7929e | |||
66c53daed6 | |||
5de3a34dd0 | |||
b94112e22a | |||
99e925e7bd | |||
d8cba0d346 | |||
39de897621 | |||
9f1a793848 | |||
be9ba88d52 | |||
0084b6fb91 | |||
75b579bafa | |||
cf5ff99d8a | |||
ea204d90b1 | |||
13f6c2c747 | |||
760f827d0d | |||
8c8e0d4dea | |||
b749495bf4 | |||
7e3eabf09f | |||
e636876c9b | |||
68953d7390 | |||
1a52c2d9f8 | |||
6afcf6d4c3 | |||
af139331b1 | |||
e79a798b88 | |||
14fb790e2a | |||
2aab02940d | |||
da07067661 | |||
b2091114b3 | |||
e09128572c | |||
e16966d092 | |||
26a8b065bc | |||
589b98d97e | |||
cb4d9372f8 | |||
6cb7fa8a1b | |||
6cdbb8a0a3 | |||
781fb51c6f | |||
17646f3067 | |||
7a4b665bb5 | |||
571b36d05f | |||
23513cf88c | |||
b475c5c1ec | |||
ee9f26ee04 | |||
8c94cea764 | |||
7c63af5ba9 | |||
7c1eae83e4 | |||
e48ff0e41c | |||
c9e3a2a9b4 |
61
.github/workflows/check.yml
vendored
Normal file
61
.github/workflows/check.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
compile:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
example:
|
||||||
|
- "BASIC"
|
||||||
|
- "DiyProIndoorV4_2"
|
||||||
|
- "DiyProIndoorV3_3"
|
||||||
|
- "TestCO2"
|
||||||
|
- "TestPM"
|
||||||
|
- "TestSht"
|
||||||
|
- "OneOpenAir"
|
||||||
|
fqbn:
|
||||||
|
- "esp8266:esp8266:d1_mini"
|
||||||
|
- "esp32:esp32:esp32c3"
|
||||||
|
include:
|
||||||
|
- fqbn: "esp8266:esp8266:d1_mini"
|
||||||
|
core: "esp8266:esp8266@3.1.2"
|
||||||
|
core_url: "https://arduino.esp8266.com/stable/package_esp8266com_index.json"
|
||||||
|
- fqbn: "esp32:esp32:esp32c3"
|
||||||
|
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"
|
||||||
|
- example: "DiyProIndoorV4_2"
|
||||||
|
fqbn: "esp32:esp32:esp32c3"
|
||||||
|
- example: "DiyProIndoorV3_3"
|
||||||
|
fqbn: "esp32:esp32:esp32c3"
|
||||||
|
- example: "OneOpenAir"
|
||||||
|
fqbn: "esp8266:esp8266:d1_mini"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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 }}'
|
||||||
|
# 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
|
||||||
|
# "hardware lab" runner that is directly connected to a relevant device.
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,2 +1,5 @@
|
|||||||
.vscode
|
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
|
build
|
||||||
|
.vscode
|
||||||
|
/.idea/
|
||||||
|
.pio
|
||||||
|
@ -24,6 +24,10 @@ If you have an older version of the AirGradient PCB not mentioned in the example
|
|||||||
|
|
||||||
If you have any questions or problems, check out [our forum](https://forum.airgradient.com/).
|
If you have any questions or problems, check out [our forum](https://forum.airgradient.com/).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Local server API documentation is available in [/docs/local-server.md](/docs/local-server.md) and AirGradient server API on [https://api.airgradient.com/public/docs/api/v1/](https://api.airgradient.com/public/docs/api/v1/).
|
||||||
|
|
||||||
## The following libraries have been integrated into this library for ease of use
|
## The following libraries have been integrated into this library for ease of use
|
||||||
|
|
||||||
- [Adafruit BusIO](https://github.com/adafruit/Adafruit_BusIO)
|
- [Adafruit BusIO](https://github.com/adafruit/Adafruit_BusIO)
|
||||||
@ -35,7 +39,9 @@ If you have any questions or problems, check out [our forum](https://forum.airgr
|
|||||||
- [Sensirion Core](https://github.com/Sensirion/arduino-core/)
|
- [Sensirion Core](https://github.com/Sensirion/arduino-core/)
|
||||||
- [Sensirion I2C SGP41](https://github.com/Sensirion/arduino-i2c-sgp41)
|
- [Sensirion I2C SGP41](https://github.com/Sensirion/arduino-i2c-sgp41)
|
||||||
- [Sensirion I2C SHT](https://github.com/Sensirion/arduino-sht)
|
- [Sensirion I2C SHT](https://github.com/Sensirion/arduino-sht)
|
||||||
- [PMS](https://github.com/fu-hsi/pms)
|
- [WiFiManager](https://github.com/tzapu/WiFiManager)
|
||||||
|
- [Arduino_JSON](https://github.com/arduino-libraries/Arduino_JSON)
|
||||||
|
- [PubSubClient](https://github.com/knolleary/pubsubclient)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||||
|
216
docs/local-server.md
Normal file
216
docs/local-server.md
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
## Local Server API
|
||||||
|
|
||||||
|
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
|
||||||
|
|
||||||
|
#### Discovery
|
||||||
|
|
||||||
|
The monitors run a mDNS discovery. So within the same network, the monitor can be accessed through:
|
||||||
|
|
||||||
|
http://airgradient_{{serialnumber}}.local
|
||||||
|
|
||||||
|
|
||||||
|
The following requests are possible:
|
||||||
|
|
||||||
|
#### Get Current Air Quality (GET)
|
||||||
|
|
||||||
|
With the path "/measures/current" you can get the current air quality data.
|
||||||
|
|
||||||
|
http://airgradient_ecda3b1eaaaf.local/measures/current
|
||||||
|
|
||||||
|
“ecda3b1eaaaf” being the serial number of your monitor.
|
||||||
|
|
||||||
|
You get the following response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"wifi": -46,
|
||||||
|
"serialno": "ecda3b1eaaaf",
|
||||||
|
"rco2": 447,
|
||||||
|
"pm01": 3,
|
||||||
|
"pm02": 7,
|
||||||
|
"pm10": 8,
|
||||||
|
"pm003Count": 442,
|
||||||
|
"atmp": 25.87,
|
||||||
|
"atmpCompensated": 24.47,
|
||||||
|
"rhum": 43,
|
||||||
|
"rhumCompensated": 49,
|
||||||
|
"tvocIndex": 100,
|
||||||
|
"tvocRaw": 33051,
|
||||||
|
"noxIndex": 1,
|
||||||
|
"noxRaw": 16307,
|
||||||
|
"boot": 6,
|
||||||
|
"bootCount": 6,
|
||||||
|
"ledMode": "pm",
|
||||||
|
"firmware": "3.1.3",
|
||||||
|
"model": "I-9PSL",
|
||||||
|
"monitorDisplayCompensatedValues": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 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)
|
||||||
|
|
||||||
|
"/config" path returns the current configuration of the monitor.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"country": "TH",
|
||||||
|
"pmStandard": "ugm3",
|
||||||
|
"ledBarMode": "pm",
|
||||||
|
"abcDays": 7,
|
||||||
|
"tvocLearningOffset": 12,
|
||||||
|
"noxLearningOffset": 12,
|
||||||
|
"mqttBrokerUrl": "",
|
||||||
|
"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)
|
||||||
|
|
||||||
|
Configuration parameters can be changed with a PUT request to the monitor, e.g.
|
||||||
|
|
||||||
|
Example to force CO2 calibration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
|
||||||
|
```
|
||||||
|
|
||||||
|
Example to set monitor to Celsius
|
||||||
|
|
||||||
|
```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 the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
|
||||||
|
|
||||||
|
#### Configuration Parameters (GET/PUT)
|
||||||
|
|
||||||
|
| 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}` |
|
||||||
|
| `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_ |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### Corrections
|
||||||
|
|
||||||
|
The `corrections` object allows configuring PM2.5 correction algorithms and parameters locally. This affects both the display and local server response values.
|
||||||
|
|
||||||
|
Example correction configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"corrections": {
|
||||||
|
"pm02": {
|
||||||
|
"correctionAlgorithm": "<Option In String>",
|
||||||
|
"slr": {
|
||||||
|
"intercept": 0,
|
||||||
|
"scalingFactor": 0,
|
||||||
|
"useEpa2021": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 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}}}}'
|
||||||
|
```
|
22
docs/ota-updates.md
Normal file
22
docs/ota-updates.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
## OTA Updates
|
||||||
|
|
||||||
|
From [firmware version 3.1.1](https://github.com/airgradienthq/arduino/tree/3.1.1) onwards, the AirGradient ONE and Open Air monitors support over the air (OTA) updates.
|
||||||
|
|
||||||
|
#### Mechanism
|
||||||
|
|
||||||
|
Upon compilation of an official release the git tag (GIT_VERSION) is compiled into the binary.
|
||||||
|
|
||||||
|
The device attempts to update to the latest version on startup and in regular intervals using URL
|
||||||
|
|
||||||
|
http://hw.airgradient.com/sensors/{deviceId}/generic/os/firmware.bin?current_firmware={GIT_VERSION}
|
||||||
|
|
||||||
|
If does pass the version it is currently running on along to the server through URL parameter 'current_firmware'.
|
||||||
|
This allows the server to identify if the device is already running on the latest version or should update.
|
||||||
|
|
||||||
|
The following scenarios are possible
|
||||||
|
|
||||||
|
1. The device is already on the latest firmware. Then the server returns a 304 with a short explanation text in the body saying this.
|
||||||
|
2. The device reports a firmware unknown to the server. A 400 with an empty payload is returned in this case and the update is not performed. This case is relevant for local changes. The GIT_VERSION then defaults to "snapshot" which is unknown to the server.
|
||||||
|
3. There is an update available. A 200 along with the binary data of the new version is returned and the update is performed.
|
||||||
|
|
||||||
|
More information about the implementation details are available here: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/ota.html
|
589
examples/BASIC/BASIC.ino
Normal file
589
examples/BASIC/BASIC.ino
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
/*
|
||||||
|
This is the code for the AirGradient DIY BASIC Air Quality Monitor with an D1
|
||||||
|
ESP8266 Microcontroller.
|
||||||
|
|
||||||
|
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
|
||||||
|
small display and can send data over Wifi.
|
||||||
|
|
||||||
|
Open source air quality monitors and kits are available:
|
||||||
|
Indoor Monitor: https://www.airgradient.com/indoor/
|
||||||
|
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"
|
||||||
|
|
||||||
|
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||||
|
can be set through the AirGradient dashboard.
|
||||||
|
|
||||||
|
If you have any questions please visit our forum at
|
||||||
|
https://forum.airgradient.com/
|
||||||
|
|
||||||
|
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AgApiClient.h"
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgSchedule.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include "LocalServer.h"
|
||||||
|
#include "OpenMetrics.h"
|
||||||
|
#include "MqttClient.h"
|
||||||
|
#include <AirGradient.h>
|
||||||
|
#include <ESP8266HTTPClient.h>
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266mDNS.h>
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
|
||||||
|
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
|
||||||
|
#define DISP_UPDATE_INTERVAL 2500 /** ms */
|
||||||
|
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define SERVER_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define MQTT_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
|
||||||
|
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
||||||
|
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
|
||||||
|
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
|
||||||
|
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
|
||||||
|
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
||||||
|
|
||||||
|
static AirGradient ag(DIY_BASIC);
|
||||||
|
static Configuration configuration(Serial);
|
||||||
|
static AgApiClient apiClient(Serial, configuration);
|
||||||
|
static Measurements measurements;
|
||||||
|
static OledDisplay oledDisplay(configuration, measurements, Serial);
|
||||||
|
static StateMachine stateMachine(oledDisplay, Serial, measurements,
|
||||||
|
configuration);
|
||||||
|
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
|
||||||
|
configuration);
|
||||||
|
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
|
||||||
|
apiClient);
|
||||||
|
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
|
||||||
|
wifiConnector);
|
||||||
|
static MqttClient mqttClient(Serial);
|
||||||
|
|
||||||
|
static AgFirmwareMode fwMode = FW_MODE_I_BASIC_40PS;
|
||||||
|
|
||||||
|
static String fwNewVersion;
|
||||||
|
|
||||||
|
static void boardInit(void);
|
||||||
|
static void failedHandler(String msg);
|
||||||
|
static void configurationUpdateSchedule(void);
|
||||||
|
static void appDispHandler(void);
|
||||||
|
static void oledDisplaySchedule(void);
|
||||||
|
static void updateTvoc(void);
|
||||||
|
static void updatePm(void);
|
||||||
|
static void sendDataToServer(void);
|
||||||
|
static void tempHumUpdate(void);
|
||||||
|
static void co2Update(void);
|
||||||
|
static void mdnsInit(void);
|
||||||
|
static void initMqtt(void);
|
||||||
|
static void factoryConfigReset(void);
|
||||||
|
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,
|
||||||
|
configurationUpdateSchedule);
|
||||||
|
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
|
||||||
|
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
|
||||||
|
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
|
||||||
|
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
|
||||||
|
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
|
||||||
|
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
|
||||||
|
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
/** Serial for print debug message */
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(100); /** For bester show log */
|
||||||
|
|
||||||
|
/** Print device ID into log */
|
||||||
|
Serial.println("Serial nr: " + ag.deviceId());
|
||||||
|
|
||||||
|
/** Initialize local configure */
|
||||||
|
configuration.begin();
|
||||||
|
|
||||||
|
/** Init I2C */
|
||||||
|
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
configuration.setAirGradient(&ag);
|
||||||
|
oledDisplay.setAirGradient(&ag);
|
||||||
|
stateMachine.setAirGradient(&ag);
|
||||||
|
wifiConnector.setAirGradient(&ag);
|
||||||
|
apiClient.setAirGradient(&ag);
|
||||||
|
openMetrics.setAirGradient(&ag);
|
||||||
|
localServer.setAirGraident(&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;
|
||||||
|
|
||||||
|
connectToWifi = !configuration.isOfflineMode();
|
||||||
|
if (connectToWifi) {
|
||||||
|
apiClient.begin();
|
||||||
|
|
||||||
|
if (wifiConnector.connect()) {
|
||||||
|
if (wifiConnector.isConnected()) {
|
||||||
|
mdnsInit();
|
||||||
|
localServer.begin();
|
||||||
|
initMqtt();
|
||||||
|
sendDataToAg();
|
||||||
|
|
||||||
|
apiClient.fetchServerConfiguration();
|
||||||
|
configSchedule.update();
|
||||||
|
if (apiClient.isFetchConfigureFailed()) {
|
||||||
|
if (apiClient.isNotAvailableOnDashboard()) {
|
||||||
|
stateMachine.displaySetAddToDashBoard();
|
||||||
|
stateMachine.displayHandle(
|
||||||
|
AgStateMachineWiFiOkServerOkSensorConfigFailed);
|
||||||
|
} else {
|
||||||
|
stateMachine.displayClearAddToDashBoard();
|
||||||
|
}
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wifiConnector.isConfigurePorttalTimeout()) {
|
||||||
|
oledDisplay.showRebooting();
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("", "", "");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Set offline mode without saving, cause wifi is not configured */
|
||||||
|
if (wifiConnector.hasConfigurated() == false) {
|
||||||
|
Serial.println("Set offline mode cause wifi is not configurated");
|
||||||
|
configuration.setOfflineModeWithoutSave(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show display Warning up */
|
||||||
|
String sn = "SN:" + ag.deviceId();
|
||||||
|
oledDisplay.setText("Warming Up", sn.c_str(), "");
|
||||||
|
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
|
||||||
|
Serial.println("Display brightness: " +
|
||||||
|
String(configuration.getDisplayBrightness()));
|
||||||
|
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
||||||
|
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
/** Handle schedule */
|
||||||
|
dispLedSchedule.run();
|
||||||
|
configSchedule.run();
|
||||||
|
agApiPostSchedule.run();
|
||||||
|
|
||||||
|
if (configuration.hasSensorS8) {
|
||||||
|
co2Schedule.run();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorPMS1) {
|
||||||
|
pmsSchedule.run();
|
||||||
|
ag.pms5003.handle();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorSHT) {
|
||||||
|
tempHumSchedule.run();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
tvocSchedule.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
watchdogFeedSchedule.run();
|
||||||
|
|
||||||
|
/** Check for handle WiFi reconnect */
|
||||||
|
wifiConnector.handle();
|
||||||
|
|
||||||
|
/** factory reset handle */
|
||||||
|
// factoryConfigReset();
|
||||||
|
|
||||||
|
/** check that local configura changed then do some action */
|
||||||
|
configUpdateHandle();
|
||||||
|
|
||||||
|
localServer._handle();
|
||||||
|
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
ag.sgp41.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
MDNS.update();
|
||||||
|
|
||||||
|
mqttSchedule.run();
|
||||||
|
mqttClient.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void co2Update(void) {
|
||||||
|
if (!configuration.hasSensorS8) {
|
||||||
|
// Device don't have S8 sensor
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int value = ag.s8.getCo2();
|
||||||
|
if (utils::isValidCO2(value)) {
|
||||||
|
measurements.update(Measurements::CO2, value);
|
||||||
|
} else {
|
||||||
|
measurements.update(Measurements::CO2, utils::getInvalidCO2());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mdnsInit(void) {
|
||||||
|
Serial.println("mDNS init");
|
||||||
|
if (!MDNS.begin(localServer.getHostname().c_str())) {
|
||||||
|
Serial.println("Init mDNS failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MDNS.addService("_airgradient", "_tcp", 80);
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
|
||||||
|
AgFirmwareModeName(fwMode));
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
|
||||||
|
|
||||||
|
MDNS.announce();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initMqtt(void) {
|
||||||
|
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("Connection to MQTT broker failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wdgFeedUpdate(void) {
|
||||||
|
ag.watchdog.reset();
|
||||||
|
Serial.println("External watchdog feed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sgp41Init(void) {
|
||||||
|
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
|
||||||
|
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
|
||||||
|
if (ag.sgp41.begin(Wire)) {
|
||||||
|
Serial.println("Init SGP41 success");
|
||||||
|
configuration.hasSensorSGP = true;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Serial.println("Init SGP41 failuire");
|
||||||
|
configuration.hasSensorSGP = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifiFactoryConfigure(void) {
|
||||||
|
WiFi.persistent(true);
|
||||||
|
WiFi.begin("airgradient", "cleanair");
|
||||||
|
WiFi.persistent(false);
|
||||||
|
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("Rebooting...", "", "");
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("", "", "");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mqttHandle(void) {
|
||||||
|
if(mqttClient.isConnected() == false) {
|
||||||
|
mqttClient.connect(String("airgradient-") + ag.deviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mqttClient.isConnected()) {
|
||||||
|
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
|
||||||
|
String topic = "airgradient/readings/" + ag.deviceId();
|
||||||
|
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
|
||||||
|
Serial.println("MQTT sync success");
|
||||||
|
} else {
|
||||||
|
Serial.println("MQTT sync failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sendDataToAg() {
|
||||||
|
/** Change oledDisplay and led state */
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
|
||||||
|
|
||||||
|
delay(1500);
|
||||||
|
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
|
||||||
|
} else {
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
|
||||||
|
}
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispSensorNotFound(String ss) {
|
||||||
|
oledDisplay.setText("Sensor", ss.c_str(), "not found");
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void boardInit(void) {
|
||||||
|
/** Display init */
|
||||||
|
oledDisplay.begin();
|
||||||
|
|
||||||
|
/** Show boot display */
|
||||||
|
Serial.println("Firmware Version: " + ag.getVersion());
|
||||||
|
|
||||||
|
if (ag.isBasic()) {
|
||||||
|
oledDisplay.setText("DIY Basic", ag.getVersion().c_str(), "");
|
||||||
|
} else {
|
||||||
|
oledDisplay.setText("AirGradient ONE",
|
||||||
|
"FW Version: ", ag.getVersion().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
|
||||||
|
ag.watchdog.begin();
|
||||||
|
|
||||||
|
/** Show message init sensor */
|
||||||
|
oledDisplay.setText("Sensor", "init...", "");
|
||||||
|
|
||||||
|
/** Init sensor SGP41 */
|
||||||
|
configuration.hasSensorSGP = false;
|
||||||
|
// if (sgp41Init() == false) {
|
||||||
|
// dispSensorNotFound("SGP41");
|
||||||
|
// }
|
||||||
|
|
||||||
|
/** Init SHT */
|
||||||
|
if (ag.sht.begin(Wire) == false) {
|
||||||
|
Serial.println("SHTx sensor not found");
|
||||||
|
configuration.hasSensorSHT = false;
|
||||||
|
dispSensorNotFound("SHT");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init S8 CO2 sensor */
|
||||||
|
if (ag.s8.begin(&Serial) == false) {
|
||||||
|
Serial.println("CO2 S8 sensor not found");
|
||||||
|
configuration.hasSensorS8 = false;
|
||||||
|
dispSensorNotFound("S8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init PMS5003 */
|
||||||
|
configuration.hasSensorPMS1 = true;
|
||||||
|
configuration.hasSensorPMS2 = false;
|
||||||
|
if (ag.pms5003.begin(&Serial) == false) {
|
||||||
|
Serial.println("PMS sensor not found");
|
||||||
|
configuration.hasSensorPMS1 = false;
|
||||||
|
|
||||||
|
dispSensorNotFound("PMS");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set S8 CO2 abc days period */
|
||||||
|
if (configuration.hasSensorS8) {
|
||||||
|
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
|
||||||
|
Serial.println("Set S8 AbcDays successful");
|
||||||
|
} else {
|
||||||
|
Serial.println("Set S8 AbcDays failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localServer.setFwMode(fwMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void failedHandler(String msg) {
|
||||||
|
while (true) {
|
||||||
|
Serial.println(msg);
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void configurationUpdateSchedule(void) {
|
||||||
|
if (apiClient.fetchServerConfiguration()) {
|
||||||
|
configUpdateHandle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void configUpdateHandle() {
|
||||||
|
if (configuration.isUpdated() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.executeCo2Calibration();
|
||||||
|
|
||||||
|
String mqttUri = configuration.getMqttBrokerUri();
|
||||||
|
if (mqttClient.isCurrentUri(mqttUri) == false) {
|
||||||
|
mqttClient.end();
|
||||||
|
initMqtt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
if (configuration.noxLearnOffsetChanged() ||
|
||||||
|
configuration.tvocLearnOffsetChanged()) {
|
||||||
|
ag.sgp41.end();
|
||||||
|
|
||||||
|
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
|
||||||
|
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
|
||||||
|
bool result = sgp41Init();
|
||||||
|
const char *resultStr = "successful";
|
||||||
|
if (!result) {
|
||||||
|
resultStr = "failure";
|
||||||
|
}
|
||||||
|
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
|
||||||
|
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
|
||||||
|
oldTvocOffset, configuration.getTvocLearningOffset(),
|
||||||
|
resultStr);
|
||||||
|
}
|
||||||
|
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
|
||||||
|
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
|
||||||
|
oldNoxOffset, configuration.getNoxLearningOffset(),
|
||||||
|
resultStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.isDisplayBrightnessChanged()) {
|
||||||
|
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
||||||
|
}
|
||||||
|
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void appDispHandler(void) {
|
||||||
|
AgStateMachineState state = AgStateMachineNormal;
|
||||||
|
|
||||||
|
/** Only show display status on online mode. */
|
||||||
|
if (configuration.isOfflineMode() == false) {
|
||||||
|
if (wifiConnector.isConnected() == false) {
|
||||||
|
state = AgStateMachineWiFiLost;
|
||||||
|
} else if (apiClient.isFetchConfigureFailed()) {
|
||||||
|
state = AgStateMachineSensorConfigFailed;
|
||||||
|
if (apiClient.isNotAvailableOnDashboard()) {
|
||||||
|
stateMachine.displaySetAddToDashBoard();
|
||||||
|
} else {
|
||||||
|
stateMachine.displayClearAddToDashBoard();
|
||||||
|
}
|
||||||
|
} else if (apiClient.isPostToServerFailed()) {
|
||||||
|
state = AgStateMachineServerLost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateMachine.displayHandle(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void oledDisplaySchedule(void) {
|
||||||
|
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateTvoc(void) {
|
||||||
|
if (!configuration.hasSensorSGP) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 {
|
||||||
|
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) {
|
||||||
|
/** Increment bootcount when send measurements data is scheduled */
|
||||||
|
measurements.bootCount++;
|
||||||
|
|
||||||
|
/** Ignore send data to server if postToAirGradient disabled */
|
||||||
|
if (configuration.isPostDataToAirGradient() == false ||
|
||||||
|
configuration.isOfflineMode()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
|
||||||
|
if (apiClient.postToServer(syncData)) {
|
||||||
|
Serial.println();
|
||||||
|
Serial.println(
|
||||||
|
"Online mode and isPostToAirGradient = true: watchdog reset");
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tempHumUpdate(void) {
|
||||||
|
if (ag.sht.measure()) {
|
||||||
|
float temp = ag.sht.getTemperature();
|
||||||
|
float rhum = ag.sht.getRelativeHumidity();
|
||||||
|
|
||||||
|
measurements.update(Measurements::Temperature, temp);
|
||||||
|
measurements.update(Measurements::Humidity, rhum);
|
||||||
|
|
||||||
|
// Update compensation temperature and humidity for SGP41
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
|
||||||
|
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
|
||||||
|
Serial.println("SHT read failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
60
examples/BASIC/LocalServer.cpp
Normal file
60
examples/BASIC/LocalServer.cpp
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#include "LocalServer.h"
|
||||||
|
|
||||||
|
LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
|
||||||
|
Measurements &measure, Configuration &config,
|
||||||
|
WifiConnector &wifiConnector)
|
||||||
|
: PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure),
|
||||||
|
config(config), wifiConnector(wifiConnector), server(80) {}
|
||||||
|
|
||||||
|
LocalServer::~LocalServer() {}
|
||||||
|
|
||||||
|
bool LocalServer::begin(void) {
|
||||||
|
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
|
||||||
|
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
|
||||||
|
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
|
||||||
|
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
|
||||||
|
server.begin();
|
||||||
|
logInfo("Init: " + getHostname() + ".local");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
String LocalServer::getHostname(void) {
|
||||||
|
return "airgradient_" + ag->deviceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_handle(void) { server.handleClient(); }
|
||||||
|
|
||||||
|
void LocalServer::_GET_config(void) {
|
||||||
|
if(ag->isOne()) {
|
||||||
|
server.send(200, "application/json", config.toString());
|
||||||
|
} else {
|
||||||
|
server.send(200, "application/json", config.toString(fwMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_PUT_config(void) {
|
||||||
|
String data = server.arg(0);
|
||||||
|
String response = "";
|
||||||
|
int statusCode = 400; // Status code for data invalid
|
||||||
|
if (config.parse(data, true)) {
|
||||||
|
statusCode = 200;
|
||||||
|
response = "Success";
|
||||||
|
} else {
|
||||||
|
response = config.getFailedMesage();
|
||||||
|
}
|
||||||
|
server.send(statusCode, "text/plain", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_GET_metrics(void) {
|
||||||
|
server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_GET_measure(void) {
|
||||||
|
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
|
||||||
|
server.send(200, "application/json", toSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
38
examples/BASIC/LocalServer.h
Normal file
38
examples/BASIC/LocalServer.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#ifndef _LOCAL_SERVER_H_
|
||||||
|
#define _LOCAL_SERVER_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "OpenMetrics.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESP8266WebServer.h>
|
||||||
|
|
||||||
|
class LocalServer : public PrintLog {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
OpenMetrics &openMetrics;
|
||||||
|
Measurements &measure;
|
||||||
|
Configuration &config;
|
||||||
|
WifiConnector &wifiConnector;
|
||||||
|
ESP8266WebServer server;
|
||||||
|
AgFirmwareMode fwMode;
|
||||||
|
|
||||||
|
public:
|
||||||
|
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
|
||||||
|
Configuration &config, WifiConnector& wifiConnector);
|
||||||
|
~LocalServer();
|
||||||
|
|
||||||
|
bool begin(void);
|
||||||
|
void setAirGraident(AirGradient *ag);
|
||||||
|
String getHostname(void);
|
||||||
|
void setFwMode(AgFirmwareMode fwMode);
|
||||||
|
void _handle(void);
|
||||||
|
void _GET_config(void);
|
||||||
|
void _PUT_config(void);
|
||||||
|
void _GET_metrics(void);
|
||||||
|
void _GET_measure(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _LOCAL_SERVER_H_ */
|
203
examples/BASIC/OpenMetrics.cpp
Normal file
203
examples/BASIC/OpenMetrics.cpp
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
#include "OpenMetrics.h"
|
||||||
|
|
||||||
|
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
|
||||||
|
WifiConnector &wifiConnector, AgApiClient &apiClient)
|
||||||
|
: measure(measure), config(config), wifiConnector(wifiConnector),
|
||||||
|
apiClient(apiClient) {}
|
||||||
|
|
||||||
|
OpenMetrics::~OpenMetrics() {}
|
||||||
|
|
||||||
|
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
const char *OpenMetrics::getApiContentType(void) {
|
||||||
|
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *OpenMetrics::getApi(void) { return "/metrics"; }
|
||||||
|
|
||||||
|
String OpenMetrics::getPayload(void) {
|
||||||
|
String response;
|
||||||
|
String current_metric_name;
|
||||||
|
const auto add_metric = [&](const String &name, const String &help,
|
||||||
|
const String &type, const String &unit = "") {
|
||||||
|
current_metric_name = "airgradient_" + name;
|
||||||
|
if (!unit.isEmpty())
|
||||||
|
current_metric_name += "_" + unit;
|
||||||
|
response += "# HELP " + current_metric_name + " " + help + "\n";
|
||||||
|
response += "# TYPE " + current_metric_name + " " + type + "\n";
|
||||||
|
if (!unit.isEmpty())
|
||||||
|
response += "# UNIT " + current_metric_name + " " + unit + "\n";
|
||||||
|
};
|
||||||
|
const auto add_metric_point = [&](const String &labels, const String &value) {
|
||||||
|
response += current_metric_name + "{" + labels + "} " + value + "\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
add_metric("info", "AirGradient device information", "info");
|
||||||
|
add_metric_point("airgradient_serial_number=\"" + ag->deviceId() +
|
||||||
|
"\",airgradient_device_type=\"" + ag->getBoardName() +
|
||||||
|
"\",airgradient_library_version=\"" + ag->getVersion() +
|
||||||
|
"\"",
|
||||||
|
"1");
|
||||||
|
|
||||||
|
add_metric("config_ok",
|
||||||
|
"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(
|
||||||
|
"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(
|
||||||
|
"wifi_rssi",
|
||||||
|
"WiFi signal strength from the AirGradient device perspective, in dBm",
|
||||||
|
"gauge", "dbm");
|
||||||
|
add_metric_point("", String(wifiConnector.RSSI()));
|
||||||
|
|
||||||
|
// Initialize default invalid value for each measurements
|
||||||
|
float _temp = utils::getInvalidTemperature();
|
||||||
|
float _hum = utils::getInvalidHumidity();
|
||||||
|
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 tvoc = utils::getInvalidVOC();
|
||||||
|
int tvocRaw = utils::getInvalidVOC();
|
||||||
|
int nox = utils::getInvalidNOx();
|
||||||
|
int noxRaw = utils::getInvalidNOx();
|
||||||
|
|
||||||
|
if (config.hasSensorSHT) {
|
||||||
|
_temp = measure.getFloat(Measurements::Temperature);
|
||||||
|
_hum = measure.getFloat(Measurements::Humidity);
|
||||||
|
atmpCompensated = _temp;
|
||||||
|
ahumCompensated = _hum;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasSensorPMS1) {
|
||||||
|
pm01 = measure.get(Measurements::PM01);
|
||||||
|
pm25 = measure.get(Measurements::PM25);
|
||||||
|
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::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::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::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::isValidPm03Count(pm03PCount)) {
|
||||||
|
add_metric("pm0d3",
|
||||||
|
"PM0.3 concentration as measured by the AirGradient PMS "
|
||||||
|
"sensor, in number of particules per 100 milliliters",
|
||||||
|
"gauge", "p100ml");
|
||||||
|
add_metric_point("", String(pm03PCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasSensorSGP) {
|
||||||
|
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(tvoc));
|
||||||
|
}
|
||||||
|
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(tvocRaw));
|
||||||
|
}
|
||||||
|
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(nox));
|
||||||
|
}
|
||||||
|
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(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 "
|
||||||
|
"sensor, in degrees Celsius",
|
||||||
|
"gauge", "celsius");
|
||||||
|
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_point("", String(atmpCompensated));
|
||||||
|
}
|
||||||
|
if (utils::isValidHumidity(_hum)) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
response += "# EOF\n";
|
||||||
|
return response;
|
||||||
|
}
|
28
examples/BASIC/OpenMetrics.h
Normal file
28
examples/BASIC/OpenMetrics.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#ifndef _OPEN_METRICS_H_
|
||||||
|
#define _OPEN_METRICS_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "AgApiClient.h"
|
||||||
|
|
||||||
|
class OpenMetrics {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
Measurements &measure;
|
||||||
|
Configuration &config;
|
||||||
|
WifiConnector &wifiConnector;
|
||||||
|
AgApiClient &apiClient;
|
||||||
|
|
||||||
|
public:
|
||||||
|
OpenMetrics(Measurements &measure, Configuration &conig,
|
||||||
|
WifiConnector &wifiConnector, AgApiClient& apiClient);
|
||||||
|
~OpenMetrics();
|
||||||
|
void setAirGradient(AirGradient *ag);
|
||||||
|
const char *getApiContentType(void);
|
||||||
|
const char* getApi(void);
|
||||||
|
String getPayload(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _OPEN_METRICS_H_ */
|
@ -1,667 +0,0 @@
|
|||||||
/*
|
|
||||||
This is the code for the AirGradient DIY BASIC Air Quality Monitor with an D1
|
|
||||||
ESP8266 Microcontroller.
|
|
||||||
|
|
||||||
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
|
|
||||||
small display and can send data over Wifi.
|
|
||||||
|
|
||||||
Open source air quality monitors and kits are available:
|
|
||||||
Indoor Monitor: https://www.airgradient.com/indoor/
|
|
||||||
Outdoor Monitor: https://www.airgradient.com/outdoor/
|
|
||||||
|
|
||||||
Build Instructions:
|
|
||||||
https://www.airgradient.com/documentation/diy-v4/
|
|
||||||
|
|
||||||
Following libraries need to be installed:
|
|
||||||
“WifiManager by tzapu, tablatronix” tested with version 2.0.16-rc.2
|
|
||||||
"Arduino_JSON" by Arduino version 0.2.0
|
|
||||||
"U8g2" by oliver version 2.34.22
|
|
||||||
|
|
||||||
Please make sure you have esp8266 board manager installed. Tested with
|
|
||||||
version 3.1.2.
|
|
||||||
|
|
||||||
Set board to "LOLIN(WEMOS) D1 R2 & mini"
|
|
||||||
|
|
||||||
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
|
||||||
can be set through the AirGradient dashboard.
|
|
||||||
|
|
||||||
If you have any questions please visit our forum at
|
|
||||||
https://forum.airgradient.com/
|
|
||||||
|
|
||||||
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AirGradient.h>
|
|
||||||
#include <Arduino_JSON.h>
|
|
||||||
#include <ESP8266HTTPClient.h>
|
|
||||||
#include <ESP8266WiFi.h>
|
|
||||||
#include <WiFiClient.h>
|
|
||||||
#include <WiFiManager.h>
|
|
||||||
|
|
||||||
#define WIFI_CONNECT_COUNTDOWN_MAX 180 /** sec */
|
|
||||||
#define WIFI_CONNECT_RETRY_MS 10000 /** ms */
|
|
||||||
#define LED_BAR_COUNT_INIT_VALUE (-1) /** */
|
|
||||||
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
|
|
||||||
#define DISP_UPDATE_INTERVAL 5000 /** ms */
|
|
||||||
#define SERVER_CONFIG_UPDATE_INTERVAL 30000 /** ms */
|
|
||||||
#define SERVER_SYNC_INTERVAL 60000 /** ms */
|
|
||||||
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
|
|
||||||
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
|
||||||
#define SENSOR_CO2_UPDATE_INTERVAL 5000 /** ms */
|
|
||||||
#define SENSOR_PM_UPDATE_INTERVAL 5000 /** ms */
|
|
||||||
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 5000 /** ms */
|
|
||||||
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
|
||||||
#define WIFI_HOTSPOT_PASSWORD_DEFAULT \
|
|
||||||
"cleanair" /** default WiFi AP password \
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Use use LED bar state
|
|
||||||
*/
|
|
||||||
typedef enum {
|
|
||||||
UseLedBarOff, /** Don't use LED bar */
|
|
||||||
UseLedBarPM, /** Use LED bar for PMS */
|
|
||||||
UseLedBarCO2, /** Use LED bar for CO2 */
|
|
||||||
} UseLedBar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Schedule handle with timing period
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class AgSchedule {
|
|
||||||
public:
|
|
||||||
AgSchedule(int period, void (*handler)(void))
|
|
||||||
: period(period), handler(handler) {}
|
|
||||||
void run(void) {
|
|
||||||
uint32_t ms = (uint32_t)(millis() - count);
|
|
||||||
if (ms >= period) {
|
|
||||||
/** Call handler */
|
|
||||||
handler();
|
|
||||||
|
|
||||||
Serial.printf("[AgSchedule] handle 0x%08x, period: %d(ms)\r\n",
|
|
||||||
(unsigned int)handler, period);
|
|
||||||
|
|
||||||
/** Update period time */
|
|
||||||
count = millis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void (*handler)(void);
|
|
||||||
int period;
|
|
||||||
int count;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief AirGradient server configuration and sync data
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class AgServer {
|
|
||||||
public:
|
|
||||||
void begin(void) {
|
|
||||||
inF = false;
|
|
||||||
inUSAQI = false;
|
|
||||||
configFailed = false;
|
|
||||||
serverFailed = false;
|
|
||||||
memset(models, 0, sizeof(models));
|
|
||||||
memset(mqttBroker, 0, sizeof(mqttBroker));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get server configuration
|
|
||||||
*
|
|
||||||
* @param id Device ID
|
|
||||||
* @return true Success
|
|
||||||
* @return false Failure
|
|
||||||
*/
|
|
||||||
bool pollServerConfig(String id) {
|
|
||||||
String uri =
|
|
||||||
"http://hw.airgradient.com/sensors/airgradient:" + id + "/one/config";
|
|
||||||
|
|
||||||
/** Init http client */
|
|
||||||
WiFiClient wifiClient;
|
|
||||||
HTTPClient client;
|
|
||||||
if (client.begin(wifiClient, uri) == false) {
|
|
||||||
configFailed = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get */
|
|
||||||
int retCode = client.GET();
|
|
||||||
if (retCode != 200) {
|
|
||||||
client.end();
|
|
||||||
configFailed = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** clear failed */
|
|
||||||
configFailed = false;
|
|
||||||
|
|
||||||
/** Get response string */
|
|
||||||
String respContent = client.getString();
|
|
||||||
client.end();
|
|
||||||
Serial.println("Get server config: " + respContent);
|
|
||||||
|
|
||||||
/** Parse JSON */
|
|
||||||
JSONVar root = JSON.parse(respContent);
|
|
||||||
if (JSON.typeof(root) == "undefined") {
|
|
||||||
/** JSON invalid */
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "country" */
|
|
||||||
if (JSON.typeof_(root["country"]) == "string") {
|
|
||||||
String country = root["country"];
|
|
||||||
if (country == "US") {
|
|
||||||
inF = true;
|
|
||||||
} else {
|
|
||||||
inF = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "pmStandard" */
|
|
||||||
if (JSON.typeof_(root["pmStandard"]) == "string") {
|
|
||||||
String standard = root["pmStandard"];
|
|
||||||
if (standard == "ugm3") {
|
|
||||||
inUSAQI = false;
|
|
||||||
} else {
|
|
||||||
inUSAQI = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "co2CalibrationRequested" */
|
|
||||||
if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") {
|
|
||||||
co2Calib = root["co2CalibrationRequested"];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "ledBarMode" */
|
|
||||||
if (JSON.typeof_(root["ledBarMode"]) == "string") {
|
|
||||||
String mode = root["ledBarMode"];
|
|
||||||
if (mode == "co2") {
|
|
||||||
ledBarMode = UseLedBarCO2;
|
|
||||||
} else if (mode == "pm") {
|
|
||||||
ledBarMode = UseLedBarPM;
|
|
||||||
} else if (mode == "off") {
|
|
||||||
ledBarMode = UseLedBarOff;
|
|
||||||
} else {
|
|
||||||
ledBarMode = UseLedBarOff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get model */
|
|
||||||
if (JSON.typeof_(root["model"]) == "string") {
|
|
||||||
String model = root["model"];
|
|
||||||
if (model.length()) {
|
|
||||||
int len =
|
|
||||||
model.length() < sizeof(models) ? model.length() : sizeof(models);
|
|
||||||
memset(models, 0, sizeof(models));
|
|
||||||
memcpy(models, model.c_str(), len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "mqttBrokerUrl" */
|
|
||||||
if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") {
|
|
||||||
String mqtt = root["mqttBrokerUrl"];
|
|
||||||
if (mqtt.length()) {
|
|
||||||
int len = mqtt.length() < sizeof(mqttBroker) ? mqtt.length()
|
|
||||||
: sizeof(mqttBroker);
|
|
||||||
memset(mqttBroker, 0, sizeof(mqttBroker));
|
|
||||||
memcpy(mqttBroker, mqtt.c_str(), len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get 'abcDays' */
|
|
||||||
if (JSON.typeof_(root["abcDays"]) == "number") {
|
|
||||||
co2AbcCalib = root["abcDays"];
|
|
||||||
} else {
|
|
||||||
co2AbcCalib = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show configuration */
|
|
||||||
showServerConfig();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool postToServer(String id, String payload) {
|
|
||||||
/**
|
|
||||||
* @brief Only post data if WiFi is connected
|
|
||||||
*/
|
|
||||||
if (WiFi.isConnected() == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("Post payload: %s\r\n", payload.c_str());
|
|
||||||
|
|
||||||
String uri =
|
|
||||||
"http://hw.airgradient.com/sensors/airgradient:" + id + "/measures";
|
|
||||||
|
|
||||||
WiFiClient wifiClient;
|
|
||||||
HTTPClient client;
|
|
||||||
if (client.begin(wifiClient, uri.c_str()) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
client.addHeader("content-type", "application/json");
|
|
||||||
int retCode = client.POST(payload);
|
|
||||||
client.end();
|
|
||||||
|
|
||||||
if ((retCode == 200) || (retCode == 429)) {
|
|
||||||
serverFailed = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
serverFailed = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get temperature configuration unit
|
|
||||||
*
|
|
||||||
* @return true F unit
|
|
||||||
* @return false C Unit
|
|
||||||
*/
|
|
||||||
bool isTemperatureUnitF(void) { return inF; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get PMS standard unit
|
|
||||||
*
|
|
||||||
* @return true USAQI
|
|
||||||
* @return false ugm3
|
|
||||||
*/
|
|
||||||
bool isPMSinUSAQI(void) { return inUSAQI; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get status of get server coniguration is failed
|
|
||||||
*
|
|
||||||
* @return true Failed
|
|
||||||
* @return false Success
|
|
||||||
*/
|
|
||||||
bool isConfigFailed(void) { return configFailed; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get status of post server configuration is failed
|
|
||||||
*
|
|
||||||
* @return true Failed
|
|
||||||
* @return false Success
|
|
||||||
*/
|
|
||||||
bool isServerFailed(void) { return serverFailed; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get request calibration CO2
|
|
||||||
*
|
|
||||||
* @return true Requested. If result = true, it's clear after function call
|
|
||||||
* @return false Not-requested
|
|
||||||
*/
|
|
||||||
bool isCo2Calib(void) {
|
|
||||||
bool ret = co2Calib;
|
|
||||||
if (ret) {
|
|
||||||
co2Calib = false;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the Co2 auto calib period
|
|
||||||
*
|
|
||||||
* @return int days, -1 if invalid.
|
|
||||||
*/
|
|
||||||
int getCo2Abccalib(void) { return co2AbcCalib; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get device configuration model name
|
|
||||||
*
|
|
||||||
* @return String Model name, empty string if server failed
|
|
||||||
*/
|
|
||||||
String getModelName(void) { return String(models); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get mqttBroker url
|
|
||||||
*
|
|
||||||
* @return String Broker url, empty if server failed
|
|
||||||
*/
|
|
||||||
String getMqttBroker(void) { return String(mqttBroker); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show server configuration parameter
|
|
||||||
*/
|
|
||||||
void showServerConfig(void) {
|
|
||||||
Serial.println("Server configuration: ");
|
|
||||||
Serial.printf(" inF: %s\r\n", inF ? "true" : "false");
|
|
||||||
Serial.printf(" inUSAQI: %s\r\n", inUSAQI ? "true" : "false");
|
|
||||||
Serial.printf(" useRGBLedBar: %d\r\n", (int)ledBarMode);
|
|
||||||
Serial.printf(" Model: %s\r\n", models);
|
|
||||||
Serial.printf(" Mqtt Broker: %s\r\n", mqttBroker);
|
|
||||||
Serial.printf(" S8 calib period: %d\r\n", co2AbcCalib);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get server config led bar mode
|
|
||||||
*
|
|
||||||
* @return UseLedBar
|
|
||||||
*/
|
|
||||||
UseLedBar getLedBarMode(void) { return ledBarMode; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool inF; /** Temperature unit, true: F, false: C */
|
|
||||||
bool inUSAQI; /** PMS unit, true: USAQI, false: ugm3 */
|
|
||||||
bool configFailed; /** Flag indicate get server configuration failed */
|
|
||||||
bool serverFailed; /** Flag indicate post data to server failed */
|
|
||||||
bool co2Calib; /** Is co2Ppmcalibration requset */
|
|
||||||
int co2AbcCalib = -1; /** update auto calibration number of day */
|
|
||||||
UseLedBar ledBarMode = UseLedBarCO2; /** */
|
|
||||||
char models[20]; /** */
|
|
||||||
char mqttBroker[256]; /** */
|
|
||||||
};
|
|
||||||
AgServer agServer;
|
|
||||||
|
|
||||||
/** Create airgradient instance for 'DIY_BASIC' board */
|
|
||||||
AirGradient ag = AirGradient(DIY_BASIC);
|
|
||||||
|
|
||||||
static int co2Ppm = -1;
|
|
||||||
static int pm25 = -1;
|
|
||||||
static float temp = -1;
|
|
||||||
static int hum = -1;
|
|
||||||
static long val;
|
|
||||||
static String wifiSSID = "";
|
|
||||||
static bool wifiHasConfig = false; /** */
|
|
||||||
|
|
||||||
static void boardInit(void);
|
|
||||||
static void failedHandler(String msg);
|
|
||||||
static void co2Calibration(void);
|
|
||||||
static void serverConfigPoll(void);
|
|
||||||
static void co2Poll(void);
|
|
||||||
static void pmPoll(void);
|
|
||||||
static void tempHumPoll(void);
|
|
||||||
static void sendDataToServer(void);
|
|
||||||
static void dispHandler(void);
|
|
||||||
static String getDevId(void);
|
|
||||||
static void updateWiFiConnect(void);
|
|
||||||
|
|
||||||
AgSchedule configSchedule(SERVER_CONFIG_UPDATE_INTERVAL, serverConfigPoll);
|
|
||||||
AgSchedule serverSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
|
|
||||||
AgSchedule dispSchedule(DISP_UPDATE_INTERVAL, dispHandler);
|
|
||||||
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Poll);
|
|
||||||
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, pmPoll);
|
|
||||||
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumPoll);
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
Serial.begin(115200);
|
|
||||||
|
|
||||||
/** Init I2C */
|
|
||||||
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
|
|
||||||
delay(1000);
|
|
||||||
|
|
||||||
/** Board init */
|
|
||||||
boardInit();
|
|
||||||
|
|
||||||
/** Init AirGradient server */
|
|
||||||
agServer.begin();
|
|
||||||
|
|
||||||
/** Show boot display */
|
|
||||||
displayShowText("DIY basic", "Lib:" + ag.getVersion(), "");
|
|
||||||
delay(2000);
|
|
||||||
|
|
||||||
/** WiFi connect */
|
|
||||||
connectToWifi();
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
wifiHasConfig = true;
|
|
||||||
sendPing();
|
|
||||||
|
|
||||||
agServer.pollServerConfig(getDevId());
|
|
||||||
if (agServer.isCo2Calib()) {
|
|
||||||
co2Calibration();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show serial number display */
|
|
||||||
ag.display.clear();
|
|
||||||
ag.display.setCursor(1, 1);
|
|
||||||
ag.display.setText("Warm Up");
|
|
||||||
ag.display.setCursor(1, 15);
|
|
||||||
ag.display.setText("Serial#");
|
|
||||||
ag.display.setCursor(1, 29);
|
|
||||||
String id = getNormalizedMac();
|
|
||||||
Serial.println("Device id: " + id);
|
|
||||||
String id1 = id.substring(0, 9);
|
|
||||||
String id2 = id.substring(9, 12);
|
|
||||||
ag.display.setText("\'" + id1);
|
|
||||||
ag.display.setCursor(1, 40);
|
|
||||||
ag.display.setText(id2 + "\'");
|
|
||||||
ag.display.show();
|
|
||||||
|
|
||||||
delay(5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
configSchedule.run();
|
|
||||||
serverSchedule.run();
|
|
||||||
dispSchedule.run();
|
|
||||||
co2Schedule.run();
|
|
||||||
pmsSchedule.run();
|
|
||||||
tempHumSchedule.run();
|
|
||||||
|
|
||||||
updateWiFiConnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void sendPing() {
|
|
||||||
JSONVar root;
|
|
||||||
root["wifi"] = WiFi.RSSI();
|
|
||||||
root["boot"] = 0;
|
|
||||||
|
|
||||||
// delay(1500);
|
|
||||||
if (agServer.postToServer(getDevId(), JSON.stringify(root))) {
|
|
||||||
// Ping Server succses
|
|
||||||
} else {
|
|
||||||
// Ping server failed
|
|
||||||
}
|
|
||||||
// delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
void displayShowText(String ln1, String ln2, String ln3) {
|
|
||||||
char buf[9];
|
|
||||||
ag.display.clear();
|
|
||||||
|
|
||||||
ag.display.setCursor(1, 1);
|
|
||||||
ag.display.setText(ln1);
|
|
||||||
ag.display.setCursor(1, 19);
|
|
||||||
ag.display.setText(ln2);
|
|
||||||
ag.display.setCursor(1, 37);
|
|
||||||
ag.display.setText(ln3);
|
|
||||||
|
|
||||||
ag.display.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wifi Manager
|
|
||||||
void connectToWifi() {
|
|
||||||
WiFiManager wifiManager;
|
|
||||||
wifiSSID = "AG-" + String(ESP.getChipId(), HEX);
|
|
||||||
wifiManager.setConfigPortalBlocking(false);
|
|
||||||
wifiManager.setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
|
||||||
wifiManager.autoConnect(wifiSSID.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
|
|
||||||
|
|
||||||
uint32_t lastTime = millis();
|
|
||||||
int count = WIFI_CONNECT_COUNTDOWN_MAX;
|
|
||||||
displayShowText(String(WIFI_CONNECT_COUNTDOWN_MAX) + " sec",
|
|
||||||
"SSID:", wifiSSID);
|
|
||||||
while (wifiManager.getConfigPortalActive()) {
|
|
||||||
wifiManager.process();
|
|
||||||
uint32_t ms = (uint32_t)(millis() - lastTime);
|
|
||||||
if (ms >= 1000) {
|
|
||||||
lastTime = millis();
|
|
||||||
displayShowText(String(count) + " sec", "SSID:", wifiSSID);
|
|
||||||
count--;
|
|
||||||
|
|
||||||
// Timeout
|
|
||||||
if (count == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!WiFi.isConnected()) {
|
|
||||||
displayShowText("Booting", "offline", "mode");
|
|
||||||
Serial.println("failed to connect and hit timeout");
|
|
||||||
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void boardInit(void) {
|
|
||||||
/** Init SHT sensor */
|
|
||||||
if (ag.sht.begin(Wire) == false) {
|
|
||||||
failedHandler("SHT init failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** CO2 init */
|
|
||||||
if (ag.s8.begin(&Serial) == false) {
|
|
||||||
failedHandler("SenseAirS8 init failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** PMS init */
|
|
||||||
if (ag.pms5003.begin(&Serial) == false) {
|
|
||||||
failedHandler("PMS5003 init failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Display init */
|
|
||||||
ag.display.begin(Wire);
|
|
||||||
ag.display.setTextColor(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void failedHandler(String msg) {
|
|
||||||
while (true) {
|
|
||||||
Serial.println(msg);
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void co2Calibration(void) {
|
|
||||||
/** Count down for co2CalibCountdown secs */
|
|
||||||
for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) {
|
|
||||||
displayShowText("CO2 calib", "after",
|
|
||||||
String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec");
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ag.s8.setBaselineCalibration()) {
|
|
||||||
displayShowText("Calib", "success", "");
|
|
||||||
delay(1000);
|
|
||||||
displayShowText("Wait for", "finish", "...");
|
|
||||||
int count = 0;
|
|
||||||
while (ag.s8.isBaseLineCalibrationDone() == false) {
|
|
||||||
delay(1000);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
displayShowText("Finish", "after", String(count) + " sec");
|
|
||||||
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
|
||||||
} else {
|
|
||||||
displayShowText("Calib", "failure!!!", "");
|
|
||||||
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void serverConfigPoll(void) {
|
|
||||||
if (agServer.pollServerConfig(getDevId())) {
|
|
||||||
if (agServer.isCo2Calib()) {
|
|
||||||
co2Calibration();
|
|
||||||
}
|
|
||||||
if (agServer.getCo2Abccalib() > 0) {
|
|
||||||
if (ag.s8.setAutoCalib(agServer.getCo2Abccalib() * 24) == false) {
|
|
||||||
Serial.println("Set S8 auto calib failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void co2Poll() {
|
|
||||||
co2Ppm = ag.s8.getCo2();
|
|
||||||
Serial.printf("CO2 index: %d\r\n", co2Ppm);
|
|
||||||
}
|
|
||||||
|
|
||||||
void pmPoll() {
|
|
||||||
if (ag.pms5003.readData()) {
|
|
||||||
pm25 = ag.pms5003.getPm25Ae();
|
|
||||||
Serial.printf("PMS2.5: %d\r\n", pm25);
|
|
||||||
} else {
|
|
||||||
pm25 = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tempHumPoll() {
|
|
||||||
if (ag.sht.measure()) {
|
|
||||||
temp = ag.sht.getTemperature();
|
|
||||||
hum = ag.sht.getRelativeHumidity();
|
|
||||||
Serial.printf("Temperature: %0.2f\r\n", temp);
|
|
||||||
Serial.printf(" Humidity: %d\r\n", hum);
|
|
||||||
} else {
|
|
||||||
Serial.println("Meaure SHT failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void sendDataToServer() {
|
|
||||||
JSONVar root;
|
|
||||||
root["wifi"] = WiFi.RSSI();
|
|
||||||
if (co2Ppm >= 0) {
|
|
||||||
root["rco2"] = co2Ppm;
|
|
||||||
}
|
|
||||||
if (pm25 >= 0) {
|
|
||||||
root["pm02"] = pm25;
|
|
||||||
}
|
|
||||||
if (temp >= 0) {
|
|
||||||
root["atmp"] = temp;
|
|
||||||
}
|
|
||||||
if (hum >= 0) {
|
|
||||||
root["rhum"] = hum;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agServer.postToServer(getDevId(), JSON.stringify(root)) == false) {
|
|
||||||
Serial.println("Post to server failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void dispHandler() {
|
|
||||||
String ln1 = "";
|
|
||||||
String ln2 = "";
|
|
||||||
String ln3 = "";
|
|
||||||
|
|
||||||
if (agServer.isPMSinUSAQI()) {
|
|
||||||
ln1 = "AQI:" + String(ag.pms5003.convertPm25ToUsAqi(pm25));
|
|
||||||
} else {
|
|
||||||
ln1 = "PM :" + String(pm25) + " ug";
|
|
||||||
}
|
|
||||||
ln2 = "CO2:" + String(co2Ppm);
|
|
||||||
|
|
||||||
if (agServer.isTemperatureUnitF()) {
|
|
||||||
ln3 = String((temp * 9 / 5) + 32).substring(0, 4) + " " + String(hum) + "%";
|
|
||||||
} else {
|
|
||||||
ln3 = String(temp).substring(0, 4) + " " + String(hum) + "%";
|
|
||||||
}
|
|
||||||
displayShowText(ln1, ln2, ln3);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getDevId(void) { return getNormalizedMac(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief WiFi reconnect handler
|
|
||||||
*/
|
|
||||||
static void updateWiFiConnect(void) {
|
|
||||||
static uint32_t lastRetry;
|
|
||||||
if (wifiHasConfig == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (WiFi.isConnected()) {
|
|
||||||
lastRetry = millis();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint32_t ms = (uint32_t)(millis() - lastRetry);
|
|
||||||
if (ms >= WIFI_CONNECT_RETRY_MS) {
|
|
||||||
lastRetry = millis();
|
|
||||||
WiFi.reconnect();
|
|
||||||
|
|
||||||
Serial.printf("Re-Connect WiFi\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String getNormalizedMac() {
|
|
||||||
String mac = WiFi.macAddress();
|
|
||||||
mac.replace(":", "");
|
|
||||||
mac.toLowerCase();
|
|
||||||
return mac;
|
|
||||||
}
|
|
641
examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino
Normal file
641
examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino
Normal file
@ -0,0 +1,641 @@
|
|||||||
|
/*
|
||||||
|
This is the code for the AirGradient DIY PRO 3.3 Air Quality Monitor with an D1
|
||||||
|
ESP8266 Microcontroller.
|
||||||
|
|
||||||
|
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
|
||||||
|
small display and can send data over Wifi.
|
||||||
|
|
||||||
|
Open source air quality monitors and kits are available:
|
||||||
|
Indoor Monitor: https://www.airgradient.com/indoor/
|
||||||
|
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"
|
||||||
|
|
||||||
|
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||||
|
can be set through the AirGradient dashboard.
|
||||||
|
|
||||||
|
If you have any questions please visit our forum at
|
||||||
|
https://forum.airgradient.com/
|
||||||
|
|
||||||
|
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AgApiClient.h"
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgSchedule.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include "LocalServer.h"
|
||||||
|
#include "OpenMetrics.h"
|
||||||
|
#include "MqttClient.h"
|
||||||
|
#include <AirGradient.h>
|
||||||
|
#include <ESP8266HTTPClient.h>
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266mDNS.h>
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
|
||||||
|
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
|
||||||
|
#define DISP_UPDATE_INTERVAL 2500 /** ms */
|
||||||
|
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define SERVER_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define MQTT_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
|
||||||
|
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
||||||
|
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
|
||||||
|
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
|
||||||
|
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
|
||||||
|
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
||||||
|
|
||||||
|
static AirGradient ag(DIY_PRO_INDOOR_V3_3);
|
||||||
|
static Configuration configuration(Serial);
|
||||||
|
static AgApiClient apiClient(Serial, configuration);
|
||||||
|
static Measurements measurements;
|
||||||
|
static OledDisplay oledDisplay(configuration, measurements, Serial);
|
||||||
|
static StateMachine stateMachine(oledDisplay, Serial, measurements,
|
||||||
|
configuration);
|
||||||
|
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
|
||||||
|
configuration);
|
||||||
|
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
|
||||||
|
apiClient);
|
||||||
|
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
|
||||||
|
wifiConnector);
|
||||||
|
static MqttClient mqttClient(Serial);
|
||||||
|
|
||||||
|
static AgFirmwareMode fwMode = FW_MODE_I_33PS;
|
||||||
|
|
||||||
|
static String fwNewVersion;
|
||||||
|
|
||||||
|
static void boardInit(void);
|
||||||
|
static void failedHandler(String msg);
|
||||||
|
static void configurationUpdateSchedule(void);
|
||||||
|
static void appDispHandler(void);
|
||||||
|
static void oledDisplaySchedule(void);
|
||||||
|
static void updateTvoc(void);
|
||||||
|
static void updatePm(void);
|
||||||
|
static void sendDataToServer(void);
|
||||||
|
static void tempHumUpdate(void);
|
||||||
|
static void co2Update(void);
|
||||||
|
static void mdnsInit(void);
|
||||||
|
static void initMqtt(void);
|
||||||
|
static void factoryConfigReset(void);
|
||||||
|
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,
|
||||||
|
configurationUpdateSchedule);
|
||||||
|
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
|
||||||
|
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
|
||||||
|
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
|
||||||
|
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
|
||||||
|
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
|
||||||
|
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
|
||||||
|
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
/** Serial for print debug message */
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(100); /** For bester show log */
|
||||||
|
|
||||||
|
/** Print device ID into log */
|
||||||
|
Serial.println("Serial nr: " + ag.deviceId());
|
||||||
|
|
||||||
|
/** Initialize local configure */
|
||||||
|
configuration.begin();
|
||||||
|
|
||||||
|
/** Init I2C */
|
||||||
|
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
configuration.setAirGradient(&ag);
|
||||||
|
oledDisplay.setAirGradient(&ag);
|
||||||
|
stateMachine.setAirGradient(&ag);
|
||||||
|
wifiConnector.setAirGradient(&ag);
|
||||||
|
apiClient.setAirGradient(&ag);
|
||||||
|
openMetrics.setAirGradient(&ag);
|
||||||
|
localServer.setAirGraident(&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;
|
||||||
|
|
||||||
|
connectToWifi = !configuration.isOfflineMode();
|
||||||
|
if (connectToWifi) {
|
||||||
|
apiClient.begin();
|
||||||
|
|
||||||
|
if (wifiConnector.connect()) {
|
||||||
|
if (wifiConnector.isConnected()) {
|
||||||
|
mdnsInit();
|
||||||
|
localServer.begin();
|
||||||
|
initMqtt();
|
||||||
|
sendDataToAg();
|
||||||
|
|
||||||
|
apiClient.fetchServerConfiguration();
|
||||||
|
configSchedule.update();
|
||||||
|
if (apiClient.isFetchConfigureFailed()) {
|
||||||
|
if (apiClient.isNotAvailableOnDashboard()) {
|
||||||
|
stateMachine.displaySetAddToDashBoard();
|
||||||
|
stateMachine.displayHandle(
|
||||||
|
AgStateMachineWiFiOkServerOkSensorConfigFailed);
|
||||||
|
} else {
|
||||||
|
stateMachine.displayClearAddToDashBoard();
|
||||||
|
}
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wifiConnector.isConfigurePorttalTimeout()) {
|
||||||
|
oledDisplay.showRebooting();
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("", "", "");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Set offline mode without saving, cause wifi is not configured */
|
||||||
|
if (wifiConnector.hasConfigurated() == false) {
|
||||||
|
Serial.println("Set offline mode cause wifi is not configurated");
|
||||||
|
configuration.setOfflineModeWithoutSave(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show display Warning up */
|
||||||
|
oledDisplay.setText("Warming Up", "Serial Number:", ag.deviceId().c_str());
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
|
||||||
|
Serial.println("Display brightness: " +
|
||||||
|
String(configuration.getDisplayBrightness()));
|
||||||
|
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
||||||
|
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
/** Handle schedule */
|
||||||
|
dispLedSchedule.run();
|
||||||
|
configSchedule.run();
|
||||||
|
agApiPostSchedule.run();
|
||||||
|
|
||||||
|
if (configuration.hasSensorS8) {
|
||||||
|
co2Schedule.run();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorPMS1) {
|
||||||
|
pmsSchedule.run();
|
||||||
|
ag.pms5003.handle();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorSHT) {
|
||||||
|
tempHumSchedule.run();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
tvocSchedule.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
watchdogFeedSchedule.run();
|
||||||
|
|
||||||
|
/** Check for handle WiFi reconnect */
|
||||||
|
wifiConnector.handle();
|
||||||
|
|
||||||
|
/** factory reset handle */
|
||||||
|
// factoryConfigReset();
|
||||||
|
|
||||||
|
/** check that local configura changed then do some action */
|
||||||
|
configUpdateHandle();
|
||||||
|
|
||||||
|
localServer._handle();
|
||||||
|
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
ag.sgp41.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
MDNS.update();
|
||||||
|
|
||||||
|
mqttSchedule.run();
|
||||||
|
mqttClient.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void co2Update(void) {
|
||||||
|
if (!configuration.hasSensorS8) {
|
||||||
|
// Device don't have S8 sensor
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int value = ag.s8.getCo2();
|
||||||
|
if (utils::isValidCO2(value)) {
|
||||||
|
measurements.update(Measurements::CO2, value);
|
||||||
|
} else {
|
||||||
|
measurements.update(Measurements::CO2, utils::getInvalidCO2());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mdnsInit(void) {
|
||||||
|
Serial.println("mDNS init");
|
||||||
|
if (!MDNS.begin(localServer.getHostname().c_str())) {
|
||||||
|
Serial.println("Init mDNS failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MDNS.addService("_airgradient", "_tcp", 80);
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
|
||||||
|
AgFirmwareModeName(fwMode));
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
|
||||||
|
|
||||||
|
MDNS.announce();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initMqtt(void) {
|
||||||
|
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("Connection to MQTT broker failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void factoryConfigReset(void) {
|
||||||
|
#if 0
|
||||||
|
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
|
||||||
|
if (factoryBtnPressTime == 0) {
|
||||||
|
factoryBtnPressTime = millis();
|
||||||
|
} else {
|
||||||
|
uint32_t ms = (uint32_t)(millis() - factoryBtnPressTime);
|
||||||
|
if (ms >= 2000) {
|
||||||
|
// Show display message: For factory keep for x seconds
|
||||||
|
if (ag.isOne() || ag.isPro4_2()) {
|
||||||
|
oledDisplay.setText("Factory reset", "keep pressed", "for 8 sec");
|
||||||
|
} else {
|
||||||
|
Serial.println("Factory reset, keep pressed for 8 sec");
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 7;
|
||||||
|
while (ag.button.getState() == ag.button.BUTTON_PRESSED) {
|
||||||
|
delay(1000);
|
||||||
|
String str = "for " + String(count) + " sec";
|
||||||
|
oledDisplay.setText("Factory reset", "keep pressed", str.c_str());
|
||||||
|
|
||||||
|
count--;
|
||||||
|
if (count == 0) {
|
||||||
|
/** Stop MQTT task first */
|
||||||
|
// if (mqttTask) {
|
||||||
|
// vTaskDelete(mqttTask);
|
||||||
|
// mqttTask = NULL;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/** Reset WIFI */
|
||||||
|
// WiFi.enableSTA(true); // Incase offline mode
|
||||||
|
// WiFi.disconnect(true, true);
|
||||||
|
wifiConnector.reset();
|
||||||
|
|
||||||
|
/** Reset local config */
|
||||||
|
configuration.reset();
|
||||||
|
|
||||||
|
oledDisplay.setText("Factory reset", "successful", "");
|
||||||
|
|
||||||
|
delay(3000);
|
||||||
|
oledDisplay.setText("", "", "");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show current content cause reset ignore */
|
||||||
|
factoryBtnPressTime = 0;
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (factoryBtnPressTime != 0) {
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
factoryBtnPressTime = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wdgFeedUpdate(void) {
|
||||||
|
ag.watchdog.reset();
|
||||||
|
Serial.println("External watchdog feed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sgp41Init(void) {
|
||||||
|
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
|
||||||
|
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
|
||||||
|
if (ag.sgp41.begin(Wire)) {
|
||||||
|
Serial.println("Init SGP41 success");
|
||||||
|
configuration.hasSensorSGP = true;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Serial.println("Init SGP41 failuire");
|
||||||
|
configuration.hasSensorSGP = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifiFactoryConfigure(void) {
|
||||||
|
WiFi.persistent(true);
|
||||||
|
WiFi.begin("airgradient", "cleanair");
|
||||||
|
WiFi.persistent(false);
|
||||||
|
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("Rebooting...", "", "");
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("", "", "");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mqttHandle(void) {
|
||||||
|
if(mqttClient.isConnected() == false) {
|
||||||
|
mqttClient.connect(String("airgradient-") + ag.deviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mqttClient.isConnected()) {
|
||||||
|
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
|
||||||
|
String topic = "airgradient/readings/" + ag.deviceId();
|
||||||
|
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
|
||||||
|
Serial.println("MQTT sync success");
|
||||||
|
} else {
|
||||||
|
Serial.println("MQTT sync failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sendDataToAg() {
|
||||||
|
/** Change oledDisplay and led state */
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
|
||||||
|
|
||||||
|
delay(1500);
|
||||||
|
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
|
||||||
|
} else {
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
|
||||||
|
}
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispSensorNotFound(String ss) {
|
||||||
|
ss = ss + " not found";
|
||||||
|
oledDisplay.setText("Sensor init", "Error:", ss.c_str());
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void boardInit(void) {
|
||||||
|
/** Display init */
|
||||||
|
oledDisplay.begin();
|
||||||
|
|
||||||
|
/** Show boot display */
|
||||||
|
Serial.println("Firmware Version: " + ag.getVersion());
|
||||||
|
|
||||||
|
oledDisplay.setText("AirGradient ONE",
|
||||||
|
"FW Version: ", ag.getVersion().c_str());
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
|
||||||
|
ag.watchdog.begin();
|
||||||
|
|
||||||
|
/** Show message init sensor */
|
||||||
|
oledDisplay.setText("Sensor", "initializing...", "");
|
||||||
|
|
||||||
|
/** Init sensor SGP41 */
|
||||||
|
if (sgp41Init() == false) {
|
||||||
|
dispSensorNotFound("SGP41");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init SHT */
|
||||||
|
if (ag.sht.begin(Wire) == false) {
|
||||||
|
Serial.println("SHTx sensor not found");
|
||||||
|
configuration.hasSensorSHT = false;
|
||||||
|
dispSensorNotFound("SHT");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init S8 CO2 sensor */
|
||||||
|
if (ag.s8.begin(&Serial) == false) {
|
||||||
|
Serial.println("CO2 S8 sensor not found");
|
||||||
|
configuration.hasSensorS8 = false;
|
||||||
|
dispSensorNotFound("S8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init PMS5003 */
|
||||||
|
configuration.hasSensorPMS1 = true;
|
||||||
|
configuration.hasSensorPMS2 = false;
|
||||||
|
if (ag.pms5003.begin(&Serial) == false) {
|
||||||
|
Serial.println("PMS sensor not found");
|
||||||
|
configuration.hasSensorPMS1 = false;
|
||||||
|
|
||||||
|
dispSensorNotFound("PMS");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set S8 CO2 abc days period */
|
||||||
|
if (configuration.hasSensorS8) {
|
||||||
|
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
|
||||||
|
Serial.println("Set S8 AbcDays successful");
|
||||||
|
} else {
|
||||||
|
Serial.println("Set S8 AbcDays failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localServer.setFwMode(FW_MODE_I_33PS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void failedHandler(String msg) {
|
||||||
|
while (true) {
|
||||||
|
Serial.println(msg);
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void configurationUpdateSchedule(void) {
|
||||||
|
if (apiClient.fetchServerConfiguration()) {
|
||||||
|
configUpdateHandle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void configUpdateHandle() {
|
||||||
|
if (configuration.isUpdated() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.executeCo2Calibration();
|
||||||
|
|
||||||
|
String mqttUri = configuration.getMqttBrokerUri();
|
||||||
|
if (mqttClient.isCurrentUri(mqttUri) == false) {
|
||||||
|
mqttClient.end();
|
||||||
|
initMqtt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
if (configuration.noxLearnOffsetChanged() ||
|
||||||
|
configuration.tvocLearnOffsetChanged()) {
|
||||||
|
ag.sgp41.end();
|
||||||
|
|
||||||
|
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
|
||||||
|
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
|
||||||
|
bool result = sgp41Init();
|
||||||
|
const char *resultStr = "successful";
|
||||||
|
if (!result) {
|
||||||
|
resultStr = "failure";
|
||||||
|
}
|
||||||
|
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
|
||||||
|
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
|
||||||
|
oldTvocOffset, configuration.getTvocLearningOffset(),
|
||||||
|
resultStr);
|
||||||
|
}
|
||||||
|
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
|
||||||
|
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
|
||||||
|
oldNoxOffset, configuration.getNoxLearningOffset(),
|
||||||
|
resultStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.isDisplayBrightnessChanged()) {
|
||||||
|
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
||||||
|
}
|
||||||
|
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void appDispHandler(void) {
|
||||||
|
AgStateMachineState state = AgStateMachineNormal;
|
||||||
|
|
||||||
|
/** Only show display status on online mode. */
|
||||||
|
if (configuration.isOfflineMode() == false) {
|
||||||
|
if (wifiConnector.isConnected() == false) {
|
||||||
|
state = AgStateMachineWiFiLost;
|
||||||
|
} else if (apiClient.isFetchConfigureFailed()) {
|
||||||
|
state = AgStateMachineSensorConfigFailed;
|
||||||
|
if (apiClient.isNotAvailableOnDashboard()) {
|
||||||
|
stateMachine.displaySetAddToDashBoard();
|
||||||
|
} else {
|
||||||
|
stateMachine.displayClearAddToDashBoard();
|
||||||
|
}
|
||||||
|
} else if (apiClient.isPostToServerFailed()) {
|
||||||
|
state = AgStateMachineServerLost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateMachine.displayHandle(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void oledDisplaySchedule(void) {
|
||||||
|
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateTvoc(void) {
|
||||||
|
if (!configuration.hasSensorSGP) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 {
|
||||||
|
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) {
|
||||||
|
/** Increment bootcount when send measurements data is scheduled */
|
||||||
|
measurements.bootCount++;
|
||||||
|
|
||||||
|
/** Ignore send data to server if postToAirGradient disabled */
|
||||||
|
if (configuration.isPostDataToAirGradient() == false ||
|
||||||
|
configuration.isOfflineMode()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
|
||||||
|
if (apiClient.postToServer(syncData)) {
|
||||||
|
Serial.println();
|
||||||
|
Serial.println(
|
||||||
|
"Online mode and isPostToAirGradient = true: watchdog reset");
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tempHumUpdate(void) {
|
||||||
|
if (ag.sht.measure()) {
|
||||||
|
float temp = ag.sht.getTemperature();
|
||||||
|
float rhum = ag.sht.getRelativeHumidity();
|
||||||
|
|
||||||
|
measurements.update(Measurements::Temperature, temp);
|
||||||
|
measurements.update(Measurements::Humidity, rhum);
|
||||||
|
|
||||||
|
// Update compensation temperature and humidity for SGP41
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
|
||||||
|
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
|
||||||
|
Serial.println("SHT read failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
60
examples/DiyProIndoorV3_3/LocalServer.cpp
Normal file
60
examples/DiyProIndoorV3_3/LocalServer.cpp
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#include "LocalServer.h"
|
||||||
|
|
||||||
|
LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
|
||||||
|
Measurements &measure, Configuration &config,
|
||||||
|
WifiConnector &wifiConnector)
|
||||||
|
: PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure),
|
||||||
|
config(config), wifiConnector(wifiConnector), server(80) {}
|
||||||
|
|
||||||
|
LocalServer::~LocalServer() {}
|
||||||
|
|
||||||
|
bool LocalServer::begin(void) {
|
||||||
|
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
|
||||||
|
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
|
||||||
|
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
|
||||||
|
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
|
||||||
|
server.begin();
|
||||||
|
logInfo("Init: " + getHostname() + ".local");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
String LocalServer::getHostname(void) {
|
||||||
|
return "airgradient_" + ag->deviceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_handle(void) { server.handleClient(); }
|
||||||
|
|
||||||
|
void LocalServer::_GET_config(void) {
|
||||||
|
if(ag->isOne()) {
|
||||||
|
server.send(200, "application/json", config.toString());
|
||||||
|
} else {
|
||||||
|
server.send(200, "application/json", config.toString(fwMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_PUT_config(void) {
|
||||||
|
String data = server.arg(0);
|
||||||
|
String response = "";
|
||||||
|
int statusCode = 400; // Status code for data invalid
|
||||||
|
if (config.parse(data, true)) {
|
||||||
|
statusCode = 200;
|
||||||
|
response = "Success";
|
||||||
|
} else {
|
||||||
|
response = config.getFailedMesage();
|
||||||
|
}
|
||||||
|
server.send(statusCode, "text/plain", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_GET_metrics(void) {
|
||||||
|
server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_GET_measure(void) {
|
||||||
|
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
|
||||||
|
server.send(200, "application/json", toSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
38
examples/DiyProIndoorV3_3/LocalServer.h
Normal file
38
examples/DiyProIndoorV3_3/LocalServer.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#ifndef _LOCAL_SERVER_H_
|
||||||
|
#define _LOCAL_SERVER_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "OpenMetrics.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESP8266WebServer.h>
|
||||||
|
|
||||||
|
class LocalServer : public PrintLog {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
OpenMetrics &openMetrics;
|
||||||
|
Measurements &measure;
|
||||||
|
Configuration &config;
|
||||||
|
WifiConnector &wifiConnector;
|
||||||
|
ESP8266WebServer server;
|
||||||
|
AgFirmwareMode fwMode;
|
||||||
|
|
||||||
|
public:
|
||||||
|
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
|
||||||
|
Configuration &config, WifiConnector& wifiConnector);
|
||||||
|
~LocalServer();
|
||||||
|
|
||||||
|
bool begin(void);
|
||||||
|
void setAirGraident(AirGradient *ag);
|
||||||
|
String getHostname(void);
|
||||||
|
void setFwMode(AgFirmwareMode fwMode);
|
||||||
|
void _handle(void);
|
||||||
|
void _GET_config(void);
|
||||||
|
void _PUT_config(void);
|
||||||
|
void _GET_metrics(void);
|
||||||
|
void _GET_measure(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _LOCAL_SERVER_H_ */
|
204
examples/DiyProIndoorV3_3/OpenMetrics.cpp
Normal file
204
examples/DiyProIndoorV3_3/OpenMetrics.cpp
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
#include "OpenMetrics.h"
|
||||||
|
|
||||||
|
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
|
||||||
|
WifiConnector &wifiConnector, AgApiClient &apiClient)
|
||||||
|
: measure(measure), config(config), wifiConnector(wifiConnector),
|
||||||
|
apiClient(apiClient) {}
|
||||||
|
|
||||||
|
OpenMetrics::~OpenMetrics() {}
|
||||||
|
|
||||||
|
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
const char *OpenMetrics::getApiContentType(void) {
|
||||||
|
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *OpenMetrics::getApi(void) { return "/metrics"; }
|
||||||
|
|
||||||
|
String OpenMetrics::getPayload(void) {
|
||||||
|
String response;
|
||||||
|
String current_metric_name;
|
||||||
|
const auto add_metric = [&](const String &name, const String &help,
|
||||||
|
const String &type, const String &unit = "") {
|
||||||
|
current_metric_name = "airgradient_" + name;
|
||||||
|
if (!unit.isEmpty())
|
||||||
|
current_metric_name += "_" + unit;
|
||||||
|
response += "# HELP " + current_metric_name + " " + help + "\n";
|
||||||
|
response += "# TYPE " + current_metric_name + " " + type + "\n";
|
||||||
|
if (!unit.isEmpty())
|
||||||
|
response += "# UNIT " + current_metric_name + " " + unit + "\n";
|
||||||
|
};
|
||||||
|
const auto add_metric_point = [&](const String &labels, const String &value) {
|
||||||
|
response += current_metric_name + "{" + labels + "} " + value + "\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
add_metric("info", "AirGradient device information", "info");
|
||||||
|
add_metric_point("airgradient_serial_number=\"" + ag->deviceId() +
|
||||||
|
"\",airgradient_device_type=\"" + ag->getBoardName() +
|
||||||
|
"\",airgradient_library_version=\"" + ag->getVersion() +
|
||||||
|
"\"",
|
||||||
|
"1");
|
||||||
|
|
||||||
|
add_metric("config_ok",
|
||||||
|
"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(
|
||||||
|
"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(
|
||||||
|
"wifi_rssi",
|
||||||
|
"WiFi signal strength from the AirGradient device perspective, in dBm",
|
||||||
|
"gauge", "dbm");
|
||||||
|
add_metric_point("", String(wifiConnector.RSSI()));
|
||||||
|
|
||||||
|
// Initialize default invalid value for each measurements
|
||||||
|
float _temp = utils::getInvalidTemperature();
|
||||||
|
float _hum = utils::getInvalidHumidity();
|
||||||
|
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 tvoc = utils::getInvalidVOC();
|
||||||
|
int tvocRaw = utils::getInvalidVOC();
|
||||||
|
int nox = utils::getInvalidNOx();
|
||||||
|
int noxRaw = utils::getInvalidNOx();
|
||||||
|
|
||||||
|
if (config.hasSensorSHT) {
|
||||||
|
_temp = measure.getFloat(Measurements::Temperature);
|
||||||
|
_hum = measure.getFloat(Measurements::Humidity);
|
||||||
|
atmpCompensated = _temp;
|
||||||
|
ahumCompensated = _hum;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasSensorPMS1) {
|
||||||
|
pm01 = measure.get(Measurements::PM01);
|
||||||
|
pm25 = measure.get(Measurements::PM25);
|
||||||
|
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::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::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::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::isValidPm03Count(pm03PCount)) {
|
||||||
|
add_metric("pm0d3",
|
||||||
|
"PM0.3 concentration as measured by the AirGradient PMS "
|
||||||
|
"sensor, in number of particules per 100 milliliters",
|
||||||
|
"gauge", "p100ml");
|
||||||
|
add_metric_point("", String(pm03PCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasSensorSGP) {
|
||||||
|
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(tvoc));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(tvocRaw));
|
||||||
|
}
|
||||||
|
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(nox));
|
||||||
|
}
|
||||||
|
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(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 "
|
||||||
|
"sensor, in degrees Celsius",
|
||||||
|
"gauge", "celsius");
|
||||||
|
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_point("", String(atmpCompensated));
|
||||||
|
}
|
||||||
|
if (utils::isValidHumidity(_hum)) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
response += "# EOF\n";
|
||||||
|
return response;
|
||||||
|
}
|
28
examples/DiyProIndoorV3_3/OpenMetrics.h
Normal file
28
examples/DiyProIndoorV3_3/OpenMetrics.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#ifndef _OPEN_METRICS_H_
|
||||||
|
#define _OPEN_METRICS_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "AgApiClient.h"
|
||||||
|
|
||||||
|
class OpenMetrics {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
Measurements &measure;
|
||||||
|
Configuration &config;
|
||||||
|
WifiConnector &wifiConnector;
|
||||||
|
AgApiClient &apiClient;
|
||||||
|
|
||||||
|
public:
|
||||||
|
OpenMetrics(Measurements &measure, Configuration &conig,
|
||||||
|
WifiConnector &wifiConnector, AgApiClient& apiClient);
|
||||||
|
~OpenMetrics();
|
||||||
|
void setAirGradient(AirGradient *ag);
|
||||||
|
const char *getApiContentType(void);
|
||||||
|
const char* getApi(void);
|
||||||
|
String getPayload(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _OPEN_METRICS_H_ */
|
682
examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino
Normal file
682
examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino
Normal file
@ -0,0 +1,682 @@
|
|||||||
|
/*
|
||||||
|
This is the code for the AirGradient DIY PRO 4.2 Air Quality Monitor with an D1
|
||||||
|
ESP8266 Microcontroller.
|
||||||
|
|
||||||
|
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
|
||||||
|
small display and can send data over Wifi.
|
||||||
|
|
||||||
|
Open source air quality monitors and kits are available:
|
||||||
|
Indoor Monitor: https://www.airgradient.com/indoor/
|
||||||
|
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"
|
||||||
|
|
||||||
|
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||||
|
can be set through the AirGradient dashboard.
|
||||||
|
|
||||||
|
If you have any questions please visit our forum at
|
||||||
|
https://forum.airgradient.com/
|
||||||
|
|
||||||
|
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AgApiClient.h"
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgSchedule.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include "LocalServer.h"
|
||||||
|
#include "OpenMetrics.h"
|
||||||
|
#include "MqttClient.h"
|
||||||
|
#include <AirGradient.h>
|
||||||
|
#include <ESP8266HTTPClient.h>
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266mDNS.h>
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
|
||||||
|
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
|
||||||
|
#define DISP_UPDATE_INTERVAL 2500 /** ms */
|
||||||
|
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define SERVER_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define MQTT_SYNC_INTERVAL 60000 /** ms */
|
||||||
|
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
|
||||||
|
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
||||||
|
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
|
||||||
|
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
|
||||||
|
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
|
||||||
|
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
||||||
|
|
||||||
|
static AirGradient ag(DIY_PRO_INDOOR_V4_2);
|
||||||
|
static Configuration configuration(Serial);
|
||||||
|
static AgApiClient apiClient(Serial, configuration);
|
||||||
|
static Measurements measurements;
|
||||||
|
static OledDisplay oledDisplay(configuration, measurements, Serial);
|
||||||
|
static StateMachine stateMachine(oledDisplay, Serial, measurements,
|
||||||
|
configuration);
|
||||||
|
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
|
||||||
|
configuration);
|
||||||
|
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
|
||||||
|
apiClient);
|
||||||
|
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
|
||||||
|
wifiConnector);
|
||||||
|
static MqttClient mqttClient(Serial);
|
||||||
|
|
||||||
|
static uint32_t factoryBtnPressTime = 0;
|
||||||
|
static AgFirmwareMode fwMode = FW_MODE_I_42PS;
|
||||||
|
|
||||||
|
static String fwNewVersion;
|
||||||
|
|
||||||
|
static void boardInit(void);
|
||||||
|
static void failedHandler(String msg);
|
||||||
|
static void configurationUpdateSchedule(void);
|
||||||
|
static void appDispHandler(void);
|
||||||
|
static void oledDisplaySchedule(void);
|
||||||
|
static void updateTvoc(void);
|
||||||
|
static void updatePm(void);
|
||||||
|
static void sendDataToServer(void);
|
||||||
|
static void tempHumUpdate(void);
|
||||||
|
static void co2Update(void);
|
||||||
|
static void mdnsInit(void);
|
||||||
|
static void initMqtt(void);
|
||||||
|
static void factoryConfigReset(void);
|
||||||
|
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,
|
||||||
|
configurationUpdateSchedule);
|
||||||
|
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
|
||||||
|
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
|
||||||
|
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
|
||||||
|
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
|
||||||
|
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
|
||||||
|
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
|
||||||
|
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
/** Serial for print debug message */
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(100); /** For bester show log */
|
||||||
|
|
||||||
|
/** Print device ID into log */
|
||||||
|
Serial.println("Serial nr: " + ag.deviceId());
|
||||||
|
|
||||||
|
/** Initialize local configure */
|
||||||
|
configuration.begin();
|
||||||
|
|
||||||
|
/** Init I2C */
|
||||||
|
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
configuration.setAirGradient(&ag);
|
||||||
|
oledDisplay.setAirGradient(&ag);
|
||||||
|
stateMachine.setAirGradient(&ag);
|
||||||
|
wifiConnector.setAirGradient(&ag);
|
||||||
|
apiClient.setAirGradient(&ag);
|
||||||
|
openMetrics.setAirGradient(&ag);
|
||||||
|
localServer.setAirGraident(&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;
|
||||||
|
|
||||||
|
/** Show message confirm offline mode, should me perform if LED bar button
|
||||||
|
* test pressed */
|
||||||
|
|
||||||
|
oledDisplay.setText(
|
||||||
|
"Press now for",
|
||||||
|
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
|
||||||
|
uint32_t startTime = millis();
|
||||||
|
while (true) {
|
||||||
|
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
|
||||||
|
configuration.setOfflineMode(!configuration.isOfflineMode());
|
||||||
|
|
||||||
|
oledDisplay.setText(
|
||||||
|
"Offline Mode",
|
||||||
|
configuration.isOfflineMode() ? " = True" : " = False", "");
|
||||||
|
delay(1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint32_t periodMs = (uint32_t)(millis() - startTime);
|
||||||
|
if (periodMs >= 3000) {
|
||||||
|
Serial.println("Set for offline mode timeout");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
connectToWifi = !configuration.isOfflineMode();
|
||||||
|
|
||||||
|
if (connectToWifi) {
|
||||||
|
apiClient.begin();
|
||||||
|
|
||||||
|
if (wifiConnector.connect()) {
|
||||||
|
if (wifiConnector.isConnected()) {
|
||||||
|
mdnsInit();
|
||||||
|
localServer.begin();
|
||||||
|
initMqtt();
|
||||||
|
sendDataToAg();
|
||||||
|
|
||||||
|
apiClient.fetchServerConfiguration();
|
||||||
|
configSchedule.update();
|
||||||
|
if (apiClient.isFetchConfigureFailed()) {
|
||||||
|
if (apiClient.isNotAvailableOnDashboard()) {
|
||||||
|
stateMachine.displaySetAddToDashBoard();
|
||||||
|
stateMachine.displayHandle(
|
||||||
|
AgStateMachineWiFiOkServerOkSensorConfigFailed);
|
||||||
|
} else {
|
||||||
|
stateMachine.displayClearAddToDashBoard();
|
||||||
|
}
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wifiConnector.isConfigurePorttalTimeout()) {
|
||||||
|
oledDisplay.showRebooting();
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("", "", "");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Set offline mode without saving, cause wifi is not configured */
|
||||||
|
if (wifiConnector.hasConfigurated() == false) {
|
||||||
|
Serial.println("Set offline mode cause wifi is not configurated");
|
||||||
|
configuration.setOfflineModeWithoutSave(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show display Warning up */
|
||||||
|
oledDisplay.setText("Warming Up", "Serial Number:", ag.deviceId().c_str());
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
|
||||||
|
Serial.println("Display brightness: " +
|
||||||
|
String(configuration.getDisplayBrightness()));
|
||||||
|
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
||||||
|
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
/** Handle schedule */
|
||||||
|
dispLedSchedule.run();
|
||||||
|
configSchedule.run();
|
||||||
|
agApiPostSchedule.run();
|
||||||
|
|
||||||
|
if (configuration.hasSensorS8) {
|
||||||
|
co2Schedule.run();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorPMS1) {
|
||||||
|
pmsSchedule.run();
|
||||||
|
ag.pms5003.handle();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorSHT) {
|
||||||
|
tempHumSchedule.run();
|
||||||
|
}
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
tvocSchedule.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
watchdogFeedSchedule.run();
|
||||||
|
|
||||||
|
/** Check for handle WiFi reconnect */
|
||||||
|
wifiConnector.handle();
|
||||||
|
|
||||||
|
/** factory reset handle */
|
||||||
|
factoryConfigReset();
|
||||||
|
|
||||||
|
/** check that local configura changed then do some action */
|
||||||
|
configUpdateHandle();
|
||||||
|
|
||||||
|
localServer._handle();
|
||||||
|
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
ag.sgp41.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
MDNS.update();
|
||||||
|
|
||||||
|
mqttSchedule.run();
|
||||||
|
mqttClient.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void co2Update(void) {
|
||||||
|
if (!configuration.hasSensorS8) {
|
||||||
|
// Device don't have S8 sensor
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int value = ag.s8.getCo2();
|
||||||
|
if (utils::isValidCO2(value)) {
|
||||||
|
measurements.update(Measurements::CO2, value);
|
||||||
|
} else {
|
||||||
|
measurements.update(Measurements::CO2, utils::getInvalidCO2());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mdnsInit(void) {
|
||||||
|
Serial.println("mDNS init");
|
||||||
|
if (!MDNS.begin(localServer.getHostname().c_str())) {
|
||||||
|
Serial.println("Init mDNS failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MDNS.addService("_airgradient", "_tcp", 80);
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
|
||||||
|
AgFirmwareModeName(fwMode));
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
|
||||||
|
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
|
||||||
|
|
||||||
|
MDNS.announce();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initMqtt(void) {
|
||||||
|
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("Connection to MQTT broker failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void factoryConfigReset(void) {
|
||||||
|
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
|
||||||
|
if (factoryBtnPressTime == 0) {
|
||||||
|
factoryBtnPressTime = millis();
|
||||||
|
} else {
|
||||||
|
uint32_t ms = (uint32_t)(millis() - factoryBtnPressTime);
|
||||||
|
if (ms >= 2000) {
|
||||||
|
// Show display message: For factory keep for x seconds
|
||||||
|
if (ag.isOne() || ag.isPro4_2()) {
|
||||||
|
oledDisplay.setText("Factory reset", "keep pressed", "for 8 sec");
|
||||||
|
} else {
|
||||||
|
Serial.println("Factory reset, keep pressed for 8 sec");
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 7;
|
||||||
|
while (ag.button.getState() == ag.button.BUTTON_PRESSED) {
|
||||||
|
delay(1000);
|
||||||
|
String str = "for " + String(count) + " sec";
|
||||||
|
oledDisplay.setText("Factory reset", "keep pressed", str.c_str());
|
||||||
|
|
||||||
|
count--;
|
||||||
|
if (count == 0) {
|
||||||
|
/** Stop MQTT task first */
|
||||||
|
// if (mqttTask) {
|
||||||
|
// vTaskDelete(mqttTask);
|
||||||
|
// mqttTask = NULL;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/** Reset WIFI */
|
||||||
|
wifiConnector.reset();
|
||||||
|
|
||||||
|
/** Reset local config */
|
||||||
|
configuration.reset();
|
||||||
|
|
||||||
|
oledDisplay.setText("Factory reset", "successful", "");
|
||||||
|
|
||||||
|
delay(3000);
|
||||||
|
oledDisplay.setText("", "", "");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show current content cause reset ignore */
|
||||||
|
factoryBtnPressTime = 0;
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (factoryBtnPressTime != 0) {
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
factoryBtnPressTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wdgFeedUpdate(void) {
|
||||||
|
ag.watchdog.reset();
|
||||||
|
Serial.println("External watchdog feed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sgp41Init(void) {
|
||||||
|
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
|
||||||
|
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
|
||||||
|
if (ag.sgp41.begin(Wire)) {
|
||||||
|
Serial.println("Init SGP41 success");
|
||||||
|
configuration.hasSensorSGP = true;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Serial.println("Init SGP41 failuire");
|
||||||
|
configuration.hasSensorSGP = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifiFactoryConfigure(void) {
|
||||||
|
WiFi.persistent(true);
|
||||||
|
WiFi.begin("airgradient", "cleanair");
|
||||||
|
WiFi.persistent(false);
|
||||||
|
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("Rebooting...", "", "");
|
||||||
|
delay(2500);
|
||||||
|
oledDisplay.setText("", "", "");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mqttHandle(void) {
|
||||||
|
if(mqttClient.isConnected() == false) {
|
||||||
|
mqttClient.connect(String("airgradient-") + ag.deviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mqttClient.isConnected()) {
|
||||||
|
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
|
||||||
|
String topic = "airgradient/readings/" + ag.deviceId();
|
||||||
|
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
|
||||||
|
Serial.println("MQTT sync success");
|
||||||
|
} else {
|
||||||
|
Serial.println("MQTT sync failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sendDataToAg() {
|
||||||
|
/** Change oledDisplay and led state */
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
|
||||||
|
|
||||||
|
delay(1500);
|
||||||
|
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
|
||||||
|
} else {
|
||||||
|
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
|
||||||
|
}
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispSensorNotFound(String ss) {
|
||||||
|
ss = ss + " not found";
|
||||||
|
oledDisplay.setText("Sensor init", "Error:", ss.c_str());
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void boardInit(void) {
|
||||||
|
/** Display init */
|
||||||
|
oledDisplay.begin();
|
||||||
|
|
||||||
|
/** Show boot display */
|
||||||
|
Serial.println("Firmware Version: " + ag.getVersion());
|
||||||
|
|
||||||
|
oledDisplay.setText("AirGradient ONE",
|
||||||
|
"FW Version: ", ag.getVersion().c_str());
|
||||||
|
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
||||||
|
|
||||||
|
ag.button.begin();
|
||||||
|
ag.watchdog.begin();
|
||||||
|
|
||||||
|
/** Run LED test on start up if button pressed */
|
||||||
|
oledDisplay.setText("Press now for", "factory WiFi", "configure");
|
||||||
|
|
||||||
|
uint32_t stime = millis();
|
||||||
|
while (true) {
|
||||||
|
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
|
||||||
|
wifiFactoryConfigure();
|
||||||
|
}
|
||||||
|
delay(1);
|
||||||
|
uint32_t ms = (uint32_t)(millis() - stime);
|
||||||
|
if (ms >= 3000) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show message init sensor */
|
||||||
|
oledDisplay.setText("Sensor", "initializing...", "");
|
||||||
|
|
||||||
|
/** Init sensor SGP41 */
|
||||||
|
if (sgp41Init() == false) {
|
||||||
|
dispSensorNotFound("SGP41");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init SHT */
|
||||||
|
if (ag.sht.begin(Wire) == false) {
|
||||||
|
Serial.println("SHTx sensor not found");
|
||||||
|
configuration.hasSensorSHT = false;
|
||||||
|
dispSensorNotFound("SHT");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init S8 CO2 sensor */
|
||||||
|
if (ag.s8.begin(&Serial) == false) {
|
||||||
|
Serial.println("CO2 S8 sensor not found");
|
||||||
|
configuration.hasSensorS8 = false;
|
||||||
|
dispSensorNotFound("S8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init PMS5003 */
|
||||||
|
configuration.hasSensorPMS1 = true;
|
||||||
|
configuration.hasSensorPMS2 = false;
|
||||||
|
if (ag.pms5003.begin(&Serial) == false) {
|
||||||
|
Serial.println("PMS sensor not found");
|
||||||
|
configuration.hasSensorPMS1 = false;
|
||||||
|
|
||||||
|
dispSensorNotFound("PMS");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set S8 CO2 abc days period */
|
||||||
|
if (configuration.hasSensorS8) {
|
||||||
|
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
|
||||||
|
Serial.println("Set S8 AbcDays successful");
|
||||||
|
} else {
|
||||||
|
Serial.println("Set S8 AbcDays failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localServer.setFwMode(FW_MODE_I_42PS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void failedHandler(String msg) {
|
||||||
|
while (true) {
|
||||||
|
Serial.println(msg);
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void configurationUpdateSchedule(void) {
|
||||||
|
if (apiClient.fetchServerConfiguration()) {
|
||||||
|
configUpdateHandle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void configUpdateHandle() {
|
||||||
|
if (configuration.isUpdated() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.executeCo2Calibration();
|
||||||
|
|
||||||
|
String mqttUri = configuration.getMqttBrokerUri();
|
||||||
|
if (mqttClient.isCurrentUri(mqttUri) == false) {
|
||||||
|
mqttClient.end();
|
||||||
|
initMqtt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
if (configuration.noxLearnOffsetChanged() ||
|
||||||
|
configuration.tvocLearnOffsetChanged()) {
|
||||||
|
ag.sgp41.end();
|
||||||
|
|
||||||
|
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
|
||||||
|
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
|
||||||
|
bool result = sgp41Init();
|
||||||
|
const char *resultStr = "successful";
|
||||||
|
if (!result) {
|
||||||
|
resultStr = "failure";
|
||||||
|
}
|
||||||
|
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
|
||||||
|
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
|
||||||
|
oldTvocOffset, configuration.getTvocLearningOffset(),
|
||||||
|
resultStr);
|
||||||
|
}
|
||||||
|
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
|
||||||
|
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
|
||||||
|
oldNoxOffset, configuration.getNoxLearningOffset(),
|
||||||
|
resultStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.isDisplayBrightnessChanged()) {
|
||||||
|
oledDisplay.setBrightness(configuration.getDisplayBrightness());
|
||||||
|
}
|
||||||
|
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void appDispHandler(void) {
|
||||||
|
AgStateMachineState state = AgStateMachineNormal;
|
||||||
|
|
||||||
|
/** Only show display status on online mode. */
|
||||||
|
if (configuration.isOfflineMode() == false) {
|
||||||
|
if (wifiConnector.isConnected() == false) {
|
||||||
|
state = AgStateMachineWiFiLost;
|
||||||
|
} else if (apiClient.isFetchConfigureFailed()) {
|
||||||
|
state = AgStateMachineSensorConfigFailed;
|
||||||
|
if (apiClient.isNotAvailableOnDashboard()) {
|
||||||
|
stateMachine.displaySetAddToDashBoard();
|
||||||
|
} else {
|
||||||
|
stateMachine.displayClearAddToDashBoard();
|
||||||
|
}
|
||||||
|
} else if (apiClient.isPostToServerFailed()) {
|
||||||
|
state = AgStateMachineServerLost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateMachine.displayHandle(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void oledDisplaySchedule(void) {
|
||||||
|
if (factoryBtnPressTime == 0) {
|
||||||
|
appDispHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateTvoc(void) {
|
||||||
|
if (!configuration.hasSensorSGP) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 {
|
||||||
|
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) {
|
||||||
|
/** Increment bootcount when send measurements data is scheduled */
|
||||||
|
measurements.bootCount++;
|
||||||
|
|
||||||
|
/** Ignore send data to server if postToAirGradient disabled */
|
||||||
|
if (configuration.isPostDataToAirGradient() == false ||
|
||||||
|
configuration.isOfflineMode()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
|
||||||
|
if (apiClient.postToServer(syncData)) {
|
||||||
|
Serial.println();
|
||||||
|
Serial.println(
|
||||||
|
"Online mode and isPostToAirGradient = true: watchdog reset");
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tempHumUpdate(void) {
|
||||||
|
if (ag.sht.measure()) {
|
||||||
|
float temp = ag.sht.getTemperature();
|
||||||
|
float rhum = ag.sht.getRelativeHumidity();
|
||||||
|
|
||||||
|
measurements.update(Measurements::Temperature, temp);
|
||||||
|
measurements.update(Measurements::Humidity, rhum);
|
||||||
|
|
||||||
|
// Update compensation temperature and humidity for SGP41
|
||||||
|
if (configuration.hasSensorSGP) {
|
||||||
|
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
|
||||||
|
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
|
||||||
|
Serial.println("SHT read failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
60
examples/DiyProIndoorV4_2/LocalServer.cpp
Normal file
60
examples/DiyProIndoorV4_2/LocalServer.cpp
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#include "LocalServer.h"
|
||||||
|
|
||||||
|
LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
|
||||||
|
Measurements &measure, Configuration &config,
|
||||||
|
WifiConnector &wifiConnector)
|
||||||
|
: PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure),
|
||||||
|
config(config), wifiConnector(wifiConnector), server(80) {}
|
||||||
|
|
||||||
|
LocalServer::~LocalServer() {}
|
||||||
|
|
||||||
|
bool LocalServer::begin(void) {
|
||||||
|
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
|
||||||
|
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
|
||||||
|
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
|
||||||
|
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
|
||||||
|
server.begin();
|
||||||
|
logInfo("Init: " + getHostname() + ".local");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
String LocalServer::getHostname(void) {
|
||||||
|
return "airgradient_" + ag->deviceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_handle(void) { server.handleClient(); }
|
||||||
|
|
||||||
|
void LocalServer::_GET_config(void) {
|
||||||
|
if(ag->isOne()) {
|
||||||
|
server.send(200, "application/json", config.toString());
|
||||||
|
} else {
|
||||||
|
server.send(200, "application/json", config.toString(fwMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_PUT_config(void) {
|
||||||
|
String data = server.arg(0);
|
||||||
|
String response = "";
|
||||||
|
int statusCode = 400; // Status code for data invalid
|
||||||
|
if (config.parse(data, true)) {
|
||||||
|
statusCode = 200;
|
||||||
|
response = "Success";
|
||||||
|
} else {
|
||||||
|
response = config.getFailedMesage();
|
||||||
|
}
|
||||||
|
server.send(statusCode, "text/plain", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_GET_metrics(void) {
|
||||||
|
server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_GET_measure(void) {
|
||||||
|
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
|
||||||
|
server.send(200, "application/json", toSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
38
examples/DiyProIndoorV4_2/LocalServer.h
Normal file
38
examples/DiyProIndoorV4_2/LocalServer.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#ifndef _LOCAL_SERVER_H_
|
||||||
|
#define _LOCAL_SERVER_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "OpenMetrics.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESP8266WebServer.h>
|
||||||
|
|
||||||
|
class LocalServer : public PrintLog {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
OpenMetrics &openMetrics;
|
||||||
|
Measurements &measure;
|
||||||
|
Configuration &config;
|
||||||
|
WifiConnector &wifiConnector;
|
||||||
|
ESP8266WebServer server;
|
||||||
|
AgFirmwareMode fwMode;
|
||||||
|
|
||||||
|
public:
|
||||||
|
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
|
||||||
|
Configuration &config, WifiConnector& wifiConnector);
|
||||||
|
~LocalServer();
|
||||||
|
|
||||||
|
bool begin(void);
|
||||||
|
void setAirGraident(AirGradient *ag);
|
||||||
|
String getHostname(void);
|
||||||
|
void setFwMode(AgFirmwareMode fwMode);
|
||||||
|
void _handle(void);
|
||||||
|
void _GET_config(void);
|
||||||
|
void _PUT_config(void);
|
||||||
|
void _GET_metrics(void);
|
||||||
|
void _GET_measure(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _LOCAL_SERVER_H_ */
|
203
examples/DiyProIndoorV4_2/OpenMetrics.cpp
Normal file
203
examples/DiyProIndoorV4_2/OpenMetrics.cpp
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
#include "OpenMetrics.h"
|
||||||
|
|
||||||
|
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
|
||||||
|
WifiConnector &wifiConnector, AgApiClient &apiClient)
|
||||||
|
: measure(measure), config(config), wifiConnector(wifiConnector),
|
||||||
|
apiClient(apiClient) {}
|
||||||
|
|
||||||
|
OpenMetrics::~OpenMetrics() {}
|
||||||
|
|
||||||
|
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
const char *OpenMetrics::getApiContentType(void) {
|
||||||
|
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *OpenMetrics::getApi(void) { return "/metrics"; }
|
||||||
|
|
||||||
|
String OpenMetrics::getPayload(void) {
|
||||||
|
String response;
|
||||||
|
String current_metric_name;
|
||||||
|
const auto add_metric = [&](const String &name, const String &help,
|
||||||
|
const String &type, const String &unit = "") {
|
||||||
|
current_metric_name = "airgradient_" + name;
|
||||||
|
if (!unit.isEmpty())
|
||||||
|
current_metric_name += "_" + unit;
|
||||||
|
response += "# HELP " + current_metric_name + " " + help + "\n";
|
||||||
|
response += "# TYPE " + current_metric_name + " " + type + "\n";
|
||||||
|
if (!unit.isEmpty())
|
||||||
|
response += "# UNIT " + current_metric_name + " " + unit + "\n";
|
||||||
|
};
|
||||||
|
const auto add_metric_point = [&](const String &labels, const String &value) {
|
||||||
|
response += current_metric_name + "{" + labels + "} " + value + "\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
add_metric("info", "AirGradient device information", "info");
|
||||||
|
add_metric_point("airgradient_serial_number=\"" + ag->deviceId() +
|
||||||
|
"\",airgradient_device_type=\"" + ag->getBoardName() +
|
||||||
|
"\",airgradient_library_version=\"" + ag->getVersion() +
|
||||||
|
"\"",
|
||||||
|
"1");
|
||||||
|
|
||||||
|
add_metric("config_ok",
|
||||||
|
"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(
|
||||||
|
"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(
|
||||||
|
"wifi_rssi",
|
||||||
|
"WiFi signal strength from the AirGradient device perspective, in dBm",
|
||||||
|
"gauge", "dbm");
|
||||||
|
add_metric_point("", String(wifiConnector.RSSI()));
|
||||||
|
|
||||||
|
// Initialize default invalid value for each measurements
|
||||||
|
float _temp = utils::getInvalidTemperature();
|
||||||
|
float _hum = utils::getInvalidHumidity();
|
||||||
|
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 tvoc = utils::getInvalidVOC();
|
||||||
|
int tvocRaw = utils::getInvalidVOC();
|
||||||
|
int nox = utils::getInvalidNOx();
|
||||||
|
int noxRaw = utils::getInvalidNOx();
|
||||||
|
|
||||||
|
if (config.hasSensorSHT) {
|
||||||
|
_temp = measure.getFloat(Measurements::Temperature);
|
||||||
|
_hum = measure.getFloat(Measurements::Humidity);
|
||||||
|
atmpCompensated = _temp;
|
||||||
|
ahumCompensated = _hum;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasSensorPMS1) {
|
||||||
|
pm01 = measure.get(Measurements::PM01);
|
||||||
|
pm25 = measure.get(Measurements::PM25);
|
||||||
|
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::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::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::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::isValidPm03Count(pm03PCount)) {
|
||||||
|
add_metric("pm0d3",
|
||||||
|
"PM0.3 concentration as measured by the AirGradient PMS "
|
||||||
|
"sensor, in number of particules per 100 milliliters",
|
||||||
|
"gauge", "p100ml");
|
||||||
|
add_metric_point("", String(pm03PCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasSensorSGP) {
|
||||||
|
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(tvoc));
|
||||||
|
}
|
||||||
|
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(tvocRaw));
|
||||||
|
}
|
||||||
|
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(nox));
|
||||||
|
}
|
||||||
|
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(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 "
|
||||||
|
"sensor, in degrees Celsius",
|
||||||
|
"gauge", "celsius");
|
||||||
|
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_point("", String(atmpCompensated));
|
||||||
|
}
|
||||||
|
if (utils::isValidHumidity(_hum)) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
response += "# EOF\n";
|
||||||
|
return response;
|
||||||
|
}
|
28
examples/DiyProIndoorV4_2/OpenMetrics.h
Normal file
28
examples/DiyProIndoorV4_2/OpenMetrics.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#ifndef _OPEN_METRICS_H_
|
||||||
|
#define _OPEN_METRICS_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "AgApiClient.h"
|
||||||
|
|
||||||
|
class OpenMetrics {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
Measurements &measure;
|
||||||
|
Configuration &config;
|
||||||
|
WifiConnector &wifiConnector;
|
||||||
|
AgApiClient &apiClient;
|
||||||
|
|
||||||
|
public:
|
||||||
|
OpenMetrics(Measurements &measure, Configuration &conig,
|
||||||
|
WifiConnector &wifiConnector, AgApiClient& apiClient);
|
||||||
|
~OpenMetrics();
|
||||||
|
void setAirGradient(AirGradient *ag);
|
||||||
|
const char *getApiContentType(void);
|
||||||
|
const char* getApi(void);
|
||||||
|
String getPayload(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _OPEN_METRICS_H_ */
|
File diff suppressed because it is too large
Load Diff
71
examples/OneOpenAir/LocalServer.cpp
Normal file
71
examples/OneOpenAir/LocalServer.cpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#include "LocalServer.h"
|
||||||
|
|
||||||
|
LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
|
||||||
|
Measurements &measure, Configuration &config,
|
||||||
|
WifiConnector &wifiConnector)
|
||||||
|
: PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure),
|
||||||
|
config(config), wifiConnector(wifiConnector) {}
|
||||||
|
|
||||||
|
LocalServer::~LocalServer() {}
|
||||||
|
|
||||||
|
bool LocalServer::begin(void) {
|
||||||
|
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
|
||||||
|
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
|
||||||
|
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
|
||||||
|
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
|
||||||
|
server.begin();
|
||||||
|
|
||||||
|
if (xTaskCreate(
|
||||||
|
[](void *param) {
|
||||||
|
LocalServer *localServer = (LocalServer *)param;
|
||||||
|
for (;;) {
|
||||||
|
localServer->_handle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webserver", 1024 * 4, this, 5, NULL) != pdTRUE) {
|
||||||
|
Serial.println("Create task handle webserver failed");
|
||||||
|
}
|
||||||
|
logInfo("Init: " + getHostname() + ".local");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
String LocalServer::getHostname(void) {
|
||||||
|
return "airgradient_" + ag->deviceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_handle(void) { server.handleClient(); }
|
||||||
|
|
||||||
|
void LocalServer::_GET_config(void) {
|
||||||
|
if(ag->isOne()) {
|
||||||
|
server.send(200, "application/json", config.toString());
|
||||||
|
} else {
|
||||||
|
server.send(200, "application/json", config.toString(fwMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_PUT_config(void) {
|
||||||
|
String data = server.arg(0);
|
||||||
|
String response = "";
|
||||||
|
int statusCode = 400; // Status code for data invalid
|
||||||
|
if (config.parse(data, true)) {
|
||||||
|
statusCode = 200;
|
||||||
|
response = "Success";
|
||||||
|
} else {
|
||||||
|
response = config.getFailedMesage();
|
||||||
|
}
|
||||||
|
server.send(statusCode, "text/plain", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_GET_metrics(void) {
|
||||||
|
server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::_GET_measure(void) {
|
||||||
|
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
|
||||||
|
server.send(200, "application/json", toSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
38
examples/OneOpenAir/LocalServer.h
Normal file
38
examples/OneOpenAir/LocalServer.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#ifndef _LOCAL_SERVER_H_
|
||||||
|
#define _LOCAL_SERVER_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "OpenMetrics.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
|
||||||
|
class LocalServer : public PrintLog {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
OpenMetrics &openMetrics;
|
||||||
|
Measurements &measure;
|
||||||
|
Configuration &config;
|
||||||
|
WifiConnector &wifiConnector;
|
||||||
|
WebServer server;
|
||||||
|
AgFirmwareMode fwMode;
|
||||||
|
|
||||||
|
public:
|
||||||
|
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
|
||||||
|
Configuration &config, WifiConnector& wifiConnector);
|
||||||
|
~LocalServer();
|
||||||
|
|
||||||
|
bool begin(void);
|
||||||
|
void setAirGraident(AirGradient *ag);
|
||||||
|
String getHostname(void);
|
||||||
|
void setFwMode(AgFirmwareMode fwMode);
|
||||||
|
void _handle(void);
|
||||||
|
void _GET_config(void);
|
||||||
|
void _PUT_config(void);
|
||||||
|
void _GET_metrics(void);
|
||||||
|
void _GET_measure(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _LOCAL_SERVER_H_ */
|
1221
examples/OneOpenAir/OneOpenAir.ino
Normal file
1221
examples/OneOpenAir/OneOpenAir.ino
Normal file
File diff suppressed because it is too large
Load Diff
240
examples/OneOpenAir/OpenMetrics.cpp
Normal file
240
examples/OneOpenAir/OpenMetrics.cpp
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
#include "OpenMetrics.h"
|
||||||
|
|
||||||
|
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
|
||||||
|
WifiConnector &wifiConnector, AgApiClient &apiClient)
|
||||||
|
: measure(measure), config(config), wifiConnector(wifiConnector),
|
||||||
|
apiClient(apiClient) {}
|
||||||
|
|
||||||
|
OpenMetrics::~OpenMetrics() {}
|
||||||
|
|
||||||
|
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
const char *OpenMetrics::getApiContentType(void) {
|
||||||
|
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *OpenMetrics::getApi(void) { return "/metrics"; }
|
||||||
|
|
||||||
|
String OpenMetrics::getPayload(void) {
|
||||||
|
String response;
|
||||||
|
String current_metric_name;
|
||||||
|
const auto add_metric = [&](const String &name, const String &help,
|
||||||
|
const String &type, const String &unit = "") {
|
||||||
|
current_metric_name = "airgradient_" + name;
|
||||||
|
if (!unit.isEmpty())
|
||||||
|
current_metric_name += "_" + unit;
|
||||||
|
response += "# HELP " + current_metric_name + " " + help + "\n";
|
||||||
|
response += "# TYPE " + current_metric_name + " " + type + "\n";
|
||||||
|
if (!unit.isEmpty())
|
||||||
|
response += "# UNIT " + current_metric_name + " " + unit + "\n";
|
||||||
|
};
|
||||||
|
const auto add_metric_point = [&](const String &labels, const String &value) {
|
||||||
|
response += current_metric_name + "{" + labels + "} " + value + "\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
add_metric("info", "AirGradient device information", "info");
|
||||||
|
add_metric_point("airgradient_serial_number=\"" + ag->deviceId() +
|
||||||
|
"\",airgradient_device_type=\"" + ag->getBoardName() +
|
||||||
|
"\",airgradient_library_version=\"" + ag->getVersion() +
|
||||||
|
"\"",
|
||||||
|
"1");
|
||||||
|
|
||||||
|
add_metric("config_ok",
|
||||||
|
"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(
|
||||||
|
"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(
|
||||||
|
"wifi_rssi",
|
||||||
|
"WiFi signal strength from the AirGradient device perspective, in dBm",
|
||||||
|
"gauge", "dbm");
|
||||||
|
add_metric_point("", String(wifiConnector.RSSI()));
|
||||||
|
|
||||||
|
// Initialize default invalid value for each measurements
|
||||||
|
float _temp = utils::getInvalidTemperature();
|
||||||
|
float _hum = utils::getInvalidHumidity();
|
||||||
|
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 tvoc = utils::getInvalidVOC();
|
||||||
|
int tvocRaw = utils::getInvalidVOC();
|
||||||
|
int nox = utils::getInvalidNOx();
|
||||||
|
int noxRaw = utils::getInvalidNOx();
|
||||||
|
|
||||||
|
// Get values
|
||||||
|
if (config.hasSensorPMS1 && config.hasSensorPMS2) {
|
||||||
|
_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;
|
||||||
|
pm25 = (measure.get(Measurements::PM25, 1) + measure.get(Measurements::PM25, 2)) / 2.0f;
|
||||||
|
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.getFloat(Measurements::Temperature);
|
||||||
|
_hum = measure.getFloat(Measurements::Humidity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasSensorPMS1) {
|
||||||
|
pm01 = measure.get(Measurements::PM01);
|
||||||
|
pm25 = measure.get(Measurements::PM25);
|
||||||
|
pm10 = measure.get(Measurements::PM10);
|
||||||
|
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (config.hasSensorPMS1) {
|
||||||
|
_temp = measure.getFloat(Measurements::Temperature, 1);
|
||||||
|
_hum = measure.getFloat(Measurements::Humidity, 1);
|
||||||
|
pm01 = measure.get(Measurements::PM01, 1);
|
||||||
|
pm25 = measure.get(Measurements::PM25, 1);
|
||||||
|
pm10 = measure.get(Measurements::PM10, 1);
|
||||||
|
pm03PCount = measure.get(Measurements::PM03_PC, 1);
|
||||||
|
}
|
||||||
|
if (config.hasSensorPMS2) {
|
||||||
|
_temp = measure.getFloat(Measurements::Temperature, 2);
|
||||||
|
_hum = measure.getFloat(Measurements::Humidity, 2);
|
||||||
|
pm01 = measure.get(Measurements::PM01, 2);
|
||||||
|
pm25 = measure.get(Measurements::PM25, 2);
|
||||||
|
pm10 = measure.get(Measurements::PM10, 2);
|
||||||
|
pm03PCount = measure.get(Measurements::PM03_PC, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = _temp;
|
||||||
|
ahumCompensated = _hum;
|
||||||
|
} else {
|
||||||
|
atmpCompensated = ag->pms5003t_1.compensateTemp(_temp);
|
||||||
|
ahumCompensated = ag->pms5003t_1.compensateHum(_hum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add measurements that valid to the metrics
|
||||||
|
if (config.hasSensorPMS1 || config.hasSensorPMS2) {
|
||||||
|
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::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::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::isValidPm03Count(pm03PCount)) {
|
||||||
|
add_metric("pm0d3",
|
||||||
|
"PM0.3 concentration as measured by the AirGradient PMS "
|
||||||
|
"sensor, in number of particules per 100 milliliters",
|
||||||
|
"gauge", "p100ml");
|
||||||
|
add_metric_point("", String(pm03PCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasSensorSGP) {
|
||||||
|
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(tvoc));
|
||||||
|
}
|
||||||
|
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(tvocRaw));
|
||||||
|
}
|
||||||
|
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(nox));
|
||||||
|
}
|
||||||
|
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(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 "
|
||||||
|
"sensor, in degrees Celsius",
|
||||||
|
"gauge", "celsius");
|
||||||
|
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_point("", String(atmpCompensated));
|
||||||
|
}
|
||||||
|
if (utils::isValidHumidity(_hum)) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
response += "# EOF\n";
|
||||||
|
return response;
|
||||||
|
}
|
28
examples/OneOpenAir/OpenMetrics.h
Normal file
28
examples/OneOpenAir/OpenMetrics.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#ifndef _OPEN_METRICS_H_
|
||||||
|
#define _OPEN_METRICS_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "AgApiClient.h"
|
||||||
|
|
||||||
|
class OpenMetrics {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
Measurements &measure;
|
||||||
|
Configuration &config;
|
||||||
|
WifiConnector &wifiConnector;
|
||||||
|
AgApiClient &apiClient;
|
||||||
|
|
||||||
|
public:
|
||||||
|
OpenMetrics(Measurements &measure, Configuration &conig,
|
||||||
|
WifiConnector &wifiConnector, AgApiClient& apiClient);
|
||||||
|
~OpenMetrics();
|
||||||
|
void setAirGradient(AirGradient *ag);
|
||||||
|
const char *getApiContentType(void);
|
||||||
|
const char* getApi(void);
|
||||||
|
String getPayload(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _OPEN_METRICS_H_ */
|
206
examples/OneOpenAir/OtaHandler.h
Normal file
206
examples/OneOpenAir/OtaHandler.h
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
#ifndef _OTA_HANDLER_H_
|
||||||
|
#define _OTA_HANDLER_H_
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <esp_err.h>
|
||||||
|
#include <esp_http_client.h>
|
||||||
|
#include <esp_ota_ops.h>
|
||||||
|
|
||||||
|
#define OTA_BUF_SIZE 1024
|
||||||
|
#define URL_BUF_SIZE 256
|
||||||
|
|
||||||
|
enum OtaUpdateOutcome {
|
||||||
|
UPDATE_PERFORMED,
|
||||||
|
ALREADY_UP_TO_DATE,
|
||||||
|
UPDATE_FAILED,
|
||||||
|
UDPATE_SKIPPED
|
||||||
|
};
|
||||||
|
|
||||||
|
enum OtaState {
|
||||||
|
OTA_STATE_BEGIN,
|
||||||
|
OTA_STATE_FAIL,
|
||||||
|
OTA_STATE_SKIP,
|
||||||
|
OTA_STATE_UP_TO_DATE,
|
||||||
|
OTA_STATE_PROCESSING,
|
||||||
|
OTA_STATE_SUCCESS
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef void(*OtaHandlerCallback_t)(OtaState state,
|
||||||
|
String message);
|
||||||
|
|
||||||
|
class OtaHandler {
|
||||||
|
public:
|
||||||
|
void updateFirmwareIfOutdated(String deviceId) {
|
||||||
|
String url = "http://hw.airgradient.com/sensors/airgradient:" + deviceId +
|
||||||
|
"/generic/os/firmware.bin";
|
||||||
|
url += "?current_firmware=";
|
||||||
|
url += GIT_VERSION;
|
||||||
|
char urlAsChar[URL_BUF_SIZE];
|
||||||
|
url.toCharArray(urlAsChar, URL_BUF_SIZE);
|
||||||
|
Serial.printf("checking for new OTA update @ %s\n", urlAsChar);
|
||||||
|
|
||||||
|
esp_http_client_config_t config = {};
|
||||||
|
config.url = urlAsChar;
|
||||||
|
OtaUpdateOutcome ret = attemptToPerformOta(&config);
|
||||||
|
Serial.println(ret);
|
||||||
|
if (this->callback) {
|
||||||
|
switch (ret) {
|
||||||
|
case OtaUpdateOutcome::UPDATE_PERFORMED:
|
||||||
|
this->callback(OtaState::OTA_STATE_SUCCESS, "");
|
||||||
|
break;
|
||||||
|
case OtaUpdateOutcome::UDPATE_SKIPPED:
|
||||||
|
this->callback(OtaState::OTA_STATE_SKIP, "");
|
||||||
|
break;
|
||||||
|
case OtaUpdateOutcome::ALREADY_UP_TO_DATE:
|
||||||
|
this->callback(OtaState::OTA_STATE_UP_TO_DATE, "");
|
||||||
|
break;
|
||||||
|
case OtaUpdateOutcome::UPDATE_FAILED:
|
||||||
|
this->callback(OtaState::OTA_STATE_FAIL, "");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHandlerCallback(OtaHandlerCallback_t callback) {
|
||||||
|
this->callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
OtaHandlerCallback_t callback;
|
||||||
|
|
||||||
|
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config) {
|
||||||
|
esp_http_client_handle_t client = esp_http_client_init(config);
|
||||||
|
if (client == NULL) {
|
||||||
|
Serial.println("Failed to initialize HTTP connection");
|
||||||
|
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = esp_http_client_open(client, 0);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
Serial.printf("Failed to open HTTP connection: %s\n",
|
||||||
|
esp_err_to_name(err));
|
||||||
|
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||||
|
}
|
||||||
|
esp_http_client_fetch_headers(client);
|
||||||
|
|
||||||
|
int httpStatusCode = esp_http_client_get_status_code(client);
|
||||||
|
if (httpStatusCode == 304) {
|
||||||
|
Serial.println("Firmware is already up to date");
|
||||||
|
cleanupHttp(client);
|
||||||
|
return OtaUpdateOutcome::ALREADY_UP_TO_DATE;
|
||||||
|
} else if (httpStatusCode != 200) {
|
||||||
|
Serial.printf("Firmware update skipped, the server returned %d\n",
|
||||||
|
httpStatusCode);
|
||||||
|
cleanupHttp(client);
|
||||||
|
return OtaUpdateOutcome::UDPATE_SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_ota_handle_t update_handle = 0;
|
||||||
|
const esp_partition_t *update_partition = NULL;
|
||||||
|
Serial.println("Starting OTA update ...");
|
||||||
|
update_partition = esp_ota_get_next_update_partition(NULL);
|
||||||
|
if (update_partition == NULL) {
|
||||||
|
Serial.println("Passive OTA partition not found");
|
||||||
|
cleanupHttp(client);
|
||||||
|
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||||
|
}
|
||||||
|
Serial.printf("Writing to partition subtype %d at offset 0x%x\n",
|
||||||
|
update_partition->subtype, update_partition->address);
|
||||||
|
|
||||||
|
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
Serial.printf("esp_ota_begin failed, error=%d\n", err);
|
||||||
|
cleanupHttp(client);
|
||||||
|
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ota_write_err = ESP_OK;
|
||||||
|
char *upgrade_data_buf = (char *)malloc(OTA_BUF_SIZE);
|
||||||
|
if (!upgrade_data_buf) {
|
||||||
|
Serial.println("Couldn't allocate memory for data buffer");
|
||||||
|
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
int binary_file_len = 0;
|
||||||
|
int totalSize = esp_http_client_get_content_length(client);
|
||||||
|
Serial.println("File size: " + String(totalSize) + String(" bytes"));
|
||||||
|
|
||||||
|
// Show display start update new firmware.
|
||||||
|
if (this->callback) {
|
||||||
|
this->callback(OtaState::OTA_STATE_BEGIN, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download file and write new firmware to OTA partition
|
||||||
|
uint32_t lastUpdate = millis();
|
||||||
|
while (1) {
|
||||||
|
int data_read =
|
||||||
|
esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
|
||||||
|
if (data_read == 0) {
|
||||||
|
if (this->callback) {
|
||||||
|
this->callback(OtaState::OTA_STATE_PROCESSING, String(100));
|
||||||
|
}
|
||||||
|
Serial.println("Connection closed, all data received");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (data_read < 0) {
|
||||||
|
Serial.println("Data read error");
|
||||||
|
if (this->callback) {
|
||||||
|
this->callback(OtaState::OTA_STATE_FAIL, "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (data_read > 0) {
|
||||||
|
ota_write_err = esp_ota_write(
|
||||||
|
update_handle, (const void *)upgrade_data_buf, data_read);
|
||||||
|
if (ota_write_err != ESP_OK) {
|
||||||
|
if (this->callback) {
|
||||||
|
this->callback(OtaState::OTA_STATE_FAIL, "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
binary_file_len += data_read;
|
||||||
|
|
||||||
|
int percent = (binary_file_len * 100) / totalSize;
|
||||||
|
uint32_t ms = (uint32_t)(millis() - lastUpdate);
|
||||||
|
if (ms >= 250) {
|
||||||
|
// sm.executeOTA(StateMachine::OtaState::OTA_STATE_PROCESSING, "",
|
||||||
|
// percent);
|
||||||
|
if (this->callback) {
|
||||||
|
this->callback(OtaState::OTA_STATE_PROCESSING,
|
||||||
|
String(percent));
|
||||||
|
}
|
||||||
|
lastUpdate = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(upgrade_data_buf);
|
||||||
|
cleanupHttp(client);
|
||||||
|
Serial.printf("# of bytes written: %d\n", binary_file_len);
|
||||||
|
|
||||||
|
esp_err_t ota_end_err = esp_ota_end(update_handle);
|
||||||
|
if (ota_write_err != ESP_OK) {
|
||||||
|
Serial.printf("Error: esp_ota_write failed! err=0x%d\n", err);
|
||||||
|
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||||
|
} else if (ota_end_err != ESP_OK) {
|
||||||
|
Serial.printf("Error: esp_ota_end failed! err=0x%d. Image is invalid",
|
||||||
|
ota_end_err);
|
||||||
|
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_ota_set_boot_partition(update_partition);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
Serial.printf("esp_ota_set_boot_partition failed! err=0x%d\n", err);
|
||||||
|
return OtaUpdateOutcome::UPDATE_FAILED;
|
||||||
|
}
|
||||||
|
return OtaUpdateOutcome::UPDATE_PERFORMED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanupHttp(esp_http_client_handle_t client) {
|
||||||
|
esp_http_client_close(client);
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
@ -1,974 +0,0 @@
|
|||||||
/*
|
|
||||||
This is the code for the AirGradient Open Air open-source hardware outdoor Air
|
|
||||||
Quality Monitor with an ESP32-C3 Microcontroller.
|
|
||||||
|
|
||||||
It is an air quality monitor for PM2.5, CO2, TVOCs, NOx, Temperature and
|
|
||||||
Humidity and can send data over Wifi.
|
|
||||||
|
|
||||||
Open source air quality monitors and kits are available:
|
|
||||||
Indoor Monitor: https://www.airgradient.com/indoor/
|
|
||||||
Outdoor Monitor: https://www.airgradient.com/outdoor/
|
|
||||||
|
|
||||||
Build Instructions:
|
|
||||||
https://www.airgradient.com/documentation/open-air-pst-kit-1-3/
|
|
||||||
|
|
||||||
The codes needs the following libraries installed:
|
|
||||||
“WifiManager by tzapu, tablatronix” tested with version 2.0.16-rc.2
|
|
||||||
"Arduino_JSON" by Arduino Version 0.2.0
|
|
||||||
|
|
||||||
Please make sure you have esp32 board manager installed. Tested with
|
|
||||||
version 2.0.11.
|
|
||||||
|
|
||||||
Important flashing settings:
|
|
||||||
- Set board to "ESP32C3 Dev Module"
|
|
||||||
- Enable "USB CDC On Boot"
|
|
||||||
- Flash frequency "80Mhz"
|
|
||||||
- Flash mode "QIO"
|
|
||||||
- Flash size "4MB"
|
|
||||||
- Partition scheme "Default 4MB with spiffs (1.2MB APP/1,5MB SPIFFS)"
|
|
||||||
- JTAG adapter "Disabled"
|
|
||||||
|
|
||||||
If you have any questions please visit our forum at
|
|
||||||
https://forum.airgradient.com/
|
|
||||||
|
|
||||||
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AirGradient.h>
|
|
||||||
#include <Arduino_JSON.h>
|
|
||||||
#include <HTTPClient.h>
|
|
||||||
#include <HardwareSerial.h>
|
|
||||||
#include <WiFiManager.h>
|
|
||||||
#include <Wire.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @brief Application state machine state
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
enum {
|
|
||||||
APP_SM_WIFI_MANAGER_MODE, /** In WiFi Manger Mode */
|
|
||||||
APP_SM_WIFI_MAMAGER_PORTAL_ACTIVE, /** WiFi Manager has connected to mobile
|
|
||||||
phone */
|
|
||||||
APP_SM_WIFI_MANAGER_STA_CONNECTING, /** After SSID and PW entered and OK
|
|
||||||
clicked, connection to WiFI network is
|
|
||||||
attempted*/
|
|
||||||
APP_SM_WIFI_MANAGER_STA_CONNECTED, /** Connecting to WiFi worked */
|
|
||||||
APP_SM_WIFI_OK_SERVER_CONNECTING, /** Once connected to WiFi an attempt to
|
|
||||||
reach the server is performed */
|
|
||||||
APP_SM_WIFI_OK_SERVER_CONNNECTED, /** Server is reachable, all fine */
|
|
||||||
/** Exceptions during WIFi Setup */
|
|
||||||
APP_SM_WIFI_MANAGER_CONNECT_FAILED, /** Cannot connect to WiFi (e.g. wrong
|
|
||||||
password, WPA Enterprise etc.) */
|
|
||||||
APP_SM_WIFI_OK_SERVER_CONNECT_FAILED, /** Connected to WiFi but server not
|
|
||||||
reachable, e.g. firewall block/
|
|
||||||
whitelisting needed etc. */
|
|
||||||
APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED, /** Server reachable but sensor
|
|
||||||
not configured correctly*/
|
|
||||||
|
|
||||||
/** During Normal Operation */
|
|
||||||
APP_SM_WIFI_LOST, /** Connection to WiFi network failed credentials incorrect
|
|
||||||
encryption not supported etc. */
|
|
||||||
APP_SM_SERVER_LOST, /** Connected to WiFi network but the server cannot be
|
|
||||||
reached through the internet, e.g. blocked by firewall
|
|
||||||
*/
|
|
||||||
APP_SM_SENSOR_CONFIG_FAILED, /** Server is reachable but there is some
|
|
||||||
configuration issue to be fixed on the server
|
|
||||||
side */
|
|
||||||
APP_SM_NORMAL,
|
|
||||||
};
|
|
||||||
|
|
||||||
#define LED_FAST_BLINK_DELAY 250 /** ms */
|
|
||||||
#define LED_SLOW_BLINK_DELAY 1000 /** ms */
|
|
||||||
#define WIFI_CONNECT_COUNTDOWN_MAX 180 /** sec */
|
|
||||||
#define WIFI_CONNECT_RETRY_MS 10000 /** ms */
|
|
||||||
#define LED_BAR_COUNT_INIT_VALUE (-1) /** */
|
|
||||||
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
|
|
||||||
#define DISP_UPDATE_INTERVAL 5000 /** ms */
|
|
||||||
#define SERVER_CONFIG_UPDATE_INTERVAL 30000 /** ms */
|
|
||||||
#define SERVER_SYNC_INTERVAL 60000 /** ms */
|
|
||||||
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
|
|
||||||
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
|
||||||
#define SENSOR_CO2_UPDATE_INTERVAL 5000 /** ms */
|
|
||||||
#define SENSOR_PM_UPDATE_INTERVAL 5000 /** ms */
|
|
||||||
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 5000 /** ms */
|
|
||||||
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
|
||||||
#define WIFI_HOTSPOT_PASSWORD_DEFAULT \
|
|
||||||
"cleanair" /** default WiFi AP password \
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Use use LED bar state
|
|
||||||
*/
|
|
||||||
typedef enum {
|
|
||||||
UseLedBarOff, /** Don't use LED bar */
|
|
||||||
UseLedBarPM, /** Use LED bar for PMS */
|
|
||||||
UseLedBarCO2, /** Use LED bar for CO2 */
|
|
||||||
} UseLedBar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Schedule handle with timing period
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class AgSchedule {
|
|
||||||
public:
|
|
||||||
AgSchedule(int period, void (*handler)(void))
|
|
||||||
: period(period), handler(handler) {}
|
|
||||||
void run(void) {
|
|
||||||
uint32_t ms = (uint32_t)(millis() - count);
|
|
||||||
if (ms >= period) {
|
|
||||||
/** Call handler */
|
|
||||||
handler();
|
|
||||||
|
|
||||||
Serial.printf("[AgSchedule] handle 0x%08x, period: %d(ms)\r\n",
|
|
||||||
(unsigned int)handler, period);
|
|
||||||
|
|
||||||
/** Update period time */
|
|
||||||
count = millis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void setPeriod(int period) { this->period = period; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void (*handler)(void);
|
|
||||||
int period;
|
|
||||||
int count;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief AirGradient server configuration and sync data
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class AgServer {
|
|
||||||
public:
|
|
||||||
void begin(void) {
|
|
||||||
inF = false;
|
|
||||||
inUSAQI = false;
|
|
||||||
configFailed = false;
|
|
||||||
serverFailed = false;
|
|
||||||
memset(models, 0, sizeof(models));
|
|
||||||
memset(mqttBroker, 0, sizeof(mqttBroker));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get server configuration
|
|
||||||
*
|
|
||||||
* @param id Device ID
|
|
||||||
* @return true Success
|
|
||||||
* @return false Failure
|
|
||||||
*/
|
|
||||||
bool pollServerConfig(String id) {
|
|
||||||
String uri =
|
|
||||||
"http://hw.airgradient.com/sensors/airgradient:" + id + "/one/config";
|
|
||||||
|
|
||||||
/** Init http client */
|
|
||||||
HTTPClient client;
|
|
||||||
if (client.begin(uri) == false) {
|
|
||||||
configFailed = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get */
|
|
||||||
int retCode = client.GET();
|
|
||||||
if (retCode != 200) {
|
|
||||||
client.end();
|
|
||||||
configFailed = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** clear failed */
|
|
||||||
configFailed = false;
|
|
||||||
|
|
||||||
/** Get response string */
|
|
||||||
String respContent = client.getString();
|
|
||||||
client.end();
|
|
||||||
Serial.println("Get server config: " + respContent);
|
|
||||||
|
|
||||||
/** Parse JSON */
|
|
||||||
JSONVar root = JSON.parse(respContent);
|
|
||||||
if (JSON.typeof(root) == "undefined") {
|
|
||||||
/** JSON invalid */
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "country" */
|
|
||||||
if (JSON.typeof_(root["country"]) == "string") {
|
|
||||||
String country = root["country"];
|
|
||||||
if (country == "US") {
|
|
||||||
inF = true;
|
|
||||||
} else {
|
|
||||||
inF = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "pmStandard" */
|
|
||||||
if (JSON.typeof_(root["pmStandard"]) == "string") {
|
|
||||||
String standard = root["pmStandard"];
|
|
||||||
if (standard == "ugm3") {
|
|
||||||
inUSAQI = false;
|
|
||||||
} else {
|
|
||||||
inUSAQI = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "co2CalibrationRequested" */
|
|
||||||
if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") {
|
|
||||||
co2Calib = root["co2CalibrationRequested"];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "ledBarMode" */
|
|
||||||
if (JSON.typeof_(root["ledBarMode"]) == "string") {
|
|
||||||
String mode = root["ledBarMode"];
|
|
||||||
if (mode == "co2") {
|
|
||||||
ledBarMode = UseLedBarCO2;
|
|
||||||
} else if (mode == "pm") {
|
|
||||||
ledBarMode = UseLedBarPM;
|
|
||||||
} else if (mode == "off") {
|
|
||||||
ledBarMode = UseLedBarOff;
|
|
||||||
} else {
|
|
||||||
ledBarMode = UseLedBarOff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get model */
|
|
||||||
if (JSON.typeof_(root["model"]) == "string") {
|
|
||||||
String model = root["model"];
|
|
||||||
if (model.length()) {
|
|
||||||
int len =
|
|
||||||
model.length() < sizeof(models) ? model.length() : sizeof(models);
|
|
||||||
memset(models, 0, sizeof(models));
|
|
||||||
memcpy(models, model.c_str(), len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get "mqttBrokerUrl" */
|
|
||||||
if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") {
|
|
||||||
String mqtt = root["mqttBrokerUrl"];
|
|
||||||
if (mqtt.length()) {
|
|
||||||
int len = mqtt.length() < sizeof(mqttBroker) ? mqtt.length()
|
|
||||||
: sizeof(mqttBroker);
|
|
||||||
memset(mqttBroker, 0, sizeof(mqttBroker));
|
|
||||||
memcpy(mqttBroker, mqtt.c_str(), len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get 'abcDays' */
|
|
||||||
if (JSON.typeof_(root["abcDays"]) == "number") {
|
|
||||||
co2AbcCalib = root["abcDays"];
|
|
||||||
} else {
|
|
||||||
co2AbcCalib = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show configuration */
|
|
||||||
showServerConfig();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool postToServer(String id, String payload) {
|
|
||||||
/**
|
|
||||||
* @brief Only post data if WiFi is connected
|
|
||||||
*/
|
|
||||||
if (WiFi.isConnected() == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("Post payload: %s\r\n", payload.c_str());
|
|
||||||
|
|
||||||
String uri =
|
|
||||||
"http://hw.airgradient.com/sensors/airgradient:" + id + "/measures";
|
|
||||||
|
|
||||||
WiFiClient wifiClient;
|
|
||||||
HTTPClient client;
|
|
||||||
if (client.begin(wifiClient, uri.c_str()) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
client.addHeader("content-type", "application/json");
|
|
||||||
int retCode = client.POST(payload);
|
|
||||||
client.end();
|
|
||||||
|
|
||||||
if ((retCode == 200) || (retCode == 429)) {
|
|
||||||
serverFailed = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
serverFailed = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get temperature configuration unit
|
|
||||||
*
|
|
||||||
* @return true F unit
|
|
||||||
* @return false C Unit
|
|
||||||
*/
|
|
||||||
bool isTemperatureUnitF(void) { return inF; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get PMS standard unit
|
|
||||||
*
|
|
||||||
* @return true USAQI
|
|
||||||
* @return false ugm3
|
|
||||||
*/
|
|
||||||
bool isPMSinUSAQI(void) { return inUSAQI; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get status of get server coniguration is failed
|
|
||||||
*
|
|
||||||
* @return true Failed
|
|
||||||
* @return false Success
|
|
||||||
*/
|
|
||||||
bool isConfigFailed(void) { return configFailed; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get status of post server configuration is failed
|
|
||||||
*
|
|
||||||
* @return true Failed
|
|
||||||
* @return false Success
|
|
||||||
*/
|
|
||||||
bool isServerFailed(void) { return serverFailed; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get request calibration CO2
|
|
||||||
*
|
|
||||||
* @return true Requested. If result = true, it's clear after function call
|
|
||||||
* @return false Not-requested
|
|
||||||
*/
|
|
||||||
bool isCo2Calib(void) {
|
|
||||||
bool ret = co2Calib;
|
|
||||||
if (ret) {
|
|
||||||
co2Calib = false;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the Co2 auto calib period
|
|
||||||
*
|
|
||||||
* @return int days, -1 if invalid.
|
|
||||||
*/
|
|
||||||
int getCo2Abccalib(void) { return co2AbcCalib; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get device configuration model name
|
|
||||||
*
|
|
||||||
* @return String Model name, empty string if server failed
|
|
||||||
*/
|
|
||||||
String getModelName(void) { return String(models); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get mqttBroker url
|
|
||||||
*
|
|
||||||
* @return String Broker url, empty if server failed
|
|
||||||
*/
|
|
||||||
String getMqttBroker(void) { return String(mqttBroker); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show server configuration parameter
|
|
||||||
*/
|
|
||||||
void showServerConfig(void) {
|
|
||||||
Serial.println("Server configuration: ");
|
|
||||||
Serial.printf(" inF: %s\r\n", inF ? "true" : "false");
|
|
||||||
Serial.printf(" inUSAQI: %s\r\n", inUSAQI ? "true" : "false");
|
|
||||||
Serial.printf(" useRGBLedBar: %d\r\n", (int)ledBarMode);
|
|
||||||
Serial.printf(" Model: %s\r\n", models);
|
|
||||||
Serial.printf(" Mqtt Broker: %s\r\n", mqttBroker);
|
|
||||||
Serial.printf(" S8 calib period: %d\r\n", co2AbcCalib);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get server config led bar mode
|
|
||||||
*
|
|
||||||
* @return UseLedBar
|
|
||||||
*/
|
|
||||||
UseLedBar getLedBarMode(void) { return ledBarMode; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool inF; /** Temperature unit, true: F, false: C */
|
|
||||||
bool inUSAQI; /** PMS unit, true: USAQI, false: ugm3 */
|
|
||||||
bool configFailed; /** Flag indicate get server configuration failed */
|
|
||||||
bool serverFailed; /** Flag indicate post data to server failed */
|
|
||||||
bool co2Calib; /** Is co2Ppmcalibration requset */
|
|
||||||
int co2AbcCalib = -1; /** update auto calibration number of day */
|
|
||||||
UseLedBar ledBarMode = UseLedBarCO2; /** */
|
|
||||||
char models[20]; /** */
|
|
||||||
char mqttBroker[256]; /** */
|
|
||||||
};
|
|
||||||
AgServer agServer;
|
|
||||||
|
|
||||||
/** Create airgradient instance for 'OPEN_AIR_OUTDOOR' board */
|
|
||||||
AirGradient ag(OPEN_AIR_OUTDOOR);
|
|
||||||
|
|
||||||
static int ledSmState = APP_SM_NORMAL;
|
|
||||||
|
|
||||||
int loopCount = 0;
|
|
||||||
|
|
||||||
WiFiManager wifiManager; /** wifi manager instance */
|
|
||||||
static bool wifiHasConfig = false;
|
|
||||||
static String wifiSSID = "";
|
|
||||||
|
|
||||||
int tvocIndex = -1;
|
|
||||||
int noxIndex = -1;
|
|
||||||
int co2Ppm = 0;
|
|
||||||
|
|
||||||
int pm25_1 = -1;
|
|
||||||
int pm01_1 = -1;
|
|
||||||
int pm10_1 = -1;
|
|
||||||
int pm03PCount_1 = -1;
|
|
||||||
float temp_1;
|
|
||||||
int hum_1;
|
|
||||||
|
|
||||||
int pm25_2 = -1;
|
|
||||||
int pm01_2 = -1;
|
|
||||||
int pm10_2 = -1;
|
|
||||||
int pm03PCount_2 = -1;
|
|
||||||
float temp_2;
|
|
||||||
int hum_2;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
enum {
|
|
||||||
FW_MODE_PST, /** PMS5003T, S8 and SGP41 */
|
|
||||||
FW_MODE_PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
|
|
||||||
FW_MODE_PP /** PMS5003T_1, PMS5003T_2 */
|
|
||||||
};
|
|
||||||
int fw_mode = FW_MODE_PST;
|
|
||||||
|
|
||||||
void boardInit(void);
|
|
||||||
void failedHandler(String msg);
|
|
||||||
void co2Calibration(void);
|
|
||||||
static String getDevId(void);
|
|
||||||
static void updateWiFiConnect(void);
|
|
||||||
static void tvocPoll(void);
|
|
||||||
static void pmPoll(void);
|
|
||||||
static void sendDataToServer(void);
|
|
||||||
static void co2Poll(void);
|
|
||||||
static void serverConfigPoll(void);
|
|
||||||
static const char *getFwMode(int mode);
|
|
||||||
|
|
||||||
AgSchedule configSchedule(SERVER_CONFIG_UPDATE_INTERVAL, serverConfigPoll);
|
|
||||||
AgSchedule serverSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
|
|
||||||
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Poll);
|
|
||||||
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, pmPoll);
|
|
||||||
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, tvocPoll);
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
Serial.begin(115200);
|
|
||||||
|
|
||||||
/** Board init */
|
|
||||||
boardInit();
|
|
||||||
|
|
||||||
/** Server init */
|
|
||||||
agServer.begin();
|
|
||||||
|
|
||||||
/** WiFi connect */
|
|
||||||
connectToWifi();
|
|
||||||
|
|
||||||
if (WiFi.isConnected()) {
|
|
||||||
wifiHasConfig = true;
|
|
||||||
sendPing();
|
|
||||||
|
|
||||||
agServer.pollServerConfig(getDevId());
|
|
||||||
if (agServer.isConfigFailed()) {
|
|
||||||
ledSmHandler(APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED);
|
|
||||||
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ledSmHandler(APP_SM_NORMAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
configSchedule.run();
|
|
||||||
serverSchedule.run();
|
|
||||||
if (fw_mode == FW_MODE_PST) {
|
|
||||||
co2Schedule.run();
|
|
||||||
}
|
|
||||||
pmsSchedule.run();
|
|
||||||
if (fw_mode == FW_MODE_PST || fw_mode == FW_MODE_PPT) {
|
|
||||||
tvocSchedule.run();
|
|
||||||
}
|
|
||||||
updateWiFiConnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendPing() {
|
|
||||||
JSONVar root;
|
|
||||||
root["wifi"] = WiFi.RSSI();
|
|
||||||
root["boot"] = loopCount;
|
|
||||||
if (agServer.postToServer(getDevId(), JSON.stringify(root))) {
|
|
||||||
ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNNECTED);
|
|
||||||
} else {
|
|
||||||
ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNECT_FAILED);
|
|
||||||
}
|
|
||||||
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void sendDataToServer(void) {
|
|
||||||
JSONVar root;
|
|
||||||
root["wifi"] = WiFi.RSSI();
|
|
||||||
root["boot"] = loopCount;
|
|
||||||
if (fw_mode == FW_MODE_PST) {
|
|
||||||
if (co2Ppm >= 0) {
|
|
||||||
root["rco2"] = co2Ppm;
|
|
||||||
}
|
|
||||||
if (pm01_1 >= 0) {
|
|
||||||
root["pm01"] = pm01_1;
|
|
||||||
}
|
|
||||||
if (pm25_1 >= 0) {
|
|
||||||
root["pm02"] = pm25_1;
|
|
||||||
}
|
|
||||||
if (pm10_1 >= 0) {
|
|
||||||
root["pm10"] = pm10_1;
|
|
||||||
}
|
|
||||||
if (pm03PCount_1 >= 0) {
|
|
||||||
root["pm003_count"] = pm03PCount_1;
|
|
||||||
}
|
|
||||||
if (tvocIndex >= 0) {
|
|
||||||
root["tvoc_index"] = tvocIndex;
|
|
||||||
}
|
|
||||||
if (noxIndex >= 0) {
|
|
||||||
root["noxIndex"] = noxIndex;
|
|
||||||
}
|
|
||||||
if (temp_1 >= 0) {
|
|
||||||
root["atmp"] = temp_1;
|
|
||||||
}
|
|
||||||
if (hum_1 >= 0) {
|
|
||||||
root["rhum"] = hum_1;
|
|
||||||
}
|
|
||||||
} else if (fw_mode == FW_MODE_PPT) {
|
|
||||||
if (tvocIndex > 0) {
|
|
||||||
root["tvoc_index"] = loopCount;
|
|
||||||
}
|
|
||||||
if (noxIndex > 0) {
|
|
||||||
root["nox_index"] = loopCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fw_mode == FW_MODE_PP || FW_MODE_PPT) {
|
|
||||||
root["pm01"] = (int)((pm01_1 + pm01_2) / 2);
|
|
||||||
root["pm02"] = (int)((pm25_1 + pm25_2) / 2);
|
|
||||||
root["pm003_count"] = (int)((pm03PCount_1 + pm03PCount_2) / 2);
|
|
||||||
root["atmp"] = (int)((temp_1 + temp_2) / 2);
|
|
||||||
root["rhum"] = (int)((hum_1 + hum_2) / 2);
|
|
||||||
root["channels"]["1"]["pm01"] = pm01_1;
|
|
||||||
root["channels"]["1"]["pm02"] = pm25_1;
|
|
||||||
root["channels"]["1"]["pm10"] = pm10_1;
|
|
||||||
root["channels"]["1"]["pm003_count"] = pm03PCount_1;
|
|
||||||
root["channels"]["1"]["atmp"] = temp_1;
|
|
||||||
root["channels"]["1"]["rhum"] = hum_1;
|
|
||||||
root["channels"]["2"]["pm01"] = pm01_2;
|
|
||||||
root["channels"]["2"]["pm02"] = pm25_2;
|
|
||||||
root["channels"]["2"]["pm10"] = pm10_2;
|
|
||||||
root["channels"]["2"]["pm003_count"] = pm03PCount_2;
|
|
||||||
root["channels"]["2"]["atmp"] = temp_2;
|
|
||||||
root["channels"]["2"]["rhum"] = hum_2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Send data to sensor */
|
|
||||||
if (agServer.postToServer(getDevId(), JSON.stringify(root))) {
|
|
||||||
resetWatchdog();
|
|
||||||
}
|
|
||||||
loopCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetWatchdog() {
|
|
||||||
Serial.println("Watchdog reset");
|
|
||||||
ag.watchdog.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool wifiMangerClientConnected(void) {
|
|
||||||
return WiFi.softAPgetStationNum() ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wifi Manager
|
|
||||||
void connectToWifi() {
|
|
||||||
wifiSSID = "airgradient-" + String(getNormalizedMac());
|
|
||||||
|
|
||||||
wifiManager.setConfigPortalBlocking(false);
|
|
||||||
wifiManager.setTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
|
||||||
|
|
||||||
wifiManager.setAPCallback([](WiFiManager *obj) {
|
|
||||||
/** This callback if wifi connnected failed and try to start configuration
|
|
||||||
* portal */
|
|
||||||
ledSmState = APP_SM_WIFI_MANAGER_MODE;
|
|
||||||
});
|
|
||||||
wifiManager.setSaveConfigCallback([]() {
|
|
||||||
/** Wifi connected save the configuration */
|
|
||||||
ledSmHandler(APP_SM_WIFI_MANAGER_STA_CONNECTED);
|
|
||||||
});
|
|
||||||
wifiManager.setSaveParamsCallback([]() {
|
|
||||||
/** Wifi set connect: ssid, password */
|
|
||||||
ledSmHandler(APP_SM_WIFI_MANAGER_STA_CONNECTING);
|
|
||||||
});
|
|
||||||
wifiManager.autoConnect(wifiSSID.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
|
|
||||||
|
|
||||||
xTaskCreate(
|
|
||||||
[](void *obj) {
|
|
||||||
while (wifiManager.getConfigPortalActive()) {
|
|
||||||
wifiManager.process();
|
|
||||||
}
|
|
||||||
vTaskDelete(NULL);
|
|
||||||
},
|
|
||||||
"wifi_cfg", 4096, NULL, 10, NULL);
|
|
||||||
|
|
||||||
uint32_t stimer = millis();
|
|
||||||
bool clientConnectChanged = false;
|
|
||||||
while (wifiManager.getConfigPortalActive()) {
|
|
||||||
if (WiFi.isConnected() == false) {
|
|
||||||
if (ledSmState == APP_SM_WIFI_MANAGER_MODE) {
|
|
||||||
uint32_t ms = (uint32_t)(millis() - stimer);
|
|
||||||
if (ms >= 100) {
|
|
||||||
stimer = millis();
|
|
||||||
ledSmHandler(ledSmState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check for client connect to change led color */
|
|
||||||
bool clientConnected = wifiMangerClientConnected();
|
|
||||||
if (clientConnected != clientConnectChanged) {
|
|
||||||
clientConnectChanged = clientConnected;
|
|
||||||
if (clientConnectChanged) {
|
|
||||||
ledSmHandler(APP_SM_WIFI_MAMAGER_PORTAL_ACTIVE);
|
|
||||||
} else {
|
|
||||||
ledSmHandler(APP_SM_WIFI_MANAGER_MODE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show display wifi connect result failed */
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
delay(2000);
|
|
||||||
if (WiFi.isConnected() == false) {
|
|
||||||
ledSmHandler(APP_SM_WIFI_MANAGER_CONNECT_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String getNormalizedMac() {
|
|
||||||
String mac = WiFi.macAddress();
|
|
||||||
mac.replace(":", "");
|
|
||||||
mac.toLowerCase();
|
|
||||||
return mac;
|
|
||||||
}
|
|
||||||
|
|
||||||
void boardInit(void) {
|
|
||||||
if (Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin()) == false) {
|
|
||||||
failedHandler("Init I2C failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
ag.watchdog.begin();
|
|
||||||
|
|
||||||
ag.button.begin();
|
|
||||||
|
|
||||||
ag.statusLed.begin();
|
|
||||||
|
|
||||||
/** detect sensor: PMS5003, PMS5003T, SGP41 and S8 */
|
|
||||||
if (ag.s8.begin(Serial1) == false) {
|
|
||||||
Serial.println("S8 not detect run mode 'PPT'");
|
|
||||||
fw_mode = FW_MODE_PPT;
|
|
||||||
|
|
||||||
/** De-initialize Serial1 */
|
|
||||||
Serial1.end();
|
|
||||||
}
|
|
||||||
if (ag.sgp41.begin(Wire) == false) {
|
|
||||||
if (fw_mode == FW_MODE_PST) {
|
|
||||||
failedHandler("Init SGP41 failed");
|
|
||||||
} else {
|
|
||||||
Serial.println("SGP41 not detect run mode 'PP'");
|
|
||||||
fw_mode = FW_MODE_PP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ag.pms5003t_1.begin(Serial0) == false) {
|
|
||||||
failedHandler("Init PMS5003T_1 failed");
|
|
||||||
}
|
|
||||||
if (fw_mode != FW_MODE_PST) {
|
|
||||||
if (ag.pms5003t_2.begin(Serial1) == false) {
|
|
||||||
failedHandler("Init PMS5003T_2 failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fw_mode != FW_MODE_PST) {
|
|
||||||
pmsSchedule.setPeriod(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("Firmware node: %s\r\n", getFwMode(fw_mode));
|
|
||||||
}
|
|
||||||
|
|
||||||
void failedHandler(String msg) {
|
|
||||||
while (true) {
|
|
||||||
Serial.println(msg);
|
|
||||||
vTaskDelay(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void co2Calibration(void) {
|
|
||||||
/** Count down for co2CalibCountdown secs */
|
|
||||||
for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) {
|
|
||||||
Serial.printf("Start CO2 calib after %d sec\r\n",
|
|
||||||
SENSOR_CO2_CALIB_COUNTDOWN_MAX - i);
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ag.s8.setBaselineCalibration()) {
|
|
||||||
Serial.println("Calibration success");
|
|
||||||
delay(1000);
|
|
||||||
Serial.println("Wait for calib finish...");
|
|
||||||
int count = 0;
|
|
||||||
while (ag.s8.isBaseLineCalibrationDone() == false) {
|
|
||||||
delay(1000);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
Serial.printf("Calib finish after %d sec\r\n", count);
|
|
||||||
delay(2000);
|
|
||||||
} else {
|
|
||||||
Serial.println("Calibration failure!!!");
|
|
||||||
delay(2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief WiFi reconnect handler
|
|
||||||
*/
|
|
||||||
static void updateWiFiConnect(void) {
|
|
||||||
static uint32_t lastRetry;
|
|
||||||
if (wifiHasConfig == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (WiFi.isConnected()) {
|
|
||||||
lastRetry = millis();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint32_t ms = (uint32_t)(millis() - lastRetry);
|
|
||||||
if (ms >= WIFI_CONNECT_RETRY_MS) {
|
|
||||||
lastRetry = millis();
|
|
||||||
WiFi.reconnect();
|
|
||||||
|
|
||||||
Serial.printf("Re-Connect WiFi\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update tvocIndexindex
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
static void tvocPoll(void) {
|
|
||||||
tvocIndex = ag.sgp41.getTvocIndex();
|
|
||||||
noxIndex = ag.sgp41.getNoxIndex();
|
|
||||||
|
|
||||||
Serial.printf("tvocIndexindex: %d\r\n", tvocIndex);
|
|
||||||
Serial.printf(" NOx index: %d\r\n", noxIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update PMS data
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
static void pmPoll(void) {
|
|
||||||
if (fw_mode == FW_MODE_PST) {
|
|
||||||
if (ag.pms5003t_1.readData()) {
|
|
||||||
pm01_1 = ag.pms5003t_1.getPm01Ae();
|
|
||||||
pm25_1 = ag.pms5003t_1.getPm25Ae();
|
|
||||||
pm25_1 = ag.pms5003t_1.getPm10Ae();
|
|
||||||
pm03PCount_1 = ag.pms5003t_1.getPm03ParticleCount();
|
|
||||||
temp_1 = ag.pms5003t_1.getTemperature();
|
|
||||||
hum_1 = ag.pms5003t_1.getRelativeHumidity();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (ag.pms5003t_1.readData() && ag.pms5003t_2.readData()) {
|
|
||||||
pm1Value01 = pm1Value01 + ag.pms5003t_1.getPm01Ae();
|
|
||||||
pm1Value25 = pm1Value25 + ag.pms5003t_1.getPm25Ae();
|
|
||||||
pm1Value10 = pm1Value10 + ag.pms5003t_1.getPm10Ae();
|
|
||||||
pm1PCount = pm1PCount + ag.pms5003t_1.getPm03ParticleCount();
|
|
||||||
pm1temp = pm1temp + ag.pms5003t_1.getTemperature();
|
|
||||||
pm1hum = pm1hum + ag.pms5003t_1.getRelativeHumidity();
|
|
||||||
pm2Value01 = pm2Value01 + ag.pms5003t_2.getPm01Ae();
|
|
||||||
pm2Value25 = pm2Value25 + ag.pms5003t_2.getPm25Ae();
|
|
||||||
pm2Value10 = pm2Value10 + ag.pms5003t_2.getPm10Ae();
|
|
||||||
pm2PCount = pm2PCount + ag.pms5003t_2.getPm03ParticleCount();
|
|
||||||
pm2temp = pm2temp + ag.pms5003t_2.getTemperature();
|
|
||||||
pm2hum = pm2hum + ag.pms5003t_2.getRelativeHumidity();
|
|
||||||
countPosition++;
|
|
||||||
if (countPosition == targetCount) {
|
|
||||||
pm01_1 = pm1Value01 / targetCount;
|
|
||||||
pm25_1 = pm1Value25 / targetCount;
|
|
||||||
pm10_1 = pm1Value10 / targetCount;
|
|
||||||
pm03PCount_1 = pm1PCount / targetCount;
|
|
||||||
temp_1 = pm1temp / targetCount;
|
|
||||||
hum_1 = pm1hum / targetCount;
|
|
||||||
pm01_2 = pm2Value01 / targetCount;
|
|
||||||
pm25_2 = pm2Value25 / targetCount;
|
|
||||||
pm10_2 = pm2Value10 / targetCount;
|
|
||||||
pm03PCount_2 = pm2PCount / targetCount;
|
|
||||||
temp_2 = pm2temp / targetCount;
|
|
||||||
hum_2 = pm2hum / targetCount;
|
|
||||||
|
|
||||||
countPosition = 0;
|
|
||||||
|
|
||||||
pm1Value01 = 0;
|
|
||||||
pm1Value25 = 0;
|
|
||||||
pm1Value10 = 0;
|
|
||||||
pm1PCount = 0;
|
|
||||||
pm1temp = 0;
|
|
||||||
pm1hum = 0;
|
|
||||||
pm2Value01 = 0;
|
|
||||||
pm2Value25 = 0;
|
|
||||||
pm2Value10 = 0;
|
|
||||||
pm2PCount = 0;
|
|
||||||
pm2temp = 0;
|
|
||||||
pm2hum = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void co2Poll(void) {
|
|
||||||
co2Ppm = ag.s8.getCo2();
|
|
||||||
Serial.printf("CO2 index: %d\r\n", co2Ppm);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void serverConfigPoll(void) {
|
|
||||||
if (agServer.pollServerConfig(getDevId())) {
|
|
||||||
/** Only support CO2 S8 sensor on FW_MODE_PST */
|
|
||||||
if (fw_mode == FW_MODE_PST) {
|
|
||||||
if (agServer.isCo2Calib()) {
|
|
||||||
co2Calibration();
|
|
||||||
}
|
|
||||||
if (agServer.getCo2Abccalib() > 0) {
|
|
||||||
if (ag.s8.setAutoCalib(agServer.getCo2Abccalib() * 24) == false) {
|
|
||||||
Serial.println("Set S8 auto calib failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getDevId(void) { return getNormalizedMac(); }
|
|
||||||
|
|
||||||
void ledBlinkDelay(uint32_t tdelay) {
|
|
||||||
ag.statusLed.setOn();
|
|
||||||
delay(tdelay);
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
delay(tdelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ledSmHandler(int sm) {
|
|
||||||
if (sm > APP_SM_NORMAL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ledSmState = sm;
|
|
||||||
switch (sm) {
|
|
||||||
case APP_SM_WIFI_MANAGER_MODE: {
|
|
||||||
ag.statusLed.setToggle();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_MAMAGER_PORTAL_ACTIVE: {
|
|
||||||
ag.statusLed.setOn();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_MANAGER_STA_CONNECTING: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_MANAGER_STA_CONNECTED: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_OK_SERVER_CONNECTING: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_OK_SERVER_CONNNECTED: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
|
|
||||||
/** two time slow blink, then off */
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
ledBlinkDelay(LED_SLOW_BLINK_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_MANAGER_CONNECT_FAILED: {
|
|
||||||
/** Three time fast blink then 2 sec pause. Repeat 3 times */
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
|
|
||||||
for (int j = 0; j < 3; j++) {
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
ledBlinkDelay(LED_FAST_BLINK_DELAY);
|
|
||||||
}
|
|
||||||
delay(2000);
|
|
||||||
}
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_OK_SERVER_CONNECT_FAILED: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
for (int j = 0; j < 3; j++) {
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
ledBlinkDelay(LED_FAST_BLINK_DELAY);
|
|
||||||
}
|
|
||||||
delay(2000);
|
|
||||||
}
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
for (int j = 0; j < 3; j++) {
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
ledBlinkDelay(LED_FAST_BLINK_DELAY);
|
|
||||||
}
|
|
||||||
delay(2000);
|
|
||||||
}
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_WIFI_LOST: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_SERVER_LOST: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_SENSOR_CONFIG_FAILED: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APP_SM_NORMAL: {
|
|
||||||
ag.statusLed.setOff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char *getFwMode(int mode) {
|
|
||||||
switch (mode) {
|
|
||||||
case FW_MODE_PST:
|
|
||||||
return "FW_MODE_PST";
|
|
||||||
case FW_MODE_PPT:
|
|
||||||
return "FW_MODE_PPT";
|
|
||||||
case FW_MODE_PP:
|
|
||||||
return "FW_MODE_PP";
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return "FW_MODE_UNKNOW";
|
|
||||||
}
|
|
@ -25,7 +25,7 @@ void setup()
|
|||||||
if (ag.s8.begin(&Serial) == false)
|
if (ag.s8.begin(&Serial) == false)
|
||||||
{
|
{
|
||||||
#else
|
#else
|
||||||
if (ag.s8.begin(Serial1) == false)
|
if (ag.s8.begin(Serial0) == false)
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
failedHandler("SenseAir S8 init failed");
|
failedHandler("SenseAir S8 init failed");
|
||||||
|
@ -10,8 +10,8 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|||||||
#ifdef ESP8266
|
#ifdef ESP8266
|
||||||
AirGradient ag = AirGradient(DIY_BASIC);
|
AirGradient ag = AirGradient(DIY_BASIC);
|
||||||
#else
|
#else
|
||||||
// AirGradient ag = AirGradient(ONE_INDOOR);
|
AirGradient ag = AirGradient(ONE_INDOOR);
|
||||||
AirGradient ag = AirGradient(OPEN_AIR_OUTDOOR);
|
// AirGradient ag = AirGradient(OPEN_AIR_OUTDOOR);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void failedHandler(String msg);
|
void failedHandler(String msg);
|
||||||
@ -35,42 +35,56 @@ void setup() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t lastRead = 0;
|
||||||
void loop() {
|
void loop() {
|
||||||
int PM2;
|
int PM2;
|
||||||
bool readResul = false;
|
bool readResul = false;
|
||||||
#ifdef ESP8266
|
|
||||||
if (ag.pms5003.readData()) {
|
|
||||||
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",
|
|
||||||
ag.pms5003.convertPm25ToUsAqi(PM2));
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
|
|
||||||
if (ag.pms5003t_1.readData()) {
|
|
||||||
PM2 = ag.pms5003t_1.getPm25Ae();
|
|
||||||
readResul = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (ag.pms5003.readData()) {
|
|
||||||
PM2 = ag.pms5003.getPm25Ae();
|
|
||||||
readResul = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readResul) {
|
uint32_t ms = (uint32_t)(millis() - lastRead);
|
||||||
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
|
if (ms >= 5000) {
|
||||||
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
|
lastRead = millis();
|
||||||
Serial.printf("PM2.5 in US AQI: %d\r\n",
|
#ifdef ESP8266
|
||||||
ag.pms5003t_1.convertPm25ToUsAqi(PM2));
|
if (ag.pms5003.connected()) {
|
||||||
} else {
|
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",
|
Serial.printf("PM2.5 in US AQI: %d\r\n",
|
||||||
ag.pms5003.convertPm25ToUsAqi(PM2));
|
ag.pms5003.convertPm25ToUsAqi(PM2));
|
||||||
|
} else {
|
||||||
|
Serial.println("PMS sensor failed");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
|
||||||
|
if (ag.pms5003t_1.connected()) {
|
||||||
|
PM2 = ag.pms5003t_1.getPm25Ae();
|
||||||
|
readResul = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ag.pms5003.connected()) {
|
||||||
|
PM2 = ag.pms5003.getPm25Ae();
|
||||||
|
readResul = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
delay(5000);
|
if (readResul) {
|
||||||
|
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
|
||||||
|
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
|
||||||
|
Serial.printf("PM2.5 in US AQI: %d\r\n",
|
||||||
|
ag.pms5003t_1.convertPm25ToUsAqi(PM2));
|
||||||
|
} else {
|
||||||
|
Serial.printf("PM2.5 in US AQI: %d\r\n",
|
||||||
|
ag.pms5003.convertPm25ToUsAqi(PM2));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println("PMS sensor failed");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
|
||||||
|
ag.pms5003t_1.handle();
|
||||||
|
} else {
|
||||||
|
ag.pms5003.handle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void failedHandler(String msg) {
|
void failedHandler(String msg) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name=AirGradient Air Quality Sensor
|
name=AirGradient Air Quality Sensor
|
||||||
version=3.0.3
|
version=3.1.14
|
||||||
author=AirGradient <support@airgradient.com>
|
author=AirGradient <support@airgradient.com>
|
||||||
maintainer=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.
|
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.
|
||||||
|
7
partitions.csv
Normal file
7
partitions.csv
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Name ,Type ,SubType ,Offset ,Size ,Flags
|
||||||
|
nvs ,data ,nvs ,0x9000 ,0x5000 ,
|
||||||
|
otadata ,data ,ota ,0xe000 ,0x2000 ,
|
||||||
|
app0 ,app ,ota_0 ,0x10000 ,0x1E0000 ,
|
||||||
|
app1 ,app ,ota_1 ,0x1F0000 ,0x1E0000 ,
|
||||||
|
spiffs ,data ,spiffs ,0x3D0000 ,0x20000 ,
|
||||||
|
coredump ,data ,coredump ,0x3F0000 ,0x10000 ,
|
|
51
platformio.ini
Normal file
51
platformio.ini
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:esp32-c3]
|
||||||
|
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)'\\"'
|
||||||
|
board_build.partitions = partitions.csv
|
||||||
|
monitor_speed = 115200
|
||||||
|
lib_deps =
|
||||||
|
aglib=symlink://../arduino
|
||||||
|
EEPROM
|
||||||
|
WebServer
|
||||||
|
ESPmDNS
|
||||||
|
FS
|
||||||
|
SPIFFS
|
||||||
|
HTTPClient
|
||||||
|
WiFiClientSecure
|
||||||
|
Update
|
||||||
|
DNSServer
|
||||||
|
|
||||||
|
[env:esp8266]
|
||||||
|
platform = espressif8266
|
||||||
|
board = d1_mini
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
lib_deps =
|
||||||
|
aglib=symlink://../arduino
|
||||||
|
EEPROM
|
||||||
|
ESP8266HTTPClient
|
||||||
|
ESP8266WebServer
|
||||||
|
DNSServer
|
||||||
|
|
||||||
|
monitor_filters = time
|
||||||
|
|
||||||
|
[platformio]
|
||||||
|
src_dir = examples/OneOpenAir
|
||||||
|
; src_dir = examples/BASIC
|
||||||
|
; src_dir = examples/DiyProIndoorV4_2
|
||||||
|
; src_dir = examples/DiyProIndoorV3_3
|
||||||
|
; src_dir = examples/TestCO2
|
||||||
|
; src_dir = examples/TestPM
|
||||||
|
; src_dir = examples/TestSht
|
201
src/AgApiClient.cpp
Normal file
201
src/AgApiClient.cpp
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
#include "AgApiClient.h"
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
|
||||||
|
#ifdef ESP8266
|
||||||
|
#include <ESP8266HTTPClient.h>
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
#else
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
AgApiClient::AgApiClient(Stream &debug, Configuration &config)
|
||||||
|
: PrintLog(debug, "ApiClient"), config(config) {}
|
||||||
|
|
||||||
|
AgApiClient::~AgApiClient() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the API client
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void AgApiClient::begin(void) {
|
||||||
|
getConfigFailed = false;
|
||||||
|
postToServerFailed = false;
|
||||||
|
logInfo("Init apiRoot: " + apiRoot);
|
||||||
|
logInfo("begin");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get configuration from AirGradient cloud
|
||||||
|
*
|
||||||
|
* @param deviceId Device ID
|
||||||
|
* @return true Success
|
||||||
|
* @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";
|
||||||
|
|
||||||
|
/** Init http client */
|
||||||
|
#ifdef ESP8266
|
||||||
|
HTTPClient client;
|
||||||
|
WiFiClient wifiClient;
|
||||||
|
if (client.begin(wifiClient, uri) == false) {
|
||||||
|
getConfigFailed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
HTTPClient client;
|
||||||
|
client.setTimeout(timeoutMs);
|
||||||
|
if (client.begin(uri) == false) {
|
||||||
|
getConfigFailed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Get data */
|
||||||
|
int retCode = client.GET();
|
||||||
|
|
||||||
|
logInfo(String("GET: ") + uri);
|
||||||
|
logInfo(String("Return code: ") + String(retCode));
|
||||||
|
|
||||||
|
if (retCode != 200) {
|
||||||
|
client.end();
|
||||||
|
getConfigFailed = true;
|
||||||
|
|
||||||
|
/** Return code 400 mean device not setup on cloud. */
|
||||||
|
if (retCode == 400) {
|
||||||
|
notAvailableOnDashboard = true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** clear failed */
|
||||||
|
getConfigFailed = false;
|
||||||
|
notAvailableOnDashboard = false;
|
||||||
|
|
||||||
|
/** Get response string */
|
||||||
|
String respContent = client.getString();
|
||||||
|
client.end();
|
||||||
|
|
||||||
|
// logInfo("Get configuration: " + respContent);
|
||||||
|
|
||||||
|
/** Parse configuration and return result */
|
||||||
|
return config.parse(respContent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Post data to AirGradient cloud
|
||||||
|
*
|
||||||
|
* @param deviceId Device Id
|
||||||
|
* @param data String JSON
|
||||||
|
* @return true Success
|
||||||
|
* @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 = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
|
||||||
|
// logInfo("Post uri: " + uri);
|
||||||
|
// logInfo("Post data: " + data);
|
||||||
|
|
||||||
|
WiFiClient wifiClient;
|
||||||
|
HTTPClient client;
|
||||||
|
client.setTimeout(timeoutMs);
|
||||||
|
if (client.begin(wifiClient, uri.c_str()) == false) {
|
||||||
|
logError("Init client failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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)) {
|
||||||
|
postToServerFailed = false;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logError("Post response failed code: " + String(retCode));
|
||||||
|
}
|
||||||
|
postToServerFailed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get failed status when get configuration from AirGradient cloud
|
||||||
|
*
|
||||||
|
* @return true Success
|
||||||
|
* @return false Failure
|
||||||
|
*/
|
||||||
|
bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get failed status when post data to AirGradient cloud
|
||||||
|
*
|
||||||
|
* @return true Success
|
||||||
|
* @return false Failure
|
||||||
|
*/
|
||||||
|
bool AgApiClient::isPostToServerFailed(void) { return postToServerFailed; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get status device has available on dashboard or not. should get after
|
||||||
|
* fetch configuration return failed
|
||||||
|
*
|
||||||
|
* @return true
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
bool AgApiClient::isNotAvailableOnDashboard(void) {
|
||||||
|
return notAvailableOnDashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgApiClient::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send the package to check the connection with cloud
|
||||||
|
*
|
||||||
|
* @param rssi WiFi RSSI
|
||||||
|
* @param bootCount Boot count
|
||||||
|
* @return true Success
|
||||||
|
* @return false Failure
|
||||||
|
*/
|
||||||
|
bool AgApiClient::sendPing(int rssi, int bootCount) {
|
||||||
|
JSONVar root;
|
||||||
|
root["wifi"] = rssi;
|
||||||
|
root["boot"] = bootCount;
|
||||||
|
return postToServer(JSON.stringify(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
String AgApiClient::getApiRoot() const { return apiRoot; }
|
||||||
|
|
||||||
|
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set http request timeout. (Default: 10s)
|
||||||
|
*
|
||||||
|
* @param timeoutMs
|
||||||
|
*/
|
||||||
|
void AgApiClient::setTimeout(uint16_t timeoutMs) {
|
||||||
|
this->timeoutMs = timeoutMs;
|
||||||
|
}
|
47
src/AgApiClient.h
Normal file
47
src/AgApiClient.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* @file AgApiClient.h
|
||||||
|
* @brief HTTP client connect post data to Aigradient cloud.
|
||||||
|
* @version 0.1
|
||||||
|
* @date 2024-Apr-02
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AG_API_CLIENT_H_
|
||||||
|
#define _AG_API_CLIENT_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "Main/PrintLog.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
class AgApiClient : public PrintLog {
|
||||||
|
private:
|
||||||
|
Configuration &config;
|
||||||
|
AirGradient *ag;
|
||||||
|
String apiRoot = "http://hw.airgradient.com";
|
||||||
|
|
||||||
|
bool getConfigFailed;
|
||||||
|
bool postToServerFailed;
|
||||||
|
bool notAvailableOnDashboard = false; // Device not setup on Airgradient cloud dashboard.
|
||||||
|
uint16_t timeoutMs = 10000; // Default set to 10s
|
||||||
|
|
||||||
|
public:
|
||||||
|
AgApiClient(Stream &stream, Configuration &config);
|
||||||
|
~AgApiClient();
|
||||||
|
|
||||||
|
void begin(void);
|
||||||
|
bool fetchServerConfiguration(void);
|
||||||
|
bool postToServer(String data);
|
||||||
|
bool isFetchConfigureFailed(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_ */
|
1382
src/AgConfigure.cpp
Normal file
1382
src/AgConfigure.cpp
Normal file
File diff suppressed because it is too large
Load Diff
104
src/AgConfigure.h
Normal file
104
src/AgConfigure.h
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#ifndef _AG_CONFIG_H_
|
||||||
|
#define _AG_CONFIG_H_
|
||||||
|
|
||||||
|
#include "App/AppDef.h"
|
||||||
|
#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;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool co2CalibrationRequested;
|
||||||
|
bool ledBarTestRequested;
|
||||||
|
bool updated;
|
||||||
|
String failedMessage;
|
||||||
|
bool _noxLearnOffsetChanged;
|
||||||
|
bool _tvocLearningOffsetChanged;
|
||||||
|
bool ledBarBrightnessChanged = false;
|
||||||
|
bool displayBrightnessChanged = false;
|
||||||
|
String otaNewFirmwareVersion;
|
||||||
|
bool _offlineMode = false;
|
||||||
|
bool _ledBarModeChanged = false;
|
||||||
|
PMCorrection pmCorrection;
|
||||||
|
|
||||||
|
AirGradient* ag;
|
||||||
|
|
||||||
|
String getLedBarModeName(LedBarMode mode);
|
||||||
|
PMCorrectionAlgorithm matchPmAlgorithm(String algorithm);
|
||||||
|
bool updatePmCorrection(JSONVar &json);
|
||||||
|
void saveConfig(void);
|
||||||
|
void loadConfig(void);
|
||||||
|
void defaultConfig(void);
|
||||||
|
void printConfig(void);
|
||||||
|
String jsonTypeInvalidMessage(String name, String type);
|
||||||
|
String jsonValueInvalidMessage(String name, String value);
|
||||||
|
void jsonInvalid(void);
|
||||||
|
void configLogInfo(String name, String fromValue, String toValue);
|
||||||
|
String getPMStandardString(bool usaqi);
|
||||||
|
String getAbcDayString(int value);
|
||||||
|
void toConfig(const char* buf);
|
||||||
|
|
||||||
|
public:
|
||||||
|
Configuration(Stream &debugLog);
|
||||||
|
~Configuration();
|
||||||
|
|
||||||
|
bool hasSensorS8 = true;
|
||||||
|
bool hasSensorPMS1 = true;
|
||||||
|
bool hasSensorPMS2 = true;
|
||||||
|
bool hasSensorSGP = true;
|
||||||
|
bool hasSensorSHT = true;
|
||||||
|
|
||||||
|
bool begin(void);
|
||||||
|
bool parse(String data, bool isLocal);
|
||||||
|
String toString(void);
|
||||||
|
String toString(AgFirmwareMode fwMode);
|
||||||
|
bool isTemperatureUnitInF(void);
|
||||||
|
String getCountry(void);
|
||||||
|
bool isPmStandardInUSAQI(void);
|
||||||
|
int getCO2CalibrationAbcDays(void);
|
||||||
|
LedBarMode getLedBarMode(void);
|
||||||
|
String getLedBarModeName(void);
|
||||||
|
bool getDisplayMode(void);
|
||||||
|
String getMqttBrokerUri(void);
|
||||||
|
bool isPostDataToAirGradient(void);
|
||||||
|
ConfigurationControl getConfigurationControl(void);
|
||||||
|
bool isCo2CalibrationRequested(void);
|
||||||
|
bool isLedBarTestRequested(void);
|
||||||
|
void reset(void);
|
||||||
|
String getModel(void);
|
||||||
|
bool isUpdated(void);
|
||||||
|
String getFailedMesage(void);
|
||||||
|
void setPostToAirGradient(bool enable);
|
||||||
|
bool noxLearnOffsetChanged(void);
|
||||||
|
bool tvocLearnOffsetChanged(void);
|
||||||
|
int getTvocLearningOffset(void);
|
||||||
|
int getNoxLearningOffset(void);
|
||||||
|
String wifiSSID(void);
|
||||||
|
String wifiPass(void);
|
||||||
|
void setAirGradient(AirGradient *ag);
|
||||||
|
bool isLedBarBrightnessChanged(void);
|
||||||
|
int getLedBarBrightness(void);
|
||||||
|
bool isDisplayBrightnessChanged(void);
|
||||||
|
int getDisplayBrightness(void);
|
||||||
|
String newFirmwareVersion(void);
|
||||||
|
bool isOfflineMode(void);
|
||||||
|
void setOfflineMode(bool offline);
|
||||||
|
void setOfflineModeWithoutSave(bool offline);
|
||||||
|
bool isLedBarModeChanged(void);
|
||||||
|
bool isMonitorDisplayCompensatedValues(void);
|
||||||
|
bool isPMCorrectionChanged(void);
|
||||||
|
bool isPMCorrectionEnabled(void);
|
||||||
|
PMCorrection getPMCorrection(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _AG_CONFIG_H_ */
|
555
src/AgOledDisplay.cpp
Normal file
555
src/AgOledDisplay.cpp
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
#include "AgOledDisplay.h"
|
||||||
|
#include "Libraries/U8g2/src/U8g2lib.h"
|
||||||
|
#include "Main/utils.h"
|
||||||
|
|
||||||
|
/** Cast U8G2 */
|
||||||
|
#define DISP() ((U8G2_SH1106_128X64_NONAME_F_HW_I2C *)(this->u8g2))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Show dashboard temperature and humdity
|
||||||
|
*
|
||||||
|
* @param hasStatus
|
||||||
|
*/
|
||||||
|
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
|
||||||
|
/** Temperature */
|
||||||
|
float temp = value.getAverage(Measurements::Temperature);
|
||||||
|
if (utils::isValidTemperature(temp)) {
|
||||||
|
float t = 0.0f;
|
||||||
|
if (config.isTemperatureUnitInF()) {
|
||||||
|
t = utils::degreeC_To_F(temp);
|
||||||
|
} else {
|
||||||
|
t = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isTemperatureUnitInF()) {
|
||||||
|
if (hasStatus) {
|
||||||
|
snprintf(buf, buf_size, "%0.1f", t);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, buf_size, "%0.1f°F", t);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasStatus) {
|
||||||
|
snprintf(buf, buf_size, "%.1f", t);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, buf_size, "%.1f°C", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { /** Show invalid value */
|
||||||
|
if (config.isTemperatureUnitInF()) {
|
||||||
|
snprintf(buf, buf_size, "-°F");
|
||||||
|
} else {
|
||||||
|
snprintf(buf, buf_size, "-°C");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DISP()->drawUTF8(1, 10, buf);
|
||||||
|
|
||||||
|
/** Show humidity */
|
||||||
|
int rhum = round(value.getAverage(Measurements::Humidity));
|
||||||
|
if (utils::isValidHumidity(rhum)) {
|
||||||
|
snprintf(buf, buf_size, "%d%%", rhum);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, buf_size, "-%%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rhum > 99.0) {
|
||||||
|
DISP()->drawStr(97, 10, buf);
|
||||||
|
} else {
|
||||||
|
DISP()->drawStr(105, 10, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OledDisplay::setCentralText(int y, String text) {
|
||||||
|
setCentralText(y, text.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OledDisplay::setCentralText(int y, const char *text) {
|
||||||
|
int x = (DISP()->getWidth() - DISP()->getStrWidth(text)) / 2;
|
||||||
|
DISP()->drawStr(x, y, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct a new Ag Oled Display:: Ag Oled Display object
|
||||||
|
*
|
||||||
|
* @param config AgConfiguration
|
||||||
|
* @param value Measurements
|
||||||
|
* @param log Serial Stream
|
||||||
|
*/
|
||||||
|
OledDisplay::OledDisplay(Configuration &config, Measurements &value,
|
||||||
|
Stream &log)
|
||||||
|
: PrintLog(log, "OledDisplay"), config(config), value(value) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set AirGradient instance
|
||||||
|
*
|
||||||
|
* @param ag Point to AirGradient instance
|
||||||
|
*/
|
||||||
|
void OledDisplay::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
OledDisplay::~OledDisplay() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize display
|
||||||
|
*
|
||||||
|
* @return true Success
|
||||||
|
* @return false Failure
|
||||||
|
*/
|
||||||
|
bool OledDisplay::begin(void) {
|
||||||
|
if (isBegin) {
|
||||||
|
logWarning("Already begin, call 'end' and try again");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
|
||||||
|
/** Create u8g2 instance */
|
||||||
|
u8g2 = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, U8X8_PIN_NONE);
|
||||||
|
if (u8g2 == NULL) {
|
||||||
|
logError("Create 'U8G2' failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init u8g2 */
|
||||||
|
if (DISP()->begin() == false) {
|
||||||
|
logError("U8G2 'begin' failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
logInfo("DIY_BASIC init");
|
||||||
|
ag->display.begin(Wire);
|
||||||
|
ag->display.setTextColor(1);
|
||||||
|
ag->display.clear();
|
||||||
|
ag->display.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show low brightness on startup. then it's completely turn off on main
|
||||||
|
* application */
|
||||||
|
int brightness = config.getDisplayBrightness();
|
||||||
|
if (brightness == 0) {
|
||||||
|
setBrightness(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
isBegin = true;
|
||||||
|
logInfo("begin");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief De-Initialize display
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void OledDisplay::end(void) {
|
||||||
|
if (!isBegin) {
|
||||||
|
logWarning("Already end, call 'begin' and try again");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
|
||||||
|
/** Free u8g2 */
|
||||||
|
delete DISP();
|
||||||
|
u8g2 = NULL;
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
ag->display.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
isBegin = false;
|
||||||
|
logInfo("end");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Show text on 3 line of display
|
||||||
|
*
|
||||||
|
* @param line1
|
||||||
|
* @param line2
|
||||||
|
* @param line3
|
||||||
|
*/
|
||||||
|
void OledDisplay::setText(String &line1, String &line2, String &line3) {
|
||||||
|
setText(line1.c_str(), line2.c_str(), line3.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Show text on 3 line of display
|
||||||
|
*
|
||||||
|
* @param line1
|
||||||
|
* @param line2
|
||||||
|
* @param line3
|
||||||
|
*/
|
||||||
|
void OledDisplay::setText(const char *line1, const char *line2,
|
||||||
|
const char *line3) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
DISP()->drawStr(1, 10, line1);
|
||||||
|
DISP()->drawStr(1, 30, line2);
|
||||||
|
DISP()->drawStr(1, 50, line3);
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
ag->display.clear();
|
||||||
|
|
||||||
|
ag->display.setCursor(1, 1);
|
||||||
|
ag->display.setText(line1);
|
||||||
|
ag->display.setCursor(1, 17);
|
||||||
|
ag->display.setText(line2);
|
||||||
|
ag->display.setCursor(1, 33);
|
||||||
|
ag->display.setText(line3);
|
||||||
|
|
||||||
|
ag->display.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set Text on 4 line
|
||||||
|
*
|
||||||
|
* @param line1
|
||||||
|
* @param line2
|
||||||
|
* @param line3
|
||||||
|
* @param line4
|
||||||
|
*/
|
||||||
|
void OledDisplay::setText(String &line1, String &line2, String &line3,
|
||||||
|
String &line4) {
|
||||||
|
setText(line1.c_str(), line2.c_str(), line3.c_str(), line4.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set Text on 4 line
|
||||||
|
*
|
||||||
|
* @param line1
|
||||||
|
* @param line2
|
||||||
|
* @param line3
|
||||||
|
* @param line4
|
||||||
|
*/
|
||||||
|
void OledDisplay::setText(const char *line1, const char *line2,
|
||||||
|
const char *line3, const char *line4) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
DISP()->drawStr(1, 10, line1);
|
||||||
|
DISP()->drawStr(1, 25, line2);
|
||||||
|
DISP()->drawStr(1, 40, line3);
|
||||||
|
DISP()->drawStr(1, 55, line4);
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
ag->display.clear();
|
||||||
|
ag->display.setCursor(0, 0);
|
||||||
|
ag->display.setText(line1);
|
||||||
|
ag->display.setCursor(0, 10);
|
||||||
|
ag->display.setText(line2);
|
||||||
|
ag->display.setCursor(0, 20);
|
||||||
|
ag->display.setText(line3);
|
||||||
|
ag->display.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update dashboard content
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void OledDisplay::showDashboard(void) { showDashboard(NULL); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update dashboard content and error status
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void OledDisplay::showDashboard(const char *status) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char strBuf[16];
|
||||||
|
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)) {
|
||||||
|
showTempHum(false, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
String strStatus = "Show status: " + String(status);
|
||||||
|
logInfo(strStatus);
|
||||||
|
|
||||||
|
int strWidth = DISP()->getStrWidth(status);
|
||||||
|
DISP()->drawStr((DISP()->getWidth() - strWidth) / 2, 10, status);
|
||||||
|
|
||||||
|
/** Show WiFi NA*/
|
||||||
|
if (strcmp(status, "WiFi N/A") == 0) {
|
||||||
|
DISP()->setFont(u8g2_font_t0_12_tf);
|
||||||
|
showTempHum(true, strBuf, sizeof(strBuf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Draw horizonal line */
|
||||||
|
DISP()->drawLine(1, 13, 128, 13);
|
||||||
|
|
||||||
|
/** Show CO2 label */
|
||||||
|
DISP()->setFont(u8g2_font_t0_12_tf);
|
||||||
|
DISP()->drawUTF8(1, 27, "CO2");
|
||||||
|
|
||||||
|
DISP()->setFont(u8g2_font_t0_22b_tf);
|
||||||
|
int co2 = round(value.getAverage(Measurements::CO2));
|
||||||
|
if (utils::isValidCO2(co2)) {
|
||||||
|
sprintf(strBuf, "%d", co2);
|
||||||
|
} else {
|
||||||
|
sprintf(strBuf, "%s", "-");
|
||||||
|
}
|
||||||
|
DISP()->drawStr(1, 48, strBuf);
|
||||||
|
|
||||||
|
/** Show CO2 value index */
|
||||||
|
DISP()->setFont(u8g2_font_t0_12_tf);
|
||||||
|
DISP()->drawStr(1, 61, "ppm");
|
||||||
|
|
||||||
|
/** Draw vertical line */
|
||||||
|
DISP()->drawLine(52, 14, 52, 64);
|
||||||
|
DISP()->drawLine(97, 14, 97, 64);
|
||||||
|
|
||||||
|
/** Draw PM2.5 label */
|
||||||
|
DISP()->setFont(u8g2_font_t0_12_tf);
|
||||||
|
DISP()->drawStr(55, 27, "PM2.5");
|
||||||
|
|
||||||
|
/** Draw PM2.5 value */
|
||||||
|
int pm25 = round(value.getAverage(Measurements::PM25));
|
||||||
|
if (utils::isValidPm(pm25)) {
|
||||||
|
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||||
|
pm25 = round(value.getCorrectedPM25(*ag, config, 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()) {
|
||||||
|
DISP()->drawUTF8(55, 61, "AQI");
|
||||||
|
} else {
|
||||||
|
DISP()->drawUTF8(55, 61, "ug/m³");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Draw tvocIndexlabel */
|
||||||
|
DISP()->setFont(u8g2_font_t0_12_tf);
|
||||||
|
DISP()->drawStr(100, 27, "VOC:");
|
||||||
|
|
||||||
|
/** Draw tvocIndexvalue */
|
||||||
|
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(nox)) {
|
||||||
|
sprintf(strBuf, "%d", nox);
|
||||||
|
} else {
|
||||||
|
sprintf(strBuf, "%s", "-");
|
||||||
|
}
|
||||||
|
DISP()->drawStr(100, 63, strBuf);
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
ag->display.clear();
|
||||||
|
|
||||||
|
/** Set 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 = round(value.getAverage(Measurements::PM25));
|
||||||
|
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
|
||||||
|
pm25 = round(value.getCorrectedPM25(*ag, config, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
ag->display.setCursor(0, 12);
|
||||||
|
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 */
|
||||||
|
float temp = value.getAverage(Measurements::Temperature);
|
||||||
|
if (utils::isValidTemperature(temp)) {
|
||||||
|
if (config.isTemperatureUnitInF()) {
|
||||||
|
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
|
||||||
|
} else {
|
||||||
|
snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", temp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (config.isTemperatureUnitInF()) {
|
||||||
|
snprintf(strBuf, sizeof(strBuf), "T:-F");
|
||||||
|
} else {
|
||||||
|
snprintf(strBuf, sizeof(strBuf), "T:-C");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ag->display.setCursor(0, 24);
|
||||||
|
ag->display.setText(strBuf);
|
||||||
|
|
||||||
|
int rhum = round(value.getAverage(Measurements::Humidity));
|
||||||
|
if (utils::isValidHumidity(rhum)) {
|
||||||
|
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
|
||||||
|
} else {
|
||||||
|
snprintf(strBuf, sizeof(strBuf), "H:- %%");
|
||||||
|
}
|
||||||
|
|
||||||
|
ag->display.setCursor(0, 36);
|
||||||
|
ag->display.setText(strBuf);
|
||||||
|
|
||||||
|
ag->display.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OledDisplay::setBrightness(int percent) {
|
||||||
|
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
|
||||||
|
if (percent == 0) {
|
||||||
|
isDisplayOff = true;
|
||||||
|
|
||||||
|
// Clear display.
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
isDisplayOff = false;
|
||||||
|
DISP()->setContrast((127 * percent) / 100);
|
||||||
|
}
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
if (percent == 0) {
|
||||||
|
isDisplayOff = true;
|
||||||
|
|
||||||
|
// Clear display.
|
||||||
|
ag->display.clear();
|
||||||
|
ag->display.show();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isDisplayOff = false;
|
||||||
|
ag->display.setContrast((255 * percent) / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
void OledDisplay::showFirmwareUpdateVersion(String version) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
setCentralText(20, "Firmware Update");
|
||||||
|
setCentralText(40, "New version");
|
||||||
|
setCentralText(60, version.c_str());
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OledDisplay::showFirmwareUpdateProgress(int percent) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
setCentralText(20, "Firmware Update");
|
||||||
|
setCentralText(50, String("Updating... ") + String(percent) + String("%"));
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OledDisplay::showFirmwareUpdateSuccess(int count) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
setCentralText(20, "Firmware Update");
|
||||||
|
setCentralText(40, "Success");
|
||||||
|
setCentralText(60, String("Rebooting... ") + String(count));
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OledDisplay::showFirmwareUpdateFailed(void) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
setCentralText(20, "Firmware Update");
|
||||||
|
setCentralText(40, "fail, will retry");
|
||||||
|
// setCentralText(60, "will retry");
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OledDisplay::showFirmwareUpdateSkipped(void) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
setCentralText(20, "Firmware Update");
|
||||||
|
setCentralText(40, "skipped");
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OledDisplay::showFirmwareUpdateUpToDate(void) {
|
||||||
|
if (isDisplayOff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
setCentralText(20, "Firmware Update");
|
||||||
|
setCentralText(40, "up to date");
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void OledDisplay::showRebooting(void) {
|
||||||
|
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
|
||||||
|
DISP()->firstPage();
|
||||||
|
do {
|
||||||
|
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||||
|
// setCentralText(20, "Firmware Update");
|
||||||
|
setCentralText(40, "Rebooting...");
|
||||||
|
// setCentralText(60, String("Retry after 24h"));
|
||||||
|
} while (DISP()->nextPage());
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
ag->display.clear();
|
||||||
|
|
||||||
|
ag->display.setCursor(0, 20);
|
||||||
|
ag->display.setText("Rebooting...");
|
||||||
|
|
||||||
|
ag->display.show();
|
||||||
|
}
|
||||||
|
}
|
52
src/AgOledDisplay.h
Normal file
52
src/AgOledDisplay.h
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#ifndef _AG_OLED_DISPLAY_H_
|
||||||
|
#define _AG_OLED_DISPLAY_H_
|
||||||
|
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "Main/PrintLog.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
class OledDisplay : public PrintLog {
|
||||||
|
private:
|
||||||
|
Configuration &config;
|
||||||
|
AirGradient *ag;
|
||||||
|
bool isBegin = false;
|
||||||
|
void *u8g2 = NULL;
|
||||||
|
Measurements &value;
|
||||||
|
bool isDisplayOff = false;
|
||||||
|
|
||||||
|
void showTempHum(bool hasStatus, char* buf, int buf_size);
|
||||||
|
void setCentralText(int y, String text);
|
||||||
|
void setCentralText(int y, const char *text);
|
||||||
|
|
||||||
|
public:
|
||||||
|
OledDisplay(Configuration &config, Measurements &value,
|
||||||
|
Stream &log);
|
||||||
|
~OledDisplay();
|
||||||
|
|
||||||
|
void setAirGradient(AirGradient *ag);
|
||||||
|
bool begin(void);
|
||||||
|
void end(void);
|
||||||
|
void setText(String &line1, String &line2, String &line3);
|
||||||
|
void setText(const char *line1, const char *line2, const char *line3);
|
||||||
|
void setText(String &line1, String &line2, String &line3, String &line4);
|
||||||
|
void setText(const char *line1, const char *line2, const char *line3,
|
||||||
|
const char *line4);
|
||||||
|
void showDashboard(void);
|
||||||
|
void showDashboard(const char *status);
|
||||||
|
void setBrightness(int percent);
|
||||||
|
#ifdef ESP32
|
||||||
|
void showFirmwareUpdateVersion(String version);
|
||||||
|
void showFirmwareUpdateProgress(int percent);
|
||||||
|
void showFirmwareUpdateSuccess(int count);
|
||||||
|
void showFirmwareUpdateFailed(void);
|
||||||
|
void showFirmwareUpdateSkipped(void);
|
||||||
|
void showFirmwareUpdateUpToDate(void);
|
||||||
|
#else
|
||||||
|
|
||||||
|
#endif
|
||||||
|
void showRebooting(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _AG_OLED_DISPLAY_H_ */
|
26
src/AgSchedule.cpp
Normal file
26
src/AgSchedule.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#include "AgSchedule.h"
|
||||||
|
|
||||||
|
AgSchedule::AgSchedule(int period, void (*handler)(void))
|
||||||
|
: period(period), handler(handler) {}
|
||||||
|
|
||||||
|
AgSchedule::~AgSchedule() {}
|
||||||
|
|
||||||
|
void AgSchedule::run(void) {
|
||||||
|
uint32_t ms = (uint32_t)(millis() - count);
|
||||||
|
if (ms >= period) {
|
||||||
|
handler();
|
||||||
|
count = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set schedule period
|
||||||
|
*
|
||||||
|
* @param period Period in ms
|
||||||
|
*/
|
||||||
|
void AgSchedule::setPeriod(int period) { this->period = period; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update period
|
||||||
|
*/
|
||||||
|
void AgSchedule::update(void) { count = millis(); }
|
20
src/AgSchedule.h
Normal file
20
src/AgSchedule.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#ifndef _AG_SCHEDULE_H_
|
||||||
|
#define _AG_SCHEDULE_H_
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
class AgSchedule {
|
||||||
|
private:
|
||||||
|
int period;
|
||||||
|
void (*handler)(void);
|
||||||
|
uint32_t count;
|
||||||
|
|
||||||
|
public:
|
||||||
|
AgSchedule(int period, void (*handler)(void));
|
||||||
|
~AgSchedule();
|
||||||
|
void run(void);
|
||||||
|
void update(void);
|
||||||
|
void setPeriod(int period);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _AG_SCHEDULE_H_ */
|
869
src/AgStateMachine.cpp
Normal file
869
src/AgStateMachine.cpp
Normal file
@ -0,0 +1,869 @@
|
|||||||
|
#include "AgStateMachine.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 */
|
||||||
|
#define LED_LONG_BLINK_DELAY 2000 /** ms */
|
||||||
|
|
||||||
|
#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_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
|
||||||
|
*
|
||||||
|
* @param r
|
||||||
|
* @param g
|
||||||
|
* @param b
|
||||||
|
*/
|
||||||
|
void StateMachine::ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b) {
|
||||||
|
if (ledBarAnimationCount < 0) {
|
||||||
|
ledBarAnimationCount = 0;
|
||||||
|
ag->ledBar.setColor(r, g, b, ledBarAnimationCount);
|
||||||
|
} else {
|
||||||
|
ledBarAnimationCount++;
|
||||||
|
if (ledBarAnimationCount >= ag->ledBar.getNumberOfLeds()) {
|
||||||
|
ledBarAnimationCount = 0;
|
||||||
|
}
|
||||||
|
ag->ledBar.setColor(r, g, b, ledBarAnimationCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief LED status blink with delay
|
||||||
|
*
|
||||||
|
* @param ms Miliseconds
|
||||||
|
*/
|
||||||
|
void StateMachine::ledStatusBlinkDelay(uint32_t ms) {
|
||||||
|
ag->statusLed.setOn();
|
||||||
|
delay(ms);
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
delay(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Led bar show PM or CO2 led color status
|
||||||
|
*
|
||||||
|
* @return true if all led bar are used, false othwerwise
|
||||||
|
*/
|
||||||
|
bool StateMachine::sensorhandleLeds(void) {
|
||||||
|
int totalLedUsed = 0;
|
||||||
|
switch (config.getLedBarMode()) {
|
||||||
|
case LedBarMode::LedBarModeCO2:
|
||||||
|
totalLedUsed = co2handleLeds();
|
||||||
|
break;
|
||||||
|
case LedBarMode::LedBarModePm:
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
totalUsed = 5;
|
||||||
|
} else if (co2Value <= 1750) {
|
||||||
|
/** RRRRRR; 6 */
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 2);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 4);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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(*ag, config, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pm25Value <= 5) {
|
||||||
|
/** G; 1 */
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 6);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 4);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
|
||||||
|
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);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
|
||||||
|
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
|
||||||
|
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) {
|
||||||
|
if (config.isCo2CalibrationRequested() && config.hasSensorS8) {
|
||||||
|
logInfo("CO2 Calibration");
|
||||||
|
|
||||||
|
/** Count down to 0 then start */
|
||||||
|
for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) {
|
||||||
|
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
|
||||||
|
String str =
|
||||||
|
"after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec";
|
||||||
|
disp.setText("Start CO2 calib", str.c_str(), "");
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
String str = String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec";
|
||||||
|
disp.setText("CO2 Calib", "after", str.c_str());
|
||||||
|
} else {
|
||||||
|
logInfo("Start CO2 calib after " +
|
||||||
|
String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec");
|
||||||
|
}
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ag->s8.setBaselineCalibration()) {
|
||||||
|
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
|
||||||
|
disp.setText("Calibration", "success", "");
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
disp.setText("CO2 Calib", "success", "");
|
||||||
|
} else {
|
||||||
|
logInfo("CO2 Calibration: success");
|
||||||
|
}
|
||||||
|
delay(1000);
|
||||||
|
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
|
||||||
|
disp.setText("Wait for", "calib done", "...");
|
||||||
|
} else {
|
||||||
|
logInfo("CO2 Calibration: Wait for calibration finish...");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Count down wait for finish */
|
||||||
|
int count = 0;
|
||||||
|
while (ag->s8.isBaseLineCalibrationDone() == false) {
|
||||||
|
delay(1000);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
|
||||||
|
String str = "after " + String(count);
|
||||||
|
disp.setText("Calib done", str.c_str(), "sec");
|
||||||
|
} else {
|
||||||
|
logInfo("CO2 Calibration: finish after " + String(count) + " sec");
|
||||||
|
}
|
||||||
|
delay(2000);
|
||||||
|
} else {
|
||||||
|
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
|
||||||
|
disp.setText("Calibration", "failure!!!", "");
|
||||||
|
} else if (ag->isBasic()) {
|
||||||
|
disp.setText("CO2 calib", "failure!!!", "");
|
||||||
|
} else {
|
||||||
|
logInfo("CO2 Calibration: failure!!!");
|
||||||
|
}
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.getCO2CalibrationAbcDays() >= 0 && config.hasSensorS8) {
|
||||||
|
int newHour = config.getCO2CalibrationAbcDays() * 24;
|
||||||
|
int curHour = ag->s8.getAbcPeriod();
|
||||||
|
if (curHour != newHour) {
|
||||||
|
String resultStr = "failure";
|
||||||
|
if (ag->s8.setAbcPeriod(config.getCO2CalibrationAbcDays() * 24)) {
|
||||||
|
resultStr = "successful";
|
||||||
|
}
|
||||||
|
String fromStr = String(curHour / 24) + " days";
|
||||||
|
if (curHour == 0) {
|
||||||
|
fromStr = "off";
|
||||||
|
}
|
||||||
|
String toStr = String(config.getCO2CalibrationAbcDays()) + " days";
|
||||||
|
if (config.getCO2CalibrationAbcDays() == 0) {
|
||||||
|
toStr = "off";
|
||||||
|
}
|
||||||
|
String msg =
|
||||||
|
"Setting S8 from " + fromStr + " to " + toStr + " " + resultStr;
|
||||||
|
logInfo(msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logWarning("CO2 S8 not available, set 'abcDays' ignored");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateMachine::ledBarTest(void) {
|
||||||
|
if (config.isLedBarTestRequested()) {
|
||||||
|
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 if(ag->isOpenAir()) {
|
||||||
|
ledBarRunTest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateMachine::ledBarPowerUpTest(void) {
|
||||||
|
if (ag->isOne()) {
|
||||||
|
ag->ledBar.clear();
|
||||||
|
}
|
||||||
|
ledBarRunTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateMachine::ledBarRunTest(void) {
|
||||||
|
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) {
|
||||||
|
int r = 0;
|
||||||
|
int g = 0;
|
||||||
|
int b = 0;
|
||||||
|
switch (color) {
|
||||||
|
case 'g':
|
||||||
|
g = 255;
|
||||||
|
break;
|
||||||
|
case 'y':
|
||||||
|
r = 255;
|
||||||
|
g = 255;
|
||||||
|
break;
|
||||||
|
case 'o':
|
||||||
|
r = 255;
|
||||||
|
g = 128;
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
r = 255;
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
b = 255;
|
||||||
|
break;
|
||||||
|
case 'w':
|
||||||
|
r = 255;
|
||||||
|
g = 255;
|
||||||
|
b = 255;
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
r = 153;
|
||||||
|
b = 153;
|
||||||
|
break;
|
||||||
|
case 'z':
|
||||||
|
r = 102;
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ag->ledBar.setColor(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct a new Ag State Machine:: Ag State Machine object
|
||||||
|
*
|
||||||
|
* @param disp OledDisplay
|
||||||
|
* @param log Serial Stream
|
||||||
|
* @param value Measurements
|
||||||
|
* @param config Configuration
|
||||||
|
*/
|
||||||
|
StateMachine::StateMachine(OledDisplay &disp, Stream &log, Measurements &value,
|
||||||
|
Configuration &config)
|
||||||
|
: PrintLog(log, "StateMachine"), disp(disp), value(value), config(config) {}
|
||||||
|
|
||||||
|
StateMachine::~StateMachine() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief OLED display show content from state value
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
void StateMachine::displayHandle(AgStateMachineState state) {
|
||||||
|
// Ignore handle if not support display
|
||||||
|
if (!(ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic())) {
|
||||||
|
if (state == AgStateMachineCo2Calibration) {
|
||||||
|
co2Calibration();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state > AgStateMachineNormal) {
|
||||||
|
logError("displayHandle: State invalid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispState = state;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case AgStateMachineWiFiManagerMode:
|
||||||
|
case AgStateMachineWiFiManagerPortalActive: {
|
||||||
|
if (wifiConnectCountDown >= 0) {
|
||||||
|
if (ag->isBasic()) {
|
||||||
|
String ssid = "\"airgradient-" + ag->deviceId() + "\" " +
|
||||||
|
String(wifiConnectCountDown) + String("s");
|
||||||
|
disp.setText("Connect tohotspot:", ssid.c_str(), "");
|
||||||
|
} else {
|
||||||
|
String line1 = String(wifiConnectCountDown) + "s to connect";
|
||||||
|
String line2 = "to WiFi hotspot:";
|
||||||
|
String line3 = "\"airgradient-";
|
||||||
|
String line4 = ag->deviceId() + "\"";
|
||||||
|
disp.setText(line1, line2, line3, line4);
|
||||||
|
}
|
||||||
|
wifiConnectCountDown--;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiManagerStaConnecting: {
|
||||||
|
disp.setText("Trying to", "connect to WiFi", "...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiManagerStaConnected: {
|
||||||
|
disp.setText("WiFi connection", "successful", "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiOkServerConnecting: {
|
||||||
|
if (ag->isBasic()) {
|
||||||
|
disp.setText("Connecting", "to", "Server...");
|
||||||
|
} else {
|
||||||
|
disp.setText("Connecting to", "Server", "...");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiOkServerConnected: {
|
||||||
|
disp.setText("Server", "connection", "successful");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiManagerConnectFailed: {
|
||||||
|
disp.setText("WiFi not", "connected", "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiOkServerConnectFailed: {
|
||||||
|
// displayShowText("Server not", "reachable", "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiOkServerOkSensorConfigFailed: {
|
||||||
|
if (ag->isBasic()) {
|
||||||
|
disp.setText("Monitor", "not on", "dashboard");
|
||||||
|
} else {
|
||||||
|
disp.setText("Monitor not", "setup on", "dashboard");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiLost: {
|
||||||
|
disp.showDashboard("WiFi N/A");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineServerLost: {
|
||||||
|
disp.showDashboard("AG Server N/A");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineSensorConfigFailed: {
|
||||||
|
if (addToDashBoard) {
|
||||||
|
uint32_t ms = (uint32_t)(millis() - addToDashboardTime);
|
||||||
|
if (ms >= 5000) {
|
||||||
|
addToDashboardTime = millis();
|
||||||
|
if (addToDashBoardToggle) {
|
||||||
|
disp.showDashboard("Add to AG Dashb.");
|
||||||
|
} else {
|
||||||
|
disp.showDashboard(ag->deviceId().c_str());
|
||||||
|
}
|
||||||
|
addToDashBoardToggle = !addToDashBoardToggle;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disp.showDashboard("");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineNormal: {
|
||||||
|
disp.showDashboard();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineCo2Calibration:
|
||||||
|
co2Calibration();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief OLED display show content as previous state updated
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void StateMachine::displayHandle(void) { displayHandle(dispState); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update status add to dashboard
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void StateMachine::displaySetAddToDashBoard(void) {
|
||||||
|
if (addToDashBoard == false) {
|
||||||
|
addToDashboardTime = 0;
|
||||||
|
addToDashBoardToggle = true;
|
||||||
|
}
|
||||||
|
addToDashBoard = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateMachine::displayClearAddToDashBoard(void) { addToDashBoard = false; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set WiFi connection coundown on dashboard
|
||||||
|
*
|
||||||
|
* @param count Seconds
|
||||||
|
*/
|
||||||
|
void StateMachine::displayWiFiConnectCountDown(int count) {
|
||||||
|
wifiConnectCountDown = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Init before start LED bar animation
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void StateMachine::ledAnimationInit(void) { ledBarAnimationCount = -1; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle LED from state, only handle LED if board type is: One Indoor or
|
||||||
|
* Open Air
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
void StateMachine::handleLeds(AgStateMachineState state) {
|
||||||
|
/** Ignore if board type if not ONE_INDOOR or OPEN_AIR_OUTDOOR */
|
||||||
|
if ((ag->getBoardType() != BoardType::ONE_INDOOR) &&
|
||||||
|
(ag->getBoardType() != BoardType::OPEN_AIR_OUTDOOR)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state > AgStateMachineNormal) {
|
||||||
|
logError("ledHandle: state invalid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ledState = state;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiManagerStaConnecting: {
|
||||||
|
/** 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();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiManagerStaConnected: {
|
||||||
|
/** Connecting to WiFi worked */
|
||||||
|
if (ag->isOne()) {
|
||||||
|
ag->ledBar.clear();
|
||||||
|
ag->ledBar.setColor(255, 255, 255);
|
||||||
|
} else {
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiOkServerConnected: {
|
||||||
|
/** Server is reachable, all fine */
|
||||||
|
if (ag->isOne()) {
|
||||||
|
ag->ledBar.clear();
|
||||||
|
ag->ledBar.setColor(0, 255, 0);
|
||||||
|
} else {
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
|
||||||
|
/** two time slow blink, then off */
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
ledStatusBlinkDelay(LED_SLOW_BLINK_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
|
||||||
|
for (int j = 0; j < 3; j++) {
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
ledStatusBlinkDelay(LED_FAST_BLINK_DELAY);
|
||||||
|
}
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiOkServerConnectFailed: {
|
||||||
|
/** 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();
|
||||||
|
for (int j = 0; j < 3; j++) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
ledStatusBlinkDelay(LED_FAST_BLINK_DELAY);
|
||||||
|
}
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
for (int j = 0; j < 3; j++) {
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
ledStatusBlinkDelay(LED_FAST_BLINK_DELAY);
|
||||||
|
}
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineWiFiLost: {
|
||||||
|
/** Connection to WiFi network failed credentials incorrect encryption not
|
||||||
|
* supported etc. */
|
||||||
|
if (ag->isOne()) {
|
||||||
|
bool allUsed = sensorhandleLeds();
|
||||||
|
if (allUsed == false) {
|
||||||
|
ag->ledBar.setColor(255, 0, 0, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineServerLost: {
|
||||||
|
/** Connected to WiFi network but the server cannot be reached through the
|
||||||
|
* internet, e.g. blocked by firewall */
|
||||||
|
if (ag->isOne()) {
|
||||||
|
bool allUsed = sensorhandleLeds();
|
||||||
|
if (allUsed == false) {
|
||||||
|
ag->ledBar.setColor(233, 183, 54, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineSensorConfigFailed: {
|
||||||
|
/** Server is reachable but there is some configuration issue to be fixed on
|
||||||
|
* the server side */
|
||||||
|
if (ag->isOne()) {
|
||||||
|
bool allUsed = sensorhandleLeds();
|
||||||
|
if (allUsed == false) {
|
||||||
|
ag->ledBar.setColor(139, 24, 248, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineNormal: {
|
||||||
|
if (ag->isOne()) {
|
||||||
|
sensorhandleLeds();
|
||||||
|
} else {
|
||||||
|
ag->statusLed.setOff();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AgStateMachineLedBarTest:
|
||||||
|
ledBarTest();
|
||||||
|
break;
|
||||||
|
case AgStateMachineLedBarPowerUpTest:
|
||||||
|
ledBarPowerUpTest();
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show LED bar color
|
||||||
|
if (ag->isOne()) {
|
||||||
|
ag->ledBar.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle LED as previous state updated
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void StateMachine::handleLeds(void) { handleLeds(ledState); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set display state
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
void StateMachine::setDisplayState(AgStateMachineState state) {
|
||||||
|
dispState = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current display state
|
||||||
|
*
|
||||||
|
* @return AgStateMachineState
|
||||||
|
*/
|
||||||
|
AgStateMachineState StateMachine::getDisplayState(void) { return dispState; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set AirGradient instance
|
||||||
|
*
|
||||||
|
* @param ag Point to AirGradient instance
|
||||||
|
*/
|
||||||
|
void StateMachine::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current LED state
|
||||||
|
*
|
||||||
|
* @return AgStateMachineState
|
||||||
|
*/
|
||||||
|
AgStateMachineState StateMachine::getLedState(void) { return ledState; }
|
||||||
|
|
||||||
|
void StateMachine::executeCo2Calibration(void) {
|
||||||
|
displayHandle(AgStateMachineCo2Calibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateMachine::executeLedBarTest(void) {
|
||||||
|
handleLeds(AgStateMachineLedBarTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateMachine::executeLedBarPowerUpTest(void) {
|
||||||
|
handleLeds(AgStateMachineLedBarPowerUpTest);
|
||||||
|
}
|
57
src/AgStateMachine.h
Normal file
57
src/AgStateMachine.h
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#ifndef _AG_STATE_MACHINE_H_
|
||||||
|
#define _AG_STATE_MACHINE_H_
|
||||||
|
|
||||||
|
#include "AgOledDisplay.h"
|
||||||
|
#include "AgValue.h"
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "Main/PrintLog.h"
|
||||||
|
#include "App/AppDef.h"
|
||||||
|
|
||||||
|
class StateMachine : public PrintLog {
|
||||||
|
private:
|
||||||
|
// AgStateMachineState state;
|
||||||
|
AgStateMachineState ledState;
|
||||||
|
AgStateMachineState dispState;
|
||||||
|
AirGradient *ag;
|
||||||
|
OledDisplay &disp;
|
||||||
|
Measurements &value;
|
||||||
|
Configuration &config;
|
||||||
|
bool addToDashBoard = false;
|
||||||
|
bool addToDashBoardToggle = false;
|
||||||
|
uint32_t addToDashboardTime;
|
||||||
|
int wifiConnectCountDown;
|
||||||
|
int ledBarAnimationCount;
|
||||||
|
|
||||||
|
void ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b);
|
||||||
|
void ledStatusBlinkDelay(uint32_t delay);
|
||||||
|
bool sensorhandleLeds(void);
|
||||||
|
int co2handleLeds(void);
|
||||||
|
int pm25handleLeds(void);
|
||||||
|
void co2Calibration(void);
|
||||||
|
void ledBarTest(void);
|
||||||
|
void ledBarPowerUpTest(void);
|
||||||
|
void ledBarRunTest(void);
|
||||||
|
void runLedTest(char color);
|
||||||
|
|
||||||
|
public:
|
||||||
|
StateMachine(OledDisplay &disp, Stream &log,
|
||||||
|
Measurements &value, Configuration& config);
|
||||||
|
~StateMachine();
|
||||||
|
void setAirGradient(AirGradient* ag);
|
||||||
|
void displayHandle(AgStateMachineState state);
|
||||||
|
void displayHandle(void);
|
||||||
|
void displaySetAddToDashBoard(void);
|
||||||
|
void displayClearAddToDashBoard(void);
|
||||||
|
void displayWiFiConnectCountDown(int count);
|
||||||
|
void ledAnimationInit(void);
|
||||||
|
void handleLeds(AgStateMachineState state);
|
||||||
|
void handleLeds(void);
|
||||||
|
void setDisplayState(AgStateMachineState state);
|
||||||
|
AgStateMachineState getDisplayState(void);
|
||||||
|
AgStateMachineState getLedState(void);
|
||||||
|
void executeCo2Calibration(void);
|
||||||
|
void executeLedBarTest(void);
|
||||||
|
void executeLedBarPowerUpTest(void);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _AG_STATE_MACHINE_H_ */
|
1068
src/AgValue.cpp
Normal file
1068
src/AgValue.cpp
Normal file
File diff suppressed because it is too large
Load Diff
217
src/AgValue.h
Normal file
217
src/AgValue.h
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
#ifndef _AG_VALUE_H_
|
||||||
|
#define _AG_VALUE_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 <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() {}
|
||||||
|
~Measurements() {}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 the Corrected PM25 object based on the correction algorithm from configuration
|
||||||
|
*
|
||||||
|
* If correction is not enabled, then will return the raw value (either average or last value)
|
||||||
|
*
|
||||||
|
* @param ag AirGradient instance
|
||||||
|
* @param config Configuration instance
|
||||||
|
* @param useAvg Use moving average value if true, otherwise use latest value
|
||||||
|
* @param ch MeasurementType channel
|
||||||
|
* @return float Corrected PM2.5 value
|
||||||
|
*/
|
||||||
|
float getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg = false, int ch = 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* build json payload for every measurements
|
||||||
|
*/
|
||||||
|
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
|
||||||
|
Configuration &config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to true if want to debug every update value
|
||||||
|
*/
|
||||||
|
void setDebug(bool debug);
|
||||||
|
|
||||||
|
// TODO: update this to use setter
|
||||||
|
int bootCount;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode, AirGradient &ag,
|
||||||
|
Configuration &config);
|
||||||
|
JSONVar buildIndoor(bool localServer, AirGradient &ag, Configuration &config);
|
||||||
|
JSONVar buildPMS(AirGradient &ag, int ch, bool allCh, bool withTempHum, bool compensate);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /** _AG_VALUE_H_ */
|
415
src/AgWiFiConnector.cpp
Normal file
415
src/AgWiFiConnector.cpp
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
#include "AgWiFiConnector.h"
|
||||||
|
#include "Libraries/WiFiManager/WiFiManager.h"
|
||||||
|
|
||||||
|
#define WIFI_CONNECT_COUNTDOWN_MAX 180
|
||||||
|
#define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair"
|
||||||
|
|
||||||
|
#define WIFI() ((WiFiManager *)(this->wifi))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set reference AirGradient instance
|
||||||
|
*
|
||||||
|
* @param ag Point to AirGradient instance
|
||||||
|
*/
|
||||||
|
void WifiConnector::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct a new Ag Wi Fi Connector:: Ag Wi Fi Connector object
|
||||||
|
*
|
||||||
|
* @param disp OledDisplay
|
||||||
|
* @param log Stream
|
||||||
|
* @param sm StateMachine
|
||||||
|
*/
|
||||||
|
WifiConnector::WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm,
|
||||||
|
Configuration &config)
|
||||||
|
: PrintLog(log, "WifiConnector"), disp(disp), sm(sm), config(config) {}
|
||||||
|
|
||||||
|
WifiConnector::~WifiConnector() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Connection to WIFI AP process. Just call one times
|
||||||
|
*
|
||||||
|
* @return true Success
|
||||||
|
* @return false Failure
|
||||||
|
*/
|
||||||
|
bool WifiConnector::connect(void) {
|
||||||
|
if (wifi == NULL) {
|
||||||
|
wifi = new WiFiManager();
|
||||||
|
if (wifi == NULL) {
|
||||||
|
logError("Create 'WiFiManger' failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); });
|
||||||
|
WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); });
|
||||||
|
WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); });
|
||||||
|
WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();});
|
||||||
|
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
|
||||||
|
disp.setText("Connecting to", "WiFi", "...");
|
||||||
|
} else {
|
||||||
|
logInfo("Connecting to WiFi...");
|
||||||
|
}
|
||||||
|
ssid = "airgradient-" + ag->deviceId();
|
||||||
|
|
||||||
|
// ssid = "AG-" + String(ESP.getChipId(), HEX);
|
||||||
|
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
|
||||||
|
|
||||||
|
WiFiManagerParameter postToAg("chbPostToAg",
|
||||||
|
"Prevent Connection to AirGradient Server", "T",
|
||||||
|
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
|
||||||
|
WIFI()->addParameter(&postToAg);
|
||||||
|
WiFiManagerParameter postToAgInfo(
|
||||||
|
"<p>Prevent connection to the AirGradient Server. Important: Only enable "
|
||||||
|
"it if you are sure you don't want to use any AirGradient cloud "
|
||||||
|
"features. As a result you will not receive automatic firmware updates "
|
||||||
|
"and your data will not reach the AirGradient dashboard.</p>");
|
||||||
|
WIFI()->addParameter(&postToAgInfo);
|
||||||
|
|
||||||
|
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
logInfo("Wait for configure portal");
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
// Task handle WiFi connection.
|
||||||
|
xTaskCreate(
|
||||||
|
[](void *obj) {
|
||||||
|
WifiConnector *connector = (WifiConnector *)obj;
|
||||||
|
while (connector->_wifiConfigPortalActive()) {
|
||||||
|
connector->_wifiProcess();
|
||||||
|
vTaskDelay(1);
|
||||||
|
}
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
},
|
||||||
|
"wifi_cfg", 4096, this, 10, NULL);
|
||||||
|
|
||||||
|
/** Wait for WiFi connect and show LED, display status */
|
||||||
|
uint32_t dispPeriod = millis();
|
||||||
|
uint32_t ledPeriod = millis();
|
||||||
|
bool clientConnectChanged = false;
|
||||||
|
|
||||||
|
AgStateMachineState stateOld = sm.getDisplayState();
|
||||||
|
while (WIFI()->getConfigPortalActive()) {
|
||||||
|
/** LED animatoin and display update content */
|
||||||
|
if (WiFi.isConnected() == false) {
|
||||||
|
/** Display countdown */
|
||||||
|
uint32_t ms;
|
||||||
|
if (ag->isOne()) {
|
||||||
|
ms = (uint32_t)(millis() - dispPeriod);
|
||||||
|
if (ms >= 1000) {
|
||||||
|
dispPeriod = millis();
|
||||||
|
sm.displayHandle();
|
||||||
|
} else {
|
||||||
|
if (stateOld != sm.getDisplayState()) {
|
||||||
|
stateOld = sm.getDisplayState();
|
||||||
|
sm.displayHandle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** LED animations */
|
||||||
|
ms = (uint32_t)(millis() - ledPeriod);
|
||||||
|
if (ms >= 100) {
|
||||||
|
ledPeriod = millis();
|
||||||
|
sm.handleLeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for client connect to change led color */
|
||||||
|
bool clientConnected = wifiClientConnected();
|
||||||
|
if (clientConnected != clientConnectChanged) {
|
||||||
|
clientConnectChanged = clientConnected;
|
||||||
|
if (clientConnectChanged) {
|
||||||
|
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
|
||||||
|
} else {
|
||||||
|
sm.ledAnimationInit();
|
||||||
|
sm.handleLeds(AgStateMachineWiFiManagerMode);
|
||||||
|
if (ag->isOne()) {
|
||||||
|
sm.displayHandle(AgStateMachineWiFiManagerMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1); // avoid watchdog timer reset.
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
_wifiProcess();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Show display wifi connect result failed */
|
||||||
|
if (WiFi.isConnected() == false) {
|
||||||
|
sm.handleLeds(AgStateMachineWiFiManagerConnectFailed);
|
||||||
|
if (ag->isOne() || ag->isPro4_2() || ag->isPro3_3() || ag->isBasic()) {
|
||||||
|
sm.displayHandle(AgStateMachineWiFiManagerConnectFailed);
|
||||||
|
}
|
||||||
|
delay(6000);
|
||||||
|
} else {
|
||||||
|
hasConfig = true;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
hasPortalConfig = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Disconnect to current connected WiFi AP
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void WifiConnector::disconnect(void) {
|
||||||
|
if (WiFi.isConnected()) {
|
||||||
|
logInfo("Disconnect");
|
||||||
|
WiFi.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Has wifi STA connected to WIFI softAP (this device)
|
||||||
|
*
|
||||||
|
* @return true Connected
|
||||||
|
* @return false Not connected
|
||||||
|
*/
|
||||||
|
bool WifiConnector::wifiClientConnected(void) {
|
||||||
|
return WiFi.softAPgetStationNum() ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle WiFiManage softAP setup completed callback
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void WifiConnector::_wifiApCallback(void) {
|
||||||
|
sm.displayWiFiConnectCountDown(WIFI_CONNECT_COUNTDOWN_MAX);
|
||||||
|
sm.setDisplayState(AgStateMachineWiFiManagerMode);
|
||||||
|
sm.ledAnimationInit();
|
||||||
|
sm.handleLeds(AgStateMachineWiFiManagerMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle WiFiManager save configuration callback
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void WifiConnector::_wifiSaveConfig(void) {
|
||||||
|
sm.setDisplayState(AgStateMachineWiFiManagerStaConnected);
|
||||||
|
sm.handleLeds(AgStateMachineWiFiManagerStaConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle WiFiManager save parameter callback
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void WifiConnector::_wifiSaveParamCallback(void) {
|
||||||
|
sm.ledAnimationInit();
|
||||||
|
sm.handleLeds(AgStateMachineWiFiManagerStaConnecting);
|
||||||
|
sm.setDisplayState(AgStateMachineWiFiManagerStaConnecting);
|
||||||
|
hasPortalConfig = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check that WiFiManager Configure portal active
|
||||||
|
*
|
||||||
|
* @return true Active
|
||||||
|
* @return false Not-Active
|
||||||
|
*/
|
||||||
|
bool WifiConnector::_wifiConfigPortalActive(void) {
|
||||||
|
return WIFI()->getConfigPortalActive();
|
||||||
|
}
|
||||||
|
void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process WiFiManager connection
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void WifiConnector::_wifiProcess() {
|
||||||
|
#ifdef ESP32
|
||||||
|
WIFI()->process();
|
||||||
|
#else
|
||||||
|
/** Wait for WiFi connect and show LED, display status */
|
||||||
|
uint32_t dispPeriod = millis();
|
||||||
|
uint32_t ledPeriod = millis();
|
||||||
|
bool clientConnectChanged = false;
|
||||||
|
AgStateMachineState stateOld = sm.getDisplayState();
|
||||||
|
|
||||||
|
while (WIFI()->getConfigPortalActive()) {
|
||||||
|
WIFI()->process();
|
||||||
|
|
||||||
|
if (WiFi.isConnected() == false) {
|
||||||
|
/** Display countdown */
|
||||||
|
uint32_t ms;
|
||||||
|
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
|
||||||
|
ms = (uint32_t)(millis() - dispPeriod);
|
||||||
|
if (ms >= 1000) {
|
||||||
|
dispPeriod = millis();
|
||||||
|
sm.displayHandle();
|
||||||
|
logInfo("displayHandle state: " + String(sm.getDisplayState()));
|
||||||
|
} else {
|
||||||
|
if (stateOld != sm.getDisplayState()) {
|
||||||
|
stateOld = sm.getDisplayState();
|
||||||
|
sm.displayHandle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** LED animations */
|
||||||
|
ms = (uint32_t)(millis() - ledPeriod);
|
||||||
|
if (ms >= 100) {
|
||||||
|
ledPeriod = millis();
|
||||||
|
sm.handleLeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for client connect to change led color */
|
||||||
|
bool clientConnected = wifiClientConnected();
|
||||||
|
if (clientConnected != clientConnectChanged) {
|
||||||
|
clientConnectChanged = clientConnected;
|
||||||
|
if (clientConnectChanged) {
|
||||||
|
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
|
||||||
|
} else {
|
||||||
|
sm.ledAnimationInit();
|
||||||
|
sm.handleLeds(AgStateMachineWiFiManagerMode);
|
||||||
|
if (ag->isOne()) {
|
||||||
|
sm.displayHandle(AgStateMachineWiFiManagerMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO This is for basic
|
||||||
|
if (ag->getBoardType() == DIY_BASIC) {
|
||||||
|
if (!WiFi.isConnected()) {
|
||||||
|
// disp.setText("Booting", "offline", "mode");
|
||||||
|
Serial.println("failed to connect and hit timeout");
|
||||||
|
delay(2500);
|
||||||
|
} else {
|
||||||
|
hasConfig = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle and reconnect WiFi
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void WifiConnector::handle(void) {
|
||||||
|
// Ignore if WiFi is not configured
|
||||||
|
if (hasConfig == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.isConnected()) {
|
||||||
|
lastRetry = millis();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retry connect WiFi each 10sec */
|
||||||
|
uint32_t ms = (uint32_t)(millis() - lastRetry);
|
||||||
|
if (ms >= 10000) {
|
||||||
|
lastRetry = millis();
|
||||||
|
WiFi.reconnect();
|
||||||
|
logInfo("Re-Connect WiFi");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Is WiFi connected
|
||||||
|
*
|
||||||
|
* @return true Connected
|
||||||
|
* @return false Disconnected
|
||||||
|
*/
|
||||||
|
bool WifiConnector::isConnected(void) { return WiFi.isConnected(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset WiFi configuretion and connection, disconnect wifi before call
|
||||||
|
* this method
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void WifiConnector::reset(void) {
|
||||||
|
if(this->wifi == NULL) {
|
||||||
|
this->wifi = new WiFiManager();
|
||||||
|
if(this->wifi == NULL){
|
||||||
|
logInfo("reset failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WIFI()->resetSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get wifi RSSI
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
int WifiConnector::RSSI(void) { return WiFi.RSSI(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get WIFI IP as string format ex: 192.168.1.1
|
||||||
|
*
|
||||||
|
* @return String
|
||||||
|
*/
|
||||||
|
String WifiConnector::localIpStr(void) { return WiFi.localIP().toString(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get status that wifi has configurated
|
||||||
|
*
|
||||||
|
* @return true Configurated
|
||||||
|
* @return false Not Configurated
|
||||||
|
*/
|
||||||
|
bool WifiConnector::hasConfigurated(void) {
|
||||||
|
if (WiFi.SSID().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get WiFi connection porttal timeout.
|
||||||
|
*
|
||||||
|
* @return true
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set wifi connect to default WiFi
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void WifiConnector::setDefault(void) {
|
||||||
|
WiFi.begin("airgradient", "cleanair");
|
||||||
|
}
|
55
src/AgWiFiConnector.h
Normal file
55
src/AgWiFiConnector.h
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#ifndef _AG_WIFI_CONNECTOR_H_
|
||||||
|
#define _AG_WIFI_CONNECTOR_H_
|
||||||
|
|
||||||
|
#include "AgOledDisplay.h"
|
||||||
|
#include "AgStateMachine.h"
|
||||||
|
#include "AirGradient.h"
|
||||||
|
#include "AgConfigure.h"
|
||||||
|
#include "Main/PrintLog.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
class WifiConnector : public PrintLog {
|
||||||
|
private:
|
||||||
|
AirGradient *ag;
|
||||||
|
OledDisplay &disp;
|
||||||
|
StateMachine &sm;
|
||||||
|
Configuration &config;
|
||||||
|
|
||||||
|
String ssid;
|
||||||
|
void *wifi = NULL;
|
||||||
|
bool hasConfig;
|
||||||
|
uint32_t lastRetry;
|
||||||
|
bool hasPortalConfig = false;
|
||||||
|
bool connectorTimeout = false;
|
||||||
|
|
||||||
|
bool wifiClientConnected(void);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void setAirGradient(AirGradient *ag);
|
||||||
|
|
||||||
|
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config);
|
||||||
|
~WifiConnector();
|
||||||
|
|
||||||
|
bool connect(void);
|
||||||
|
void disconnect(void);
|
||||||
|
void handle(void);
|
||||||
|
void _wifiApCallback(void);
|
||||||
|
void _wifiSaveConfig(void);
|
||||||
|
void _wifiSaveParamCallback(void);
|
||||||
|
bool _wifiConfigPortalActive(void);
|
||||||
|
void _wifiTimeoutCallback(void);
|
||||||
|
void _wifiProcess();
|
||||||
|
bool isConnected(void);
|
||||||
|
void reset(void);
|
||||||
|
int RSSI(void);
|
||||||
|
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_ */
|
@ -1,6 +1,9 @@
|
|||||||
#include "AirGradient.h"
|
#include "AirGradient.h"
|
||||||
|
#ifdef ESP8266
|
||||||
#define AG_LIB_VER "3.0.3"
|
#include <ESP8266WiFi.h>
|
||||||
|
#else
|
||||||
|
#include "WiFi.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
AirGradient::AirGradient(BoardType type)
|
AirGradient::AirGradient(BoardType type)
|
||||||
: pms5003(type), pms5003t_1(type), pms5003t_2(type), s8(type), sgp41(type),
|
: pms5003(type), pms5003t_1(type), pms5003t_2(type), s8(type), sgp41(type),
|
||||||
@ -33,6 +36,52 @@ int AirGradient::getI2cSclPin(void) {
|
|||||||
return bsp->I2C.scl_pin;
|
return bsp->I2C.scl_pin;
|
||||||
}
|
}
|
||||||
|
|
||||||
String AirGradient::getVersion(void) { return AG_LIB_VER; }
|
String AirGradient::getVersion(void) { return GIT_VERSION; }
|
||||||
|
|
||||||
BoardType AirGradient::getBoardType(void) { return boardType; }
|
BoardType AirGradient::getBoardType(void) { return boardType; }
|
||||||
|
|
||||||
|
double AirGradient::round2(double value) {
|
||||||
|
double ret;
|
||||||
|
if (value >= 0) {
|
||||||
|
ret = (int)(value * 100 + 0.5f);
|
||||||
|
} else {
|
||||||
|
ret = (int)(value * 100 - 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
String AirGradient::getBoardName(void) {
|
||||||
|
return String(getBoardDefName(boardType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Board Type is ONE_INDOOR
|
||||||
|
*
|
||||||
|
* @return true ONE_INDOOR
|
||||||
|
* @return false Other
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AirGradient::isPro3_3(void) {
|
||||||
|
return boardType == BoardType::DIY_PRO_INDOOR_V3_3;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AirGradient::isBasic(void) { return boardType == BoardType::DIY_BASIC; }
|
||||||
|
|
||||||
|
String AirGradient::deviceId(void) {
|
||||||
|
String mac = WiFi.macAddress();
|
||||||
|
mac.replace(":", "");
|
||||||
|
mac.toLowerCase();
|
||||||
|
return mac;
|
||||||
|
}
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
#ifndef _AIR_GRADIENT_H_
|
#ifndef _AIR_GRADIENT_H_
|
||||||
#define _AIR_GRADIENT_H_
|
#define _AIR_GRADIENT_H_
|
||||||
|
|
||||||
#include "display/oled.h"
|
#include "Display/Display.h"
|
||||||
#include "main/BoardDef.h"
|
#include "Main/BoardDef.h"
|
||||||
#include "main/HardwareWatchdog.h"
|
#include "Main/HardwareWatchdog.h"
|
||||||
#include "main/LedBar.h"
|
#include "Main/LedBar.h"
|
||||||
#include "main/PushButton.h"
|
#include "Main/PushButton.h"
|
||||||
#include "main/StatusLed.h"
|
#include "Main/StatusLed.h"
|
||||||
#include "pms/pms5003.h"
|
#include "PMS/PMS5003.h"
|
||||||
#include "pms/pms5003t.h"
|
#include "PMS/PMS5003T.h"
|
||||||
#include "s8/s8.h"
|
#include "S8/S8.h"
|
||||||
#include "sgp41/sgp41.h"
|
#include "Sgp41/Sgp41.h"
|
||||||
#include "sht/sht.h"
|
#include "Sht/Sht.h"
|
||||||
|
#include "Main/utils.h"
|
||||||
|
|
||||||
|
#ifndef GIT_VERSION
|
||||||
|
#define GIT_VERSION "3.1.14-snap"
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Class with define all the sensor has supported by Airgradient. Each
|
* @brief Class with define all the sensor has supported by Airgradient. Each
|
||||||
@ -107,6 +112,67 @@ public:
|
|||||||
*/
|
*/
|
||||||
String getVersion(void);
|
String getVersion(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Board Name object
|
||||||
|
*
|
||||||
|
* @return String
|
||||||
|
*/
|
||||||
|
String getBoardName(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Round double value with for 2 decimal
|
||||||
|
*
|
||||||
|
* @param valuem Round value
|
||||||
|
* @return double
|
||||||
|
*/
|
||||||
|
double round2(double value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check that Airgradient object is ONE_INDOOR
|
||||||
|
*
|
||||||
|
* @return true Yes
|
||||||
|
* @return false No
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @return true Yes
|
||||||
|
* @return false No
|
||||||
|
*/
|
||||||
|
bool isPro4_2(void);
|
||||||
|
/**
|
||||||
|
* @brief Check that Airgradient object is DIY_PRO 3.7 indoor
|
||||||
|
*
|
||||||
|
* @return true Yes
|
||||||
|
* @return false No
|
||||||
|
*/
|
||||||
|
bool isPro3_3(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check that Airgradient object is DIY_BASIC
|
||||||
|
*
|
||||||
|
* @return true Yes
|
||||||
|
* @return false No
|
||||||
|
*/
|
||||||
|
bool isBasic(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get device Id
|
||||||
|
*
|
||||||
|
* @return String
|
||||||
|
*/
|
||||||
|
String deviceId(void);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BoardType boardType;
|
BoardType boardType;
|
||||||
};
|
};
|
||||||
|
27
src/App/AppDef.cpp
Normal file
27
src/App/AppDef.cpp
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#include "AppDef.h"
|
||||||
|
|
||||||
|
const char *AgFirmwareModeName(AgFirmwareMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case FW_MODE_I_9PSL:
|
||||||
|
return "I-9PSL";
|
||||||
|
case FW_MODE_O_1PP:
|
||||||
|
return "O-1PP";
|
||||||
|
case FW_MODE_O_1PPT:
|
||||||
|
return "O-1PPT";
|
||||||
|
case FW_MODE_O_1PST:
|
||||||
|
return "O-1PST";
|
||||||
|
case FW_MODE_O_1PS:
|
||||||
|
return "0-1PS";
|
||||||
|
case FW_MODE_O_1P:
|
||||||
|
return "O-1P";
|
||||||
|
case FW_MODE_I_42PS:
|
||||||
|
return "DIY-PRO-I-4.2PS";
|
||||||
|
case FW_MODE_I_33PS:
|
||||||
|
return "DIY-PRO-I-3.3PS";
|
||||||
|
case FW_MODE_I_BASIC_40PS:
|
||||||
|
return "DIY-BASIC-I-4.0PS";
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
122
src/App/AppDef.h
Normal file
122
src/App/AppDef.h
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#ifndef _APP_DEF_H_
|
||||||
|
#define _APP_DEF_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Application state machine state
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
enum AgStateMachineState {
|
||||||
|
/** In WiFi Manger Mode */
|
||||||
|
AgStateMachineWiFiManagerMode,
|
||||||
|
|
||||||
|
/** WiFi Manager has connected to mobile phone */
|
||||||
|
AgStateMachineWiFiManagerPortalActive,
|
||||||
|
|
||||||
|
/** After SSID and PW entered and OK clicked, connection to WiFI network is
|
||||||
|
attempted*/
|
||||||
|
AgStateMachineWiFiManagerStaConnecting,
|
||||||
|
|
||||||
|
/** Connecting to WiFi worked */
|
||||||
|
AgStateMachineWiFiManagerStaConnected,
|
||||||
|
|
||||||
|
/** Once connected to WiFi an attempt to reach the server is performed */
|
||||||
|
AgStateMachineWiFiOkServerConnecting,
|
||||||
|
|
||||||
|
/** Server is reachable, all fine */
|
||||||
|
AgStateMachineWiFiOkServerConnected,
|
||||||
|
|
||||||
|
/** =================================== *
|
||||||
|
* Exceptions during WIFi Setup *
|
||||||
|
* =================================== **/
|
||||||
|
/** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */
|
||||||
|
AgStateMachineWiFiManagerConnectFailed,
|
||||||
|
|
||||||
|
/** Connected to WiFi but server not reachable, e.g. firewall
|
||||||
|
block/whitelisting needed etc. */
|
||||||
|
AgStateMachineWiFiOkServerConnectFailed,
|
||||||
|
|
||||||
|
/** Server reachable but sensor not configured correctly*/
|
||||||
|
AgStateMachineWiFiOkServerOkSensorConfigFailed,
|
||||||
|
|
||||||
|
/** =================================== *
|
||||||
|
* During Normal Operation *
|
||||||
|
* =================================== **/
|
||||||
|
|
||||||
|
/** Connection to WiFi network failed credentials incorrect encryption not
|
||||||
|
supported etc. */
|
||||||
|
AgStateMachineWiFiLost,
|
||||||
|
|
||||||
|
/** Connected to WiFi network but the server cannot be reached through the
|
||||||
|
internet, e.g. blocked by firewall */
|
||||||
|
AgStateMachineServerLost,
|
||||||
|
|
||||||
|
/** Server is reachable but there is some configuration issue to be fixed on
|
||||||
|
the server side */
|
||||||
|
AgStateMachineSensorConfigFailed,
|
||||||
|
|
||||||
|
/** CO2 calibration */
|
||||||
|
AgStateMachineCo2Calibration,
|
||||||
|
|
||||||
|
/* LED bar testing */
|
||||||
|
AgStateMachineLedBarTest,
|
||||||
|
AgStateMachineLedBarPowerUpTest,
|
||||||
|
|
||||||
|
/** OTA perform, show display status */
|
||||||
|
AgStateMachineOtaPerform,
|
||||||
|
|
||||||
|
/** LED: Show working state.
|
||||||
|
* Display: Show dashboard */
|
||||||
|
AgStateMachineNormal,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RGB LED bar mode for ONE_INDOOR board
|
||||||
|
*/
|
||||||
|
enum LedBarMode {
|
||||||
|
/** Don't use LED bar */
|
||||||
|
LedBarModeOff,
|
||||||
|
|
||||||
|
/** Use LED bar for show PM2.5 value level */
|
||||||
|
LedBarModePm,
|
||||||
|
|
||||||
|
/** Use LED bar for show CO2 value level */
|
||||||
|
LedBarModeCO2,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ConfigurationControl {
|
||||||
|
/** Allow set configuration from local over device HTTP server */
|
||||||
|
ConfigurationControlLocal,
|
||||||
|
|
||||||
|
/** Allow set configuration from Airgradient cloud */
|
||||||
|
ConfigurationControlCloud,
|
||||||
|
|
||||||
|
/** Allow set configuration from Local and Cloud */
|
||||||
|
ConfigurationControlBoth
|
||||||
|
};
|
||||||
|
|
||||||
|
enum PMCorrectionAlgorithm {
|
||||||
|
Unknown, // Unknown algorithm
|
||||||
|
None, // No PM correction
|
||||||
|
EPA_2021,
|
||||||
|
SLR_PMS5003_20220802,
|
||||||
|
SLR_PMS5003_20220803,
|
||||||
|
SLR_PMS5003_20220824,
|
||||||
|
SLR_PMS5003_20231030,
|
||||||
|
SLR_PMS5003_20231218,
|
||||||
|
SLR_PMS5003_20240104,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum AgFirmwareMode {
|
||||||
|
FW_MODE_I_9PSL, /** ONE_INDOOR */
|
||||||
|
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
|
||||||
|
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
|
||||||
|
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
|
||||||
|
FW_MODE_O_1PS, /** PMS5003T, S8 */
|
||||||
|
FW_MODE_O_1P, /** PMS5003T */
|
||||||
|
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
|
||||||
|
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
|
||||||
|
FW_MODE_I_BASIC_40PS, /** DIY_BASIC 4.0 */
|
||||||
|
};
|
||||||
|
const char *AgFirmwareModeName(AgFirmwareMode mode);
|
||||||
|
|
||||||
|
#endif /** _APP_DEF_H_ */
|
@ -1,6 +1,6 @@
|
|||||||
#include "oled.h"
|
#include "Display.h"
|
||||||
#include "../library/Adafruit_SH110x/Adafruit_SH110X.h"
|
#include "../Libraries/Adafruit_SH110x/Adafruit_SH110X.h"
|
||||||
#include "../library/Adafruit_SSD1306_Wemos_OLED/Adafruit_SSD1306.h"
|
#include "../Libraries/Adafruit_SSD1306_Wemos_OLED/Adafruit_SSD1306.h"
|
||||||
|
|
||||||
#define disp(func) \
|
#define disp(func) \
|
||||||
if (this->_boardType == DIY_BASIC) { \
|
if (this->_boardType == DIY_BASIC) { \
|
@ -1,7 +1,7 @@
|
|||||||
#ifndef _AIR_GRADIENT_OLED_H_
|
#ifndef _AIR_GRADIENT_OLED_H_
|
||||||
#define _AIR_GRADIENT_OLED_H_
|
#define _AIR_GRADIENT_OLED_H_
|
||||||
|
|
||||||
#include "../main/BoardDef.h"
|
#include "../Main/BoardDef.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user