Compare commits
820 Commits
Author | SHA1 | Date | |
---|---|---|---|
831c844c24 | |||
060a7f6815 | |||
d8eb6b3c1a | |||
969858b5cb | |||
09b5805686 | |||
b09b753339 | |||
ddb3dba131 | |||
e780b0ace6 | |||
e82da5401e | |||
50a98acde4 | |||
7049d21a41 | |||
d5cdeaa9f3 | |||
09207c6923 | |||
0a64424196 | |||
5b38ca222b | |||
9ee35341a5 | |||
cec0514444 | |||
626a2240fa | |||
174ec6568f | |||
6b55719399 | |||
e2084f0738 | |||
5e07923690 | |||
04049439b1 | |||
c148d256d7 | |||
02849a1938 | |||
074337a96d | |||
4daa817a0b | |||
81a4502952 | |||
764e2eae38 | |||
79bf9811be | |||
9475724d0c | |||
e7603a7659 | |||
9bba89722e | |||
81945a358e | |||
3d26a54d69 | |||
b70ee75d50 | |||
c6846c818a | |||
0b1c901a76 | |||
83504c8628 | |||
4487992748 | |||
3c8a65a329 | |||
673d564ddb | |||
423eb4808f | |||
18a710ffc2 | |||
040cb79a4d | |||
52d3dc03f1 | |||
1c6bc3ec55 | |||
34d7c93e14 | |||
fee1dc25d6 | |||
9fb01d42f4 | |||
7bb013939c | |||
0da21155e7 | |||
7a153cc0ea | |||
b079c35e6b | |||
6051e183b8 | |||
c95379b957 | |||
0cae8bc185 | |||
5902a4c8e4 | |||
66818cd075 | |||
c1a6ddc68f | |||
20a32dd22c | |||
263dc9934e | |||
61b863b7f1 | |||
e01c1029fe | |||
ba5d817739 | |||
a91747e379 | |||
029457c3fa | |||
55710dd4d9 | |||
4886163cda | |||
7c57477238 | |||
9ed58d1853 | |||
6c52b038e9 | |||
2f69932ef7 | |||
1d96a274a6 | |||
df9f6dfc95 | |||
3fc02b3f54 | |||
958ed0bd80 | |||
e9be9dcc83 | |||
7fbab82088 | |||
decdecdf22 | |||
145c612867 | |||
37de127887 | |||
baf80ce250 | |||
80100e2475 | |||
d9c3fc6ec4 | |||
67d377a514 | |||
fff982f35f | |||
86cd90b94a | |||
656509c74d | |||
01f83cb02e | |||
5c9c25c6b5 | |||
9291598209 | |||
429adb5e5e | |||
4e651afc8c | |||
859abbe177 | |||
f079bb30d2 | |||
070a103234 | |||
ef87cde9d6 | |||
ea5e23b307 | |||
c2a26e78a0 | |||
0297059e91 | |||
30622fca99 | |||
7c2aa35e4f | |||
e93009f31c | |||
26db6372cd | |||
d94ebbc570 | |||
299234ac40 | |||
76b2b3f940 | |||
bf09b746c7 | |||
b5c67cb0b1 | |||
5f40a327b3 | |||
66b0c63de5 | |||
cc3228f49a | |||
8728589ca1 | |||
4b356920c2 | |||
c94b886360 | |||
e056e44917 | |||
3b00fa69b8 | |||
e0720ac580 | |||
0861c2dcaa | |||
59fc0c409b | |||
6d63fdf643 | |||
033358e2c2 | |||
47034f62b4 | |||
71a21ce7e6 | |||
3f5e5eebbb | |||
f9be400a5d | |||
54808ac076 | |||
063bb2a227 | |||
93f79173b2 | |||
615c2389e7 | |||
f972637cca | |||
4c7e72b8e7 | |||
d4b4f51c3c | |||
1c42ff083d | |||
17d2e62b15 | |||
38aebeb50a | |||
b0f5263829 | |||
3226c14b6d | |||
0e26aa1b5d | |||
830f652bf9 | |||
bd9dbec663 | |||
29d701780a | |||
15869be234 | |||
4b09b98524 | |||
afd498074b | |||
e851d0781c | |||
2c27c6904c | |||
03f1b969c2 | |||
85ba13de12 | |||
6ec545b00e | |||
05dbe60db2 | |||
d2ee3a5d24 | |||
1839664137 | |||
154f3ecf8a | |||
b75e40b800 | |||
84a358291b | |||
0e41b2d630 | |||
3e48a562e7 | |||
f0c4df42b7 | |||
a50e1e2472 | |||
c8f0e6a0d2 | |||
1537d5d480 | |||
32c78f6018 | |||
c5c0dae4bb | |||
4bb97fc8be | |||
a28931493a | |||
af16c1c060 | |||
1666923ab3 | |||
89475ddf95 | |||
20db9d699b | |||
88c2437907 | |||
e9b27185b4 | |||
4534f7319a | |||
c842346724 | |||
92e74feabd | |||
cc0fd88068 | |||
56809a412c | |||
6a83743e2a | |||
faaf051e39 | |||
5bc1821ef9 | |||
280ea5e997 | |||
e95627ece6 | |||
80b9ae11d8 | |||
1937e3d59e | |||
107fb21331 | |||
ccc1ab463a | |||
38e792b88d | |||
aeee0cad01 | |||
401326d00d | |||
fb0dcad54d | |||
3556e4a96a | |||
283646a699 | |||
6312612ada | |||
c1f22674e2 | |||
40d38a75d8 | |||
39ef69cbdf | |||
3473e30e2e | |||
566f8a63b4 | |||
9e4d52454b | |||
5f5e985309 | |||
d638573ca7 | |||
4c165b31f5 | |||
2be91b3968 | |||
3ca2d1d208 | |||
aad12fc868 | |||
79fbd901bd | |||
3644dc43fe | |||
03fa62d8f0 | |||
902a768f28 | |||
1de9344f43 | |||
46f6309b77 | |||
a6b48acb41 | |||
1b4d89e1a1 | |||
0d2b0fb657 | |||
9f08af44b0 | |||
6b661cdeb7 | |||
dc299c4b54 | |||
2f595b4e41 | |||
a30535f75f | |||
a513943cba | |||
96bb6952fb | |||
10653bfe26 | |||
c7f89fa7b7 | |||
b11c461b60 | |||
404c14aad2 | |||
bfbae680fd | |||
3ae5982380 | |||
db2c2ef052 | |||
593547cdbe | |||
673c46950d | |||
ae0b4038d4 | |||
cac0bd5355 | |||
3d6203dabf | |||
1db8fbefe9 | |||
d850d27dc1 | |||
f49e4a4b37 | |||
75f88b0009 | |||
c6961b3ca8 | |||
ade72ff3b8 | |||
9fbbea22ff | |||
7b0381dea3 | |||
5867d0f1d5 | |||
a98d77e0c3 | |||
641003f9d1 | |||
0275aee370 | |||
ea46b812c1 | |||
16c932962a | |||
f90b2e1a07 | |||
3a9bb16c09 | |||
bb754edc51 | |||
1d991b1004 | |||
3ebcc584a4 | |||
4d40ae421c | |||
3004a82e7e | |||
4af5ca2665 | |||
e6696f3d41 | |||
2b33823162 | |||
bf0768c7da | |||
33e2977eb4 | |||
85e779cfc2 | |||
4783684443 | |||
3b0c77ca4d | |||
eeba41f497 | |||
fd1f35f6d8 | |||
eb76eff403 | |||
4673999dda | |||
83aa6a4502 | |||
8a87b865e6 | |||
c3068be6e9 | |||
63bb5f8ddb | |||
8548d3e9f4 | |||
f7e1363da9 | |||
2ffe0a62aa | |||
2cda36ed0d | |||
7de2d0cc30 | |||
f478dd16c8 | |||
43ca0a2c2e | |||
84884d0c15 | |||
f36f860c2e | |||
e47a9057ea | |||
399b4ca1dc | |||
2e4f4643fa | |||
0ccf46c219 | |||
76a2f332d7 | |||
ed344d3e1a | |||
2082a2fa93 | |||
e145d32714 | |||
a2c19438c0 | |||
ac838efdb5 | |||
751d4e8380 | |||
6925b1ac9a | |||
77a23b4202 | |||
ea91cf9b6c | |||
467b3e8637 | |||
2a5cf78b68 | |||
9c09b82efd | |||
60d01c0d94 | |||
e7a91c53bc | |||
4e41fd5d71 | |||
fe4389bff4 | |||
9325830fad | |||
b86f0d45e3 | |||
210f0a5ff9 | |||
c841476ca4 | |||
359394af53 | |||
b8e10f473e | |||
cb511903ef | |||
ebb3f01dcd | |||
2e0ba26c97 | |||
c1a4758c6c | |||
0370a8aa15 | |||
863a37132a | |||
612317d976 | |||
8873bacf55 | |||
bf2388b121 | |||
b3918bd1fb | |||
2a6fce674e | |||
2f0663ced0 | |||
3adf58537a | |||
e10c9ff854 | |||
12c6ec9910 | |||
d108b63a57 | |||
6e212714fc | |||
866684eb30 | |||
9d01479406 | |||
20245f2110 | |||
3890919f54 | |||
76e40fea8c | |||
c4024f49fb | |||
ca5fc8d65b | |||
fd2cef153e | |||
507b958fdf | |||
335c29ebb1 | |||
2907d6f14e | |||
c8d5b546ed | |||
b7cfdc4c4d | |||
994d281e02 | |||
39470384e4 | |||
c25ba764bf | |||
826ff00f42 | |||
520550037d | |||
90f336dee7 | |||
0d39643e76 | |||
21232ec49d | |||
b7339de31f | |||
013fb94307 | |||
e16373a64d | |||
f929623443 | |||
59587ce2b7 | |||
9ec74450a5 | |||
28096e9faf | |||
682378a47c | |||
a1861be7b7 | |||
99ddd24432 | |||
29491e4cbe | |||
87cc3fc45f | |||
7471d8079a | |||
8b0fe967f1 | |||
6f1cef4e67 | |||
02b63ff816 | |||
228bf83e92 | |||
d3534cda52 | |||
aafaa42a68 | |||
2e9ff0d7dd | |||
244b7814a6 | |||
28d27ee8fd | |||
753f22923c | |||
c45901706f | |||
663836e277 | |||
d39e10908d | |||
c52962d628 | |||
6b65efd3d6 | |||
8bb87a75ef | |||
1afcca25a1 | |||
17238cff86 | |||
03e2afbf54 | |||
104d58a8c0 | |||
7a988ea6c1 | |||
54ed83cb89 | |||
e461b92c9f | |||
db21648e91 | |||
a9654506f5 | |||
63f653d5cd | |||
b049a23657 | |||
d6766ef68b | |||
6c3259b94b | |||
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 |
64
.github/workflows/check.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
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"
|
||||
core_version: "3.1.2"
|
||||
core_url: "https://arduino.esp8266.com/stable/package_esp8266com_index.json"
|
||||
- fqbn: "esp32:esp32:esp32c3"
|
||||
core: "esp32:esp32"
|
||||
core_version: "2.0.17"
|
||||
core_url: "https://espressif.github.io/arduino-esp32/package_esp32_index.json"
|
||||
board_options: "JTAGAdapter=default,CDCOnBoot=cdc,PartitionScheme=min_spiffs,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=verbose,EraseFlash=none"
|
||||
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-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'true'
|
||||
- uses: arduino/compile-sketches@v1.1.2
|
||||
with:
|
||||
fqbn: ${{ matrix.fqbn }}
|
||||
sketch-paths: |
|
||||
examples/${{ matrix.example }}
|
||||
libraries: |
|
||||
- source-path: ./
|
||||
cli-compile-flags: |
|
||||
- --warnings
|
||||
- none
|
||||
- --board-options
|
||||
- "${{ matrix.board_options }}"
|
||||
platforms: |
|
||||
- name: ${{ matrix.core }}
|
||||
version: ${{ matrix.core_version}}
|
||||
source-url: ${{ matrix.core_url }}
|
||||
enable-deltas-report: true
|
||||
|
||||
# TODO: at this point it would be a good idea to run some smoke tests on
|
||||
# the resulting image (e.g. that it boots successfully and sends metrics)
|
||||
# but that would either require a high fidelity device emulator, or a
|
||||
# "hardware lab" runner that is directly connected to a relevant device.
|
10
.gitignore
vendored
@ -1,2 +1,10 @@
|
||||
.vscode
|
||||
*.DS_Store
|
||||
build
|
||||
.vscode
|
||||
/.idea/
|
||||
.pio
|
||||
.cache
|
||||
.clangd
|
||||
logs
|
||||
gen_compile_commands.py
|
||||
compile_commands.json
|
||||
|
6
.gitmodules
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
[submodule "src/Libraries/airgradient-client"]
|
||||
path = src/Libraries/airgradient-client
|
||||
url = ../../airgradienthq/airgradient-client.git
|
||||
[submodule "src/Libraries/airgradient-ota"]
|
||||
path = src/Libraries/airgradient-ota
|
||||
url = ../../airgradienthq/airgradient-ota.git
|
@ -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/).
|
||||
|
||||
## 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
|
||||
|
||||
- [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 I2C SGP41](https://github.com/Sensirion/arduino-i2c-sgp41)
|
||||
- [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
|
||||
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||
|
105
docs/howto-compile.md
Normal file
@ -0,0 +1,105 @@
|
||||
# How to compile AirGradient firmware on Arduino IDE
|
||||
|
||||
## Prequisite
|
||||
|
||||
Arduino IDE version 2.x ([download](https://www.arduino.cc/en/software))
|
||||
|
||||
> For AirGradient model ONE and Open Air, the codebase **WILL NOT** work on the latest major version of arduino-esp32 which is *3.x* . This related to when installing "esp32 by Espressif Systems" in board manager. Instead use version **2.0.17**, please follow the first step carefully.
|
||||
|
||||
## Steps for ESP32C3 based board (ONE and Open Air Model)
|
||||
|
||||
1. Install "esp32 by Espressif Systems" in board manager with version **2.0.17** (Tools ➝ Board ➝ Boards Manager ➝ search for `"espressif"`)
|
||||
|
||||

|
||||
|
||||
2. Install AirGradient library
|
||||
|
||||
#### Version < 3.2.0
|
||||
|
||||
Using library manager install the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
|
||||
|
||||

|
||||
|
||||
#### Version >= 3.3.0
|
||||
|
||||
- From your terminal, go to Arduino libraries folder (windows and mac: `Documents/Arduino/libraries` or linux: `~/Arduino/Libraries`).
|
||||
- With **git** cli, execute this command `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
|
||||
- Restart Arduino IDE
|
||||
|
||||
3. On tools tab, follow settings below
|
||||
|
||||
```
|
||||
Board ➝ ESP32C3 Dev Module
|
||||
USB CDC On Boot ➝ Enabled
|
||||
CPU Frequency ➝ 160MHz (WiFi)
|
||||
Core Debug Level ➝ Info
|
||||
Erase All Flash Before Sketch Upload ➝ Enabled (or choose as needed)
|
||||
Flash Frequency ➝ 80MHz
|
||||
Flash Mode ➝ QIO
|
||||
Flash Size ➝ 4MB (32Mb)
|
||||
JTAG Adapter ➝ Disabled
|
||||
Partition Scheme ➝ Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)
|
||||
Upload Speed ➝ 921600
|
||||
```
|
||||
|
||||
4. Open sketch to compile (File ➝ Examples ➝ AirGradient Air Quality Sensor ➝ OneOpenAir). This sketch for AirGradient ONE and Open Air monitor model
|
||||
5. Compile
|
||||
|
||||

|
||||
|
||||
## Steps for ESP8266 based board (DIY model)
|
||||
|
||||
1. Add esp8266 board by adding http://arduino.esp8266.com/stable/package_esp8266com_index.json into Additional Board Manager URLs field (File ➝ Preferences ➝ Additional boards manager URLs)
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
Make sure python pyserial module installed globally in the environment by executing:
|
||||
|
||||
`$ sudo apt install -y python3-pyserial`
|
||||
|
||||
or
|
||||
|
||||
`$ pip install pyserial`
|
||||
|
||||
Choose based on how python installed on your machine. But most user, using `apt` is better.
|
||||
|
||||
## How to contribute
|
||||
|
||||
The instructions above are the instructions for how to build an official release of the AirGradient firmware using the Arduino IDE. If you intend to make changes that will you intent to contribute back to the main project, instead of installing the AirGradient library, check out the repo at `Documents/Arduino/libraries` (for Windows and Mac), or `~/Arduino/Libraries` (Linux). If you installed the library, you can remove it from the library manager in the Arduino IDE, or just delete the directory.
|
||||
|
||||
**NOTE:** When cloning the repository, for version >= 3.3.0 it has submodule, please use `--recursive` flag like this: `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
|
||||
|
||||
Please follow github [contributing to a project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) tutorial to contribute to this project.
|
||||
|
||||
There are 2 environment options to compile this project, PlatformIO and ArduinoIDE.
|
||||
|
||||
- For PlatformIO, it should work out of the box
|
||||
- For arduino, files in `src` folder and also from `Examples` can be modified at `Documents/Arduino/libraries` for Windows and Mac, and `~/Arduino/Libraries` for Linux
|
||||
|
||||
|
||||
|
BIN
docs/images/additional-board.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/images/ag-lib.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
docs/images/compiled-esp8266.png
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
docs/images/compiled.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
docs/images/esp32-board.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
docs/images/esp8266-board.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
docs/images/linux-failed.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
docs/images/settings-esp8266.png
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
docs/images/settings.png
Normal file
After Width: | Height: | Size: 106 KiB |
259
docs/local-server.md
Normal file
@ -0,0 +1,259 @@
|
||||
## 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": "",
|
||||
"httpDomain": "",
|
||||
"temperatureUnit": "c",
|
||||
"configurationControl": "local",
|
||||
"postDataToAirGradient": true,
|
||||
"ledBarBrightness": 100,
|
||||
"displayBrightness": 100,
|
||||
"offlineMode": false,
|
||||
"model": "I-9PSL",
|
||||
"monitorDisplayCompensatedValues": true,
|
||||
"corrections": {
|
||||
"pm02": {
|
||||
"correctionAlgorithm": "epa_2021",
|
||||
"slr": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Set Configuration Parameters (PUT)
|
||||
|
||||
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 | Maximum 255 characters. Set value to empty string to disable mqtt connection. | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` |
|
||||
| `httpDomain` | Domain name for http request. (version > 3.3.2) | String | Maximum 255 characters. Set value to empty string to set http domain to default airgradient | `{"httpDomain": "sub.domain.com"}` |
|
||||
| `temperatureUnit` | Temperature unit shown on the display. | String | `c` or `C`: Degree Celsius °C <br>`f` or `F`: Degree Fahrenheit °F | `{"temperatureUnit": "c"}` |
|
||||
| `configurationControl` | The configuration source of the device. | String | `both`: Accept local and cloud configuration <br>`local`: Accept only local configuration <br>`cloud`: Accept only cloud configuration | `{"configurationControl": "both"}` |
|
||||
| `postDataToAirGradient` | Send data to AirGradient cloud. | Boolean | `true`: Enabled <br>`false`: Disabled | `{"postDataToAirGradient": true}` |
|
||||
| `co2CalibrationRequested` | Can be set to trigger a calibration. | Boolean | `true`: CO2 calibration (400ppm) will be triggered | `{"co2CalibrationRequested": true}` |
|
||||
| `ledBarTestRequested` | Can be set to trigger a test. | Boolean | `true` : LEDs will run test sequence | `{"ledBarTestRequested": true}` |
|
||||
| `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 12}` |
|
||||
| `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` |
|
||||
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on 3.1.9) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
|
||||
| `corrections` | Sets correction options to display and measurement values on local server response. (version >= 3.1.11) | Object | _see corrections section_ | _see corrections section_ |
|
||||
|
||||
|
||||
**Notes**
|
||||
|
||||
- `offlineMode` : the device will disable all network operation, and only show measurements on the display and ledbar; Read-Only; Change can be apply using reset button on boot.
|
||||
- `disableCloudConnection` : disable every request to AirGradient server, means features like post data to AirGradient server, configuration from AirGradient server and automatic firmware updates are disabled. This configuration overrides `configurationControl` and `postDataToAirGradient`; Read-Only; Change can be apply from wifi setup webpage.
|
||||
|
||||
### Corrections
|
||||
|
||||
The `corrections` object allows configuring PM2.5, Temperature and Humidity correction algorithms and parameters locally. This affects both the display, local server response and open metrics values.
|
||||
|
||||
Example correction configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"corrections": {
|
||||
"pm02": {
|
||||
"correctionAlgorithm": "<Option In String>",
|
||||
"slr": {
|
||||
"intercept": 0,
|
||||
"scalingFactor": 0,
|
||||
"useEpa2021": false
|
||||
}
|
||||
},
|
||||
"atmp": {
|
||||
"correctionAlgorithm": "<Option In String>",
|
||||
"slr": {
|
||||
"intercept": 0,
|
||||
"scalingFactor": 0
|
||||
}
|
||||
},
|
||||
"rhum": {
|
||||
"correctionAlgorithm": "<Option In String>",
|
||||
"slr": {
|
||||
"intercept": 0,
|
||||
"scalingFactor": 0
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### PM 2.5
|
||||
|
||||
Field Name: `pm02`
|
||||
|
||||
| Algorithm | Value | Description | SLR required |
|
||||
|------------|-------------|------|---------|
|
||||
| Raw | `"none"` | No correction (default) | No |
|
||||
| EPA 2021 | `"epa_2021"` | Use EPA 2021 correction factors on top of raw value | No |
|
||||
| PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes |
|
||||
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
|
||||
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
|
||||
|
||||
**NOTES**:
|
||||
|
||||
- Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false`
|
||||
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)
|
||||
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
|
||||
|
||||
**Examples**:
|
||||
|
||||
- PMS5003_20231030
|
||||
|
||||
```bash
|
||||
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":true}}}}'
|
||||
```
|
||||
|
||||
- PMS5003_20231218
|
||||
|
||||
```bash
|
||||
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0.03525,"useEpa2021":true}}}}'
|
||||
```
|
||||
|
||||
- PMS5003_20240104
|
||||
|
||||
```bash
|
||||
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}'
|
||||
```
|
||||
|
||||
#### Temperature & Humidity
|
||||
|
||||
Field Name:
|
||||
- Temperature: `atmp`
|
||||
- Humidity: `rhum`
|
||||
|
||||
| Algorithm | Value | Description | SLR required |
|
||||
|------------|-------------|------|---------|
|
||||
| Raw | `"none"` | No correction (default) | No |
|
||||
| AirGradient Standard Correction | `"ag_pms5003t_2024"` | Using standard airgradient correction (for outdoor monitor)| No |
|
||||
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
|
||||
|
||||
*Table above apply for both Temperature and Humidity*
|
||||
|
||||
**Example**
|
||||
|
||||
```bash
|
||||
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"atmp":{"correctionAlgorithm":"custom","slr":{"intercept":0.2,"scalingFactor":1.1}}}}'
|
||||
```
|
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
|
604
examples/BASIC/BASIC.ino
Normal file
@ -0,0 +1,604 @@
|
||||
/*
|
||||
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/
|
||||
|
||||
Compile Instructions:
|
||||
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
|
||||
|
||||
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||
can be set through the AirGradient dashboard.
|
||||
|
||||
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(configuration);
|
||||
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);
|
||||
measurements.setAirGradient(&ag);
|
||||
|
||||
/** Example set custom API root URL */
|
||||
// apiClient.setApiRoot("https://example.custom.api");
|
||||
|
||||
/** Init sensor */
|
||||
boardInit();
|
||||
setMeasurementMaxPeriod();
|
||||
|
||||
// Uncomment below line to print every measurements reading update
|
||||
// measurements.setDebug(true);
|
||||
|
||||
/** Connecting wifi */
|
||||
bool connectToWifi = false;
|
||||
|
||||
connectToWifi = !configuration.isOfflineMode();
|
||||
if (connectToWifi) {
|
||||
apiClient.begin();
|
||||
|
||||
if (wifiConnector.connect()) {
|
||||
if (wifiConnector.isConnected()) {
|
||||
mdnsInit();
|
||||
localServer.begin();
|
||||
initMqtt();
|
||||
sendDataToAg();
|
||||
|
||||
if (configuration.getConfigurationControl() !=
|
||||
ConfigurationControl::ConfigurationControlLocal) {
|
||||
apiClient.fetchServerConfiguration();
|
||||
}
|
||||
configSchedule.update();
|
||||
if (apiClient.isFetchConfigurationFailed()) {
|
||||
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());
|
||||
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 (configuration.isOfflineMode() ||
|
||||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
|
||||
Serial.println("Ignore fetch server configuration. Either mode is offline "
|
||||
"or configurationControl set to local");
|
||||
apiClient.resetFetchConfigurationStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiClient.fetchServerConfiguration()) {
|
||||
configUpdateHandle();
|
||||
}
|
||||
}
|
||||
|
||||
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.isFetchConfigurationFailed()) {
|
||||
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 */
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(bootCount);
|
||||
|
||||
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
|
||||
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
|
||||
"or post data to server disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
Serial.println("WiFi not connected, skipping data transmission to AG server");
|
||||
return;
|
||||
}
|
||||
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
Serial.println();
|
||||
Serial.println("Online mode and isPostToAirGradient = true");
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
|
||||
static void tempHumUpdate(void) {
|
||||
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
@ -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());
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
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_ */
|
204
examples/BASIC/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.isFetchConfigurationFailed() ? "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 rhumCompensated = 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;
|
||||
rhumCompensated = _hum;
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
pm01 = measure.get(Measurements::PM01);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
nox = measure.get(Measurements::NOx);
|
||||
noxRaw = measure.get(Measurements::NOxRaw);
|
||||
}
|
||||
|
||||
if (config.hasSensorS8) {
|
||||
co2 = measure.get(Measurements::CO2);
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
if (utils::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(rhumCompensated)) {
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the "
|
||||
"AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(rhumCompensated));
|
||||
}
|
||||
|
||||
response += "# EOF\n";
|
||||
return response;
|
||||
}
|
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;
|
||||
}
|
656
examples/DiyProIndoorV3_3/DiyProIndoorV3_3.ino
Normal file
@ -0,0 +1,656 @@
|
||||
/*
|
||||
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/
|
||||
|
||||
Compile Instructions:
|
||||
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
|
||||
|
||||
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||
can be set through the AirGradient dashboard.
|
||||
|
||||
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(configuration);
|
||||
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);
|
||||
measurements.setAirGradient(&ag);
|
||||
|
||||
/** Example set custom API root URL */
|
||||
// apiClient.setApiRoot("https://example.custom.api");
|
||||
|
||||
/** Init sensor */
|
||||
boardInit();
|
||||
setMeasurementMaxPeriod();
|
||||
|
||||
// Uncomment below line to print every measurements reading update
|
||||
// measurements.setDebug(true);
|
||||
|
||||
/** Connecting wifi */
|
||||
bool connectToWifi = false;
|
||||
|
||||
connectToWifi = !configuration.isOfflineMode();
|
||||
if (connectToWifi) {
|
||||
apiClient.begin();
|
||||
|
||||
if (wifiConnector.connect()) {
|
||||
if (wifiConnector.isConnected()) {
|
||||
mdnsInit();
|
||||
localServer.begin();
|
||||
initMqtt();
|
||||
sendDataToAg();
|
||||
|
||||
if (configuration.getConfigurationControl() !=
|
||||
ConfigurationControl::ConfigurationControlLocal) {
|
||||
apiClient.fetchServerConfiguration();
|
||||
}
|
||||
configSchedule.update();
|
||||
if (apiClient.isFetchConfigurationFailed()) {
|
||||
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());
|
||||
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 (configuration.isOfflineMode() ||
|
||||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
|
||||
Serial.println("Ignore fetch server configuration. Either mode is offline "
|
||||
"or configurationControl set to local");
|
||||
apiClient.resetFetchConfigurationStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiClient.fetchServerConfiguration()) {
|
||||
configUpdateHandle();
|
||||
}
|
||||
}
|
||||
|
||||
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.isFetchConfigurationFailed()) {
|
||||
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 */
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(bootCount);
|
||||
|
||||
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
|
||||
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
|
||||
"or post data to server disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
Serial.println("WiFi not connected, skipping data transmission to AG server");
|
||||
return;
|
||||
}
|
||||
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
Serial.println();
|
||||
Serial.println("Online mode and isPostToAirGradient = true");
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
|
||||
static void tempHumUpdate(void) {
|
||||
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
@ -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());
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
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_ */
|
205
examples/DiyProIndoorV3_3/OpenMetrics.cpp
Normal file
@ -0,0 +1,205 @@
|
||||
#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.isFetchConfigurationFailed() ? "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 rhumCompensated = 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;
|
||||
rhumCompensated = _hum;
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
pm01 = measure.get(Measurements::PM01);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
nox = measure.get(Measurements::NOx);
|
||||
noxRaw = measure.get(Measurements::NOxRaw);
|
||||
}
|
||||
|
||||
if (config.hasSensorS8) {
|
||||
co2 = measure.get(Measurements::CO2);
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
if (utils::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(rhumCompensated)) {
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the "
|
||||
"AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(rhumCompensated));
|
||||
}
|
||||
|
||||
response += "# EOF\n";
|
||||
return response;
|
||||
}
|
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_ */
|
697
examples/DiyProIndoorV4_2/DiyProIndoorV4_2.ino
Normal file
@ -0,0 +1,697 @@
|
||||
/*
|
||||
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/
|
||||
|
||||
Compile Instructions:
|
||||
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
|
||||
|
||||
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
|
||||
can be set through the AirGradient dashboard.
|
||||
|
||||
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(configuration);
|
||||
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);
|
||||
measurements.setAirGradient(&ag);
|
||||
|
||||
/** Example set custom API root URL */
|
||||
// apiClient.setApiRoot("https://example.custom.api");
|
||||
|
||||
/** Init sensor */
|
||||
boardInit();
|
||||
setMeasurementMaxPeriod();
|
||||
|
||||
// Uncomment below line to print every measurements reading update
|
||||
// measurements.setDebug(true);
|
||||
|
||||
/** Connecting wifi */
|
||||
bool connectToWifi = false;
|
||||
|
||||
/** 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();
|
||||
|
||||
if (configuration.getConfigurationControl() !=
|
||||
ConfigurationControl::ConfigurationControlLocal) {
|
||||
apiClient.fetchServerConfiguration();
|
||||
}
|
||||
configSchedule.update();
|
||||
if (apiClient.isFetchConfigurationFailed()) {
|
||||
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());
|
||||
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 (configuration.isOfflineMode() ||
|
||||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
|
||||
Serial.println("Ignore fetch server configuration. Either mode is offline "
|
||||
"or configurationControl set to local");
|
||||
apiClient.resetFetchConfigurationStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiClient.fetchServerConfiguration()) {
|
||||
configUpdateHandle();
|
||||
}
|
||||
}
|
||||
|
||||
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.isFetchConfigurationFailed()) {
|
||||
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 */
|
||||
int bootCount = measurements.bootCount() + 1;
|
||||
measurements.setBootCount(bootCount);
|
||||
|
||||
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
|
||||
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
|
||||
"or post data to server disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (wifiConnector.isConnected() == false) {
|
||||
Serial.println("WiFi not connected, skipping data transmission to AG server");
|
||||
return;
|
||||
}
|
||||
|
||||
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
|
||||
if (apiClient.postToServer(syncData)) {
|
||||
Serial.println();
|
||||
Serial.println("Online mode and isPostToAirGradient = true");
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
|
||||
static void tempHumUpdate(void) {
|
||||
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
@ -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());
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
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_ */
|
204
examples/DiyProIndoorV4_2/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.isFetchConfigurationFailed() ? "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 rhumCompensated = 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;
|
||||
rhumCompensated = _hum;
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
pm01 = measure.get(Measurements::PM01);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
nox = measure.get(Measurements::NOx);
|
||||
noxRaw = measure.get(Measurements::NOxRaw);
|
||||
}
|
||||
|
||||
if (config.hasSensorS8) {
|
||||
co2 = measure.get(Measurements::CO2);
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
if (utils::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(rhumCompensated)) {
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the "
|
||||
"AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(rhumCompensated));
|
||||
}
|
||||
|
||||
response += "# EOF\n";
|
||||
return response;
|
||||
}
|
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_ */
|
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());
|
||||
server.send(200, "application/json", toSend);
|
||||
}
|
||||
|
||||
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
|
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_ */
|
1697
examples/OneOpenAir/OneOpenAir.ino
Normal file
255
examples/OneOpenAir/OpenMetrics.cpp
Normal file
@ -0,0 +1,255 @@
|
||||
#include "OpenMetrics.h"
|
||||
|
||||
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
|
||||
WifiConnector &wifiConnector)
|
||||
: measure(measure), config(config), wifiConnector(wifiConnector) {}
|
||||
|
||||
OpenMetrics::~OpenMetrics() {}
|
||||
|
||||
void OpenMetrics::setAirGradient(AirGradient *ag) {
|
||||
this->ag = ag;
|
||||
}
|
||||
|
||||
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
|
||||
this->agClient = client;
|
||||
}
|
||||
|
||||
const char *OpenMetrics::getApiContentType(void) {
|
||||
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
|
||||
}
|
||||
|
||||
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("", agClient->isLastFetchConfigSucceed() ? "1" : "0");
|
||||
|
||||
add_metric(
|
||||
"post_ok",
|
||||
"1 if the AirGradient device was able to successfully send to the server",
|
||||
"gauge");
|
||||
add_metric_point("", agClient->isLastPostMeasureSucceed() ? "1" : "0");
|
||||
|
||||
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 rhumCompensated = utils::getInvalidHumidity();
|
||||
int tvoc = utils::getInvalidVOC();
|
||||
int tvocRaw = utils::getInvalidVOC();
|
||||
int nox = utils::getInvalidNOx();
|
||||
int noxRaw = utils::getInvalidNOx();
|
||||
|
||||
// Get values
|
||||
if (config.hasSensorPMS1 && config.hasSensorPMS2) {
|
||||
_temp = (measure.getFloat(Measurements::Temperature, 1) +
|
||||
measure.getFloat(Measurements::Temperature, 2)) /
|
||||
2.0f;
|
||||
_hum = (measure.getFloat(Measurements::Humidity, 1) +
|
||||
measure.getFloat(Measurements::Humidity, 2)) /
|
||||
2.0f;
|
||||
pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f;
|
||||
float correctedPm25_1 = measure.getCorrectedPM25(false, 1);
|
||||
float correctedPm25_2 = measure.getCorrectedPM25(false, 2);
|
||||
float correctedPm25 = (correctedPm25_1 + correctedPm25_2) / 2.0f;
|
||||
pm25 = round(correctedPm25);
|
||||
pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f;
|
||||
pm03PCount =
|
||||
(measure.get(Measurements::PM03_PC, 1) + measure.get(Measurements::PM03_PC, 2)) / 2.0f;
|
||||
} else {
|
||||
if (ag->isOne()) {
|
||||
if (config.hasSensorSHT) {
|
||||
_temp = measure.getFloat(Measurements::Temperature);
|
||||
_hum = measure.getFloat(Measurements::Humidity);
|
||||
}
|
||||
|
||||
if (config.hasSensorPMS1) {
|
||||
pm01 = measure.get(Measurements::PM01);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC);
|
||||
}
|
||||
} else {
|
||||
if (config.hasSensorPMS1) {
|
||||
_temp = measure.getFloat(Measurements::Temperature, 1);
|
||||
_hum = measure.getFloat(Measurements::Humidity, 1);
|
||||
pm01 = measure.get(Measurements::PM01, 1);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 1);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10, 1);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC, 1);
|
||||
}
|
||||
if (config.hasSensorPMS2) {
|
||||
_temp = measure.getFloat(Measurements::Temperature, 2);
|
||||
_hum = measure.getFloat(Measurements::Humidity, 2);
|
||||
pm01 = measure.get(Measurements::PM01, 2);
|
||||
float correctedPm = measure.getCorrectedPM25(false, 2);
|
||||
pm25 = round(correctedPm);
|
||||
pm10 = measure.get(Measurements::PM10, 2);
|
||||
pm03PCount = measure.get(Measurements::PM03_PC, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.hasSensorSGP) {
|
||||
tvoc = measure.get(Measurements::TVOC);
|
||||
tvocRaw = measure.get(Measurements::TVOCRaw);
|
||||
nox = measure.get(Measurements::NOx);
|
||||
noxRaw = measure.get(Measurements::NOxRaw);
|
||||
}
|
||||
|
||||
if (config.hasSensorS8) {
|
||||
co2 = measure.get(Measurements::CO2);
|
||||
}
|
||||
|
||||
/** Get temperature and humidity compensated */
|
||||
if (ag->isOne()) {
|
||||
atmpCompensated = round(measure.getCorrectedTempHum(Measurements::Temperature));
|
||||
rhumCompensated = round(measure.getCorrectedTempHum(Measurements::Humidity));
|
||||
} else {
|
||||
atmpCompensated = round((measure.getCorrectedTempHum(Measurements::Temperature, 1) +
|
||||
measure.getCorrectedTempHum(Measurements::Temperature, 2)) /
|
||||
2.0f);
|
||||
rhumCompensated = round((measure.getCorrectedTempHum(Measurements::Humidity, 1) +
|
||||
measure.getCorrectedTempHum(Measurements::Humidity, 2)) /
|
||||
2.0f);
|
||||
}
|
||||
|
||||
// Add measurements that valid to the metrics
|
||||
if (config.hasSensorPMS1 || config.hasSensorPMS2) {
|
||||
if (utils::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(rhumCompensated)) {
|
||||
add_metric("humidity_compensated",
|
||||
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
|
||||
"gauge", "percent");
|
||||
add_metric_point("", String(rhumCompensated));
|
||||
}
|
||||
|
||||
response += "# EOF\n";
|
||||
return response;
|
||||
}
|
29
examples/OneOpenAir/OpenMetrics.h
Normal file
@ -0,0 +1,29 @@
|
||||
#ifndef _OPEN_METRICS_H_
|
||||
#define _OPEN_METRICS_H_
|
||||
|
||||
#include "AgConfigure.h"
|
||||
#include "AgValue.h"
|
||||
#include "AgWiFiConnector.h"
|
||||
#include "AirGradient.h"
|
||||
#include "Libraries/airgradient-client/src/airgradientClient.h"
|
||||
|
||||
class OpenMetrics {
|
||||
private:
|
||||
AirGradient *ag;
|
||||
AirgradientClient *agClient;
|
||||
Measurements &measure;
|
||||
Configuration &config;
|
||||
WifiConnector &wifiConnector;
|
||||
|
||||
public:
|
||||
OpenMetrics(Measurements &measure, Configuration &config,
|
||||
WifiConnector &wifiConnector);
|
||||
~OpenMetrics();
|
||||
void setAirGradient(AirGradient *ag);
|
||||
void setAirgradientClient(AirgradientClient *client);
|
||||
const char *getApiContentType(void);
|
||||
const char* getApi(void);
|
||||
String getPayload(void);
|
||||
};
|
||||
|
||||
#endif /** _OPEN_METRICS_H_ */
|
@ -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)
|
||||
{
|
||||
#else
|
||||
if (ag.s8.begin(Serial1) == false)
|
||||
if (ag.s8.begin(Serial0) == false)
|
||||
{
|
||||
#endif
|
||||
failedHandler("SenseAir S8 init failed");
|
||||
|
@ -10,8 +10,8 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||
#ifdef ESP8266
|
||||
AirGradient ag = AirGradient(DIY_BASIC);
|
||||
#else
|
||||
// AirGradient ag = AirGradient(ONE_INDOOR);
|
||||
AirGradient ag = AirGradient(OPEN_AIR_OUTDOOR);
|
||||
AirGradient ag = AirGradient(ONE_INDOOR);
|
||||
// AirGradient ag = AirGradient(OPEN_AIR_OUTDOOR);
|
||||
#endif
|
||||
|
||||
void failedHandler(String msg);
|
||||
@ -35,42 +35,56 @@ void setup() {
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t lastRead = 0;
|
||||
void loop() {
|
||||
int PM2;
|
||||
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) {
|
||||
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 {
|
||||
uint32_t ms = (uint32_t)(millis() - lastRead);
|
||||
if (ms >= 5000) {
|
||||
lastRead = millis();
|
||||
#ifdef ESP8266
|
||||
if (ag.pms5003.connected()) {
|
||||
PM2 = ag.pms5003.getPm25Ae();
|
||||
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
|
||||
Serial.printf("PM2.5 in US AQI: %d\r\n",
|
||||
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) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
name=AirGradient Air Quality Sensor
|
||||
version=3.0.3
|
||||
version=3.3.9
|
||||
author=AirGradient <support@airgradient.com>
|
||||
maintainer=AirGradient <support@airgradient.com>
|
||||
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.
|
||||
|
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
@ -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 AG_LOG_LEVEL=AG_LOG_LEVEL_INFO -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
|
||||
board_build.partitions = partitions.csv
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
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
|
215
src/AgApiClient.cpp
Normal file
@ -0,0 +1,215 @@
|
||||
#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) {
|
||||
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.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
|
||||
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
|
||||
if (apiRootChanged) {
|
||||
// If apiRoot is changed, assume not using https
|
||||
if (client.begin(uri) == false) {
|
||||
logError("Begin HTTPClient failed (GET)");
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// By default, airgradient using https
|
||||
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
|
||||
logError("Begin HTTPClient using tls failed (GET)");
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/** 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();
|
||||
|
||||
/** 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) {
|
||||
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
|
||||
#ifdef ESP8266
|
||||
HTTPClient client;
|
||||
WiFiClient wifiClient;
|
||||
if (client.begin(wifiClient, uri) == false) {
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
HTTPClient client;
|
||||
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
|
||||
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
|
||||
if (apiRootChanged) {
|
||||
// If apiRoot is changed, assume not using https
|
||||
if (client.begin(uri) == false) {
|
||||
logError("Begin HTTPClient failed (POST)");
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// By default, airgradient using https
|
||||
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
|
||||
logError("Begin HTTPClient using tls failed (POST)");
|
||||
getConfigFailed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
client.addHeader("content-type", "application/json");
|
||||
int retCode = client.POST(data);
|
||||
client.end();
|
||||
|
||||
logInfo(String("POST: ") + uri);
|
||||
logInfo(String("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::isFetchConfigurationFailed(void) { return getConfigFailed; }
|
||||
|
||||
/**
|
||||
* @brief Reset status of get configuration from AirGradient cloud
|
||||
*/
|
||||
void AgApiClient::resetFetchConfigurationStatus(void) { getConfigFailed = false; }
|
||||
|
||||
/**
|
||||
* @brief Get failed status when post data to AirGradient cloud
|
||||
*
|
||||
* @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->apiRootChanged = true;
|
||||
this->apiRoot = apiRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set http request timeout. (Default: 10s)
|
||||
*
|
||||
* @param timeoutMs
|
||||
*/
|
||||
void AgApiClient::setTimeout(uint16_t timeoutMs) {
|
||||
this->timeoutMs = timeoutMs;
|
||||
}
|
54
src/AgApiClient.h
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @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;
|
||||
#ifdef ESP8266
|
||||
// ESP8266 not support HTTPS
|
||||
String apiRoot = "http://hw.airgradient.com";
|
||||
#else
|
||||
String apiRoot = "https://hw.airgradient.com";
|
||||
#endif
|
||||
|
||||
bool apiRootChanged = false; // Indicate if setApiRoot() is called
|
||||
bool getConfigFailed;
|
||||
bool postToServerFailed;
|
||||
bool notAvailableOnDashboard = false; // Device not setup on Airgradient cloud dashboard.
|
||||
uint16_t timeoutMs = 15000; // Default set to 15s
|
||||
|
||||
public:
|
||||
AgApiClient(Stream &stream, Configuration &config);
|
||||
~AgApiClient();
|
||||
|
||||
void begin(void);
|
||||
bool fetchServerConfiguration(void);
|
||||
bool postToServer(String data);
|
||||
bool isFetchConfigurationFailed(void);
|
||||
void resetFetchConfigurationStatus(void);
|
||||
bool isPostToServerFailed(void);
|
||||
bool isNotAvailableOnDashboard(void);
|
||||
void setAirGradient(AirGradient *ag);
|
||||
bool sendPing(int rssi, int bootCount);
|
||||
String getApiRoot() const;
|
||||
void setApiRoot(const String &apiRoot);
|
||||
void setTimeout(uint16_t timeoutMs);
|
||||
};
|
||||
|
||||
#endif /** _AG_API_CLIENT_H_ */
|
1612
src/AgConfigure.cpp
Normal file
128
src/AgConfigure.h
Normal file
@ -0,0 +1,128 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
struct TempHumCorrection {
|
||||
TempHumCorrectionAlgorithm algorithm;
|
||||
float intercept;
|
||||
float scalingFactor;
|
||||
bool changed;
|
||||
};
|
||||
|
||||
private:
|
||||
bool co2CalibrationRequested;
|
||||
bool ledBarTestRequested;
|
||||
bool updated;
|
||||
bool commandRequested = false;
|
||||
String failedMessage;
|
||||
bool _noxLearnOffsetChanged;
|
||||
bool _tvocLearningOffsetChanged;
|
||||
bool ledBarBrightnessChanged = false;
|
||||
bool displayBrightnessChanged = false;
|
||||
String otaNewFirmwareVersion;
|
||||
bool _offlineMode = false;
|
||||
bool _ledBarModeChanged = false;
|
||||
PMCorrection pmCorrection;
|
||||
TempHumCorrection tempCorrection;
|
||||
TempHumCorrection rhumCorrection;
|
||||
|
||||
AirGradient* ag;
|
||||
|
||||
String getLedBarModeName(LedBarMode mode);
|
||||
PMCorrectionAlgorithm matchPmAlgorithm(String algorithm);
|
||||
TempHumCorrectionAlgorithm matchTempHumAlgorithm(String algorithm);
|
||||
bool updatePmCorrection(JSONVar &json);
|
||||
bool updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
|
||||
const char *correctionName);
|
||||
void saveConfig(void);
|
||||
void loadConfig(void);
|
||||
void defaultConfig(void);
|
||||
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;
|
||||
|
||||
typedef void (*ConfigurationUpdatedCallback_t)();
|
||||
void setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback);
|
||||
|
||||
bool begin(void);
|
||||
bool parse(String data, bool isLocal);
|
||||
String toString(void);
|
||||
String toString(AgFirmwareMode fwMode);
|
||||
bool isTemperatureUnitInF(void);
|
||||
String getCountry(void);
|
||||
bool isPmStandardInUSAQI(void);
|
||||
int getCO2CalibrationAbcDays(void);
|
||||
LedBarMode getLedBarMode(void);
|
||||
String getLedBarModeName(void);
|
||||
bool getDisplayMode(void);
|
||||
String getMqttBrokerUri(void);
|
||||
String getHttpDomain(void);
|
||||
bool isPostDataToAirGradient(void);
|
||||
ConfigurationControl getConfigurationControl(void);
|
||||
bool isCo2CalibrationRequested(void);
|
||||
bool isLedBarTestRequested(void);
|
||||
void reset(void);
|
||||
String getModel(void);
|
||||
bool isUpdated(void);
|
||||
bool isCommandRequested(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 isCloudConnectionDisabled(void);
|
||||
void setDisableCloudConnection(bool disable);
|
||||
bool isLedBarModeChanged(void);
|
||||
bool isMonitorDisplayCompensatedValues(void);
|
||||
bool isPMCorrectionChanged(void);
|
||||
bool isPMCorrectionEnabled(void);
|
||||
PMCorrection getPMCorrection(void);
|
||||
TempHumCorrection getTempCorrection(void);
|
||||
TempHumCorrection getHumCorrection(void);
|
||||
private:
|
||||
ConfigurationUpdatedCallback_t _callback;
|
||||
};
|
||||
|
||||
#endif /** _AG_CONFIG_H_ */
|
601
src/AgOledDisplay.cpp
Normal file
@ -0,0 +1,601 @@
|
||||
#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))
|
||||
|
||||
static const unsigned char WIFI_ISSUE_BITS[] = {
|
||||
0xd8, 0xc6, 0xde, 0xde, 0xc7, 0xf8, 0xd1, 0xe2, 0xdc, 0xce, 0xcc,
|
||||
0xcc, 0xc0, 0xc0, 0xd0, 0xc2, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0};
|
||||
|
||||
static const unsigned char CLOUD_ISSUE_BITS[] = {
|
||||
0x70, 0xc0, 0x88, 0xc0, 0x04, 0xc1, 0x04, 0xcf, 0x02, 0xd0, 0x01,
|
||||
0xe0, 0x01, 0xe0, 0x01, 0xe0, 0xa2, 0xd0, 0x4c, 0xce, 0xa0, 0xc0};
|
||||
|
||||
// Offline mode icon
|
||||
static unsigned char OFFLINE_BITS[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x62, 0x00,
|
||||
0xE6, 0x00, 0xFE, 0x1F, 0xFE, 0x1F, 0xE6, 0x00, 0x62, 0x00,
|
||||
0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
// {
|
||||
// 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x62, 0x00, 0xE2, 0x00,
|
||||
// 0xFE, 0x1F, 0xFE, 0x1F, 0xE2, 0x00, 0x62, 0x00, 0x60, 0x00, 0x30, 0x00,
|
||||
// 0x00, 0x00, 0x00, 0x00, };
|
||||
/**
|
||||
* @brief Show dashboard temperature and humdity
|
||||
*
|
||||
* @param hasStatus
|
||||
*/
|
||||
void OledDisplay::showTempHum(bool hasStatus) {
|
||||
char buf[10];
|
||||
/** Temperature */
|
||||
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
|
||||
if (utils::isValidTemperature(temp)) {
|
||||
float t = 0.0f;
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
t = utils::degreeC_To_F(temp);
|
||||
} else {
|
||||
t = temp;
|
||||
}
|
||||
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
if (hasStatus) {
|
||||
snprintf(buf, sizeof(buf), "%0.1f", t);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%0.1f°F", t);
|
||||
}
|
||||
} else {
|
||||
if (hasStatus) {
|
||||
snprintf(buf, sizeof(buf), "%.1f", t);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%.1f°C", t);
|
||||
}
|
||||
}
|
||||
} else { /** Show invalid value */
|
||||
if (config.isTemperatureUnitInF()) {
|
||||
snprintf(buf, sizeof(buf), "-°F");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "-°C");
|
||||
}
|
||||
}
|
||||
DISP()->drawUTF8(1, 10, buf);
|
||||
|
||||
/** Show humidity */
|
||||
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
|
||||
if (utils::isValidHumidity(rhum)) {
|
||||
snprintf(buf, sizeof(buf), "%d%%", rhum);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "-%%");
|
||||
}
|
||||
|
||||
if (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);
|
||||
}
|
||||
|
||||
void OledDisplay::showIcon(int x, int y, xbm_icon_t *icon) {
|
||||
DISP()->drawXBM(x, y, icon->width, icon->height, icon->icon);
|
||||
}
|
||||
/**
|
||||
* @brief Construct a new Ag Oled Display:: Ag Oled Display object
|
||||
*
|
||||
* @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(DashBoardStatusNone); }
|
||||
|
||||
/**
|
||||
* @brief Update dashboard content and error status
|
||||
*
|
||||
*/
|
||||
void OledDisplay::showDashboard(DashboardStatus status) {
|
||||
if (isDisplayOff) {
|
||||
return;
|
||||
}
|
||||
|
||||
char strBuf[16];
|
||||
const int icon_pos_x = 64;
|
||||
xbm_icon_t xbm_icon = {
|
||||
.width = 0,
|
||||
.height = 0,
|
||||
.icon = nullptr,
|
||||
};
|
||||
|
||||
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
|
||||
DISP()->firstPage();
|
||||
do {
|
||||
DISP()->setFont(u8g2_font_t0_16_tf);
|
||||
switch (status) {
|
||||
case DashBoardStatusNone: {
|
||||
// Maybe show signal strength?
|
||||
showTempHum(false);
|
||||
break;
|
||||
}
|
||||
case DashBoardStatusWiFiIssue: {
|
||||
DISP()->drawXBM(icon_pos_x, 0, 14, 11, WIFI_ISSUE_BITS);
|
||||
showTempHum(false);
|
||||
break;
|
||||
}
|
||||
case DashBoardStatusServerIssue: {
|
||||
DISP()->drawXBM(icon_pos_x, 0, 14, 11, CLOUD_ISSUE_BITS);
|
||||
showTempHum(false);
|
||||
break;
|
||||
}
|
||||
case DashBoardStatusAddToDashboard: {
|
||||
setCentralText(10, "Add To Dashboard");
|
||||
break;
|
||||
}
|
||||
case DashBoardStatusDeviceId: {
|
||||
setCentralText(10, ag->deviceId().c_str());
|
||||
break;
|
||||
}
|
||||
case DashBoardStatusOfflineMode: {
|
||||
DISP()->drawXBM(icon_pos_x, 0, 14, 14, OFFLINE_BITS);
|
||||
showTempHum(false); // First true
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/** 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(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(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.getCorrectedTempHum(Measurements::Temperature, 1);
|
||||
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.1f 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.getCorrectedTempHum(Measurements::Humidity, 1));
|
||||
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();
|
||||
}
|
||||
}
|
67
src/AgOledDisplay.h
Normal file
@ -0,0 +1,67 @@
|
||||
#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;
|
||||
|
||||
typedef struct {
|
||||
int width;
|
||||
int height;
|
||||
unsigned char *icon;
|
||||
} xbm_icon_t;
|
||||
|
||||
void showTempHum(bool hasStatus);
|
||||
void setCentralText(int y, String text);
|
||||
void setCentralText(int y, const char *text);
|
||||
void showIcon(int x, int y, xbm_icon_t *icon);
|
||||
|
||||
public:
|
||||
OledDisplay(Configuration &config, Measurements &value, Stream &log);
|
||||
~OledDisplay();
|
||||
|
||||
enum DashboardStatus {
|
||||
DashBoardStatusNone,
|
||||
DashBoardStatusWiFiIssue,
|
||||
DashBoardStatusServerIssue,
|
||||
DashBoardStatusAddToDashboard,
|
||||
DashBoardStatusDeviceId,
|
||||
DashBoardStatusOfflineMode,
|
||||
};
|
||||
|
||||
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(DashboardStatus 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
@ -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
@ -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_ */
|
874
src/AgStateMachine.cpp
Normal file
@ -0,0 +1,874 @@
|
||||
#include "AgStateMachine.h"
|
||||
#include "AgOledDisplay.h"
|
||||
|
||||
#define LED_TEST_BLINK_DELAY 50 /** ms */
|
||||
#define LED_FAST_BLINK_DELAY 250 /** ms */
|
||||
#define LED_SLOW_BLINK_DELAY 1000 /** ms */
|
||||
#define LED_SHORT_BLINK_DELAY 500 /** ms */
|
||||
#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(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(OledDisplay::DashBoardStatusWiFiIssue);
|
||||
break;
|
||||
}
|
||||
case AgStateMachineServerLost: {
|
||||
disp.showDashboard(OledDisplay::DashBoardStatusServerIssue);
|
||||
break;
|
||||
}
|
||||
case AgStateMachineSensorConfigFailed: {
|
||||
if (addToDashBoard) {
|
||||
uint32_t ms = (uint32_t)(millis() - addToDashboardTime);
|
||||
if (ms >= 5000) {
|
||||
addToDashboardTime = millis();
|
||||
if (addToDashBoardToggle) {
|
||||
disp.showDashboard(OledDisplay::DashBoardStatusAddToDashboard);
|
||||
} else {
|
||||
disp.showDashboard(OledDisplay::DashBoardStatusDeviceId);
|
||||
}
|
||||
addToDashBoardToggle = !addToDashBoardToggle;
|
||||
}
|
||||
} else {
|
||||
disp.showDashboard();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AgStateMachineNormal: {
|
||||
if (config.isOfflineMode()) {
|
||||
disp.showDashboard(
|
||||
OledDisplay::DashBoardStatusOfflineMode);
|
||||
} else {
|
||||
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
@ -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_ */
|
1524
src/AgValue.cpp
Normal file
270
src/AgValue.h
Normal file
@ -0,0 +1,270 @@
|
||||
#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 <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class Measurements {
|
||||
private:
|
||||
// Generic struct for update indication for respective value
|
||||
struct Update {
|
||||
int invalidCounter; // Counting on how many invalid value that are passed to update function
|
||||
int max; // Maximum length of the period of the moving average
|
||||
float avg; // Moving average value, updated every update function called
|
||||
};
|
||||
|
||||
// Reading type for sensor value that outputs float
|
||||
struct FloatValue {
|
||||
float sumValues; // Total value from each update
|
||||
std::vector<float> listValues; // List of update value that are kept
|
||||
Update update;
|
||||
};
|
||||
|
||||
// Reading type for sensor value that outputs integer
|
||||
struct IntegerValue {
|
||||
unsigned long sumValues; // Total value from each update; unsigned long to accomodate TVOx and
|
||||
// NOx raw data
|
||||
std::vector<int> listValues; // List of update value that are kept
|
||||
Update update;
|
||||
};
|
||||
|
||||
public:
|
||||
Measurements(Configuration &config);
|
||||
~Measurements() {}
|
||||
|
||||
struct Measures {
|
||||
float temperature[2];
|
||||
float humidity[2];
|
||||
float co2;
|
||||
float tvoc; // Index value
|
||||
float tvoc_raw;
|
||||
float nox; // Index value
|
||||
float nox_raw;
|
||||
float pm_01[2]; // pm 1.0 atmospheric environment
|
||||
float pm_25[2]; // pm 2.5 atmospheric environment
|
||||
float pm_10[2]; // pm 10 atmospheric environment
|
||||
float pm_01_sp[2]; // pm 1.0 standard particle
|
||||
float pm_25_sp[2]; // pm 2.5 standard particle
|
||||
float pm_10_sp[2]; // pm 10 standard particle
|
||||
float pm_03_pc[2]; // particle count 0.3
|
||||
float pm_05_pc[2]; // particle count 0.5
|
||||
float pm_01_pc[2]; // particle count 1.0
|
||||
float pm_25_pc[2]; // particle count 2.5
|
||||
float pm_5_pc[2]; // particle count 5.0
|
||||
float pm_10_pc[2]; // particle count 10
|
||||
int bootCount;
|
||||
int signal;
|
||||
uint32_t freeHeap;
|
||||
};
|
||||
|
||||
void setAirGradient(AirGradient *ag);
|
||||
|
||||
// Enumeration for every AG measurements
|
||||
enum MeasurementType {
|
||||
Temperature,
|
||||
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
|
||||
};
|
||||
|
||||
void printCurrentAverage();
|
||||
|
||||
/**
|
||||
* @brief Set each MeasurementType maximum period length for moving average
|
||||
*
|
||||
* @param type the target measurement type to set
|
||||
* @param max the maximum period length
|
||||
*/
|
||||
void maxPeriod(MeasurementType, int max);
|
||||
|
||||
/**
|
||||
* @brief update target measurement type with new value.
|
||||
* Each MeasurementType has last raw value and moving average value based on max period
|
||||
* This function is for MeasurementType that use INT as the data type
|
||||
*
|
||||
* @param type measurement type that will be updated
|
||||
* @param val (int) the new value
|
||||
* @param ch (int) the MeasurementType channel, not every MeasurementType has more than 1 channel.
|
||||
* Currently maximum channel is 2. Default: 1 (channel 1)
|
||||
* @return false if new value invalid consecutively reach threshold (max period)
|
||||
* @return true otherwise
|
||||
*/
|
||||
bool update(MeasurementType type, int val, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief update target measurement type with new value.
|
||||
* Each MeasurementType has last raw value and moving average value based on max period
|
||||
* This function is for MeasurementType that use FLOAT as the data type
|
||||
*
|
||||
* @param type measurement type that will be updated
|
||||
* @param val (float) the new value
|
||||
* @param ch (int) the MeasurementType channel, not every MeasurementType has more than 1 channel.
|
||||
* Currently maximum channel is 2. Default: 1 (channel 1)
|
||||
* @return false if new value invalid consecutively reach threshold (max period)
|
||||
* @return true otherwise
|
||||
*/
|
||||
bool update(MeasurementType type, float val, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief Get the target measurement latest value
|
||||
*
|
||||
* @param type measurement type that will be retrieve
|
||||
* @param ch target type value channel
|
||||
* @return int measurement type value
|
||||
*/
|
||||
int get(MeasurementType type, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief Get the target measurement latest value
|
||||
*
|
||||
* @param type measurement type that will be retrieve
|
||||
* @param ch target type value channel
|
||||
* @return float measurement type value
|
||||
*/
|
||||
float getFloat(MeasurementType type, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief Get the target measurement average value
|
||||
*
|
||||
* @param type measurement type that will be retrieve
|
||||
* @param ch target type value channel
|
||||
* @return moving average value of target measurements type
|
||||
*/
|
||||
float getAverage(MeasurementType type, int ch = 1);
|
||||
|
||||
/**
|
||||
* @brief Get Temperature or Humidity correction value
|
||||
* Only if correction is applied from configuration or forceCorrection is True
|
||||
*
|
||||
* @param type measurement type either Temperature or Humidity
|
||||
* @param ch target type value channel
|
||||
* @param forceCorrection force using correction even though config correction is not applied, but
|
||||
* not for CUSTOM
|
||||
* @return correction value
|
||||
*/
|
||||
float getCorrectedTempHum(MeasurementType type, int ch = 1, bool forceCorrection = false);
|
||||
|
||||
/**
|
||||
* @brief Get the Corrected PM25 object based on the correction algorithm from configuration
|
||||
*
|
||||
* If correction is not enabled, then will return the raw value (either average or last value)
|
||||
*
|
||||
* @param useAvg Use moving average value if true, otherwise use latest value
|
||||
* @param ch MeasurementType channel
|
||||
* @param forceCorrection force using correction even though config correction is not applied, default to EPA
|
||||
* @return float Corrected PM2.5 value
|
||||
*/
|
||||
float getCorrectedPM25(bool useAvg = false, int ch = 1, bool forceCorrection = false);
|
||||
|
||||
/**
|
||||
* build json payload for every measurements
|
||||
*/
|
||||
String toString(bool localServer, AgFirmwareMode fwMode, int rssi);
|
||||
|
||||
Measures getMeasures();
|
||||
|
||||
std::string buildMeasuresPayload(Measures &measures);
|
||||
|
||||
/**
|
||||
* Set to true if want to debug every update value
|
||||
*/
|
||||
void setDebug(bool debug);
|
||||
|
||||
int bootCount();
|
||||
void setBootCount(int bootCount);
|
||||
|
||||
#ifndef ESP8266
|
||||
void setResetReason(esp_reset_reason_t reason);
|
||||
#endif
|
||||
|
||||
private:
|
||||
Configuration &config;
|
||||
AirGradient *ag;
|
||||
|
||||
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
|
||||
FloatValue _temperature[2];
|
||||
FloatValue _humidity[2];
|
||||
IntegerValue _co2;
|
||||
IntegerValue _tvoc; // Index value
|
||||
IntegerValue _tvoc_raw;
|
||||
IntegerValue _nox; // Index value
|
||||
IntegerValue _nox_raw;
|
||||
IntegerValue _pm_01[2]; // pm 1.0 atmospheric environment
|
||||
IntegerValue _pm_25[2]; // pm 2.5 atmospheric environment
|
||||
IntegerValue _pm_10[2]; // pm 10 atmospheric environment
|
||||
IntegerValue _pm_01_sp[2]; // pm 1.0 standard particle
|
||||
IntegerValue _pm_25_sp[2]; // pm 2.5 standard particle
|
||||
IntegerValue _pm_10_sp[2]; // pm 10 standard particle
|
||||
IntegerValue _pm_03_pc[2]; // particle count 0.3
|
||||
IntegerValue _pm_05_pc[2]; // particle count 0.5
|
||||
IntegerValue _pm_01_pc[2]; // particle count 1.0
|
||||
IntegerValue _pm_25_pc[2]; // particle count 2.5
|
||||
IntegerValue _pm_5_pc[2]; // particle count 5.0
|
||||
IntegerValue _pm_10_pc[2]; // particle count 10
|
||||
int _bootCount;
|
||||
int _resetReason;
|
||||
bool _debug = false;
|
||||
|
||||
/**
|
||||
* @brief Get PMS5003 firmware version string
|
||||
*
|
||||
* @param fwCode
|
||||
* @return String
|
||||
*/
|
||||
String pms5003FirmwareVersion(int fwCode);
|
||||
/**
|
||||
* @brief Get PMS5003T firmware version string
|
||||
*
|
||||
* @param fwCode
|
||||
* @return String
|
||||
*/
|
||||
String pms5003TFirmwareVersion(int fwCode);
|
||||
/**
|
||||
* @brief Get firmware version string
|
||||
*
|
||||
* @param prefix Prefix firmware string
|
||||
* @param fwCode Version code
|
||||
* @return string
|
||||
*/
|
||||
String pms5003FirmwareVersionBase(String prefix, int fwCode);
|
||||
|
||||
/**
|
||||
* Convert AgValue Type to string representation of the value
|
||||
*/
|
||||
String measurementTypeStr(MeasurementType type);
|
||||
|
||||
/**
|
||||
* @brief check if provided channel is a valid channel or not
|
||||
* abort program if invalid
|
||||
*/
|
||||
void validateChannel(int ch);
|
||||
|
||||
void printCurrentPMAverage(int ch);
|
||||
|
||||
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode);
|
||||
JSONVar buildIndoor(bool localServer);
|
||||
JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate);
|
||||
};
|
||||
|
||||
#endif /** _AG_VALUE_H_ */
|
413
src/AgWiFiConnector.cpp
Normal file
@ -0,0 +1,413 @@
|
||||
#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 disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T",
|
||||
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
|
||||
WIFI()->addParameter(&disableCloud);
|
||||
WiFiManagerParameter disableCloudInfo(
|
||||
"<p>Prevent connection to the AirGradient Server. Important: Only enable "
|
||||
"it if you are sure you don't want to use any AirGradient cloud "
|
||||
"features. As a result you will not receive automatic firmware updates, "
|
||||
"configuration settings from cloud and the measure data will not reach the AirGradient dashboard.</p>");
|
||||
WIFI()->addParameter(&disableCloudInfo);
|
||||
|
||||
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
|
||||
|
||||
logInfo("Wait for configure portal");
|
||||
|
||||
#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(disableCloud.getValue());
|
||||
logInfo("Setting disableCloudConnection set from " +
|
||||
String(config.isCloudConnectionDisabled() ? "True" : "False") + String(" to ") +
|
||||
String(result == "T" ? "True" : "False") + String(" successful"));
|
||||
config.setDisableCloudConnection(result == "T");
|
||||
}
|
||||
hasPortalConfig = false;
|
||||
}
|
||||
|
||||
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
@ -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"
|
||||
|
||||
#define AG_LIB_VER "3.0.3"
|
||||
#ifdef ESP8266
|
||||
#include <ESP8266WiFi.h>
|
||||
#else
|
||||
#include "WiFi.h"
|
||||
#endif
|
||||
|
||||
AirGradient::AirGradient(BoardType 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;
|
||||
}
|
||||
|
||||
String AirGradient::getVersion(void) { return AG_LIB_VER; }
|
||||
String AirGradient::getVersion(void) { return GIT_VERSION; }
|
||||
|
||||
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,62 @@
|
||||
#ifndef _AIR_GRADIENT_H_
|
||||
#define _AIR_GRADIENT_H_
|
||||
|
||||
#include "display/oled.h"
|
||||
#include "main/BoardDef.h"
|
||||
#include "main/HardwareWatchdog.h"
|
||||
#include "main/LedBar.h"
|
||||
#include "main/PushButton.h"
|
||||
#include "main/StatusLed.h"
|
||||
#include "pms/pms5003.h"
|
||||
#include "pms/pms5003t.h"
|
||||
#include "s8/s8.h"
|
||||
#include "sgp41/sgp41.h"
|
||||
#include "sht/sht.h"
|
||||
#include "Display/Display.h"
|
||||
#include "Main/BoardDef.h"
|
||||
#include "Main/HardwareWatchdog.h"
|
||||
#include "Main/LedBar.h"
|
||||
#include "Main/PushButton.h"
|
||||
#include "Main/StatusLed.h"
|
||||
#include "PMS/PMS5003.h"
|
||||
#include "PMS/PMS5003T.h"
|
||||
#include "S8/S8.h"
|
||||
#include "Sgp41/Sgp41.h"
|
||||
#include "Sht/Sht.h"
|
||||
#include "Main/utils.h"
|
||||
|
||||
#ifndef GIT_VERSION
|
||||
#define GIT_VERSION "3.3.9-snap"
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef ESP8266
|
||||
// Airgradient server root ca certificate
|
||||
const char *const AG_SERVER_ROOT_CA =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIF4jCCA8oCCQD7MgvcaVWxkTANBgkqhkiG9w0BAQsFADCBsjELMAkGA1UEBhMC\n"
|
||||
"VEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAOBgNVBAcMB01hZSBSaW0xGTAXBgNV\n"
|
||||
"BAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNVBAsMC1NlbnNvciBMYWJzMSgwJgYD\n"
|
||||
"VQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFicyBSb290IENBMSEwHwYJKoZIhvcN\n"
|
||||
"AQkBFhJjYUBhaXJncmFkaWVudC5jb20wHhcNMjEwOTE3MTE0NDE3WhcNNDEwOTEy\n"
|
||||
"MTE0NDE3WjCBsjELMAkGA1UEBhMCVEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAO\n"
|
||||
"BgNVBAcMB01hZSBSaW0xGTAXBgNVBAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNV\n"
|
||||
"BAsMC1NlbnNvciBMYWJzMSgwJgYDVQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFi\n"
|
||||
"cyBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJjYUBhaXJncmFkaWVudC5jb20wggIi\n"
|
||||
"MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6XkVQ4O9d5GcUjPYRgF/uaY6O\n"
|
||||
"5ry1xCGvotxkEeKkBk99lB1oNUUfNsP5bwuDci4XKfY9Ro6/jmkfHSVcPAwUnjAt\n"
|
||||
"BcHqZtA/cMXykaynf9yXPxPQN7XLu/Rk32RIfb90sIGS318xgNziCYvzWZmlxpxc\n"
|
||||
"3gUcAgGtamlgZ6wD3yOHVo8B9aFNvmP16QwkUm8fKDHunJG+iX2Bxa4ka5FJovhG\n"
|
||||
"TnUwtso6Vrn0JaWF9qWcPZE0JZMjFW8PYRriyJmHwr/nAXfPPKphD1oRO+oA7/jq\n"
|
||||
"dYkrJw6+OHfFXnPB1xkeh4OPBzcCZHT5XWNfwBYazYpjcJa9ngGFSmg8lX1ac23C\n"
|
||||
"zea1XJmSrPwbZbWxoQznnf7Y78mRjruYKgSP8rf74KYvBe/HGPL5NQyXQ3l6kwmu\n"
|
||||
"CCUqfcC0wCWEtWESxwSdFE2qQii8CZ12kQExzvR2PrOIyKQYSdkGx9/RBZtAVPXP\n"
|
||||
"hmLuRBQYHrF5Cxf1oIbBK8OMoNVgBm6ftt15t9Sq9dH5Aup2YR6WEJkVaYkYzZzK\n"
|
||||
"X7M+SQcdbXp+hAO8PFpABJxkaDAO2kiB5Ov7pDYPAcmNFqnJT48AY0TZJeVeCa5W\n"
|
||||
"sIv3lPvB/XcFjP0+aZxxNSEEwpGPUYgvKUYUUmb0NammlYQwZHKaShPEmZ3UZ0bp\n"
|
||||
"VNt4p6374nzO376sSwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQB/LfBPgTx7xKQB\n"
|
||||
"JNMUhah17AFAn050NiviGJOHdPQely6u3DmJGg+ijEVlPWO1FEW3it+LOuNP5zOu\n"
|
||||
"bhq8paTYIxPxtALIxw5ksykX9woDuX3H6FF9mPdQIbL7ft+3ZtZ4FWPui9dUtaPe\n"
|
||||
"ZBmDFDi4U29nhWZK68JSp5QkWjfaYLV/vtag7120eVyGEPFZ0UAuTUNqpw+stOt9\n"
|
||||
"gJ2ZxNx13xJ8ZnLK7qz1crPe8/8IVAdxbVLoY7JaWPLc//+VF+ceKicy8+4gV7zN\n"
|
||||
"Gnq2IyM+CHFz8VYMLbW+3eVp4iJjTa72vae116kozboEIUVN9rgLqIKyVqQXiuoN\n"
|
||||
"g3xY+yfncPB2+H/+lfyy6mepPIfgksd3+KeNxFADSc5EVY2JKEdorRodnAh7a8K6\n"
|
||||
"WjTYgq+GjWXU2uQW2SyPt6Tu33OT8nBnu3NB80eT8WXgdVCkgsuyCuLvNRf1Xmze\n"
|
||||
"igvurpU6JmQ1GlLgLJo8omJHTh1zIbkR9injPYne2v9ciHCoP6+LDEqe+rOsvPCB\n"
|
||||
"C/o/iZ4svmYX4fWGuU7GgqZE8hhrC3+GdOTf2ADC752cYCZxBidXGtkrGNoHQKmQ\n"
|
||||
"KCOMFBxZIvWteB3tUo3BKYz1D2CvKWz1wV4moc5JHkOgS+jqxhvOkQ/vfQBQ1pUY\n"
|
||||
"TMui9BSwU7B1G2XjdLbfF3Dc67zaSg==\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Class with define all the sensor has supported by Airgradient. Each
|
||||
@ -107,6 +152,67 @@ public:
|
||||
*/
|
||||
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:
|
||||
BoardType boardType;
|
||||
};
|
||||
|
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";
|
||||
}
|
125
src/App/AppDef.h
Normal file
@ -0,0 +1,125 @@
|
||||
#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 {
|
||||
COR_ALGO_PM_UNKNOWN, // Unknown algorithm
|
||||
COR_ALGO_PM_NONE, // No PM correction
|
||||
COR_ALGO_PM_EPA_2021,
|
||||
COR_ALGO_PM_SLR_CUSTOM,
|
||||
};
|
||||
|
||||
// Don't change the order of the enum
|
||||
enum TempHumCorrectionAlgorithm {
|
||||
COR_ALGO_TEMP_HUM_UNKNOWN, // Unknown algorithm
|
||||
COR_ALGO_TEMP_HUM_NONE, // No PM correction
|
||||
COR_ALGO_TEMP_HUM_AG_PMS5003T_2024,
|
||||
COR_ALGO_TEMP_HUM_SLR_CUSTOM
|
||||
};
|
||||
|
||||
enum AgFirmwareMode {
|
||||
FW_MODE_I_9PSL, /** ONE_INDOOR */
|
||||
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
|
||||
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
|
||||
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
|
||||
FW_MODE_O_1PS, /** PMS5003T, S8 */
|
||||
FW_MODE_O_1P, /** PMS5003T */
|
||||
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
|
||||
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
|
||||
FW_MODE_I_BASIC_40PS, /** DIY_BASIC 4.0 */
|
||||
};
|
||||
const char *AgFirmwareModeName(AgFirmwareMode mode);
|
||||
|
||||
#endif /** _APP_DEF_H_ */
|
@ -1,6 +1,6 @@
|
||||
#include "oled.h"
|
||||
#include "../library/Adafruit_SH110x/Adafruit_SH110X.h"
|
||||
#include "../library/Adafruit_SSD1306_Wemos_OLED/Adafruit_SSD1306.h"
|
||||
#include "Display.h"
|
||||
#include "../Libraries/Adafruit_SH110x/Adafruit_SH110X.h"
|
||||
#include "../Libraries/Adafruit_SSD1306_Wemos_OLED/Adafruit_SSD1306.h"
|
||||
|
||||
#define disp(func) \
|
||||
if (this->_boardType == DIY_BASIC) { \
|
@ -1,7 +1,7 @@
|
||||
#ifndef _AIR_GRADIENT_OLED_H_
|
||||
#define _AIR_GRADIENT_OLED_H_
|
||||
|
||||
#include "../main/BoardDef.h"
|
||||
#include "../Main/BoardDef.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|