mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63b0291961 | ||
|
|
3b07903bf4 | ||
|
|
1f196895ce | ||
|
|
35e02f54e5 | ||
|
|
e2495843ff | ||
|
|
95e9f9a751 | ||
|
|
396a6e07db | ||
|
|
7735d7f0cb | ||
|
|
93711d981d | ||
|
|
3af9706f87 | ||
|
|
66e6088bf1 | ||
|
|
bb1629af90 | ||
|
|
da99137023 | ||
|
|
86d9004c9b | ||
|
|
350f19cd93 | ||
|
|
5776341b86 | ||
|
|
ac42d9a342 | ||
|
|
a7d1755bcf | ||
|
|
55f1fd7ce6 | ||
|
|
9401c20df6 | ||
|
|
69aea5ce35 | ||
|
|
9f111ab8bd | ||
|
|
bb1008435f | ||
|
|
d8046146b5 | ||
|
|
3054123b7f | ||
|
|
3b488423a8 | ||
|
|
aa4705e1e3 | ||
|
|
ac38bc20f5 | ||
|
|
10c068178e | ||
|
|
1b2cc44bc3 | ||
|
|
9cd5f62f9b | ||
|
|
1788aa3c4a | ||
|
|
c4ab152566 | ||
|
|
e5174d1009 | ||
|
|
f056893f87 | ||
|
|
f119b02815 | ||
|
|
f5db6b13d1 | ||
|
|
b94f34f06a | ||
|
|
a0a7b7e263 | ||
|
|
d533f4d754 | ||
|
|
d62f680132 | ||
|
|
c2e1e7f95c | ||
|
|
4b1226bd05 | ||
|
|
64c987a560 | ||
|
|
ca373b2b8e | ||
|
|
afb5bb61b8 | ||
|
|
b2a17d81bf | ||
|
|
e6a0813666 | ||
|
|
2b40362ff7 | ||
|
|
32655b7226 | ||
|
|
b67b7828b9 | ||
|
|
8e4c612fa3 | ||
|
|
7e2fa84daf | ||
|
|
0fad7f50d1 | ||
|
|
47342ed6f7 | ||
|
|
9333ec68ed | ||
|
|
922059b472 | ||
|
|
b541d9cd9c | ||
|
|
7ebbcfe360 | ||
|
|
a37f4fc41c | ||
|
|
e978bc059c | ||
|
|
94020043a5 | ||
|
|
8cc5f290fa | ||
|
|
4cca898927 | ||
|
|
76efbd5c36 | ||
|
|
d82d0a1b9f | ||
|
|
b1720d2981 | ||
|
|
d8f8d5478a | ||
|
|
45675eae3e | ||
|
|
91451f58bf | ||
|
|
304d489c2f | ||
|
|
607dfdb606 | ||
|
|
0d84861270 | ||
|
|
e76e4c27af | ||
|
|
c2af8e55d6 | ||
|
|
20535edf27 | ||
|
|
73da98b3ec | ||
|
|
c09a3584dd | ||
|
|
0b734828ba | ||
|
|
5656e5707b | ||
|
|
3efb536629 | ||
|
|
9df7acdeeb | ||
|
|
18145e42ba | ||
|
|
0e2016879b | ||
|
|
680968ff30 | ||
|
|
94529cd08b | ||
|
|
7d95727761 | ||
|
|
f328cd05cd | ||
|
|
72956f563f | ||
|
|
1cc7df59c2 | ||
|
|
439af6e6b1 | ||
|
|
99f770c493 | ||
|
|
5a663db42e | ||
|
|
ba3f1986f2 | ||
|
|
6a92492bee | ||
|
|
0d07e09a44 | ||
|
|
d6bf07fd71 | ||
|
|
5a8e869a7e | ||
|
|
465b44d734 | ||
|
|
eb27c525e3 | ||
|
|
3a303eeb45 | ||
|
|
9b8de0d77f | ||
|
|
df250c9074 | ||
|
|
b735c61f6e | ||
|
|
cad9ffc83f | ||
|
|
e9bbb6c588 | ||
|
|
88c378f0a2 | ||
|
|
aa8856adf3 | ||
|
|
4611c75dfb | ||
|
|
8cf3b43efb | ||
|
|
ca5df00d60 | ||
|
|
4c0d7fa777 | ||
|
|
ccf04aa5b0 | ||
|
|
297123b5d7 | ||
|
|
32b6bda04c | ||
|
|
bacaf521e4 | ||
|
|
430a9bcdcb | ||
|
|
26a33e3e11 | ||
|
|
118ca1ec30 | ||
|
|
fe9774fe0f | ||
|
|
7cdaf5c785 | ||
|
|
0080e3581e | ||
|
|
6eb8432357 | ||
|
|
71937d1155 | ||
|
|
9f5981e2fd | ||
|
|
701d58fd07 | ||
|
|
dbefaed1a2 | ||
|
|
476952c2da | ||
|
|
48d4dbcfe5 | ||
|
|
9e4b7cb66d | ||
|
|
7fa6387d99 | ||
|
|
993b1ad9a9 | ||
|
|
696b920273 | ||
|
|
4ff7d7049a | ||
|
|
30efa2e028 | ||
|
|
6d23d7591f | ||
|
|
9eee18ab32 | ||
|
|
0ae78ee339 | ||
|
|
15459925b0 | ||
|
|
d5536fbc69 | ||
|
|
ac2cd86f21 | ||
|
|
3545410ba4 | ||
|
|
dda1c45fa9 | ||
|
|
297a1af260 | ||
|
|
c34770ec32 | ||
|
|
1b5d71bac7 | ||
|
|
6fb63f834a | ||
|
|
2b24b91f88 | ||
|
|
b676daf37c | ||
|
|
7bdcfd501e | ||
|
|
60efff754e | ||
|
|
62d951c8e4 | ||
|
|
bb8aec4ce5 | ||
|
|
201ad2e838 | ||
|
|
d04d1f677b | ||
|
|
9abf5abdf6 | ||
|
|
5820298640 | ||
|
|
4dcf0cc78b | ||
|
|
94bb90322b | ||
|
|
b90af1ac4d | ||
|
|
e0c5fb7daf | ||
|
|
e9dec77a83 | ||
|
|
8e0a17e5c8 | ||
|
|
58baeaba6f | ||
|
|
5ded0c9ef8 | ||
|
|
7d498ed50a | ||
|
|
f2ed2cb997 | ||
|
|
b8205e71f3 | ||
|
|
605d1e897a | ||
|
|
01bd8a398b | ||
|
|
65fec94317 | ||
|
|
06c63720f6 | ||
|
|
14aeb7575f | ||
|
|
afd328b3b2 | ||
|
|
9e408e41a8 | ||
|
|
6522cb30a8 | ||
|
|
260a9abee8 | ||
|
|
3c2a05312b | ||
|
|
b05a71b618 | ||
|
|
f9a47786b7 | ||
|
|
a2c33744f6 | ||
|
|
317c026952 | ||
|
|
b5671834e4 | ||
|
|
7078bd1786 | ||
|
|
1af8f4540d | ||
|
|
b257590e98 | ||
|
|
ca4b63220c | ||
|
|
0ab747c62a | ||
|
|
c6e2b1b349 | ||
|
|
9c7fed4fe1 | ||
|
|
c56eec9018 | ||
|
|
e7bb9bc28e | ||
|
|
137410e184 | ||
|
|
86e413dc2a | ||
|
|
8d34744b29 | ||
|
|
833420fe99 | ||
|
|
b7ca079d9d | ||
|
|
13b43076f9 | ||
|
|
cd01cbf77d | ||
|
|
483b83489b | ||
|
|
7bffda3fe6 | ||
|
|
590e212ddb | ||
|
|
379ffee475 | ||
|
|
1e628625aa | ||
|
|
c9393744a4 | ||
|
|
918e02ddfd | ||
|
|
9cd81e3af9 | ||
|
|
ba6336587e | ||
|
|
6a85acdf85 | ||
|
|
88b97c7caa | ||
|
|
ea4ee025f2 | ||
|
|
278c57b4ec | ||
|
|
91826229cb | ||
|
|
9cec5a25a8 | ||
|
|
da02b59a21 | ||
|
|
e9a02d4668 | ||
|
|
b4368308ca | ||
|
|
47ce0110af | ||
|
|
eb5ba12df1 | ||
|
|
d70f358f3e | ||
|
|
1f3b2e34ae | ||
|
|
0994c4b3f8 | ||
|
|
0d61a6b55f | ||
|
|
e830fa26e3 | ||
|
|
acdad80a43 | ||
|
|
8f2c7ae870 | ||
|
|
713fcfc740 | ||
|
|
e6d21b8436 | ||
|
|
0aecb22904 | ||
|
|
fd7f400b25 | ||
|
|
d8c81ecca1 | ||
|
|
8ecf0575a1 | ||
|
|
e9d0012cce | ||
|
|
c987ee93c8 | ||
|
|
8279c5be2a | ||
|
|
5acc7cc9aa | ||
|
|
14cf6b81d4 | ||
|
|
36ed8c1af6 | ||
|
|
a581de29f3 | ||
|
|
4ab63aa1e9 | ||
|
|
c2081aaae7 | ||
|
|
abd52d4754 | ||
|
|
5052c675be | ||
|
|
755aca1643 | ||
|
|
068c32ad27 | ||
|
|
fa76838ee5 | ||
|
|
4d999b1456 | ||
|
|
78ca3ee39a | ||
|
|
68f5195260 | ||
|
|
4086e46f6c | ||
|
|
cf9d85fd6d | ||
|
|
7c05228def | ||
|
|
924cc06bb2 | ||
|
|
c3c741a2c0 | ||
|
|
6e057dfbb2 | ||
|
|
c8a8cc2f31 | ||
|
|
c12d3c4a6e | ||
|
|
9f6dcad791 | ||
|
|
62ba184cc8 | ||
|
|
cfe2619069 | ||
|
|
810f144cff | ||
|
|
553ae00bac | ||
|
|
d0bdf7bdaa | ||
|
|
8452df79bb | ||
|
|
72b5331610 | ||
|
|
9d061ab24e | ||
|
|
927e3e8db0 | ||
|
|
deeff9fa89 | ||
|
|
654d692025 | ||
|
|
f1779c97a1 | ||
|
|
9553022b40 | ||
|
|
c77ab9e62c | ||
|
|
2c28203c29 | ||
|
|
05807f56d8 | ||
|
|
fd30eff2bf | ||
|
|
aa20fbc789 | ||
|
|
ac089992e8 | ||
|
|
2001438d6f | ||
|
|
5928293c89 | ||
|
|
5e9b0091da | ||
|
|
aa59dcfe0f | ||
|
|
5bab658031 | ||
|
|
08b2f4d83d | ||
|
|
2d2b9f82f3 | ||
|
|
ef8bef1f4d | ||
|
|
ef09264b6d | ||
|
|
f9091951be | ||
|
|
925eb79066 | ||
|
|
917f8bbfb0 | ||
|
|
620ca70086 | ||
|
|
55d7ccbd7a | ||
|
|
f199906e9d | ||
|
|
70776a0a05 | ||
|
|
94754e60b5 | ||
|
|
8ce698427d | ||
|
|
8984863fe6 | ||
|
|
cdd8a406a8 | ||
|
|
d395e496a1 | ||
|
|
e87141eb04 | ||
|
|
8c1e890c36 | ||
|
|
109a618c1f | ||
|
|
ce37e5c8ff | ||
|
|
8503260101 | ||
|
|
2ce0850d80 | ||
|
|
3bf0d4bed3 | ||
|
|
69c60e8e4e | ||
|
|
a83ac6e926 | ||
|
|
14f70dde76 | ||
|
|
5fa729170a | ||
|
|
b78efd36df | ||
|
|
8448d66ea7 | ||
|
|
bcf1dd69ff | ||
|
|
7820fb72e7 | ||
|
|
3bec5ade1a | ||
|
|
4d37843af8 | ||
|
|
87fcf08ee0 | ||
|
|
f5dd402ba2 | ||
|
|
960ef96805 | ||
|
|
d157694b12 | ||
|
|
1e90d22d3b | ||
|
|
f16af759d1 | ||
|
|
e8297f9b8f | ||
|
|
8bfc228695 | ||
|
|
9b100c34dc | ||
|
|
c47fc907a9 | ||
|
|
34e30277d7 | ||
|
|
cd9e2070c3 | ||
|
|
1431ca7615 | ||
|
|
a9507c98b5 | ||
|
|
65d3affda3 | ||
|
|
fe8d2970a0 | ||
|
|
d805cbdc0c | ||
|
|
3e8560c570 | ||
|
|
848a107b16 | ||
|
|
5578cb85c6 | ||
|
|
e24108fa45 | ||
|
|
2dd2eb1d7e | ||
|
|
04a39c2917 | ||
|
|
dac0018d77 | ||
|
|
c48173e32a | ||
|
|
133dd8cd52 | ||
|
|
0dbffbb730 | ||
|
|
8aca0beccb | ||
|
|
77bd201d13 | ||
|
|
0780e4fe02 | ||
|
|
c710e9ecbe | ||
|
|
2fdba90efb | ||
|
|
9d9d314a0c | ||
|
|
bd59d98b91 | ||
|
|
f122cc9509 | ||
|
|
1c72f4c944 | ||
|
|
42532f995b | ||
|
|
f714000fb9 | ||
|
|
83e9bbc9db | ||
|
|
ef8e26c87e | ||
|
|
0d04a14352 | ||
|
|
f9b86f6870 | ||
|
|
e933a20d20 | ||
|
|
6119a9efdf | ||
|
|
9eac697606 | ||
|
|
87ec3c8d8d | ||
|
|
73b465329c | ||
|
|
245d24b31b | ||
|
|
1fc27318d3 | ||
|
|
6eb463d646 | ||
|
|
fcc662c70a | ||
|
|
60c1b8617d | ||
|
|
28a0dcbf13 | ||
|
|
0602cdcb41 | ||
|
|
6205ddc3d8 | ||
|
|
e759a48847 | ||
|
|
b3d45031a5 | ||
|
|
db74856951 | ||
|
|
0b68256215 | ||
|
|
6e05a9ff27 | ||
|
|
41cb00ecaa | ||
|
|
4d2a6b2bac | ||
|
|
1e85c0e35e | ||
|
|
82701d9ec7 | ||
|
|
e071e3ad8f | ||
|
|
e394283fda | ||
|
|
4e4942b511 | ||
|
|
1fb70a9b44 | ||
|
|
f0d3a24c11 | ||
|
|
86a97d7669 | ||
|
|
506b3c48f3 | ||
|
|
16bd0f4f3c | ||
|
|
78f00ffe0a | ||
|
|
a72930eebf | ||
|
|
8d967a6dfc | ||
|
|
321034b99e | ||
|
|
d8c7d2a6f0 | ||
|
|
fc48649129 | ||
|
|
12c114d11b | ||
|
|
2c84651286 | ||
|
|
b34efd0206 | ||
|
|
54904d316b | ||
|
|
35e9f41b8b | ||
|
|
b5508762ef | ||
|
|
3a66412db4 | ||
|
|
5def8f84d4 | ||
|
|
e1a263b47f | ||
|
|
3828790109 | ||
|
|
a7fb6d5caf | ||
|
|
08fbe310b7 | ||
|
|
9e29835ac8 | ||
|
|
e454c9969a | ||
|
|
08efbfcfe7 | ||
|
|
726bdfae46 | ||
|
|
61916627ed | ||
|
|
d1656421bb | ||
|
|
fac59e1982 | ||
|
|
31be25d54b | ||
|
|
994609f1e0 | ||
|
|
4319a66da9 | ||
|
|
998243c60c | ||
|
|
9af72535ef | ||
|
|
2cedf7b368 | ||
|
|
af3c3671a2 | ||
|
|
80a8dfd554 | ||
|
|
bc06bb0a1b | ||
|
|
57a9f17335 | ||
|
|
b2f87abd1b | ||
|
|
01538e69c1 | ||
|
|
3c0c3ff93e | ||
|
|
f99eda8a9f | ||
|
|
d71a14194b | ||
|
|
95d116cea3 | ||
|
|
f4a577bbc2 | ||
|
|
634fac4be0 | ||
|
|
b67d1656d8 | ||
|
|
01b909a1d2 | ||
|
|
5edf38550a | ||
|
|
7458674c7b | ||
|
|
0486034c6a | ||
|
|
6029a39f05 | ||
|
|
3e9f9a103e | ||
|
|
062e66bb3c | ||
|
|
7b52e709db | ||
|
|
135e872caa | ||
|
|
6515190124 | ||
|
|
32075b32cd | ||
|
|
6b6a960075 | ||
|
|
5aebb1dd51 | ||
|
|
293f705b0f | ||
|
|
e9d75a08b8 | ||
|
|
709cfffb95 | ||
|
|
6c08094e41 | ||
|
|
4a98ba0e29 | ||
|
|
3f93afe9c2 | ||
|
|
eb771a0608 | ||
|
|
89fcfe9680 | ||
|
|
b61bc261bf | ||
|
|
decd2942c5 | ||
|
|
dd8bbea8c5 | ||
|
|
bdb1ab3c32 | ||
|
|
e173946284 | ||
|
|
f7d4c166ae | ||
|
|
893568052a | ||
|
|
9967d27148 | ||
|
|
cc4acba4ab | ||
|
|
87009dc443 | ||
|
|
2f76338ec7 | ||
|
|
f3c1964e1c | ||
|
|
13132edd9e | ||
|
|
0a7d7469fd | ||
|
|
725f824e58 | ||
|
|
a6438edb3c | ||
|
|
9669bfd1a3 | ||
|
|
210b4c5d01 | ||
|
|
ef962adc57 | ||
|
|
c9665adfaf | ||
|
|
12fde678e5 | ||
|
|
351761b9a7 | ||
|
|
0fd0c33222 | ||
|
|
636027cabc | ||
|
|
7882c0e9a9 | ||
|
|
9d6ce13b92 | ||
|
|
72bcbd8a80 | ||
|
|
56ad40d083 | ||
|
|
22e35b2a03 | ||
|
|
f93d444966 | ||
|
|
d90f6854e9 | ||
|
|
a4993a8441 | ||
|
|
768ead409e | ||
|
|
fb29462e73 | ||
|
|
2447a3fd74 | ||
|
|
e45bdb1831 | ||
|
|
a2355889ba | ||
|
|
4014daeb71 | ||
|
|
7d7e228b9e | ||
|
|
9a85b107fa | ||
|
|
8190ec6077 | ||
|
|
3771a79a55 | ||
|
|
4d94476fa3 | ||
|
|
5d9a51f671 | ||
|
|
e50d30cd5e | ||
|
|
6e1e698cf9 | ||
|
|
f06f4ea5dc | ||
|
|
646af21d30 | ||
|
|
3f8f3eb1ad | ||
|
|
bd8320a45a | ||
|
|
58428ec1f8 | ||
|
|
999da6d1a1 | ||
|
|
368ea928f0 | ||
|
|
7acc77bcb1 | ||
|
|
2e7af253ab | ||
|
|
d0e70fdd6b | ||
|
|
8f9edd18a6 | ||
|
|
89eb952d18 | ||
|
|
d1e395e78f | ||
|
|
286e5d13d9 | ||
|
|
ff26ed4e4c | ||
|
|
edce3fc84c | ||
|
|
f26638e38b | ||
|
|
1b8d870c22 | ||
|
|
34d9a975b5 | ||
|
|
e2d7ae7533 | ||
|
|
41810a344a | ||
|
|
6e1104efbf | ||
|
|
422ce3e165 | ||
|
|
7f414f5224 | ||
|
|
5b997e0597 | ||
|
|
6335512ded | ||
|
|
7446edf547 | ||
|
|
4775b1df51 | ||
|
|
8a8e363cc3 | ||
|
|
b5218d1b93 | ||
|
|
e0e0150f78 | ||
|
|
5685e7d9b4 | ||
|
|
35a1ab2e8d | ||
|
|
8483118860 | ||
|
|
49c6cec998 | ||
|
|
a1c53d785f | ||
|
|
debb20e41b | ||
|
|
d20b85a7b8 | ||
|
|
c3ff582374 | ||
|
|
cccc7c9ae6 | ||
|
|
318742e183 | ||
|
|
7fbb941383 | ||
|
|
4873c7c632 | ||
|
|
f0441f65b7 | ||
|
|
ea1a09130f | ||
|
|
8b685d6826 | ||
|
|
621ac63154 | ||
|
|
d6cf147e5b | ||
|
|
30fd2b13a6 | ||
|
|
6131c2d38a | ||
|
|
bbc98a7e74 | ||
|
|
9353fb01d3 | ||
|
|
8f6950d3eb | ||
|
|
0865a36a5e | ||
|
|
c6591ce7b9 | ||
|
|
0e75b669b7 | ||
|
|
3717e323b9 | ||
|
|
1282c4d5b5 | ||
|
|
862e2dbde2 | ||
|
|
aaf2544c93 | ||
|
|
e25277b20c | ||
|
|
944450840a | ||
|
|
540ee694fe | ||
|
|
71f997b5c7 | ||
|
|
fd6bf74720 | ||
|
|
65ad141008 | ||
|
|
4add3127d4 | ||
|
|
3e2f8ae9d8 | ||
|
|
bbe984c93b | ||
|
|
cf1b1a93a6 | ||
|
|
0ec7a58b7b | ||
|
|
7da803389c | ||
|
|
5a3829abf9 | ||
|
|
6459e4fd4d | ||
|
|
df38c54a66 | ||
|
|
65cb95585d | ||
|
|
9d3cc69b56 | ||
|
|
5dab5a4685 | ||
|
|
69e08e7871 | ||
|
|
c9af1b11a7 | ||
|
|
cac71e368e | ||
|
|
2267f34fb5 | ||
|
|
205d30e958 | ||
|
|
7dc75eacde | ||
|
|
9a4c0f1580 | ||
|
|
ccfca4b663 | ||
|
|
41214ad7eb | ||
|
|
501c824993 | ||
|
|
73f2a8c366 | ||
|
|
38b6c878a2 | ||
|
|
370be061e4 | ||
|
|
2209f987b7 | ||
|
|
7f0bd1e41f | ||
|
|
55577692fe | ||
|
|
2d021bbab8 | ||
|
|
256c889715 | ||
|
|
c3fd7d9622 | ||
|
|
3c280b12a8 | ||
|
|
080f7751e0 | ||
|
|
1d0970a882 | ||
|
|
bef46b9e72 | ||
|
|
4f1196aa69 | ||
|
|
ea67dc22c0 | ||
|
|
b091ee00d8 | ||
|
|
ec1d474d7b | ||
|
|
7d01b99e72 | ||
|
|
29bb8a7d22 | ||
|
|
b95cadbe9a | ||
|
|
e1e061e46a | ||
|
|
686f7f74ee | ||
|
|
669dad4206 | ||
|
|
8e7b43a029 | ||
|
|
fe772f6bf5 | ||
|
|
193fdf177c | ||
|
|
613ec354bd | ||
|
|
6dbc0017ba | ||
|
|
37e100672a | ||
|
|
7d42423894 | ||
|
|
85b2319b59 | ||
|
|
dbf91a4e8c | ||
|
|
aad1a8d9d2 | ||
|
|
c0f29a6fc3 | ||
|
|
34e1516524 | ||
|
|
740005b21e | ||
|
|
b42b2f23c2 | ||
|
|
0e4af48d54 | ||
|
|
9d00937f34 | ||
|
|
0cd0b75d9e | ||
|
|
c88df86313 | ||
|
|
1c191c1d14 | ||
|
|
12fc6516d2 | ||
|
|
448e079bc8 | ||
|
|
b4cc27cb56 | ||
|
|
f9d1c9bd6f | ||
|
|
d9c27b98d4 | ||
|
|
4f0a87a0a5 | ||
|
|
9ce498c5b6 | ||
|
|
393e7e45df | ||
|
|
1e10ffa49a | ||
|
|
2610dcb2c5 | ||
|
|
ed80635e32 | ||
|
|
c52186079a | ||
|
|
7f12e202d4 | ||
|
|
97e7d30c66 | ||
|
|
04552e40d8 | ||
|
|
0d8b96ebf3 | ||
|
|
5827c2f8cb | ||
|
|
8871bdd311 | ||
|
|
338544bc6a | ||
|
|
43051f9f51 | ||
|
|
7ab5f6d5b2 | ||
|
|
3a0723cf66 | ||
|
|
103c37d71f | ||
|
|
42241d6e36 | ||
|
|
b17c328f0f | ||
|
|
c1f018ca7c | ||
|
|
d6e091f587 | ||
|
|
8b579bb8a7 | ||
|
|
b2f6ff4a1f | ||
|
|
805072c4c4 | ||
|
|
247aecb2fc | ||
|
|
c78b76ba2e | ||
|
|
85d7f2a549 | ||
|
|
16c37e5389 | ||
|
|
b963e7f95a | ||
|
|
1bf3fc04ea | ||
|
|
4f5e21647f | ||
|
|
f2bbb5f4e2 | ||
|
|
34b7d1ea03 | ||
|
|
28cf8eed16 | ||
|
|
51432d0ef7 | ||
|
|
c651ab4b62 | ||
|
|
3123eac5c8 | ||
|
|
8f03b5df10 | ||
|
|
2911bb33ee | ||
|
|
2074d85da7 | ||
|
|
593eda9b87 | ||
|
|
3579733f5c | ||
|
|
6d4c971b49 | ||
|
|
1b8ce61522 | ||
|
|
f1a1f44a08 | ||
|
|
915023ccb9 | ||
|
|
792ed84f4b | ||
|
|
5ee1ae78c0 | ||
|
|
4cceb95a42 | ||
|
|
c6027058c9 | ||
|
|
493a73c205 | ||
|
|
e9c4cdb2c6 | ||
|
|
768a965810 | ||
|
|
c6ecad1b3a | ||
|
|
5a5f1c631b | ||
|
|
573f8113ef | ||
|
|
440f312575 | ||
|
|
dd7a686181 | ||
|
|
6a139688f3 | ||
|
|
b855c0a440 | ||
|
|
02de95c529 | ||
|
|
e18bc49a2a | ||
|
|
704a7ae393 | ||
|
|
2c41af00f5 | ||
|
|
afbad01ee1 | ||
|
|
cba606b846 | ||
|
|
8d5e9e3977 | ||
|
|
24de319c7e | ||
|
|
ad623cb586 | ||
|
|
f2f0aabe3e | ||
|
|
37cdda33c7 | ||
|
|
c9ca210803 | ||
|
|
2db93d28c7 | ||
|
|
d140ccff84 | ||
|
|
a51298ea54 | ||
|
|
f1fd553ac6 | ||
|
|
caa32c4bc2 | ||
|
|
0e6ed38986 | ||
|
|
a39b96be9c | ||
|
|
713bf957c3 | ||
|
|
74ceba7409 | ||
|
|
08bb2322aa | ||
|
|
9ab4deaf5b | ||
|
|
36ad6f80c4 | ||
|
|
2fac07cbf0 | ||
|
|
85dd747949 | ||
|
|
32495668d8 | ||
|
|
fc84e7a260 | ||
|
|
933f86720a | ||
|
|
abbc110656 | ||
|
|
17f2316ea8 | ||
|
|
24d317b9da | ||
|
|
40d522d8b6 | ||
|
|
0f2ba754e2 | ||
|
|
f65972aa55 | ||
|
|
e235a45d5a | ||
|
|
aeba5478ee | ||
|
|
b9f6b472a7 | ||
|
|
b9fcc8fe22 | ||
|
|
5a06f29c5b | ||
|
|
4fdefaf67d | ||
|
|
3a9d70f581 | ||
|
|
906a9d89cb | ||
|
|
c0187a60e1 | ||
|
|
0ab6f57239 | ||
|
|
9917f619b3 | ||
|
|
8f2791f908 | ||
|
|
6d92ae5a1f | ||
|
|
8d7da2fd8b | ||
|
|
94115006f6 | ||
|
|
00b14d5d1c | ||
|
|
bff3d75f91 | ||
|
|
e3d91a03ff | ||
|
|
b3e19041ff | ||
|
|
10088c71f3 | ||
|
|
01a5646317 | ||
|
|
5066b86102 | ||
|
|
f1e657e2b8 | ||
|
|
7f6039a6b7 | ||
|
|
24a25fc687 | ||
|
|
128214a159 | ||
|
|
718a8c8f4c | ||
|
|
3c8af53440 | ||
|
|
042640924f | ||
|
|
428c9fcbd5 | ||
|
|
9e265b44b5 | ||
|
|
fe17307e09 | ||
|
|
3fafb3304a | ||
|
|
f8ae461bfd | ||
|
|
d27fa7df61 | ||
|
|
16bc717ceb | ||
|
|
8065a1eaa4 | ||
|
|
174bb615e6 | ||
|
|
82309e7a84 | ||
|
|
b682d8996b | ||
|
|
c5b3637350 | ||
|
|
5b3ac4236e | ||
|
|
87c2eb42b6 | ||
|
|
10f17d0472 | ||
|
|
7ec3b99020 | ||
|
|
49104d4bbb | ||
|
|
41b29fa08c | ||
|
|
ef70b49e98 | ||
|
|
4a6631511b | ||
|
|
a7b992450f | ||
|
|
10db048d61 | ||
|
|
3ae0b40ad9 | ||
|
|
7495fe2999 | ||
|
|
c9a7112cbb | ||
|
|
32ccac5936 | ||
|
|
32d6f11217 | ||
|
|
13cd054c15 | ||
|
|
c2cbe3015b | ||
|
|
4e71d53c54 | ||
|
|
02c531a247 | ||
|
|
6270630d1a | ||
|
|
7279333a17 | ||
|
|
6319955cdc | ||
|
|
14cd0d563a | ||
|
|
fd3f5bbb2d | ||
|
|
9e3061201e | ||
|
|
208ae5c7ea | ||
|
|
56d3e6d737 | ||
|
|
5eea21afa9 | ||
|
|
e4605775a2 | ||
|
|
9e5c1b5fe2 | ||
|
|
3891c0a631 | ||
|
|
fe8f38aa59 | ||
|
|
851a38e7fa | ||
|
|
7f6bd107e6 | ||
|
|
28ed839274 | ||
|
|
c358378a27 | ||
|
|
d090dd3dd7 | ||
|
|
10d6deadc2 | ||
|
|
19d578e04b | ||
|
|
881f50fb7e | ||
|
|
57fc15a20a | ||
|
|
941865ec85 | ||
|
|
6f84964894 | ||
|
|
88d2cfbff4 | ||
|
|
839a421ff1 | ||
|
|
7a57a1395d | ||
|
|
31d53b18be | ||
|
|
8d4f745abf | ||
|
|
e970686064 | ||
|
|
159674bbba | ||
|
|
7ad6470adf | ||
|
|
de85162267 | ||
|
|
8ddc86f470 | ||
|
|
c9e82ae920 | ||
|
|
0741dbde2e | ||
|
|
e3b7dc2641 | ||
|
|
d07f73263a | ||
|
|
f60a1d1b06 | ||
|
|
424f48f0a8 | ||
|
|
03a40915ee | ||
|
|
b7d230cc40 | ||
|
|
8cbd0fa54e | ||
|
|
0749e1bdd7 | ||
|
|
655db109c1 | ||
|
|
109dfa5c58 | ||
|
|
3cc1fabae7 | ||
|
|
81045e6433 | ||
|
|
84752d8027 | ||
|
|
36840fc712 | ||
|
|
3378a4a846 | ||
|
|
21f1016d89 | ||
|
|
4f86945d7e | ||
|
|
278c320e22 | ||
|
|
29edaed0e7 | ||
|
|
32e51ea37b | ||
|
|
9346cee76c | ||
|
|
19d6101a1b | ||
|
|
68201cd965 | ||
|
|
1fade32d66 | ||
|
|
158dac1e68 | ||
|
|
721bb417bf | ||
|
|
0c5e005866 | ||
|
|
0a1bd9f934 | ||
|
|
5e487d6edc | ||
|
|
1f9a13a42e | ||
|
|
3796af5930 | ||
|
|
8af108262c | ||
|
|
b63e8b575b | ||
|
|
a17fabd7fe | ||
|
|
5a34002d5d | ||
|
|
6295d128aa | ||
|
|
724447a654 | ||
|
|
9f6cc23beb | ||
|
|
6e1b2ed32c | ||
|
|
8a17ef9c03 | ||
|
|
3de9a34e3b | ||
|
|
4db3eca51e | ||
|
|
ca5609d8e6 | ||
|
|
53df6ac612 | ||
|
|
108af72d5f | ||
|
|
5ae132a7e9 | ||
|
|
5a6a365f0a | ||
|
|
92debb4bf8 | ||
|
|
ea9134afd2 | ||
|
|
bce3607504 | ||
|
|
051966c856 | ||
|
|
d0ed01433d | ||
|
|
7b4fd38b6b | ||
|
|
58d23513a1 | ||
|
|
39da3e6d6c | ||
|
|
6749122eed | ||
|
|
f210000c94 | ||
|
|
327db84d18 | ||
|
|
ff636abea8 | ||
|
|
951b39b088 | ||
|
|
6ba4a2523c | ||
|
|
dfd48c2c81 | ||
|
|
3eb8d39565 | ||
|
|
ae95ca2c7b | ||
|
|
edbaf5acdd | ||
|
|
7c4963fac2 | ||
|
|
c2bdbfafb9 | ||
|
|
4e57d74e87 | ||
|
|
5e27194204 | ||
|
|
b516085a3d | ||
|
|
f333f271e4 | ||
|
|
35bf4a94bd | ||
|
|
e01101eff1 | ||
|
|
39f20a6adc | ||
|
|
3d7074166c | ||
|
|
415f98d9fb | ||
|
|
e5edf536a5 | ||
|
|
f9a8f5bdab | ||
|
|
64075bc7f9 | ||
|
|
9eec7f97dd | ||
|
|
0c6d4df7a7 | ||
|
|
aaad9ca4c3 | ||
|
|
7a5a72eed4 | ||
|
|
89cfdfc67f | ||
|
|
374e9f515f | ||
|
|
9d16a68665 | ||
|
|
ea994d5ccd | ||
|
|
97398d7ce2 | ||
|
|
9f4156437b | ||
|
|
cb7cbec1f8 | ||
|
|
f3db838ae1 | ||
|
|
1d6a7275e7 | ||
|
|
1ee8f5a1a9 | ||
|
|
076a355098 | ||
|
|
14c5169ac9 | ||
|
|
b09cc31416 | ||
|
|
41b4717148 | ||
|
|
d8d26b26c2 | ||
|
|
98c78a2929 | ||
|
|
d86351dff4 | ||
|
|
e9cf051cb2 | ||
|
|
08ae001a83 | ||
|
|
b49849f8c0 | ||
|
|
603f2b5631 | ||
|
|
55b336e6d8 | ||
|
|
625c8e2f9e | ||
|
|
26ba373009 | ||
|
|
8c1e070582 | ||
|
|
3e66273c3e | ||
|
|
9da297f405 | ||
|
|
18cb6af0e4 | ||
|
|
a6dc373f5c | ||
|
|
25b880cf9b | ||
|
|
5730ce351a | ||
|
|
62f5ce6e0c | ||
|
|
91383f9bc1 | ||
|
|
ca3a726e88 | ||
|
|
43a035091e | ||
|
|
d98a59cca3 | ||
|
|
a247a5ecb4 | ||
|
|
c43551f85a | ||
|
|
cbb32fdd61 | ||
|
|
bc4c07f315 | ||
|
|
369b627b3b | ||
|
|
089c487576 | ||
|
|
ba3a2c8451 | ||
|
|
8e7663b52b | ||
|
|
78f7de38ad | ||
|
|
3d3518a10a | ||
|
|
e342fd9b71 | ||
|
|
addb045a0c | ||
|
|
bab1201dcb | ||
|
|
611246bd75 | ||
|
|
3dcf3c0927 | ||
|
|
7f2fdee9c8 | ||
|
|
1c890c0c49 | ||
|
|
5243036dbf | ||
|
|
a94d897ee4 | ||
|
|
aa734eb606 | ||
|
|
a863167b23 | ||
|
|
79eb0fee47 | ||
|
|
3e10f984e1 | ||
|
|
17410d38c2 | ||
|
|
1f7dc79b20 | ||
|
|
95b48454e8 | ||
|
|
4424e66222 | ||
|
|
75d007d1c8 | ||
|
|
46929e0ee6 | ||
|
|
8e28011f13 | ||
|
|
617e52cb84 | ||
|
|
2c735ab999 | ||
|
|
fc4942db6e | ||
|
|
3914fee227 | ||
|
|
ffc85a81fe | ||
|
|
1c4011bfb4 | ||
|
|
57ef33a7fc | ||
|
|
9a76b3d94d | ||
|
|
0af804a2f1 | ||
|
|
4d715890f6 | ||
|
|
f5fa886c93 | ||
|
|
e49335d811 | ||
|
|
8e07025e38 | ||
|
|
7cc86e57be | ||
|
|
b22686c8a1 | ||
|
|
2b9ddea331 | ||
|
|
6aaa83a266 | ||
|
|
b1b34a3c97 | ||
|
|
44c85a2315 | ||
|
|
da66231e47 | ||
|
|
2e08e7de32 | ||
|
|
ebdfa063a7 | ||
|
|
a3a04f1f9a | ||
|
|
cfb0d2bc5f | ||
|
|
e8d1ea17cb | ||
|
|
0d5ebde9af | ||
|
|
6894c89441 | ||
|
|
b0c61688d6 | ||
|
|
68762e1741 | ||
|
|
a21f8cbc39 | ||
|
|
19d533b7d5 | ||
|
|
ccfddefaa1 | ||
|
|
95253e834d | ||
|
|
53a27f154e | ||
|
|
53c8addfdc | ||
|
|
e5c263c414 | ||
|
|
e015efe8d0 | ||
|
|
5f78d38f46 | ||
|
|
fa04972cb1 | ||
|
|
8697cab0ae | ||
|
|
c8d89db529 | ||
|
|
ff973e1639 | ||
|
|
d61d9a67b1 | ||
|
|
a4795b77f7 | ||
|
|
46a9431475 | ||
|
|
a832af02ca | ||
|
|
d4540f1197 | ||
|
|
d1639e228a | ||
|
|
fdfaecce8a | ||
|
|
380a63d519 | ||
|
|
7e1455778d | ||
|
|
00fb96aa29 | ||
|
|
7fb35a9540 | ||
|
|
cb02e72123 | ||
|
|
85317530e8 | ||
|
|
e55741f201 | ||
|
|
8595ea5d2c | ||
|
|
9a3f501a24 | ||
|
|
3b6d5f790a | ||
|
|
dbb091d1b7 | ||
|
|
867eb4e689 | ||
|
|
42cd1aeb8d | ||
|
|
a3678c6c22 | ||
|
|
0fb86c3d8c | ||
|
|
4db69a3790 | ||
|
|
2761656d78 | ||
|
|
e56d22cfb1 | ||
|
|
c13c076b46 | ||
|
|
f820dc78be | ||
|
|
af507d798e | ||
|
|
a52740ecff | ||
|
|
1f691ae35c | ||
|
|
65c06c6e2b | ||
|
|
3777fcc846 | ||
|
|
de2deb8e69 | ||
|
|
7189e36c59 | ||
|
|
8dbe045655 | ||
|
|
ead2b7353c | ||
|
|
ceaf53f604 | ||
|
|
e5879b2101 | ||
|
|
2a50d797a0 | ||
|
|
2af1c62bd1 | ||
|
|
f3be7c7c0b | ||
|
|
90024b8603 | ||
|
|
d883ce8ae1 | ||
|
|
8364a6a85d | ||
|
|
8c906af563 | ||
|
|
5aafb89a3f | ||
|
|
b266cd5934 | ||
|
|
1a61362f74 | ||
|
|
7625680daf | ||
|
|
d7976381e6 | ||
|
|
5c6252fe45 | ||
|
|
a5ff5cb9e4 | ||
|
|
d66cd47df8 | ||
|
|
eb560629d6 | ||
|
|
0897c5800c | ||
|
|
648274ab3d | ||
|
|
2f4015e797 | ||
|
|
a702fa6c75 | ||
|
|
9a571226cd | ||
|
|
fa05d2c574 | ||
|
|
0ae9ca3d55 | ||
|
|
d6aec20bc3 | ||
|
|
3fdf53bf06 | ||
|
|
a1050d014f | ||
|
|
52d5427654 | ||
|
|
e8c0b52470 | ||
|
|
4763783fe1 | ||
|
|
ad6b8bdc86 | ||
|
|
d09aff2f6d | ||
|
|
0b6341bea4 | ||
|
|
6465d72f6e | ||
|
|
33704176e0 | ||
|
|
df5857464a | ||
|
|
eb434a0187 | ||
|
|
d7e3c8d005 | ||
|
|
3d2453acc5 | ||
|
|
342eacfe37 | ||
|
|
cf51018512 | ||
|
|
db2a1336e9 | ||
|
|
b151c19b3c | ||
|
|
2aef1344b4 | ||
|
|
4592914fad | ||
|
|
8e710b9d92 | ||
|
|
eee3fb72b2 | ||
|
|
12de39135d | ||
|
|
80470fb553 | ||
|
|
31ab2b67d0 | ||
|
|
4d18bfb113 | ||
|
|
37dbd667f4 | ||
|
|
e43a7d0b75 | ||
|
|
c6f7ac0012 | ||
|
|
64798ebbf1 | ||
|
|
985f8118dd | ||
|
|
69ddda0ee4 | ||
|
|
f59c150729 | ||
|
|
14ce25081a | ||
|
|
81b37dfffc | ||
|
|
1f4a8fe499 | ||
|
|
7b9804d5a2 | ||
|
|
a93b601cd7 | ||
|
|
6525a0bd3d | ||
|
|
2c357d7e86 | ||
|
|
5f28843843 | ||
|
|
6e43c1eb41 | ||
|
|
66c653ff94 | ||
|
|
606a1c3128 | ||
|
|
63dd714805 | ||
|
|
9160b5bd3b | ||
|
|
04a9dda74b | ||
|
|
d1ab6ec6db | ||
|
|
b62f488b6b | ||
|
|
8185eaa002 | ||
|
|
8592647cbf | ||
|
|
f2580463e4 | ||
|
|
c4563802f3 | ||
|
|
68e3eb5bc8 | ||
|
|
cadf6b4a80 | ||
|
|
75f34d9b59 | ||
|
|
47d9082e18 | ||
|
|
0aecffc822 | ||
|
|
179c0309a5 | ||
|
|
89af31245b | ||
|
|
68e32a10c0 | ||
|
|
cee18a7ce3 | ||
|
|
f9c4c87c61 | ||
|
|
06d75bf7b1 | ||
|
|
9637d9473e | ||
|
|
0de9d12930 | ||
|
|
eb4594c8e4 | ||
|
|
58eb7113e7 | ||
|
|
898768092c | ||
|
|
c4905c23b7 | ||
|
|
3950a3b859 | ||
|
|
8f90e0f5d7 | ||
|
|
0fc051a445 | ||
|
|
5e9248651f | ||
|
|
362a561a35 | ||
|
|
136c0c9cba | ||
|
|
486ea7db2c | ||
|
|
ada93b721a | ||
|
|
29b2f02ae4 | ||
|
|
004f222f40 | ||
|
|
612e18be35 | ||
|
|
0b0eb6159c | ||
|
|
1a44dd53e6 | ||
|
|
65349cb546 | ||
|
|
e9b36f9084 | ||
|
|
bd9c3b31fb | ||
|
|
c819d46902 | ||
|
|
2edd37eeb8 | ||
|
|
45a325bd34 | ||
|
|
9c72bf98f7 | ||
|
|
628307dbf7 | ||
|
|
c8455f5cd0 | ||
|
|
7360be5f60 | ||
|
|
3135bc5e89 | ||
|
|
c810633ee3 | ||
|
|
0ddb835072 | ||
|
|
a8c13a7c96 | ||
|
|
7a17c98828 | ||
|
|
8ffb4cbfa9 | ||
|
|
0cc4487e42 | ||
|
|
b696b2c29a | ||
|
|
bd0da6c7a6 | ||
|
|
1f500fddd1 | ||
|
|
a231ae5deb | ||
|
|
5f8f1ce5e6 | ||
|
|
022b15d2c8 | ||
|
|
2a0ac55f87 | ||
|
|
abedb61ca1 | ||
|
|
fd15f986d3 | ||
|
|
60c22d672f | ||
|
|
1b261f2371 | ||
|
|
31887ff36a | ||
|
|
c3d01d33e1 | ||
|
|
e0b8acb8dd | ||
|
|
039702327e | ||
|
|
fac1575c96 | ||
|
|
560d4113f9 | ||
|
|
c93cd80f58 | ||
|
|
04d47882a7 | ||
|
|
028bdeeb17 | ||
|
|
44e828e77a | ||
|
|
801ae82ef5 | ||
|
|
a209509d98 | ||
|
|
74bccc04f7 | ||
|
|
fd27cef1ae | ||
|
|
6708ddf37b | ||
|
|
83c271fed4 | ||
|
|
8a375ec340 | ||
|
|
f99156e29f | ||
|
|
413b76cd81 | ||
|
|
d11c763bce | ||
|
|
f9cc4b641f | ||
|
|
852d90beec | ||
|
|
745c3a91fd | ||
|
|
276795079e | ||
|
|
faf307c7bb | ||
|
|
566bcbe063 | ||
|
|
b99487785d | ||
|
|
0962d718ec | ||
|
|
7f11a5d144 | ||
|
|
e6217a18fc | ||
|
|
ae007befd0 | ||
|
|
43d8d94dd2 | ||
|
|
53733447ae | ||
|
|
6f727ff2df | ||
|
|
3660ccdda5 | ||
|
|
f775d9c22c | ||
|
|
99031ce133 | ||
|
|
b306a5ca72 | ||
|
|
33a5e5b3cb | ||
|
|
2582dd8735 | ||
|
|
f82141c98c | ||
|
|
9cde9a8170 | ||
|
|
3f617c7455 | ||
|
|
6f0c16d74f | ||
|
|
7b23c89204 | ||
|
|
55c751c133 | ||
|
|
0f75d745fe | ||
|
|
ba1aa5496d | ||
|
|
6217ef8e6a | ||
|
|
6e65f39895 | ||
|
|
031b7c97bc | ||
|
|
bdc7da660a | ||
|
|
f33cb4fb32 | ||
|
|
c1038ea371 | ||
|
|
7b7a94bb51 | ||
|
|
f840442549 | ||
|
|
1208e03e61 | ||
|
|
62c621f783 | ||
|
|
f83a482fb3 | ||
|
|
5691eb1604 | ||
|
|
3eaa24b15a | ||
|
|
e71b985c0d | ||
|
|
823767cb8b | ||
|
|
0b06e465ce | ||
|
|
478d291bd2 | ||
|
|
13fa4ffa3a | ||
|
|
e2bbd527d1 | ||
|
|
598baf4451 | ||
|
|
b4e688a543 | ||
|
|
217faa43a3 | ||
|
|
f84d973236 | ||
|
|
1351245d09 | ||
|
|
17dd3a4689 | ||
|
|
dfbb819e59 | ||
|
|
ab4f334723 | ||
|
|
c77e9cbcc9 | ||
|
|
93a7d280f1 | ||
|
|
5587532f01 | ||
|
|
1150a76035 | ||
|
|
3a79c6eb99 | ||
|
|
9111febd19 | ||
|
|
687dc20f3e | ||
|
|
ec40e32c3f | ||
|
|
7137082e74 | ||
|
|
90bac5d580 | ||
|
|
0780115c7b | ||
|
|
1db01f08e6 | ||
|
|
b05dff34b7 | ||
|
|
0bef4467b3 | ||
|
|
53489e9c3d | ||
|
|
92fe78f213 | ||
|
|
83177e9c55 | ||
|
|
ab5902e273 | ||
|
|
5dfb2e9ad6 | ||
|
|
e0548dc4b4 | ||
|
|
be2a9737ae | ||
|
|
2bd9e7dd99 | ||
|
|
f29d63018c | ||
|
|
c1d8249b10 | ||
|
|
311c80d475 | ||
|
|
0c04092300 | ||
|
|
99f37db88e | ||
|
|
2ad6d0897c | ||
|
|
caa808c265 | ||
|
|
d8683d9f50 | ||
|
|
807933270b | ||
|
|
ead84aea36 | ||
|
|
9fd11f13b6 | ||
|
|
1a58897ab4 | ||
|
|
7ba1f3ada5 | ||
|
|
e6cbb8fa05 | ||
|
|
81d48d7612 | ||
|
|
ab23ca7b18 | ||
|
|
94eef239eb | ||
|
|
30f5a220ce | ||
|
|
9350856d33 | ||
|
|
a69c5e2b93 | ||
|
|
777c2fab1b | ||
|
|
ec1a55bffc | ||
|
|
ffdc117a6e | ||
|
|
44419e10a2 | ||
|
|
a0237d80f6 | ||
|
|
41990177ce | ||
|
|
acc6624406 | ||
|
|
c2f11e8bd9 | ||
|
|
c44d3643a8 | ||
|
|
e9dd81d163 | ||
|
|
6ff9712b65 | ||
|
|
f9b6db797f | ||
|
|
d3274bc5b9 | ||
|
|
e36f7959c7 | ||
|
|
614516596b | ||
|
|
e09c5803b2 | ||
|
|
a95000e91a | ||
|
|
070448db4e | ||
|
|
659df9cb27 | ||
|
|
c03570faba | ||
|
|
3d550ab78f | ||
|
|
ec23ed45b0 | ||
|
|
faed661d25 | ||
|
|
0ead57671c | ||
|
|
e55057f686 | ||
|
|
87911808ab | ||
|
|
8f38a0fbaf | ||
|
|
5ef78c6938 | ||
|
|
57d9d1f33e | ||
|
|
ce8c9fea61 | ||
|
|
97b74f2b19 | ||
|
|
413959df5a | ||
|
|
12dfccf9e3 | ||
|
|
72aaccf4e5 | ||
|
|
cac2d9973b | ||
|
|
d2bd81121d | ||
|
|
20dbfed7c6 | ||
|
|
e74b39d833 | ||
|
|
50e8b1d278 |
@@ -0,0 +1,23 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
compile-tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
environment: ["esp8266", "esp32"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: python -m pip install --upgrade platformio==6.1.19
|
||||
- run: ./scripts/compile-check.sh --platform ${{ matrix.environment }}
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Publish release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: python -m pip install --upgrade platformio==6.1.19
|
||||
- run: ./scripts/prepare-release.sh "$GITHUB_REF_NAME"
|
||||
- run: gh release create "$GITHUB_REF_NAME" --generate-notes --title "$GITHUB_REF_NAME"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# PlatformIO
|
||||
.pio/
|
||||
.pioenvs/
|
||||
.piolibdeps/
|
||||
.vscode/
|
||||
.cache/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.vscode/
|
||||
!.vscode/extensions.json
|
||||
|
||||
# PlatformIO project overrides
|
||||
/platformio_override.ini
|
||||
/build_output/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build artifacts
|
||||
*.o
|
||||
*.elf
|
||||
*.hex
|
||||
*.bin
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
node_modules/
|
||||
@@ -0,0 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## 3.0.3
|
||||
|
||||
- Declare the ESP8266 and ESP32 async TCP libraries explicitly in the PlatformIO package manifest so clean consumers resolve the headers required by the asynchronous portal.
|
||||
|
||||
## 3.0.2
|
||||
|
||||
- Update the pinned DFTE dependency to the iterator-lifecycle and configuration-safe 1.0.2 release.
|
||||
|
||||
## 3.0.1
|
||||
|
||||
- Redact WiFi, AP, portal-form, and custom-parameter values from diagnostic logs.
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- Establish `device-framework` as the independently maintained canonical branch.
|
||||
- Add safe default parameter construction and allocation-failure handling.
|
||||
- Pin the DFTE dependency used by PlatformIO builds.
|
||||
|
||||
@@ -20,3 +20,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
This is a fork of tzapu/WiFiManager (https://github.com/tzapu/WiFiManager)
|
||||
Original work Copyright (c) 2015 tzapu
|
||||
|
||||
This software has been modified and includes contributions Copyright (c) 2024
|
||||
Alex Hope-O'Connor (https://github.com/alexhopeoconnor). The original
|
||||
license and copyright notice above must be preserved in all copies as required
|
||||
by the MIT License.
|
||||
|
||||
@@ -1,39 +1,277 @@
|
||||
# WiFiManager
|
||||
ESP8266 WiFi Connection manager with save to EEPROM and web config portal
|
||||
|
||||
First attempt at a library. Lots more changes and fixes to do. Adding examples also needs doing.
|
||||
This repository is a **breaking fork** of upstream [`tzapu/WiFiManager`](https://github.com/tzapu/WiFiManager).
|
||||
It is not a drop-in replacement for upstream behavior, APIs, templates, or portal customization patterns.
|
||||
|
||||
This works with the ESP8266 Arduino platform https://github.com/esp8266/Arduino
|
||||
If you are evaluating this fork, assume that core web-portal architecture has changed and review the code before adopting it in an existing upstream-based project.
|
||||
|
||||
####Quick Start
|
||||
- Checkout library to your Arduino libraries folder
|
||||
**This repository:** [alexhopeoconnor/WiFiManager](https://github.com/alexhopeoconnor/WiFiManager)
|
||||
**Upstream:** [tzapu/WiFiManager](https://github.com/tzapu/WiFiManager)
|
||||
|
||||
- Include in your sketch
|
||||
## Basic provisioning
|
||||
|
||||
```Arduino
|
||||
#include <WiFiClient.h>
|
||||
#include <EEPROM.h>
|
||||
#include <ESP8266mDNS.h>
|
||||
For a standalone Arduino project, create one long-lived `WiFiManager` and call
|
||||
`autoConnect` during setup. It first tries saved station credentials; when that
|
||||
fails it starts the configuration portal with the supplied AP name and password.
|
||||
|
||||
```cpp
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
WiFiManager wifi;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
wifi.setConfigPortalTimeout(180); // seconds; 0 leaves it open
|
||||
|
||||
if (!wifi.autoConnect("Device Setup", "change-me")) {
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Normal application work after WiFi is connected.
|
||||
}
|
||||
```
|
||||
|
||||
- Initialise library, the paramater is the Acces Point Name
|
||||
```
|
||||
//parameter is eeprom start
|
||||
WiFiManager wifi(0);
|
||||
The AP password must meet the Wi-Fi password requirements. For applications
|
||||
built on DeviceFramework, configure `setConfigDevicePassword(...)` instead:
|
||||
DeviceFramework passes the same validated password to WiFiManager, Arduino OTA,
|
||||
HTTP Basic authentication, and WebSerial.
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
This fork intentionally modernizes and restructures the configuration portal.
|
||||
Notable differences from upstream include:
|
||||
|
||||
- The portal now serves a **single HTML shell** from `GET /`.
|
||||
- Client navigation is handled as a **SPA with hash routing**.
|
||||
- Interactive behavior is exposed through **JSON APIs under `/api/...`** plus firmware upload at **`POST /u`**.
|
||||
- Legacy multi-page portal routes and legacy root-template override paths have been removed.
|
||||
- The portal rendering pipeline is built around **DFTE** instead of upstream's monolithic HTML string assembly.
|
||||
- JSON endpoints are expected to be **data-first**, not derived from generated HTML fragments.
|
||||
|
||||
## Current Improvements And Modernization
|
||||
|
||||
This fork currently includes the following architectural improvements:
|
||||
|
||||
- A **single-shell portal architecture** with embedded bootstrap JSON and embedded application JS.
|
||||
- A **SPA-based configuration UI** for WiFi setup, parameters, info views, device actions, and OTA flow.
|
||||
- A **clean API surface** for WiFi scanning, WiFi save, parameters, info, status, restart, erase, portal exit, and captive-portal close behavior.
|
||||
- **Captive portal redirect handling** retained while removing duplicate legacy UI architecture.
|
||||
- **Data-first JSON generation** for portal APIs, including info/device/about data, instead of HTML-to-JSON parsing.
|
||||
- **Capability-driven UI flags** in bootstrap/API payloads so features like info, update, erase, and action visibility can be controlled by backend state.
|
||||
- **Portal bootstrap contract v2**: nested `brand`, `context`, `pages`, `actions`, `layout`, `extraHomeCards`; Wi-Fi meta params include `kind` (`field` | `html`) for first-class custom HTML parameters.
|
||||
- **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows.
|
||||
- **Connect-on-save handoff**: when portal save triggers a station connect, `/api/wifi/connect-status` now reports a brief success state with `stationIp` and `redirectUrl` so the SPA can show the new address and redirect before the AP shuts down.
|
||||
- **Request-scoped shell rendering**: the root portal page is built for each `GET /` from `WM_ROOT_SHELL_TEMPLATE` using a fresh placeholder registry. Shell inputs are `%PAGE_TITLE%`, `%STYLES%`, `%BOOTSTRAP_JSON%`, `%PORTAL_APP_JS%`, and `%PORTAL_APPEND_JS%` — filled in `WiFiManagerHandlers` from WiFiManager state and embedded assets (not from a server-wide template registry).
|
||||
- **Customization via WiFiManager `portal*` APIs** (`portalSetBrandTitle`, `portalSetPageInfoVisible`, `portalSetLayoutParamsLocation`, `portalAddParameter`, asset hooks, etc.) and JSON under `/api/...`, not by exposing placeholder-registry mutation to consumers.
|
||||
- A clearer separation between:
|
||||
- shell rendering (handlers + SPA bootstrap)
|
||||
- JSON API responses
|
||||
- captive portal behavior
|
||||
- OTA handling
|
||||
- Updated tests focused on the **shell contract**, **bootstrap payloads**, and **API JSON shapes** rather than removed legacy portal pages.
|
||||
|
||||
## Portal Customization Boundary
|
||||
|
||||
Stable, supported portal customization is intentionally scoped:
|
||||
|
||||
- `portalSetBrand*` for title, intro text, and logo SVG, plus `portalSetContextIdentityText(...)` for the user-facing runtime identity string on the home view.
|
||||
- `portalSetPage*`, `portalSetAction*`, `portalSetLayout*`, and `portalSetBehavior*` for built-in portal capabilities and runtime behavior.
|
||||
- `portalAddParameter(...)` for first-class custom parameters, including raw HTML blocks inside parameter-rendering surfaces (`#/wifi` or `#/setup`).
|
||||
- `portalAddInfoSection(...)` and `portalAddHomeCard(...)` for structured extra content rendered by the built-in SPA.
|
||||
- `portalAppendCss(...)`, `portalOverrideCss(...)`, and `portalAppendJs(...)` for light theming and enhancement hooks.
|
||||
|
||||
Not part of the stable API:
|
||||
|
||||
- arbitrary HTML injection into home/info/nav/shell
|
||||
- replacing built-in SPA routing or action flow
|
||||
- depending on undocumented DOM IDs or route internals
|
||||
- treating `include/templates/*` as a supported consumer override surface
|
||||
|
||||
Appended JS should enhance rather than replace the built-in SPA. The documented hook contract is:
|
||||
|
||||
- `wm:ready` with `detail.boot`
|
||||
- `wm:view-changed` with `detail.route`
|
||||
|
||||
If a consumer needs custom live widgets, new primary navigation concepts, or new backend-to-frontend workflows, that is considered **fork territory** rather than portal customization.
|
||||
|
||||
## Portal Customization Example
|
||||
|
||||
```cpp
|
||||
WiFiManager wm;
|
||||
|
||||
wm.portalSetBrandTitle("Solar Battery Monitor Setup");
|
||||
wm.portalSetContextIdentityText("Solar Battery Monitor");
|
||||
wm.portalSetBrandHomeIntro(
|
||||
"Connect your monitor to WiFi, then review battery and inverter settings."
|
||||
);
|
||||
wm.portalSetBrandLogoSvg(
|
||||
"<svg viewBox='0 0 24 24' aria-hidden='true'>"
|
||||
"<path d='M12 2L4 12h5l-1 10 8-10h-5z'></path>"
|
||||
"</svg>"
|
||||
);
|
||||
|
||||
wm.portalSetPageInfoVisible(true);
|
||||
wm.portalSetPageUpdateVisible(false);
|
||||
wm.portalSetActionEraseVisible(false);
|
||||
wm.portalSetActionRestartVisible(true);
|
||||
wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
|
||||
|
||||
wm.portalSetBehaviorCaptivePortalEnabled(true);
|
||||
wm.portalSetBehaviorConnectOnSave(true);
|
||||
wm.portalSetBehaviorExitAllowed(true);
|
||||
|
||||
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Masked);
|
||||
wm.portalSetFieldStaticIpVisibility(PortalFieldVisibility::Auto);
|
||||
wm.portalSetFieldStaticDnsVisibility(PortalFieldVisibility::Auto);
|
||||
|
||||
WiFiManagerParameter mqttHost(
|
||||
"mqtt_host",
|
||||
"MQTT host",
|
||||
"broker.local",
|
||||
64,
|
||||
"placeholder='broker.local'"
|
||||
);
|
||||
wm.portalAddParameter(&mqttHost);
|
||||
|
||||
// Raw HTML remains first-class for custom parameters, but only inside the
|
||||
// parameter-rendering surfaces (#/wifi or #/setup), not arbitrary portal regions.
|
||||
WiFiManagerParameter gpsHelp(
|
||||
"<div class='wm-callout wm-callout--info'>"
|
||||
"<p>GPS options below apply only when a GPS module is connected.</p>"
|
||||
"</div>"
|
||||
);
|
||||
wm.portalAddParameter(&gpsHelp);
|
||||
|
||||
PortalInfoSection battery;
|
||||
battery.id = "battery";
|
||||
battery.title = "Battery";
|
||||
battery.items.push_back({"soc", "State of charge", "84%"});
|
||||
battery.items.push_back({"voltage", "Voltage", "13.2V"});
|
||||
wm.portalAddInfoSection(battery);
|
||||
|
||||
PortalHomeCard solar;
|
||||
solar.id = "solar";
|
||||
solar.title = "Solar summary";
|
||||
solar.kind = PortalHomeCardKind::KeyValue;
|
||||
solar.items.push_back({"pv", "PV input", "420W"});
|
||||
solar.items.push_back({"load", "Load", "180W"});
|
||||
wm.portalAddHomeCard(solar);
|
||||
|
||||
wm.portalAppendCss(
|
||||
".wm-brand-logo svg{width:40px;height:40px;display:block;}"
|
||||
".wm-hero-intro{max-width:42ch;}"
|
||||
);
|
||||
|
||||
wm.portalAppendJs(
|
||||
"document.addEventListener('wm:ready', function(e){"
|
||||
" console.log('Portal booted', e.detail.boot);"
|
||||
"});"
|
||||
);
|
||||
```
|
||||
|
||||
- In your setup function add
|
||||
```
|
||||
//parameter is name of access point
|
||||
wifi.autoConnect("AP-NAME");
|
||||
## WiFi Connect Status API
|
||||
|
||||
When `portalSetBehaviorConnectOnSave(true)` is enabled, saving WiFi credentials queues a station join and the SPA polls `GET /api/wifi/connect-status`.
|
||||
|
||||
Response shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"state": "waiting | success | failed",
|
||||
"message": "human readable status",
|
||||
"wifiStatus": "WL_CONNECTED",
|
||||
"stationIp": "192.168.1.42",
|
||||
"redirectUrl": "http://192.168.1.42/"
|
||||
}
|
||||
```
|
||||
|
||||
After you write your sketch and start the ESP, it will try to connect to WiFi. If it fails it starts in Access Point mode.
|
||||
While in AP mode, connect to it then open a browser to the gateway IP, default 192.168.4.1, configure wifi, save and it should reboot and connect.
|
||||
Notes:
|
||||
|
||||
- `stationIp` and `redirectUrl` are present only after a successful station join.
|
||||
- If the portal HTTP server is not on port `80`, `redirectUrl` includes the active port.
|
||||
- On success, WiFiManager keeps the portal alive briefly so the client can read the success payload and navigate to the new device address before the captive AP is shut down.
|
||||
- This improves the handoff on typical home networks, but it is not a universal guarantee: client captive-portal helpers, browser behavior, DHCP timing, and network isolation can still affect whether the redirect completes automatically.
|
||||
|
||||
## Dependencies
|
||||
|
||||
This fork depends on **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)) and **ESP32Async/ESPAsyncWebServer**.
|
||||
|
||||
The published `library.json` resolves DFTE from the maintained, pinned
|
||||
`v1.0.2` Git tag. No sibling checkout is required to consume WiFiManager.
|
||||
When developing both libraries together, use a local `symlink://` or
|
||||
`file://` dependency in your own ignored PlatformIO override.
|
||||
|
||||
|
||||
####Inspiration
|
||||
- http://www.esp8266.com/viewtopic.php?f=29&t=2520
|
||||
## Installation
|
||||
|
||||
To be continued...
|
||||
Use the release tag and list the asynchronous web/TCP packages at project level.
|
||||
PlatformIO installs the manifest dependencies automatically, but its dependency
|
||||
finder requires these headers to be direct project dependencies before it adds
|
||||
their include paths:
|
||||
|
||||
```ini
|
||||
[common]
|
||||
lib_deps =
|
||||
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.0.3
|
||||
ESP32Async/ESPAsyncWebServer@3.9.1
|
||||
|
||||
[env:esp8266]
|
||||
extends = common
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${common.lib_deps}
|
||||
ESP32Async/ESPAsyncTCP@2.0.0
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
lib_deps =
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
${common.lib_deps}
|
||||
ESP32Async/AsyncTCP@3.4.9
|
||||
```
|
||||
|
||||
The text after `#` is a Git ref. PlatformIO clones the repository and checks
|
||||
out that tag; it does not download a GitHub Release asset. A tag gives a
|
||||
reproducible library input. Use a local checkout only while actively changing
|
||||
the library:
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
WiFiManager=file:///path/to/WiFiManager
|
||||
```
|
||||
|
||||
## Tests and releases
|
||||
|
||||
The CI workflow compiles a minimal clean consumer project for both supported
|
||||
targets. These commands need no connected board:
|
||||
|
||||
```bash
|
||||
./scripts/compile-check.sh --platform esp8266
|
||||
./scripts/compile-check.sh --platform esp32
|
||||
```
|
||||
|
||||
Before publishing a version, update `library.json`, `CHANGELOG.md`, and this
|
||||
README, run both checks, then create the annotated tag:
|
||||
|
||||
```bash
|
||||
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
|
||||
```
|
||||
|
||||
Push the branch and tag. GitHub Actions validates the PlatformIO package again
|
||||
and creates the GitHub Release from that tag.
|
||||
|
||||
## AI Assistance Notice
|
||||
|
||||
Parts of this codebase have been developed and refactored with the aid of AI coding agents under human direction and review.
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE).
|
||||
|
||||
-326
@@ -1,326 +0,0 @@
|
||||
/**************************************************************
|
||||
* WiFiManager is a library for the ESP8266/Arduino platform
|
||||
* (https://github.com/esp8266/Arduino) to enable easy
|
||||
* configuration and reconfiguration of WiFi credentials and
|
||||
* store them in EEPROM.
|
||||
* inspired by http://www.esp8266.com/viewtopic.php?f=29&t=2520
|
||||
* https://github.com/chriscook8/esp-arduino-apboot
|
||||
* Built by AlexT https://github.com/tzapu
|
||||
* Licensed under MIT license
|
||||
**************************************************************/
|
||||
|
||||
#include "WiFiManager.h"
|
||||
|
||||
MDNSResponder mdns;
|
||||
WiFiServer server(80);
|
||||
|
||||
|
||||
WiFiManager::WiFiManager(int eepromStart)
|
||||
{
|
||||
_eepromStart = eepromStart;
|
||||
}
|
||||
|
||||
void WiFiManager::begin() {
|
||||
begin("NoNetESP");
|
||||
}
|
||||
|
||||
void WiFiManager::begin(char const *apName) {
|
||||
_apName = apName;
|
||||
start = millis();
|
||||
EEPROM.begin(512);
|
||||
delay(10);
|
||||
}
|
||||
|
||||
boolean WiFiManager::autoConnect() {
|
||||
return autoConnect("NoNetESP");
|
||||
}
|
||||
|
||||
boolean WiFiManager::autoConnect(char const *apName) {
|
||||
begin(apName);
|
||||
|
||||
|
||||
DEBUG_PRINT("");
|
||||
DEBUG_PRINT("AutoConnect");
|
||||
// read eeprom for ssid and pass
|
||||
String ssid = getSSID();
|
||||
String pass = getPassword();
|
||||
|
||||
if ( ssid.length() > 1 ) {
|
||||
DEBUG_PRINT("Waiting for Wifi to connect");
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid.c_str(), pass.c_str());
|
||||
if ( hasConnected() ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//setup AP
|
||||
beginConfigMode();
|
||||
//start portal and loop
|
||||
startWebConfig();
|
||||
return false;
|
||||
}
|
||||
|
||||
String WiFiManager::getSSID() {
|
||||
if (_ssid == "") {
|
||||
DEBUG_PRINT("Reading EEPROM SSID");
|
||||
_ssid = getEEPROMString(0, 32);
|
||||
DEBUG_PRINT("SSID: ");
|
||||
DEBUG_PRINT(_ssid);
|
||||
}
|
||||
return _ssid;
|
||||
}
|
||||
|
||||
String WiFiManager::getPassword() {
|
||||
if (_pass == "") {
|
||||
DEBUG_PRINT("Reading EEPROM Password");
|
||||
_pass = getEEPROMString(32, 64);
|
||||
DEBUG_PRINT("Password: ");
|
||||
DEBUG_PRINT(_pass);
|
||||
}
|
||||
return _pass;
|
||||
}
|
||||
|
||||
String WiFiManager::getEEPROMString(int start, int len) {
|
||||
String string = "";
|
||||
for (int i = _eepromStart + start; i < _eepromStart + start + len; i++) {
|
||||
//DEBUG_PRINT(i);
|
||||
string += char(EEPROM.read(i));
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
void WiFiManager::setEEPROMString(int start, int len, String string) {
|
||||
int si = 0;
|
||||
for (int i = _eepromStart + start; i < _eepromStart + start + len; i++) {
|
||||
char c;
|
||||
if (si < string.length()) {
|
||||
c = string[si];
|
||||
DEBUG_PRINT("Wrote: ");
|
||||
DEBUG_PRINT(c);
|
||||
} else {
|
||||
c = 0;
|
||||
}
|
||||
EEPROM.write(i, c);
|
||||
si++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
boolean WiFiManager::hasConnected(void) {
|
||||
int c = 0;
|
||||
while ( c < 50 ) {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
return true;
|
||||
}
|
||||
delay(200);
|
||||
DEBUG_PRINT(".");
|
||||
c++;
|
||||
}
|
||||
DEBUG_PRINT("");
|
||||
DEBUG_PRINT("Could not connect to WiFi");
|
||||
return false;
|
||||
}
|
||||
|
||||
void WiFiManager::startWebConfig() {
|
||||
DEBUG_PRINT("");
|
||||
DEBUG_PRINT("WiFi connected");
|
||||
DEBUG_PRINT(WiFi.localIP());
|
||||
DEBUG_PRINT(WiFi.softAPIP());
|
||||
if (!mdns.begin(_apName, WiFi.localIP())) {
|
||||
DEBUG_PRINT("Error setting up MDNS responder!");
|
||||
while (1) {
|
||||
delay(1000);
|
||||
}
|
||||
}
|
||||
DEBUG_PRINT("mDNS responder started");
|
||||
// Start the server
|
||||
server.begin();
|
||||
DEBUG_PRINT("Server started");
|
||||
|
||||
while (serverLoop() == WM_WAIT) {
|
||||
//looping
|
||||
if(timeout > 0 && start + timeout < millis()) {
|
||||
//we passed timeout value, release
|
||||
DEBUG_PRINT("timeout reached");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_PRINT("Setup done");
|
||||
delay(5000);
|
||||
ESP.reset();
|
||||
|
||||
}
|
||||
|
||||
String WiFiManager::beginConfigMode(void) {
|
||||
|
||||
WiFi.softAP(_apName);
|
||||
DEBUG_PRINT("Started Soft Access Point");
|
||||
IPAddress apIp = WiFi.softAPIP();
|
||||
char ip[24];
|
||||
sprintf(ip, "%d.%d.%d.%d", apIp[0], apIp[1], apIp[2], apIp[3]);
|
||||
return ip;
|
||||
}
|
||||
|
||||
int WiFiManager::serverLoop()
|
||||
{
|
||||
// Check for any mDNS queries and send responses
|
||||
mdns.update();
|
||||
|
||||
// Check if a client has connected
|
||||
WiFiClient client = server.available();
|
||||
if (!client) {
|
||||
return (WM_WAIT);
|
||||
}
|
||||
|
||||
DEBUG_PRINT("New client");
|
||||
|
||||
// Wait for data from client to become available
|
||||
while (client.connected() && !client.available()) {
|
||||
delay(1);
|
||||
}
|
||||
|
||||
// Read the first line of HTTP request
|
||||
String req = client.readStringUntil('\r');
|
||||
|
||||
// First line of HTTP request looks like "GET /path HTTP/1.1"
|
||||
// Retrieve the "/path" part by finding the spaces
|
||||
int addr_start = req.indexOf(' ');
|
||||
int addr_end = req.indexOf(' ', addr_start + 1);
|
||||
if (addr_start == -1 || addr_end == -1) {
|
||||
DEBUG_PRINT("Invalid request: ");
|
||||
DEBUG_PRINT(req);
|
||||
return (WM_WAIT);
|
||||
}
|
||||
req = req.substring(addr_start + 1, addr_end);
|
||||
DEBUG_PRINT("Request: ");
|
||||
DEBUG_PRINT(req);
|
||||
client.flush();
|
||||
|
||||
String s;
|
||||
if (req == "/")
|
||||
{
|
||||
|
||||
s = HTTP_200;
|
||||
String head = HTTP_HEAD;
|
||||
head.replace("{v}", "Config ESP");
|
||||
s += head;
|
||||
s += HTTP_SCRIPT;
|
||||
s += HTTP_STYLE;
|
||||
s += HTTP_HEAD_END;
|
||||
|
||||
int n = WiFi.scanNetworks();
|
||||
DEBUG_PRINT("scan done");
|
||||
if (n == 0) {
|
||||
DEBUG_PRINT("no networks found");
|
||||
s += "<div>No networks found. Refresh to scan again.</div>";
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
DEBUG_PRINT(WiFi.SSID(i));
|
||||
DEBUG_PRINT(WiFi.RSSI(i));
|
||||
String item = HTTP_ITEM;
|
||||
item.replace("{v}", WiFi.SSID(i));
|
||||
s += item;
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
s += HTTP_FORM;
|
||||
s += HTTP_END;
|
||||
|
||||
DEBUG_PRINT("Sending config page");
|
||||
}
|
||||
else if ( req.startsWith("/s") ) {
|
||||
String qssid;
|
||||
qssid = urldecode(req.substring(8, req.indexOf('&')).c_str());
|
||||
DEBUG_PRINT(qssid);
|
||||
DEBUG_PRINT("");
|
||||
String qpass;
|
||||
qpass = urldecode(req.substring(req.lastIndexOf('=') + 1).c_str());
|
||||
DEBUG_PRINT(qpass);
|
||||
DEBUG_PRINT("");
|
||||
|
||||
setEEPROMString(0, 32, qssid);
|
||||
setEEPROMString(32, 64, qpass);
|
||||
|
||||
EEPROM.commit();
|
||||
|
||||
s = HTTP_200;
|
||||
String head = HTTP_HEAD;
|
||||
head.replace("{v}", "Saved config");
|
||||
s += HTTP_STYLE;
|
||||
s += HTTP_HEAD_END;
|
||||
s += "saved to eeprom...<br/>resetting in 10 seconds";
|
||||
s += HTTP_END;
|
||||
client.print(s);
|
||||
client.flush();
|
||||
|
||||
DEBUG_PRINT("Saved WiFiConfig...restarting.");
|
||||
return WM_DONE;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = HTTP_404;
|
||||
DEBUG_PRINT("Sending 404");
|
||||
}
|
||||
|
||||
client.print(s);
|
||||
DEBUG_PRINT("Done with client");
|
||||
return (WM_WAIT);
|
||||
}
|
||||
|
||||
String WiFiManager::urldecode(const char *src)
|
||||
{
|
||||
String decoded = "";
|
||||
char a, b;
|
||||
while (*src) {
|
||||
if ((*src == '%') &&
|
||||
((a = src[1]) && (b = src[2])) &&
|
||||
(isxdigit(a) && isxdigit(b))) {
|
||||
if (a >= 'a')
|
||||
a -= 'a' - 'A';
|
||||
if (a >= 'A')
|
||||
a -= ('A' - 10);
|
||||
else
|
||||
a -= '0';
|
||||
if (b >= 'a')
|
||||
b -= 'a' - 'A';
|
||||
if (b >= 'A')
|
||||
b -= ('A' - 10);
|
||||
else
|
||||
b -= '0';
|
||||
|
||||
decoded += char(16 * a + b);
|
||||
src += 3;
|
||||
} else if (*src == '+') {
|
||||
decoded += ' ';
|
||||
*src++;
|
||||
} else {
|
||||
decoded += *src;
|
||||
*src++;
|
||||
}
|
||||
}
|
||||
decoded += '\0';
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
void WiFiManager::resetSettings() {
|
||||
//need to call it only after lib has been started with autoConnect or begin
|
||||
setEEPROMString(0, 32, "-");
|
||||
setEEPROMString(32, 64, "-");
|
||||
|
||||
EEPROM.commit();
|
||||
DEBUG_PRINT("settings invalidated");
|
||||
delay(200);
|
||||
WiFi.disconnect();
|
||||
}
|
||||
|
||||
void WiFiManager::setTimeout(unsigned long seconds) {
|
||||
timeout = seconds * 1000;
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**************************************************************
|
||||
* WiFiManager is a library for the ESP8266/Arduino platform
|
||||
* (https://github.com/esp8266/Arduino) to enable easy
|
||||
* configuration and reconfiguration of WiFi credentials and
|
||||
* store them in EEPROM.
|
||||
* inspired by http://www.esp8266.com/viewtopic.php?f=29&t=2520
|
||||
* https://github.com/chriscook8/esp-arduino-apboot
|
||||
* Built by AlexT https://github.com/tzapu
|
||||
* Licensed under MIT license
|
||||
**************************************************************/
|
||||
|
||||
#ifndef WiFiManager_h
|
||||
#define WiFiManager_h
|
||||
|
||||
#include <ESP8266WiFi.h>
|
||||
|
||||
#include <EEPROM.h>
|
||||
#include <ESP8266mDNS.h>
|
||||
#include <WiFiClient.h>
|
||||
|
||||
#define DEBUG //until arduino ide can include defines at compile time from main sketch
|
||||
|
||||
#ifdef DEBUG
|
||||
#define DEBUG_PRINT(x) Serial.println(x)
|
||||
#else
|
||||
#define DEBUG_PRINT(x)
|
||||
#endif
|
||||
|
||||
|
||||
class WiFiManager
|
||||
{
|
||||
public:
|
||||
WiFiManager(int eepromStart);
|
||||
void begin();
|
||||
void begin(char const *apName);
|
||||
|
||||
boolean autoConnect();
|
||||
boolean autoConnect(char const *apName);
|
||||
|
||||
boolean hasConnected();
|
||||
|
||||
String beginConfigMode(void);
|
||||
void startWebConfig();
|
||||
|
||||
String getSSID();
|
||||
String getPassword();
|
||||
void resetSettings();
|
||||
//for conveniennce
|
||||
String urldecode(const char*);
|
||||
|
||||
//sets timeout before webserver loop ends and exits even if there has been no setup.
|
||||
//usefully for devices that failed to connect at some point and got stuck in a webserver loop
|
||||
//in seconds
|
||||
void setTimeout(unsigned long seconds);
|
||||
|
||||
private:
|
||||
const int WM_DONE = 0;
|
||||
const int WM_WAIT = 10;
|
||||
|
||||
const String HTTP_404 = "HTTP/1.1 404 Not Found\r\n\r\n";
|
||||
const String HTTP_200 = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
|
||||
const String HTTP_HEAD = "<!DOCTYPE html><html lang=\"en\"><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>{v}</title>";
|
||||
const String HTTP_STYLE = "<style>div,input {margin-bottom: 5px;}body{width:200px;display:block;margin-left:auto;margin-right:auto;}</style>";
|
||||
const String HTTP_SCRIPT = "<script>function c(l){document.getElementById('s').value=l.innerText||l.textContent;document.getElementById('p').focus();}</script>";
|
||||
const String HTTP_HEAD_END = "</head><body>";
|
||||
const String HTTP_ITEM = "<div><a href='#' onclick='c(this)'>{v}</a></div>";
|
||||
const String HTTP_FORM = "<form method='get' action='s'><input id='s' name='ssid' length=32 placeholder='SSID'><input id='p' name='pass' length=64 placeholder='password'><br/><input type='submit'></form>";
|
||||
const String HTTP_END = "</body></html>";
|
||||
|
||||
int _eepromStart;
|
||||
const char* _apName = "no-net";
|
||||
String _ssid = "";
|
||||
String _pass = "";
|
||||
unsigned long timeout = 0;
|
||||
unsigned long start = 0;
|
||||
|
||||
|
||||
String getEEPROMString(int start, int len);
|
||||
void setEEPROMString(int start, int len, String string);
|
||||
int serverLoop();
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
@@ -1,30 +0,0 @@
|
||||
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
|
||||
|
||||
//needed for library
|
||||
#include <EEPROM.h>
|
||||
#include <ESP8266mDNS.h>
|
||||
#include <WiFiManager.h> //https://github.com/tzapu/WiFiManager
|
||||
|
||||
//this intitilises the library, parameter is eeprom start
|
||||
WiFiManager wifi(0);
|
||||
|
||||
void setup() {
|
||||
// put your setup code here, to run once:
|
||||
Serial.begin(115200);
|
||||
|
||||
|
||||
//WiFiManager
|
||||
//fetches ssid and pass from eeprom and tries to connect
|
||||
//if it does not connect it starts an access point with the specified name
|
||||
//here "AutoConnectAP"
|
||||
//and goes into a blocking loop awaiting configuration
|
||||
wifi.autoConnect("AutoConnectAP");
|
||||
|
||||
//if you get here you have connected to the WiFi
|
||||
Serial.println("connected...yeey :)");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// put your main code here, to run repeatedly:
|
||||
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
|
||||
|
||||
//needed for library
|
||||
#include <EEPROM.h>
|
||||
#include <ESP8266mDNS.h>
|
||||
#include <WiFiManager.h> //https://github.com/tzapu/WiFiManager
|
||||
|
||||
//this intitilises the library, parameter is eeprom start
|
||||
WiFiManager wifi(0);
|
||||
|
||||
void setup() {
|
||||
// put your setup code here, to run once:
|
||||
Serial.begin(115200);
|
||||
|
||||
|
||||
//WiFiManager
|
||||
//sets timeout until configuration portal gets turned off
|
||||
//useful to make it all retry or go to sleep
|
||||
//in seconds
|
||||
wifi.setTimeout(120);
|
||||
|
||||
//fetches ssid and pass from eeprom and tries to connect
|
||||
//if it does not connect it starts an access point with the specified name
|
||||
//here "AutoConnectAP"
|
||||
//and goes into a blocking loop awaiting configuration
|
||||
if(!wifi.autoConnect("AutoConnectAP")) {
|
||||
Serial.println("failed to connect and hit timeout");
|
||||
delay(5000);
|
||||
//reset and try again, or maybe put it to deep sleep
|
||||
ESP.reset();
|
||||
delay(5000);
|
||||
}
|
||||
|
||||
//if you get here you have connected to the WiFi
|
||||
Serial.println("connected...yeey :)");
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// put your main code here, to run repeatedly:
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* WiFiManagerDfteLogger.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Optional bridge from DFTE's logging hooks to WiFiManager::log.
|
||||
* Enabled only when the sketch / library build defines WM_DFTE_LOGGING.
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerDfteLogger_h
|
||||
#define WiFiManagerDfteLogger_h
|
||||
|
||||
#if defined(ESP8266) || defined(ESP32)
|
||||
|
||||
#include <DeviceFrameworkTemplateEngineDebug.h>
|
||||
#include "WiFiManager.h"
|
||||
|
||||
/**
|
||||
* Forwards DeviceFrameworkTemplateEngineLogger calls to WiFiManager logging
|
||||
* with subsystem WM/DFTE.
|
||||
*/
|
||||
class WiFiManagerDfteLogger : public DeviceFrameworkTemplateEngineLogger {
|
||||
public:
|
||||
explicit WiFiManagerDfteLogger(WiFiManager* wm) : _wm(wm) {}
|
||||
|
||||
void error(const String& msg) override { logLine(WiFiManagerLogLevel::Error, msg); }
|
||||
void warn(const String& msg) override { logLine(WiFiManagerLogLevel::Warn, msg); }
|
||||
void info(const String& msg) override { logLine(WiFiManagerLogLevel::Debug, msg); }
|
||||
void debug(const String& msg) override { logLine(WiFiManagerLogLevel::Trace, msg); }
|
||||
|
||||
private:
|
||||
WiFiManager* _wm;
|
||||
|
||||
void logLine(WiFiManagerLogLevel level, const String& msg) {
|
||||
if (!_wm) {
|
||||
return;
|
||||
}
|
||||
_wm->log(level, "WM/DFTE", msg);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // ESP8266 || ESP32
|
||||
|
||||
#endif // WiFiManagerDfteLogger_h
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* WiFiManagerHandlers.h
|
||||
*
|
||||
* HTTP request handlers for WiFiManager: single HTML shell + JSON APIs.
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerHandlers_h
|
||||
#define WiFiManagerHandlers_h
|
||||
|
||||
#if defined(ESP8266) || defined(ESP32)
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <memory>
|
||||
#include "WiFiManager.h"
|
||||
|
||||
#ifndef WM_TEMPLATE_REGISTRY_CAPACITY
|
||||
#define WM_TEMPLATE_REGISTRY_CAPACITY 16
|
||||
#endif
|
||||
|
||||
// Portal UI customization is driven by WiFiManager `portal*` APIs and JSON (/api/...) — not by mutating template
|
||||
// placeholder registries.
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// FORM FIELD NAMES (for IP configuration forms)
|
||||
|
||||
const char S_ip[] PROGMEM = "ip";
|
||||
const char S_gw[] PROGMEM = "gw";
|
||||
const char S_sn[] PROGMEM = "sn";
|
||||
const char S_dns[] PROGMEM = "dns";
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// HTTP HEADERS
|
||||
|
||||
const char HTTP_HEAD_CT[] PROGMEM = "text/html";
|
||||
const char HTTP_HEAD_CT2[] PROGMEM = "text/plain";
|
||||
const char HTTP_HEAD_CORS[] PROGMEM = "Access-Control-Allow-Origin";
|
||||
const char HTTP_HEAD_CORS_ALLOW_ALL[] PROGMEM = "*";
|
||||
|
||||
class WiFiManagerHandlers {
|
||||
public:
|
||||
WiFiManagerHandlers(WiFiManager* wm);
|
||||
|
||||
void handleRoot(AsyncWebServerRequest *request);
|
||||
void handleNotFound(AsyncWebServerRequest *request);
|
||||
void handleRequest(AsyncWebServerRequest *request);
|
||||
void handleUpdating(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
||||
void handleUpdateDone(AsyncWebServerRequest *request);
|
||||
|
||||
void handleApiBootstrap(AsyncWebServerRequest *request);
|
||||
void handleApiWifiScanStatus(AsyncWebServerRequest *request);
|
||||
void handleApiWifiScan(AsyncWebServerRequest *request);
|
||||
void handleApiWifiMeta(AsyncWebServerRequest *request);
|
||||
void handleApiWifiSave(AsyncWebServerRequest *request);
|
||||
void handleApiWifiConnectStatus(AsyncWebServerRequest *request);
|
||||
void handleApiParamsGet(AsyncWebServerRequest *request);
|
||||
void handleApiParamsSave(AsyncWebServerRequest *request);
|
||||
void handleApiInfo(AsyncWebServerRequest *request);
|
||||
void handleApiStatus(AsyncWebServerRequest *request);
|
||||
void handleApiDeviceRestart(AsyncWebServerRequest *request);
|
||||
void handleApiDeviceErase(AsyncWebServerRequest *request, boolean optionalErase = false);
|
||||
void handleApiPortalClose(AsyncWebServerRequest *request);
|
||||
void handleApiPortalExit(AsyncWebServerRequest *request);
|
||||
|
||||
boolean captivePortal(AsyncWebServerRequest *request);
|
||||
void stopCaptivePortal();
|
||||
|
||||
/** True when the HTTP Host header does not match the portal’s canonical host:port (captive redirect needed). */
|
||||
static bool shouldRedirectCaptiveForHost(const String& requestHost, const String& serverLocWithPort);
|
||||
|
||||
void doParamSave(WiFiManager::WiFiManagerRequestArgs requestArgs);
|
||||
|
||||
/** Portal bootstrap JSON (same payload as GET /api/bootstrap). Public for tests and host integration. */
|
||||
String buildPortalBootstrapJson();
|
||||
/** Same JSON body as GET /api/wifi/meta (tests + embedding). */
|
||||
String buildApiWifiMetaJson();
|
||||
/** Same JSON body as GET /api/info (tests + embedding). */
|
||||
String buildApiInfoJson();
|
||||
/** Same JSON body as GET /api/params (tests + embedding). */
|
||||
String buildApiParamsGetJson();
|
||||
/** Same JSON body as GET /api/status (tests + embedding). */
|
||||
String buildApiStatusJson();
|
||||
/** GET /api/wifi/connect-status (portal connect progress). */
|
||||
String buildApiWifiConnectStatusJson();
|
||||
|
||||
/** Fixed JSON bodies for POST action endpoints (single source for handlers + tests). */
|
||||
static String jsonApiWifiScanAccepted();
|
||||
static String jsonApiDeviceRestartScheduled();
|
||||
static String jsonApiParamsSaveOk();
|
||||
static String jsonApiPortalCloseOk();
|
||||
static String jsonApiPortalExitOk();
|
||||
static String jsonApiPortalExitForbidden();
|
||||
static String jsonApiOtaUpdateSuccess();
|
||||
static String jsonApiEraseResponse(boolean success);
|
||||
|
||||
private:
|
||||
WiFiManager* _wm;
|
||||
void collectVisibleScanResults(std::vector<const WiFiManager::WiFiScanNetwork*>& networks);
|
||||
void appendVisibleScanResultsJson(String& json, const std::vector<const WiFiManager::WiFiScanNetwork*>& networks);
|
||||
|
||||
void applyWifiAndParamsFromRequest(AsyncWebServerRequest *request);
|
||||
void buildPlainStatusSummary(String& out);
|
||||
void appendPortalJsonStaticFields(String& json, bool& first);
|
||||
void appendPortalJsonCustomParams(String& json, bool& first);
|
||||
|
||||
String composePortalStylesheet() const;
|
||||
void appendPortalExtraInfoSectionsJson(String& json, bool& first);
|
||||
void appendPortalExtraHomeCardsJson(String& json, bool& first);
|
||||
|
||||
void appendJsonKvItem(String& json, bool& first, const char* key, const String& label, const String& value);
|
||||
void appendOneInfoItemForId(String& json, bool& first, const char* id);
|
||||
void appendInfoSectionFromIds(String& json, const char* const* ids, size_t count, bool& first);
|
||||
|
||||
void appendApiInfoActionsJson(String& json);
|
||||
|
||||
static void sendApiJson(AsyncWebServerRequest *request, int code, const String& json);
|
||||
};
|
||||
|
||||
#endif // defined(ESP8266) || defined(ESP32)
|
||||
|
||||
#endif // WiFiManagerHandlers_h
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* WiFiManagerLogLevel.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Compile-time and runtime log levels for WiFiManager. Higher numeric values
|
||||
* mean more verbose output. Matches the old 0..5 tier model for build flags.
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerLogLevel_h
|
||||
#define WiFiManagerLogLevel_h
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
/** Default subsystem tag for core WiFiManager messages (macro for C++11 embedded toolchains). */
|
||||
#define kWiFiMgrLogSubsystem "WM"
|
||||
|
||||
enum class WiFiManagerLogLevel : uint8_t {
|
||||
Silent = 0,
|
||||
Error = 1,
|
||||
Warn = 2,
|
||||
Info = 3,
|
||||
Debug = 4,
|
||||
Trace = 5,
|
||||
};
|
||||
|
||||
/** Full line delivered to Print or WiFiManagerLogSink (prefix and tags included). */
|
||||
struct WiFiManagerLogMessage {
|
||||
WiFiManagerLogLevel level;
|
||||
const char* subsystem;
|
||||
String line;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* WiFiManagerLogSink.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Optional host-provided sink for WiFiManager log output. When set, logs are
|
||||
* delivered here instead of the configured Print stream (usually Serial).
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerLogSink_h
|
||||
#define WiFiManagerLogSink_h
|
||||
|
||||
#include "WiFiManagerLogLevel.h"
|
||||
|
||||
class WiFiManagerLogSink {
|
||||
public:
|
||||
virtual void log(const WiFiManagerLogMessage& msg) = 0;
|
||||
virtual ~WiFiManagerLogSink() = default;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* WiFiManagerLogTemplates.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Template implementations for WiFiManager::log(...). Included at the end of WiFiManager.h.
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerLogTemplates_h
|
||||
#define WiFiManagerLogTemplates_h
|
||||
|
||||
#include "WiFiManagerLogLevel.h"
|
||||
#include <IPAddress.h>
|
||||
#include <WString.h>
|
||||
|
||||
#if defined(ESP8266) || defined(ESP32)
|
||||
|
||||
inline String wmLogStringifyArg(const __FlashStringHelper* v) {
|
||||
return String(v);
|
||||
}
|
||||
|
||||
inline String wmLogStringifyArg(const IPAddress& v) {
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline String wmLogStringifyArg(const T& v) {
|
||||
return String(v);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline void WiFiManager::log(WiFiManagerLogLevel level, const char* subsystem, T&& text) {
|
||||
#ifndef WM_NO_LOG
|
||||
emitLogImpl(level, subsystem, wmLogStringifyArg(text), String());
|
||||
#endif
|
||||
}
|
||||
|
||||
template<typename T, typename U>
|
||||
inline void WiFiManager::log(WiFiManagerLogLevel level, const char* subsystem, T&& a, U&& b) {
|
||||
#ifndef WM_NO_LOG
|
||||
emitLogImpl(level, subsystem, wmLogStringifyArg(a), wmLogStringifyArg(b));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif // ESP8266 || ESP32
|
||||
|
||||
#endif // WiFiManagerLogTemplates_h
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* WiFiManagerParameter.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerParameter_h
|
||||
#define WiFiManagerParameter_h
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
class WiFiManager;
|
||||
|
||||
#ifndef WIFI_MANAGER_MAX_PARAMS
|
||||
#define WIFI_MANAGER_MAX_PARAMS 5 // params will autoincrement and realloc by this amount when max is reached
|
||||
#endif
|
||||
|
||||
#define WFM_LABEL_BEFORE 1
|
||||
#define WFM_LABEL_AFTER 2
|
||||
#define WFM_NO_LABEL 0
|
||||
#define WFM_LABEL_DEFAULT 1
|
||||
|
||||
class WiFiManagerParameter {
|
||||
public:
|
||||
/**
|
||||
Create custom parameters that can be added to the WiFiManager setup web page
|
||||
@id is used for HTTP queries and must not contain spaces nor other special characters
|
||||
*/
|
||||
WiFiManagerParameter();
|
||||
explicit WiFiManagerParameter(const char *custom);
|
||||
WiFiManagerParameter(const char *id, const char *label);
|
||||
WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length);
|
||||
WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom);
|
||||
WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement);
|
||||
virtual ~WiFiManagerParameter();
|
||||
|
||||
WiFiManagerParameter(const WiFiManagerParameter&) = delete;
|
||||
WiFiManagerParameter& operator=(const WiFiManagerParameter&) = delete;
|
||||
|
||||
const char *getID() const;
|
||||
const char *getValue() const;
|
||||
const char *getLabel() const;
|
||||
const char *getPlaceholder() const; // @deprecated, use getLabel
|
||||
int getValueLength() const;
|
||||
int getLabelPlacement() const;
|
||||
virtual const char *getCustomHTML() const;
|
||||
void setValue(const char *defaultValue, int length);
|
||||
|
||||
protected:
|
||||
void init(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement);
|
||||
|
||||
const char *_id;
|
||||
const char *_label;
|
||||
char *_value;
|
||||
int _length;
|
||||
int _labelPlacement;
|
||||
|
||||
const char *_customHTML;
|
||||
friend class WiFiManager;
|
||||
};
|
||||
|
||||
#endif // WiFiManagerParameter_h
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* WiFiManagerServer.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* HTTP server for the config portal: single HTML shell (GET /), JSON under /api/..., OTA POST /u,
|
||||
* captive-portal redirects, and 404 handling. No legacy page routes.
|
||||
*
|
||||
* Supported surface (authoritative; registerRoutes mirrors this):
|
||||
* GET /
|
||||
* GET /api/bootstrap
|
||||
* GET /api/wifi/scan-status
|
||||
* POST /api/wifi/scan
|
||||
* GET /api/wifi/meta
|
||||
* POST /api/wifi/save
|
||||
* GET /api/wifi/connect-status
|
||||
* GET /api/params
|
||||
* POST /api/params/save
|
||||
* GET /api/info
|
||||
* GET /api/status
|
||||
* POST /api/device/restart
|
||||
* POST /api/device/erase
|
||||
* POST /api/portal/close
|
||||
* POST /api/portal/exit
|
||||
* POST /u (multipart firmware upload; JSON completion response)
|
||||
* notFound -> captive redirect or 404
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerServer_h
|
||||
#define WiFiManagerServer_h
|
||||
|
||||
#if defined(ESP8266) || defined(ESP32)
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <DNSServer.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
class WiFiManager;
|
||||
class WiFiManagerHandlers;
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
class WiFiManagerDfteLogger;
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// HTTP routes (portal API only; no legacy /wifi, /info, … page URLs)
|
||||
|
||||
const char R_root[] PROGMEM = "/";
|
||||
|
||||
const char R_api_bootstrap[] PROGMEM = "/api/bootstrap";
|
||||
const char R_api_wifi_scan_status[] PROGMEM = "/api/wifi/scan-status";
|
||||
const char R_api_wifi_scan[] PROGMEM = "/api/wifi/scan";
|
||||
const char R_api_wifi_meta[] PROGMEM = "/api/wifi/meta";
|
||||
const char R_api_wifi_save[] PROGMEM = "/api/wifi/save";
|
||||
const char R_api_wifi_connect_status[] PROGMEM = "/api/wifi/connect-status";
|
||||
const char R_api_params[] PROGMEM = "/api/params";
|
||||
const char R_api_params_save[] PROGMEM = "/api/params/save";
|
||||
const char R_api_info[] PROGMEM = "/api/info";
|
||||
const char R_api_status[] PROGMEM = "/api/status";
|
||||
const char R_api_device_restart[] PROGMEM = "/api/device/restart";
|
||||
const char R_api_device_erase[] PROGMEM = "/api/device/erase";
|
||||
const char R_api_portal_close[] PROGMEM = "/api/portal/close";
|
||||
const char R_api_portal_exit[] PROGMEM = "/api/portal/exit";
|
||||
|
||||
const char R_updatedone[] PROGMEM = "/u";
|
||||
|
||||
class WiFiManagerServer {
|
||||
public:
|
||||
WiFiManagerServer(WiFiManager* wm);
|
||||
~WiFiManagerServer();
|
||||
|
||||
void createServer(uint16_t port);
|
||||
void registerRoutes();
|
||||
void setupDNSD();
|
||||
void processDNS();
|
||||
void shutdownServer();
|
||||
AsyncWebServer* getServer() { return server.get(); }
|
||||
DNSServer* getDNSServer() { return dnsServer.get(); }
|
||||
|
||||
private:
|
||||
WiFiManager* _wm;
|
||||
std::unique_ptr<WiFiManagerHandlers> _handlers;
|
||||
std::unique_ptr<AsyncWebServer> server;
|
||||
std::unique_ptr<DNSServer> dnsServer;
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
std::unique_ptr<WiFiManagerDfteLogger> _dfteLogger;
|
||||
bool _wmOwnsDfteLogSink = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // defined(ESP8266) || defined(ESP32)
|
||||
|
||||
#endif // WiFiManagerServer_h
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* WiFiManagerUtils.h
|
||||
* Pure utility functions for WiFiManager
|
||||
* String conversions, HTML encoding, and helper calculations
|
||||
* These functions have no dependency on WiFiManager instance state
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
#ifndef _WIFI_MANAGER_UTILS_H_
|
||||
#define _WIFI_MANAGER_UTILS_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Forward declaration
|
||||
class IPAddress;
|
||||
|
||||
namespace WiFiManagerUtils {
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// WIFI STATUS ARRAYS (for status string conversion)
|
||||
|
||||
extern const char * const WIFI_STA_STATUS[] PROGMEM;
|
||||
extern const char * const WIFI_MODES[] PROGMEM;
|
||||
|
||||
#ifdef ESP32
|
||||
extern const char * const AUTH_MODE_NAMES[] PROGMEM;
|
||||
#elif defined(ESP8266)
|
||||
extern const char * const AUTH_MODE_NAMES[] PROGMEM;
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// STRING CONVERSION UTILITIES
|
||||
|
||||
/**
|
||||
* Convert WiFi status code to readable string
|
||||
* @param status WiFi status code
|
||||
* @return Status string or "Unknown" if invalid
|
||||
*/
|
||||
String getStatusString(uint8_t status);
|
||||
|
||||
|
||||
/**
|
||||
* Convert WiFi mode code to readable string
|
||||
* @param mode WiFi mode code
|
||||
* @return Mode string or "Unknown" if invalid
|
||||
*/
|
||||
String getModeString(uint8_t mode);
|
||||
|
||||
/**
|
||||
* Convert encryption/auth mode to readable string
|
||||
* @param authmode Encryption type code
|
||||
* @return Encryption type string
|
||||
*/
|
||||
String getEncryptionString(uint8_t authmode);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// HTML UTILITIES
|
||||
|
||||
/**
|
||||
* Encode HTML entities in a string
|
||||
* @param str String to encode
|
||||
* @param whitespace If true, also encode spaces as  
|
||||
* @return Encoded string
|
||||
*/
|
||||
String htmlEntities(const String& str, bool whitespace = false);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// IP/STRING UTILITIES
|
||||
|
||||
/**
|
||||
* Check if string is a valid IP address
|
||||
* @param str String to check
|
||||
* @return true if valid IP address
|
||||
*/
|
||||
bool isValidIP(const String& str);
|
||||
|
||||
/**
|
||||
* Convert IPAddress to String
|
||||
* @param ip IPAddress to convert
|
||||
* @return IP address string
|
||||
*/
|
||||
String ipToString(IPAddress ip);
|
||||
|
||||
/**
|
||||
* Validate AP password format (8-63 characters)
|
||||
* @param password Password to validate
|
||||
* @return true if valid
|
||||
*/
|
||||
bool isValidAPPassword(const String& password);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// HELPER CALCULATIONS
|
||||
|
||||
/**
|
||||
* Convert RSSI value to quality percentage (0-100)
|
||||
* @param rssi RSSI value in dBm
|
||||
* @return Quality percentage (0-100)
|
||||
*/
|
||||
int rssiToQuality(int rssi);
|
||||
|
||||
} // namespace WiFiManagerUtils
|
||||
|
||||
#endif // _WIFI_MANAGER_UTILS_H_
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* CSS.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* CSS stylesheet for WiFiManager web interface.
|
||||
* Embedded styles to avoid external file dependencies.
|
||||
*/
|
||||
|
||||
#ifndef _CSS_TEMPLATES_H_
|
||||
#define _CSS_TEMPLATES_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
/* Design tokens */
|
||||
":root{"
|
||||
"--wm-bg:#f0f4f8;"
|
||||
"--wm-surface:#ffffff;"
|
||||
"--wm-text:#0f172a;"
|
||||
"--wm-muted:#64748b;"
|
||||
"--wm-border:#e2e8f0;"
|
||||
"--wm-brand:#0ea5e9;"
|
||||
"--wm-brand-hover:#0284c7;"
|
||||
"--wm-danger:#dc2626;"
|
||||
"--wm-danger-hover:#b91c1c;"
|
||||
"--wm-success:#16a34a;"
|
||||
"--wm-radius:12px;"
|
||||
"--wm-radius-sm:8px;"
|
||||
"--wm-shadow:0 1px 3px rgba(15,23,42,.08),0 4px 16px rgba(15,23,42,.06);"
|
||||
"--wm-font:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;"
|
||||
"}"
|
||||
"body.invert{"
|
||||
"--wm-bg:#0c1222;"
|
||||
"--wm-surface:#151d2e;"
|
||||
"--wm-text:#f1f5f9;"
|
||||
"--wm-muted:#94a3b8;"
|
||||
"--wm-border:#334155;"
|
||||
"--wm-brand:#38bdf8;"
|
||||
"--wm-brand-hover:#7dd3fc;"
|
||||
"--wm-danger:#f87171;"
|
||||
"--wm-danger-hover:#fca5a5;"
|
||||
"--wm-success:#4ade80;"
|
||||
"--wm-shadow:0 4px 24px rgba(0,0,0,.45);"
|
||||
"}"
|
||||
/* Base */
|
||||
"*,*::before,*::after{box-sizing:border-box}"
|
||||
"body.portal{margin:0;min-height:100vh;background:var(--wm-bg);color:var(--wm-text);"
|
||||
"font-family:var(--wm-font);font-size:16px;line-height:1.5;-webkit-text-size-adjust:100%;text-align:left}"
|
||||
"a{color:var(--wm-brand);font-weight:600;text-decoration:none}"
|
||||
"a:hover{color:var(--wm-brand-hover);text-decoration:underline}"
|
||||
/* Layout shell */
|
||||
".wm-layout{width:100%;max-width:420px;margin:0 auto;padding:16px 18px 28px;}"
|
||||
"h1,h2,h3{margin:0 0 .35em;font-weight:700;letter-spacing:-.02em}"
|
||||
"h1{font-size:1.5rem}"
|
||||
"h2{font-size:1.15rem}"
|
||||
"h3{font-size:1rem}"
|
||||
"p{margin:.5em 0}"
|
||||
/* Top navigation — single row; overflow scrollable but scrollbar hidden (touch still pans) */
|
||||
".wm-nav{display:flex;flex-wrap:nowrap;align-items:stretch;gap:4px;margin:0 0 20px;padding:4px 6px;"
|
||||
"overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;-ms-overflow-style:none;"
|
||||
"background:var(--wm-surface);border-radius:var(--wm-radius);box-shadow:var(--wm-shadow);border:1px solid var(--wm-border)}"
|
||||
".wm-nav::-webkit-scrollbar{display:none;width:0;height:0}"
|
||||
".wm-nav-link{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;padding:6px 8px;border-radius:var(--wm-radius-sm);"
|
||||
"font-size:.8rem;font-weight:600;color:var(--wm-muted);text-decoration:none;border:1px solid transparent;white-space:nowrap}"
|
||||
".wm-nav-link:hover{color:var(--wm-brand);background:rgba(14,165,233,.08);text-decoration:none}"
|
||||
"body.invert .wm-nav-link:hover{background:rgba(56,189,248,.12)}"
|
||||
".wm-nav-link--active{color:var(--wm-text);background:rgba(14,165,233,.15);border-color:rgba(14,165,233,.35)}"
|
||||
"body.invert .wm-nav-link--active{background:rgba(56,189,248,.2);border-color:rgba(56,189,248,.4)}"
|
||||
".wm-nav .wm-icon-label{gap:4px}"
|
||||
".wm-nav .wm-icon{width:.95rem;height:.95rem;flex:0 0 .95rem}"
|
||||
".wm-icon-label{display:inline-flex;align-items:center;gap:8px;min-width:0}"
|
||||
".wm-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1rem;height:1.1rem;flex:0 0 1.1rem;color:currentColor}"
|
||||
".wm-icon svg{display:block;width:100%;height:100%}"
|
||||
/* Hero / page chrome */
|
||||
".wm-hero{margin-bottom:20px}"
|
||||
".wm-hero-title{font-size:1.65rem;line-height:1.2;margin-bottom:.25em}"
|
||||
".wm-hero-sub{font-size:1rem;color:var(--wm-muted);font-weight:500;margin:0}"
|
||||
".wm-brand-logo{margin-bottom:12px;line-height:0}"
|
||||
".wm-brand-logo svg{display:block;max-width:160px;height:auto}"
|
||||
".wm-hero-intro{font-size:.95rem;color:var(--wm-muted);margin:.5em 0 0;line-height:1.45;max-width:42ch}"
|
||||
".wm-page-head{margin-bottom:16px}"
|
||||
".wm-page-head h1{font-size:1.45rem}"
|
||||
".wm-page-desc{font-size:.95rem;color:var(--wm-muted);margin:.35em 0 0}"
|
||||
/* Cards */
|
||||
".wm-card{background:var(--wm-surface);border:1px solid var(--wm-border);border-radius:var(--wm-radius);"
|
||||
"box-shadow:var(--wm-shadow);padding:18px 16px;margin-bottom:16px;text-align:left}"
|
||||
".wm-card-title{font-size:1rem;margin:0 0 12px;padding-bottom:10px;border-bottom:1px solid var(--wm-border);"
|
||||
"color:var(--wm-text)}"
|
||||
".wm-card-subtitle{margin:-4px 0 12px;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
|
||||
".wm-lead{margin:0 0 8px;font-size:.95rem;color:var(--wm-muted);line-height:1.5}"
|
||||
".wm-home-summary{margin:0 0 14px;font-size:1rem;font-weight:600;line-height:1.45;color:var(--wm-text)}"
|
||||
/* Callout / status */
|
||||
".wm-callout{padding:12px 14px;border-radius:var(--wm-radius-sm);border:1px solid var(--wm-border);"
|
||||
"background:rgba(14,165,233,.06);margin-bottom:16px}"
|
||||
"body.invert .wm-callout{background:rgba(56,189,248,.08)}"
|
||||
".wm-callout--info{border-left:4px solid var(--wm-brand)}"
|
||||
".wm-info-summary{font-weight:600;color:var(--wm-text)}"
|
||||
".wm-status{margin:0 0 12px;padding:10px 12px;font-size:.92rem;font-weight:700;color:var(--wm-brand);"
|
||||
"text-align:center;border-radius:var(--wm-radius-sm);background:rgba(14,165,233,.08);border:1px solid rgba(14,165,233,.18)}"
|
||||
".wm-status--warning{color:#c2410c;background:rgba(249,115,22,.12);border-color:rgba(249,115,22,.28)}"
|
||||
".wm-status--caution{color:#a16207;background:rgba(234,179,8,.14);border-color:rgba(234,179,8,.3)}"
|
||||
".wm-status--danger{color:var(--wm-danger);background:rgba(220,38,38,.12);border-color:rgba(220,38,38,.28)}"
|
||||
"body.invert .wm-status{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.22)}"
|
||||
"body.invert .wm-status--warning{background:rgba(249,115,22,.18);border-color:rgba(249,115,22,.34)}"
|
||||
"body.invert .wm-status--caution{background:rgba(234,179,8,.18);border-color:rgba(234,179,8,.34)}"
|
||||
"body.invert .wm-status--danger{background:rgba(220,38,38,.18);border-color:rgba(220,38,38,.34)}"
|
||||
/* Form fields */
|
||||
".wm-field{margin-bottom:14px;text-align:left}"
|
||||
".wm-field:last-child{margin-bottom:0}"
|
||||
".wm-field label{display:block;font-size:.82rem;font-weight:600;color:var(--wm-muted);margin-bottom:6px}"
|
||||
"input[type=text],input[type=password],input[type=number],input[type=email],select{"
|
||||
"display:block;width:100%;padding:12px 14px;font-size:1rem;font-family:inherit;"
|
||||
"color:var(--wm-text);background:var(--wm-bg);border:1px solid var(--wm-border);border-radius:var(--wm-radius-sm);"
|
||||
"-webkit-appearance:none;appearance:none}"
|
||||
"input:focus,select:focus{outline:2px solid var(--wm-brand);outline-offset:0;border-color:var(--wm-brand)}"
|
||||
"input[type=radio],input[type=checkbox]{width:auto;margin-right:8px;accent-color:var(--wm-brand)}"
|
||||
".wm-checkbox-row{display:flex;align-items:center;gap:8px;margin:12px 0;font-size:.92rem}"
|
||||
".wm-checkbox-row label{display:inline;margin:0;font-weight:500;color:var(--wm-text)}"
|
||||
".wm-form-actions{margin-top:16px}"
|
||||
/* Buttons */
|
||||
".wm-btn{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 18px;"
|
||||
"font-size:1rem;font-weight:600;font-family:inherit;border-radius:var(--wm-radius-sm);border:0;cursor:pointer;"
|
||||
"transition:background .15s,transform .1s,opacity .15s}"
|
||||
".wm-btn:disabled{opacity:.5;cursor:not-allowed}"
|
||||
".wm-btn:active{transform:scale(.98)}"
|
||||
"a.wm-btn{box-sizing:border-box;text-decoration:none}"
|
||||
".wm-btn .wm-icon-label{justify-content:center}"
|
||||
".wm-btn--block{width:100%}"
|
||||
".wm-btn--primary{background:var(--wm-brand);color:#fff}"
|
||||
".wm-btn--primary:hover{background:var(--wm-brand-hover)}"
|
||||
".wm-btn--secondary{background:var(--wm-bg);color:var(--wm-text);border:1px solid var(--wm-border)}"
|
||||
".wm-btn--secondary:hover{border-color:var(--wm-brand);color:var(--wm-brand)}"
|
||||
".wm-btn--danger{background:var(--wm-danger);color:#fff}"
|
||||
".wm-btn--danger:hover{background:var(--wm-danger-hover)}"
|
||||
"input[type=file]{width:100%;padding:10px;border:1px dashed var(--wm-border);border-radius:var(--wm-radius-sm);"
|
||||
"background:var(--wm-bg);font-size:.9rem}"
|
||||
/* Legacy: unclassed submit in forms */
|
||||
"button[type=submit],.wm-form-actions .wm-btn--primary{background:var(--wm-brand);color:#fff}"
|
||||
"button[type=submit]:hover{background:var(--wm-brand-hover)}"
|
||||
/* Device actions / button stack */
|
||||
".wm-actions-stack{display:flex;flex-direction:column;gap:10px;margin-top:4px}"
|
||||
".wm-btn-group{display:flex;flex-direction:column;gap:10px}"
|
||||
/* WiFi scan list */
|
||||
".wm-scan-list{margin:0 0 12px;border:1px solid var(--wm-border);border-radius:var(--wm-radius-sm);"
|
||||
"overflow:hidden;background:var(--wm-bg)}"
|
||||
".wm-scan-row{display:flex;align-items:center;gap:10px;min-height:48px;padding:10px 12px;"
|
||||
"border-bottom:1px solid var(--wm-border);text-decoration:none;color:inherit;cursor:pointer}"
|
||||
".wm-scan-row:last-child{border-bottom:0}"
|
||||
".wm-scan-row:hover{background:rgba(14,165,233,.1)}"
|
||||
"body.invert .wm-scan-row:hover{background:rgba(56,189,248,.12)}"
|
||||
".wm-scan-ssid{flex:1;min-width:0;font-weight:600;font-size:.95rem;text-align:left;word-break:break-word}"
|
||||
".wm-scan-meta{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-shrink:0;min-width:78px}"
|
||||
".wm-scan-signal{display:block;flex-shrink:0;font-size:.8rem;color:var(--wm-muted);font-weight:600;min-width:38px;text-align:right}"
|
||||
".wm-scan-lock,.wm-scan-bars{display:block;flex:0 0 16px}"
|
||||
// quality icons
|
||||
".q{position:relative;height:16px;width:16px;margin:0;padding:0;flex-shrink:0}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px}.ql .q{float:left}.q:after,.q:before{content:'';position:absolute;left:0;top:0;width:16px;height:16px;display:block;background-repeat:no-repeat;background-position:16px 0;"
|
||||
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');}"
|
||||
".wm-scan-lock:after,.wm-scan-bars:before{content:none}"
|
||||
"@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.q:before,.q:after {"
|
||||
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');"
|
||||
"background-size: 95px 16px;}}"
|
||||
// messages from firmware / server
|
||||
".msg{padding:16px;margin:12px 0;border-radius:var(--wm-radius-sm);border:1px solid var(--wm-border);"
|
||||
"border-left-width:4px;border-left-color:var(--wm-muted);background:var(--wm-surface);text-align:left}"
|
||||
".msg h4{margin:0 0 8px;font-size:1rem}"
|
||||
".msg.P{border-left-color:var(--wm-brand)}.msg.P h4{color:var(--wm-brand)}"
|
||||
".msg.D{border-left-color:var(--wm-danger)}.msg.D h4{color:var(--wm-danger)}"
|
||||
".msg.S{border-left-color:var(--wm-success)}.msg.S h4{color:var(--wm-success)}"
|
||||
"dt{font-weight:700;font-size:.85rem;color:var(--wm-muted)}dd{margin:2px 0 12px 0;padding:0;font-size:.95rem}"
|
||||
"td{vertical-align:top}"
|
||||
".h{display:none}"
|
||||
/* Info page key-value grid */
|
||||
".wm-kv{margin:0;padding:0}"
|
||||
".wm-kv dt{padding-top:10px}"
|
||||
".wm-kv dt:first-of-type{padding-top:0}"
|
||||
".wm-kv dd{margin-left:0;padding-bottom:4px;border-bottom:1px solid var(--wm-border)}"
|
||||
".wm-kv dd:last-of-type{border-bottom:0}"
|
||||
/* Feedback areas */
|
||||
"#wm-wifi-msg,#wm-param-msg{font-size:.9rem;margin-top:12px;min-height:1.2em;color:var(--wm-muted)}"
|
||||
"#wm-ota-log{font-size:.8rem;background:var(--wm-bg);padding:12px;border-radius:var(--wm-radius-sm);"
|
||||
"border:1px solid var(--wm-border);overflow:auto;max-height:160px}"
|
||||
// invert legacy body
|
||||
"body.invert{background-color:var(--wm-bg)}"
|
||||
"body.invert .msg{border-color:var(--wm-border)}"
|
||||
"body.invert .q[role=img]{-webkit-filter:invert(1);filter:invert(1)}"
|
||||
":disabled{opacity:.55}"
|
||||
// toast + modal
|
||||
".wm-toast{position:fixed;left:50%;top:12px;transform:translateX(-50%);z-index:10001;max-width:92%;padding:12px 16px;border-radius:var(--wm-radius-sm);"
|
||||
"box-shadow:var(--wm-shadow);font-size:.95rem;text-align:center;font-weight:600}"
|
||||
".wm-toast.ok{background:rgba(22,163,74,.15);border:1px solid var(--wm-success);color:var(--wm-text)}"
|
||||
".wm-toast.err{background:rgba(220,38,38,.12);border:1px solid var(--wm-danger);color:var(--wm-text)}"
|
||||
".wm-dialog{position:fixed;inset:0;z-index:10000;display:none;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}"
|
||||
".wm-dialog-backdrop{position:absolute;inset:0;background:rgba(15,23,42,.5)}"
|
||||
".wm-dialog-panel{position:relative;z-index:1;background:var(--wm-surface);border-radius:var(--wm-radius);padding:20px 18px;max-width:400px;width:100%;"
|
||||
"box-shadow:var(--wm-shadow);border:1px solid var(--wm-border);text-align:left}"
|
||||
".wm-dialog-msg{margin:0 0 16px;line-height:1.45;color:var(--wm-text)}"
|
||||
".wm-dialog-actions{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}"
|
||||
".wm-dialog-actions .wm-btn{min-width:108px}"
|
||||
".wm-dialog-ok,.wm-btn.wm-dialog-ok{background:var(--wm-brand)!important;color:#fff!important}"
|
||||
".wm-dialog-ok:hover{background:var(--wm-brand-hover)!important}"
|
||||
"body.invert .wm-toast.ok{background:rgba(22,163,74,.2)}"
|
||||
"body.invert .wm-toast.err{background:rgba(220,38,38,.2)}"
|
||||
"</style>";
|
||||
|
||||
#endif // _CSS_TEMPLATES_H_
|
||||
@@ -0,0 +1,800 @@
|
||||
/**
|
||||
* PortalAppJS.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Embedded SPA for WiFiManager config portal (hash routing + JSON APIs).
|
||||
*/
|
||||
|
||||
#ifndef _WM_PORTAL_APP_JS_H_
|
||||
#define _WM_PORTAL_APP_JS_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char PORTAL_APP_JS[] PROGMEM = R"rawliteral(
|
||||
(function(){
|
||||
'use strict';
|
||||
var boot={};
|
||||
try{var el=document.getElementById('wm-bootstrap');if(el&&el.textContent)boot=JSON.parse(el.textContent);}catch(e){boot={};}
|
||||
|
||||
// --- Core / DOM ---
|
||||
function $(id){return document.getElementById(id);}
|
||||
function esc(t){if(!t)return'';return String(t).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]);});}
|
||||
|
||||
// --- Toast / dialog ---
|
||||
var _wmToastTimer=null;
|
||||
var _wmPortalTimeoutTimer=null;
|
||||
function showToast(msg,isErr){
|
||||
var el=$('wm-toast');
|
||||
if(!el)return;
|
||||
el.textContent=msg||'';
|
||||
el.className='wm-toast '+(isErr?'err':'ok');
|
||||
el.style.display='block';
|
||||
if(_wmToastTimer)clearTimeout(_wmToastTimer);
|
||||
_wmToastTimer=setTimeout(function(){el.style.display='none';el.textContent='';},4500);
|
||||
}
|
||||
function showConfirm(msg,onOk){
|
||||
var dlg=$('wm-dialog'),dmsg=$('wm-dialog-msg'),bk=$('wm-dialog-backdrop'),ok=$('wm-dialog-ok'),cancel=$('wm-dialog-cancel');
|
||||
if(!dlg||!dmsg||!ok||!cancel){if(onOk)onOk();return;}
|
||||
dmsg.textContent=msg||'';
|
||||
dlg.style.display='flex';
|
||||
dlg.setAttribute('aria-hidden','false');
|
||||
function hide(){
|
||||
dlg.style.display='none';
|
||||
dlg.setAttribute('aria-hidden','true');
|
||||
ok.onclick=null;
|
||||
cancel.onclick=null;
|
||||
if(bk)bk.onclick=null;
|
||||
}
|
||||
function onCancel(){hide();}
|
||||
cancel.onclick=onCancel;
|
||||
if(bk)bk.onclick=onCancel;
|
||||
ok.onclick=function(){hide();if(onOk)onOk();};
|
||||
}
|
||||
|
||||
function bindPortalChromeEvents(){
|
||||
var back=$('wm-nav-back');
|
||||
if(back){
|
||||
back.addEventListener('click', function(ev){ ev.preventDefault(); history.back(); });
|
||||
}
|
||||
var r=$('wm-restart-btn'), e=$('wm-exit-btn'), c=$('wm-close-captive-btn'), er=$('wm-erase-btn');
|
||||
if(r && window.portalRestart) r.addEventListener('click', window.portalRestart);
|
||||
if(e && window.portalExit) e.addEventListener('click', window.portalExit);
|
||||
if(c && window.portalCloseCaptive) c.addEventListener('click', window.portalCloseCaptive);
|
||||
if(er && window.portalErase) er.addEventListener('click', window.portalErase);
|
||||
}
|
||||
|
||||
function bindFormSubmitHandlers(){
|
||||
var wf=$('wm-wifi-form'), pf=$('wm-param-form'), of=$('wm-ota-form');
|
||||
if(wf && window.portalWifiSave) wf.addEventListener('submit', window.portalWifiSave);
|
||||
if(pf && window.portalParamSave) pf.addEventListener('submit', window.portalParamSave);
|
||||
if(of && window.portalOtaSubmit) of.addEventListener('submit', window.portalOtaSubmit);
|
||||
}
|
||||
|
||||
// --- View shell ---
|
||||
function stopPortalTimeoutCountdown(){
|
||||
if(_wmPortalTimeoutTimer){
|
||||
clearInterval(_wmPortalTimeoutTimer);
|
||||
_wmPortalTimeoutTimer=null;
|
||||
}
|
||||
}
|
||||
|
||||
function setView(html){
|
||||
var a=$('app');
|
||||
if(!a)return;
|
||||
stopPortalTimeoutCountdown();
|
||||
stopWifiScanPolling();
|
||||
stopWifiConnectPolling();
|
||||
a.innerHTML="<div class='wm-layout'>"+html+"</div>";
|
||||
bindPortalChromeEvents();
|
||||
bindFormSubmitHandlers();
|
||||
}
|
||||
|
||||
function formatDurationSeconds(totalSeconds){
|
||||
totalSeconds=Math.max(0,Math.floor(totalSeconds||0));
|
||||
var hours=Math.floor(totalSeconds/3600);
|
||||
var minutes=Math.floor((totalSeconds%3600)/60);
|
||||
var seconds=totalSeconds%60;
|
||||
if(hours>0){
|
||||
return hours+":"+String(minutes).padStart(2,'0')+":"+String(seconds).padStart(2,'0');
|
||||
}
|
||||
return minutes+":"+String(seconds).padStart(2,'0');
|
||||
}
|
||||
|
||||
function startPortalTimeoutCountdown(initialSeconds){
|
||||
var el=$('wm-portal-timeout');
|
||||
var remaining=Math.max(0,Math.floor(initialSeconds||0));
|
||||
if(!el||remaining<=0)return;
|
||||
function levelClass(seconds){
|
||||
if(seconds<10)return'wm-status--danger';
|
||||
if(seconds<30)return'wm-status--caution';
|
||||
if(seconds<60)return'wm-status--warning';
|
||||
return'';
|
||||
}
|
||||
function render(){
|
||||
if(!el)return;
|
||||
el.className='wm-status'+(levelClass(remaining)?' '+levelClass(remaining):'');
|
||||
el.textContent='Captive portal timeout: '+formatDurationSeconds(remaining);
|
||||
}
|
||||
render();
|
||||
_wmPortalTimeoutTimer=setInterval(function(){
|
||||
remaining=Math.max(0,remaining-1);
|
||||
render();
|
||||
if(remaining<=0)stopPortalTimeoutCountdown();
|
||||
},1000);
|
||||
}
|
||||
|
||||
// --- Field render (WiFi / params meta) ---
|
||||
function renderStandardField(f){
|
||||
var id=f.id||f.name||'';
|
||||
var nm=f.name||f.id||'';
|
||||
var domId=(id==='s')?'wm-s':(id==='p')?'wm-p':('wm-f-'+id);
|
||||
var t=f.type||'text';
|
||||
var h='<div class="wm-field">';
|
||||
h+='<label for="'+domId+'">'+esc(f.label||'')+'</label>';
|
||||
h+='<input id="'+domId+'" name="'+esc(nm)+'" type="'+esc(t)+'"';
|
||||
if(f.maxlength)h+=' maxlength="'+f.maxlength+'"';
|
||||
if(f.value!==undefined&&f.value!==null)h+=' value="'+esc(f.value)+'"';
|
||||
if(f.placeholder)h+=' placeholder="'+esc(f.placeholder)+'"';
|
||||
if(f.customAttrs)h+=' '+f.customAttrs;
|
||||
h+='/>';
|
||||
h+='</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
function renderMetaField(f){
|
||||
if(!f)return'';
|
||||
if(f.kind==='html')return f.html||'';
|
||||
if(f.kind==='field' || !f.kind)return renderStandardField(f);
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderFieldList(arr){
|
||||
if(!arr||!arr.length)return'';
|
||||
var h='',i;
|
||||
for(i=0;i<arr.length;i++)h+=renderMetaField(arr[i]);
|
||||
return h;
|
||||
}
|
||||
|
||||
function iconSvg(name){
|
||||
var body='';
|
||||
switch(name){
|
||||
case 'home':
|
||||
body="<path d='M3 10.5 12 3l9 7.5'/><path d='M5 9.5V20h14V9.5'/>";
|
||||
break;
|
||||
case 'wifi':
|
||||
body="<path d='M4.5 9.5a12 12 0 0 1 15 0'/><path d='M7.5 12.5a7.5 7.5 0 0 1 9 0'/><path d='M10.5 15.5a3.5 3.5 0 0 1 3 0'/><circle cx='12' cy='19' r='1.2' fill='currentColor' stroke='none'/>";
|
||||
break;
|
||||
case 'setup':
|
||||
body="<line x1='5' y1='6' x2='19' y2='6'/><circle cx='9' cy='6' r='2'/><line x1='5' y1='12' x2='19' y2='12'/><circle cx='15' cy='12' r='2'/><line x1='5' y1='18' x2='19' y2='18'/><circle cx='11' cy='18' r='2'/>";
|
||||
break;
|
||||
case 'info':
|
||||
body="<circle cx='12' cy='12' r='9'/><line x1='12' y1='10.5' x2='12' y2='16.5'/><circle cx='12' cy='7.2' r='1' fill='currentColor' stroke='none'/>";
|
||||
break;
|
||||
case 'update':
|
||||
body="<path d='M12 4v10'/><path d='m8.5 10.5 3.5 3.5 3.5-3.5'/><path d='M5 19h14'/>";
|
||||
break;
|
||||
case 'back':
|
||||
body="<path d='M10 6 4 12l6 6'/><path d='M4 12h16'/>";
|
||||
break;
|
||||
case 'restart':
|
||||
body="<path d='M20 12a8 8 0 1 1-2.3-5.7'/><path d='M20 4v5h-5'/>";
|
||||
break;
|
||||
case 'exit':
|
||||
body="<path d='M10 6H6v12h4'/><path d='M14 8l4 4-4 4'/><path d='M9 12h9'/>";
|
||||
break;
|
||||
case 'close':
|
||||
body="<path d='M4 12a8 8 0 1 0 16 0A8 8 0 1 0 4 12'/><path d='m8.5 8.5 7 7'/>";
|
||||
break;
|
||||
case 'erase':
|
||||
body="<path d='M5 7h14'/><path d='M9 7V5h6v2'/><path d='M8 7l1 12h6l1-12'/><path d='M10 10v6'/><path d='M14 10v6'/>";
|
||||
break;
|
||||
case 'status':
|
||||
body="<circle cx='12' cy='12' r='9'/><path d='M12 7v5l3 2'/>";
|
||||
break;
|
||||
case 'device':
|
||||
body="<rect x='7' y='7' width='10' height='10' rx='1.5'/><path d='M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4'/>";
|
||||
break;
|
||||
case 'diagnostics':
|
||||
body="<path d='M4 17h3l2-5 3 3 2-7 2 9h4'/>";
|
||||
break;
|
||||
case 'about':
|
||||
body="<circle cx='12' cy='12' r='9'/><path d='M12 10.5v5.5'/><circle cx='12' cy='7.2' r='1' fill='currentColor' stroke='none'/>";
|
||||
break;
|
||||
case 'actions':
|
||||
body="<path d='M12 3v18'/><path d='M3 12h18'/><path d='m6.5 6.5 11 11'/><path d='m17.5 6.5-11 11'/>";
|
||||
break;
|
||||
case 'scan':
|
||||
body="<path d='M4 7h16'/><path d='M4 12h10'/><path d='M4 17h7'/><path d='m16 15 4 4'/><circle cx='14' cy='13' r='4'/>";
|
||||
break;
|
||||
case 'save':
|
||||
body="<path d='M5 4h11l3 3v13H5z'/><path d='M8 4v5h8'/><path d='M9 19v-5h6v5'/>";
|
||||
break;
|
||||
case 'connect':
|
||||
body="<path d='M4.5 12a7.5 7.5 0 0 1 7.5-7.5'/><path d='m17 6 2 2-2 2'/><path d='M12 4.5H19'/>";
|
||||
break;
|
||||
case 'upload':
|
||||
body="<path d='M12 20V8'/><path d='m8.5 11.5 3.5-3.5 3.5 3.5'/><path d='M5 20h14'/>";
|
||||
break;
|
||||
}
|
||||
if(!body)return'';
|
||||
return "<span class='wm-icon wm-icon--"+name+"' aria-hidden='true'><svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'>"+body+"</svg></span>";
|
||||
}
|
||||
|
||||
function labelWithIcon(name,label){
|
||||
return "<span class='wm-icon-label'>"+iconSvg(name)+"<span>"+esc(label||'')+"</span></span>";
|
||||
}
|
||||
|
||||
function cardTitleHtml(name,title){
|
||||
return "<h2 class='wm-card-title'>"+labelWithIcon(name,title)+"</h2>";
|
||||
}
|
||||
|
||||
function sectionCardHtml(name,title,subtitle,rows){
|
||||
if(!rows||!rows.length)return'';
|
||||
var h="<div class='wm-card'>"+cardTitleHtml(name,title);
|
||||
if(subtitle)h+="<p class='wm-card-subtitle'>"+esc(subtitle)+"</p>";
|
||||
h+="<dl class='wm-kv'>";
|
||||
for(var i=0;i<rows.length;i++){
|
||||
h+="<dt>"+esc(rows[i].label)+"</dt><dd>"+rows[i].value+"</dd>";
|
||||
}
|
||||
h+="</dl></div>";
|
||||
return h;
|
||||
}
|
||||
|
||||
function rowsWithoutKeys(rows,blocked){
|
||||
if(!rows||!rows.length)return[];
|
||||
var out=[],i,row,key;
|
||||
for(i=0;i<rows.length;i++){
|
||||
row=rows[i];
|
||||
if(!row)continue;
|
||||
key=row.key||'';
|
||||
if(blocked.indexOf(key)!==-1)continue;
|
||||
out.push(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function navBar(active){
|
||||
active=active||'home';
|
||||
var pages=boot.pages||{};
|
||||
var layout=boot.layout||{};
|
||||
var actions=boot.actions||{};
|
||||
var pinWifi=(layout.paramsLocation||'wifi')==='wifi';
|
||||
var ps=pages.setup||{};
|
||||
var pi=pages.info||{};
|
||||
var pu=pages.update||{};
|
||||
var h="<nav class='wm-nav' aria-label='Configuration'>";
|
||||
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>"+labelWithIcon('home','Home')+"</a>";
|
||||
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>"+labelWithIcon('wifi','WiFi')+"</a>";
|
||||
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>"+labelWithIcon('setup','Setup')+"</a>";}
|
||||
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>"+labelWithIcon('info','Info')+"</a>";}
|
||||
if(pu.visible){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>"+labelWithIcon('update','Update')+"</a>";}
|
||||
if(actions.back&&actions.back.visible){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>"+labelWithIcon('back','Back')+"</a>";}
|
||||
h+="</nav>";
|
||||
return h;
|
||||
}
|
||||
|
||||
function deviceActionsHtml(){
|
||||
var actions=boot.actions||{};
|
||||
var h='', ok=false;
|
||||
if(actions.restart&&actions.restart.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>"+labelWithIcon('restart','Restart device')+"</button>";ok=true;}
|
||||
if(actions.exitPortal&&actions.exitPortal.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>"+labelWithIcon('exit','Exit portal')+"</button>";ok=true;}
|
||||
if(actions.closeCaptive&&actions.closeCaptive.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>"+labelWithIcon('close','Stop captive portal')+"</button>";ok=true;}
|
||||
if(!ok)return'';
|
||||
return "<div class='wm-card'>"+cardTitleHtml('device','Device')+"<p class='wm-card-subtitle'>Quick device-level actions while the portal is open.</p><div class='wm-btn-group'>"+h+"</div></div>";
|
||||
}
|
||||
|
||||
function renderExtraHomeCards(cards){
|
||||
if(!cards||!cards.length)return'';
|
||||
var out='',i,c,j,k,cls;
|
||||
for(i=0;i<cards.length;i++){
|
||||
c=cards[i];
|
||||
if(!c)continue;
|
||||
out+="<div class='wm-card'>";
|
||||
if(c.title){out+="<h2 class='wm-card-title'>"+esc(c.title)+"</h2>";}
|
||||
if(c.kind==='text'||c.kind==='callout'){
|
||||
cls=c.kind==='callout'?'wm-callout wm-callout--info':'wm-lead';
|
||||
out+="<p class='"+cls+"'>"+esc(c.text||'')+"</p>";
|
||||
}else{
|
||||
out+="<dl class='wm-kv'>";
|
||||
if(c.items){for(j=0;j<c.items.length;j++){k=c.items[j];out+="<dt>"+esc(k.label)+"</dt><dd>"+esc(k.value)+"</dd>";}}
|
||||
out+="</dl>";
|
||||
}
|
||||
out+="</div>";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function viewHome(){
|
||||
var brand=boot.brand||{};
|
||||
var ctx=boot.context||{};
|
||||
var t=esc(brand.title||'WiFiManager');
|
||||
var ident=ctx.identityText?esc(ctx.identityText):'';
|
||||
var summary=ctx.statusSummary||'';
|
||||
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
|
||||
var html=navBar('home');
|
||||
html+="<header class='wm-hero'>";
|
||||
if(brand.logoSvg){html+="<div class='wm-brand-logo'>"+brand.logoSvg+"</div>";}
|
||||
html+="<h1 class='wm-hero-title'>"+t+"</h1>";
|
||||
if(ident){html+="<p class='wm-hero-sub'>"+ident+"</p>";}
|
||||
if(brand.homeIntro){html+="<p class='wm-hero-intro'>"+esc(brand.homeIntro)+"</p>";}
|
||||
html+="</header>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
|
||||
if(summary){html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";}
|
||||
if(timeoutSeconds>0){html+="<p class='wm-status' id='wm-portal-timeout'></p>";}
|
||||
html+="<p class='wm-lead'>Use <strong>WiFi</strong> to scan networks and connect. Open <strong>Info</strong> for addresses and firmware actions.</p>";
|
||||
html+="</div>";
|
||||
html+=renderExtraHomeCards(boot.extraHomeCards||[]);
|
||||
html+=deviceActionsHtml();
|
||||
setView(html);
|
||||
if(timeoutSeconds>0)startPortalTimeoutCountdown(timeoutSeconds);
|
||||
}
|
||||
|
||||
// --- HTTP ---
|
||||
function api(path, opt){
|
||||
opt=opt||{};
|
||||
return fetch(path,{method:opt.method||'GET',headers:opt.headers,body:opt.body,credentials:'same-origin'})
|
||||
.then(function(r){return r.text().then(function(t){return {ok:r.ok,status:r.status,body:t};});});
|
||||
}
|
||||
|
||||
// --- WiFi scan view ---
|
||||
var _wmWifiScanPollTimer=null;
|
||||
var _wmWifiScanPollToken=0;
|
||||
|
||||
function getScanResultsBox(){
|
||||
return $('wm-scan-results');
|
||||
}
|
||||
|
||||
function setScanRefreshEnabled(enabled){
|
||||
var btn=$('wm-refresh-scan');
|
||||
if(btn)btn.disabled=(enabled===false);
|
||||
}
|
||||
|
||||
function stopWifiScanPolling(){
|
||||
_wmWifiScanPollToken++;
|
||||
if(_wmWifiScanPollTimer){
|
||||
clearTimeout(_wmWifiScanPollTimer);
|
||||
_wmWifiScanPollTimer=null;
|
||||
}
|
||||
setScanRefreshEnabled(true);
|
||||
}
|
||||
|
||||
function startWifiScanPolling(box){
|
||||
stopWifiScanPolling();
|
||||
setScanRefreshEnabled(false);
|
||||
var token=++_wmWifiScanPollToken;
|
||||
function poll(){
|
||||
api('/api/wifi/scan-status').then(function(res){
|
||||
if(token!==_wmWifiScanPollToken)return;
|
||||
var d={};
|
||||
try{d=JSON.parse(res.body);}catch(e){return;}
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanList(d);
|
||||
if(!d.scanning){
|
||||
stopWifiScanPolling();
|
||||
return;
|
||||
}
|
||||
_wmWifiScanPollTimer=setTimeout(poll,800);
|
||||
}).catch(function(){
|
||||
if(token!==_wmWifiScanPollToken)return;
|
||||
stopWifiScanPolling();
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanError('Portal became unreachable while scanning.');
|
||||
});
|
||||
}
|
||||
poll();
|
||||
}
|
||||
|
||||
function renderScanList(data){
|
||||
if(!data||data.scanning){
|
||||
return "<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>Scanning for networks…</p></div>";
|
||||
}
|
||||
if(data.error==='timeout'){
|
||||
return "<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>Scan timed out. Try Scan again.</p></div>";
|
||||
}
|
||||
if(data.error==='failed'){
|
||||
return "<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>Scan failed.</p></div>";
|
||||
}
|
||||
if(!data.networks||data.count===0){
|
||||
return "<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>No networks found.</p></div>";
|
||||
}
|
||||
var html="<div class='wm-scan-list'>",i,n,pct,qi,enc;
|
||||
for(i=0;i<data.networks.length;i++){
|
||||
n=data.networks[i];
|
||||
pct=(n.quality||0)+'%';
|
||||
qi=Math.round((n.quality/100)*3)+1;if(qi<1)qi=1;if(qi>4)qi=4;
|
||||
html+="<a class='wm-scan-row' href='#p' data-ssid='"+esc(n.ssid)+"'>";
|
||||
html+="<span class='wm-scan-ssid'>"+esc(n.ssid)+"</span>";
|
||||
html+="<span class='wm-scan-meta'>";
|
||||
if(n.encrypted){html+="<span class='wm-scan-lock q l' aria-hidden='true'></span>";}
|
||||
html+="<span role='img' aria-label='"+pct+"' title='"+pct+"' class='wm-scan-bars q q-"+qi+"'></span>";
|
||||
html+="<span class='wm-scan-signal'>"+pct+"</span>";
|
||||
html+="</span>";
|
||||
html+="</a>";
|
||||
}
|
||||
html+="</div>";
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderScanError(msg){
|
||||
return "<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>"+esc(msg||'Failed to reach portal while scanning.')+"</p></div>";
|
||||
}
|
||||
|
||||
function shouldAutoStartWifiScan(data){
|
||||
if(!data)return true;
|
||||
if(data.scanning)return false;
|
||||
if((data.lastscan||0)>0)return false;
|
||||
if(data.results_valid)return false;
|
||||
if(data.error==='timeout' || data.error==='failed')return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function wifiRefresh(){
|
||||
var box=getScanResultsBox();
|
||||
if(!box)return;
|
||||
setScanRefreshEnabled(false);
|
||||
box.innerHTML="<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>Starting scan…</p></div>";
|
||||
api('/api/wifi/scan',{method:'POST'}).then(function(res){
|
||||
box=getScanResultsBox()||box;
|
||||
if(!res.ok){
|
||||
setScanRefreshEnabled(true);
|
||||
if(box)box.innerHTML=renderScanError('Failed to start WiFi scan.');
|
||||
showToast('Failed to start WiFi scan.',true);
|
||||
return;
|
||||
}
|
||||
startWifiScanPolling(box);
|
||||
}).catch(function(){
|
||||
stopWifiScanPolling();
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanError('Failed to reach portal while starting scan.');
|
||||
showToast('Failed to reach portal while starting scan.',true);
|
||||
});
|
||||
}
|
||||
|
||||
function bindWifiViewEvents(){
|
||||
var refreshBtn=$('wm-refresh-scan');
|
||||
if(refreshBtn){
|
||||
refreshBtn.addEventListener('click', wifiRefresh);
|
||||
}
|
||||
|
||||
var showPass=$('wm-showpass');
|
||||
if(showPass){
|
||||
showPass.addEventListener('change', function(){
|
||||
var p=$('wm-p');
|
||||
if(p)p.type=this.checked?'text':'password';
|
||||
});
|
||||
}
|
||||
|
||||
var scanResults=$('wm-scan-results');
|
||||
if(scanResults){
|
||||
scanResults.addEventListener('click', function(ev){
|
||||
var el=ev.target;
|
||||
while(el && el !== scanResults){
|
||||
if(el.tagName==='A' && el.getAttribute('data-ssid')){
|
||||
ev.preventDefault();
|
||||
var inp=$('wm-s');
|
||||
var pass=$('wm-p');
|
||||
if(inp)inp.value=el.getAttribute('data-ssid')||'';
|
||||
if(pass){
|
||||
if(pass.scrollIntoView)pass.scrollIntoView({behavior:'smooth',block:'center'});
|
||||
if(pass.focus)pass.focus();
|
||||
}else if(inp){
|
||||
if(inp.scrollIntoView)inp.scrollIntoView({behavior:'smooth',block:'center'});
|
||||
if(inp.focus)inp.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
el=el.parentElement;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function viewWifi(){
|
||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading WiFi options…</p></div>");
|
||||
api('/api/wifi/meta').then(function(res){
|
||||
try{var m=JSON.parse(res.body);}catch(e){m={};}
|
||||
var html=navBar('wifi');
|
||||
html+="<div class='wm-page-head'><h1>WiFi</h1><p class='wm-page-desc'>Pick a network below, confirm SSID and password, then connect.</p></div>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('scan','Networks nearby');
|
||||
html+="<div id='wm-scan-results'></div>";
|
||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
|
||||
html+="<form id='wm-wifi-form'>";
|
||||
html+=renderFieldList(m.wifiFields||[]);
|
||||
var wf=m.wifiFields||[];
|
||||
var hasPass=false;
|
||||
var i;
|
||||
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
|
||||
if(hasPass){
|
||||
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
|
||||
}
|
||||
html+=renderFieldList(m.staticFields||[]);
|
||||
html+=renderFieldList(m.params||[]);
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('connect','Save and connect')+"</button></div></form>";
|
||||
html+="<div id='wm-wifi-msg'></div></div>";
|
||||
setView(html);
|
||||
bindWifiViewEvents();
|
||||
setScanRefreshEnabled(true);
|
||||
api('/api/wifi/scan-status').then(function(r2){
|
||||
try{var d=JSON.parse(r2.body);}catch(e){d={};}
|
||||
var box=getScanResultsBox();
|
||||
if(shouldAutoStartWifiScan(d)){
|
||||
wifiRefresh();
|
||||
return;
|
||||
}
|
||||
if(box)box.innerHTML=renderScanList(d);
|
||||
if(d&&d.scanning){startWifiScanPolling(box);}
|
||||
}).catch(function(){
|
||||
var box=getScanResultsBox();
|
||||
if(box)box.innerHTML=renderScanError('Failed to reach portal while loading scan status.');
|
||||
setScanRefreshEnabled(true);
|
||||
});
|
||||
}).catch(function(){
|
||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Failed to load WiFi options. Reconnect to the portal and try again.</p></div>");
|
||||
});
|
||||
}
|
||||
|
||||
// --- WiFi connect polling ---
|
||||
var _wmWifiConnectPollTimer=null;
|
||||
var _wmWifiConnectPollToken=0;
|
||||
var _wmWifiConnectPending=false;
|
||||
|
||||
function stopWifiConnectPolling(){
|
||||
_wmWifiConnectPollToken++;
|
||||
if(_wmWifiConnectPollTimer){
|
||||
clearTimeout(_wmWifiConnectPollTimer);
|
||||
_wmWifiConnectPollTimer=null;
|
||||
}
|
||||
}
|
||||
|
||||
function pollWifiConnectStatus(){
|
||||
stopWifiConnectPolling();
|
||||
var token=++_wmWifiConnectPollToken;
|
||||
function poll(){
|
||||
api('/api/wifi/connect-status').then(function(res){
|
||||
if(token!==_wmWifiConnectPollToken)return;
|
||||
var j={};
|
||||
var msg=$('wm-wifi-msg');
|
||||
try{j=JSON.parse(res.body);}catch(e){}
|
||||
if(msg&&j.message){
|
||||
msg.innerHTML=esc(j.message);
|
||||
}
|
||||
if(j.state==='success'){
|
||||
stopWifiConnectPolling();
|
||||
_wmWifiConnectPending=false;
|
||||
if(msg && j.stationIp){
|
||||
msg.innerHTML="Connected to WiFi. Redirecting to <a href='"+esc(j.redirectUrl||("http://"+j.stationIp+"/"))+"'>"+esc(j.stationIp)+"</a>…";
|
||||
}
|
||||
showToast(j.message||'WiFi connected',false);
|
||||
if(j.redirectUrl){
|
||||
setTimeout(function(){ window.location.href=j.redirectUrl; }, 1400);
|
||||
}else{
|
||||
location.hash='#/';
|
||||
}
|
||||
}else if(j.state==='failed'){
|
||||
stopWifiConnectPolling();
|
||||
_wmWifiConnectPending=false;
|
||||
showToast(j.message||'WiFi connect failed',true);
|
||||
}else{
|
||||
_wmWifiConnectPollTimer=setTimeout(poll,700);
|
||||
}
|
||||
}).catch(function(){
|
||||
if(token!==_wmWifiConnectPollToken)return;
|
||||
stopWifiConnectPolling();
|
||||
var msg=$('wm-wifi-msg');
|
||||
if(msg){
|
||||
msg.innerHTML=_wmWifiConnectPending
|
||||
? 'Portal became unreachable after saving. If the device joined your WiFi network, reconnect to it there.'
|
||||
: 'Portal became unreachable while checking connection status.';
|
||||
}
|
||||
_wmWifiConnectPending=false;
|
||||
});
|
||||
}
|
||||
poll();
|
||||
}
|
||||
|
||||
window.portalWifiSave=function(ev){
|
||||
ev.preventDefault();
|
||||
var fd=new FormData(document.getElementById('wm-wifi-form'));
|
||||
var msg=$('wm-wifi-msg');
|
||||
if(msg)msg.innerHTML='Submitting...';
|
||||
api('/api/wifi/save',{method:'POST',body:fd}).then(function(res){
|
||||
var j={};
|
||||
try{j=JSON.parse(res.body);}catch(e){}
|
||||
if(msg){
|
||||
msg.innerHTML=(j&&j.message)?esc(j.message):(res.ok?'Queued.':'Error');
|
||||
}
|
||||
if(res.status===202){
|
||||
_wmWifiConnectPending=true;
|
||||
pollWifiConnectStatus();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
// --- Info view ---
|
||||
function viewInfo(){
|
||||
setView(navBar('info')+"<div class='wm-card'><p class='wm-lead'>Loading device info…</p></div>");
|
||||
api('/api/info').then(function(res){
|
||||
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
||||
var html=navBar('info');
|
||||
html+="<div class='wm-page-head'><h1>Device info</h1><p class='wm-page-desc'>Connection status, hardware, and actions.</p></div>";
|
||||
var st=d.status;
|
||||
if(st && typeof st==='object' && !Array.isArray(st)){
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
|
||||
if(st.summary){html+="<p class='wm-callout wm-callout--info wm-info-summary'>"+esc(st.summary)+"</p>";}
|
||||
html+="<dl class='wm-kv'>";
|
||||
html+="<dt>Connected</dt><dd>"+esc(st.connected?'Yes':'No')+"</dd>";
|
||||
if(st.ssid!==undefined && st.ssid!=='')html+="<dt>SSID</dt><dd>"+esc(st.ssid)+"</dd>";
|
||||
if(st.stationIp)html+="<dt>Station IP</dt><dd>"+esc(st.stationIp)+"</dd>";
|
||||
if(st.apIp)html+="<dt>Portal IP</dt><dd>"+esc(st.apIp)+"</dd>";
|
||||
html+="</dl></div>";
|
||||
} else {
|
||||
html+=sectionCardHtml('status','Status','Connection summary and main addresses.',d.status||[]);
|
||||
}
|
||||
html+=sectionCardHtml('device','Device','Runtime, memory, and hardware details.',d.device||[]);
|
||||
html+=sectionCardHtml('diagnostics','WiFi details','Network diagnostics and interface metadata.',rowsWithoutKeys(d.wifi||[],['conx','stassid','staip','apip']));
|
||||
html+=sectionCardHtml('about','About','Library, SDK, and build information.',d.about||[]);
|
||||
if(d.extraSections){
|
||||
var xi,zs,zrows,zi,it;
|
||||
for(xi=0;xi<d.extraSections.length;xi++){
|
||||
zs=d.extraSections[xi];
|
||||
zrows=[];
|
||||
if(zs.items){for(zi=0;zi<zs.items.length;zi++){it=zs.items[zi];zrows.push({label:it.label,value:esc(it.value)});}}
|
||||
html+=sectionCardHtml('info',zs.title||'Details','',zrows);
|
||||
}
|
||||
}
|
||||
var act=d.actions||{};
|
||||
var actionsHtml='', hasActions=false;
|
||||
if(act.showRestart){actionsHtml+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>"+labelWithIcon('restart','Restart device')+"</button>";hasActions=true;}
|
||||
if(act.showExitPortal){actionsHtml+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>"+labelWithIcon('exit','Exit portal')+"</button>";hasActions=true;}
|
||||
if(act.showCloseCaptive){actionsHtml+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>"+labelWithIcon('close','Stop captive portal')+"</button>";hasActions=true;}
|
||||
if(act.showUpdate){actionsHtml+="<a class='wm-btn wm-btn--secondary wm-btn--block' href='#/update'>"+labelWithIcon('update','Firmware update')+"</a>";hasActions=true;}
|
||||
if(act.showErase){actionsHtml+="<button type='button' class='wm-btn wm-btn--danger wm-btn--block' id='wm-erase-btn'>"+labelWithIcon('erase','Erase WiFi config')+"</button>";hasActions=true;}
|
||||
if(hasActions){
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('actions','Actions')+"<p class='wm-card-subtitle'>Maintenance and recovery actions for this device.</p><div class='wm-btn-group'>"+actionsHtml+"</div></div>";
|
||||
}
|
||||
setView(html);
|
||||
});
|
||||
}
|
||||
|
||||
function portalConfirmPost(url, promptMsg){
|
||||
showConfirm(promptMsg||'Continue?',function(){
|
||||
api(url,{method:'POST'}).then(function(res){
|
||||
try{var j=JSON.parse(res.body);}catch(e){j={};}
|
||||
var m=(j&&j.message)?j.message:(res.ok?'OK':'Error');
|
||||
showToast(m,!res.ok);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.portalRestart=function(){portalConfirmPost('/api/device/restart','Restart the device?');};
|
||||
window.portalExit=function(){portalConfirmPost('/api/portal/exit','Exit the configuration portal?');};
|
||||
window.portalCloseCaptive=function(){portalConfirmPost('/api/portal/close','Stop redirecting clients to this portal (captive detection off)?');};
|
||||
|
||||
window.portalErase=function(){
|
||||
showConfirm('Erase WiFi configuration?',function(){
|
||||
api('/api/device/erase',{method:'POST'}).then(function(res){
|
||||
try{var j=JSON.parse(res.body);}catch(e){j={};}
|
||||
showToast((j&&j.message)?j.message:(res.ok?'Done':'Error'),!res.ok);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// --- Params / OTA ---
|
||||
function viewSetup(){
|
||||
setView(navBar('setup')+"<div class='wm-card'><p class='wm-lead'>Loading parameters…</p></div>");
|
||||
api('/api/params').then(function(res){
|
||||
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
||||
var html=navBar('setup');
|
||||
html+="<div class='wm-page-head'><h1>Setup</h1><p class='wm-page-desc'>Custom parameters stored on this device.</p></div>";
|
||||
html+="<div class='wm-card'><form id='wm-param-form'>";
|
||||
html+=renderFieldList(d.params||[]);
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('save','Save parameters')+"</button></div>";
|
||||
html+="</form><div id='wm-param-msg'></div></div>";
|
||||
setView(html);
|
||||
});
|
||||
}
|
||||
|
||||
window.portalParamSave=function(ev){
|
||||
ev.preventDefault();
|
||||
var fd=new FormData(document.getElementById('wm-param-form'));
|
||||
var msg=$('wm-param-msg');
|
||||
if(msg)msg.innerHTML='Saving...';
|
||||
api('/api/params/save',{method:'POST',body:fd}).then(function(res){
|
||||
try{var j=JSON.parse(res.body);}catch(e){j={};}
|
||||
if(msg)msg.innerHTML=(j&&j.message)?esc(j.message):'Saved.';
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
function viewUpdate(){
|
||||
var pages=boot.pages||{};
|
||||
var pupd=pages.update||{};
|
||||
var html=navBar('update');
|
||||
html+="<div class='wm-page-head'><h1>Firmware update</h1><p class='wm-page-desc'>Flash a new firmware build (.bin). The device reboots when done.</p></div>";
|
||||
if(!pupd.visible){
|
||||
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this configuration.</p></div>";
|
||||
setView(html);
|
||||
return;
|
||||
}
|
||||
html+="<div class='wm-card'><form id='wm-ota-form'>";
|
||||
html+="<div class='wm-field'><label for='wm-ota-file'>Firmware file</label>";
|
||||
html+="<input type='file' id='wm-ota-file' name='update' accept='.bin,.bin.gz'/></div>";
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('upload','Upload firmware')+"</button></div>";
|
||||
html+="</form>";
|
||||
html+="<p class='wm-lead' style='margin-top:14px;margin-bottom:8px'>Choose the correct binary for this chip. Upload progress appears below.</p>";
|
||||
html+="<pre id='wm-ota-log'></pre></div>";
|
||||
setView(html);
|
||||
}
|
||||
|
||||
window.portalOtaSubmit=function(ev){
|
||||
ev.preventDefault();
|
||||
var fi=$('wm-ota-file');
|
||||
var logEl=$('wm-ota-log');
|
||||
if(!fi||!fi.files||!fi.files[0]){
|
||||
if(logEl)logEl.textContent='Choose a firmware file first.';
|
||||
return false;
|
||||
}
|
||||
if(logEl)logEl.textContent='Uploading…';
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.upload.onprogress=function(e){
|
||||
if(!logEl||!e.lengthComputable)return;
|
||||
logEl.textContent='Uploading… '+Math.round(100*e.loaded/e.total)+'%';
|
||||
};
|
||||
xhr.onreadystatechange=function(){
|
||||
if(xhr.readyState!==4)return;
|
||||
var t=xhr.responseText||'';
|
||||
if(logEl)logEl.textContent=t;
|
||||
var httpOk=xhr.status>=200&&xhr.status<300;
|
||||
try{
|
||||
var j=JSON.parse(t);
|
||||
if(j&&j.message){
|
||||
if(logEl)logEl.textContent=j.message;
|
||||
showToast(j.message,j.ok===false);
|
||||
return;
|
||||
}
|
||||
}catch(e){}
|
||||
showToast(httpOk?'Upload finished.':('HTTP '+xhr.status),!httpOk);
|
||||
};
|
||||
xhr.open('POST','/u');
|
||||
var fd=new FormData();
|
||||
fd.append('update',fi.files[0]);
|
||||
xhr.send(fd);
|
||||
return false;
|
||||
};
|
||||
|
||||
// --- Routing ---
|
||||
function route(){
|
||||
var h=location.hash||'#/';
|
||||
if(h.indexOf('#/')!==0)h='#/';
|
||||
var pages=boot.pages||{};
|
||||
var ps=pages.setup||{};
|
||||
var pi=pages.info||{};
|
||||
var pu=pages.update||{};
|
||||
if(h==='#/setup'&&ps.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
|
||||
if(h==='#/info'&&pi.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
|
||||
if(h==='#/update'&&pu.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
|
||||
if(h==='#/'||h==='#'){viewHome();}
|
||||
else if(h==='#/wifi'){viewWifi();}
|
||||
else if(h==='#/info'){viewInfo();}
|
||||
else if(h==='#/setup'){viewSetup();}
|
||||
else if(h==='#/update'){viewUpdate();}
|
||||
else {viewHome();h='#/';}
|
||||
try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:h}}));}catch(e){}
|
||||
}
|
||||
|
||||
function bootPortal(){
|
||||
route();
|
||||
try{document.dispatchEvent(new CustomEvent('wm:ready',{detail:{boot:boot}}));}catch(e){}
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange',route);
|
||||
window.addEventListener('load',bootPortal);
|
||||
})();
|
||||
|
||||
)rawliteral";
|
||||
|
||||
#endif // _WM_PORTAL_APP_JS_H_
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* RootShell.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Single-page portal shell: only HTML document served for GET /.
|
||||
* Dynamic UI is driven by embedded JS + JSON APIs under /api/...
|
||||
*
|
||||
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%, %PORTAL_APPEND_JS%) are filled
|
||||
* per request in WiFiManagerHandlers::handleRoot; do not treat placeholders as a customization API.
|
||||
*/
|
||||
|
||||
#ifndef _WM_ROOT_SHELL_TEMPLATE_H_
|
||||
#define _WM_ROOT_SHELL_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char WM_ROOT_SHELL_TEMPLATE[] PROGMEM =
|
||||
"<!DOCTYPE html>"
|
||||
"<html lang='en'>"
|
||||
"<head>"
|
||||
"<meta name='format-detection' content='telephone=no'>"
|
||||
"<meta charset='UTF-8'/>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||
"<title>%PAGE_TITLE%</title>"
|
||||
"%STYLES%"
|
||||
"</head>"
|
||||
"<body class='portal'>"
|
||||
"<div id='wm-toast' class='wm-toast' aria-live='polite' role='status' style='display:none'></div>"
|
||||
"<div id='wm-dialog' class='wm-dialog' style='display:none' aria-hidden='true'>"
|
||||
"<div class='wm-dialog-backdrop' id='wm-dialog-backdrop'></div>"
|
||||
"<div class='wm-dialog-panel'>"
|
||||
"<p class='wm-dialog-msg' id='wm-dialog-msg'></p>"
|
||||
"<div class='wm-dialog-actions'>"
|
||||
"<button type='button' id='wm-dialog-cancel' class='wm-btn wm-btn--secondary'>Cancel</button>"
|
||||
"<button type='button' id='wm-dialog-ok' class='wm-btn wm-btn--primary wm-dialog-ok'>OK</button>"
|
||||
"</div></div></div>"
|
||||
"<div id='app'></div>"
|
||||
"<script id='wm-bootstrap' type='application/json'>%BOOTSTRAP_JSON%</script>"
|
||||
"<script>"
|
||||
"%PORTAL_APP_JS%"
|
||||
"</script>"
|
||||
"<script>"
|
||||
"%PORTAL_APPEND_JS%"
|
||||
"</script>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
|
||||
#endif // _WM_ROOT_SHELL_TEMPLATE_H_
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* WiFiManagerParameter.cpp
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
#include "WiFiManagerParameter.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
WiFiManagerParameter::WiFiManagerParameter()
|
||||
: WiFiManagerParameter("")
|
||||
{
|
||||
}
|
||||
|
||||
WiFiManagerParameter::WiFiManagerParameter(const char *custom) {
|
||||
_id = NULL;
|
||||
_label = NULL;
|
||||
_length = 0;
|
||||
_value = nullptr;
|
||||
_labelPlacement = WFM_LABEL_DEFAULT;
|
||||
_customHTML = custom;
|
||||
}
|
||||
|
||||
WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label) {
|
||||
init(id, label, "", 0, "", WFM_LABEL_DEFAULT);
|
||||
}
|
||||
|
||||
WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length) {
|
||||
init(id, label, defaultValue, length, "", WFM_LABEL_DEFAULT);
|
||||
}
|
||||
|
||||
WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom) {
|
||||
init(id, label, defaultValue, length, custom, WFM_LABEL_DEFAULT);
|
||||
}
|
||||
|
||||
WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement) {
|
||||
init(id, label, defaultValue, length, custom, labelPlacement);
|
||||
}
|
||||
|
||||
void WiFiManagerParameter::init(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement) {
|
||||
_id = id;
|
||||
_label = label;
|
||||
_labelPlacement = labelPlacement;
|
||||
_customHTML = custom;
|
||||
_length = 0;
|
||||
_value = nullptr;
|
||||
setValue(defaultValue,length);
|
||||
}
|
||||
|
||||
WiFiManagerParameter::~WiFiManagerParameter() {
|
||||
if (_value != NULL) {
|
||||
delete[] _value;
|
||||
}
|
||||
_length=0; // setting length 0, ideally the entire parameter should be removed, or added to wifimanager scope so it follows
|
||||
}
|
||||
|
||||
// WiFiManagerParameter& WiFiManagerParameter::operator=(const WiFiManagerParameter& rhs){
|
||||
// Serial.println("copy assignment op called");
|
||||
// (*this->_value) = (*rhs._value);
|
||||
// return *this;
|
||||
// }
|
||||
|
||||
// @note debug is not available in wmparameter class
|
||||
void WiFiManagerParameter::setValue(const char *defaultValue, int length) {
|
||||
if (!_id || length < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_length != length || _value == nullptr) {
|
||||
char* replacement = new (std::nothrow) char[static_cast<size_t>(length) + 1];
|
||||
if (!replacement) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete[] _value;
|
||||
_value = replacement;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
memset(_value, 0, static_cast<size_t>(_length) + 1);
|
||||
|
||||
if (defaultValue != nullptr) {
|
||||
strncpy(_value, defaultValue, _length);
|
||||
}
|
||||
}
|
||||
const char* WiFiManagerParameter::getValue() const {
|
||||
// Serial.println(printf("Address of _value is %p\n", (void *)_value));
|
||||
return _value;
|
||||
}
|
||||
const char* WiFiManagerParameter::getID() const {
|
||||
return _id;
|
||||
}
|
||||
const char* WiFiManagerParameter::getPlaceholder() const {
|
||||
return _label;
|
||||
}
|
||||
const char* WiFiManagerParameter::getLabel() const {
|
||||
return _label;
|
||||
}
|
||||
int WiFiManagerParameter::getValueLength() const {
|
||||
return _length;
|
||||
}
|
||||
int WiFiManagerParameter::getLabelPlacement() const {
|
||||
return _labelPlacement;
|
||||
}
|
||||
const char* WiFiManagerParameter::getCustomHTML() const {
|
||||
return _customHTML;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* WiFiManagerServer.cpp
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* HTTP server lifecycle and route registration only. HTML shell rendering is request-scoped in
|
||||
* WiFiManagerHandlers (WM_ROOT_SHELL_TEMPLATE + per-request placeholder registry).
|
||||
* Route inventory matches WiFiManagerServer.h (GET /, /api/..., POST /u, onNotFound).
|
||||
*/
|
||||
|
||||
#include "WiFiManagerServer.h"
|
||||
#include "WiFiManager.h"
|
||||
#include "WiFiManagerHandlers.h"
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
#include "WiFiManagerDfteLogger.h"
|
||||
#include <DeviceFrameworkTemplateEngineDebug.h>
|
||||
#endif
|
||||
|
||||
#if defined(ESP8266) || defined(ESP32)
|
||||
|
||||
WiFiManagerServer::WiFiManagerServer(WiFiManager* wm)
|
||||
: _wm(wm), _handlers(std::make_unique<WiFiManagerHandlers>(wm)) {}
|
||||
|
||||
WiFiManagerServer::~WiFiManagerServer() = default;
|
||||
|
||||
void WiFiManagerServer::createServer(uint16_t port) {
|
||||
if (server) {
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("Server already exists, shutting down first"));
|
||||
#endif
|
||||
shutdownServer();
|
||||
}
|
||||
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("Starting Web Portal"));
|
||||
#endif
|
||||
|
||||
if (port != 80) {
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("http server started with custom port: "), port);
|
||||
#endif
|
||||
}
|
||||
|
||||
server.reset(new AsyncWebServer(port));
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
// DFTE logging is still owned by server lifecycle; it is intentionally independent of
|
||||
// request-scoped shell rendering / placeholder registration.
|
||||
if (!deviceFrameworkTemplateEngineIsLoggingEnabled()) {
|
||||
if (!_dfteLogger) {
|
||||
_dfteLogger = std::make_unique<WiFiManagerDfteLogger>(_wm);
|
||||
}
|
||||
deviceFrameworkTemplateEngineEnableLogging(_dfteLogger.get(), static_cast<const void*>(this));
|
||||
_wmOwnsDfteLogSink = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerRoutes() {
|
||||
if (!server) {
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Error, kWiFiMgrLogSubsystem, F("[ERROR] Server not created, call createServer() first"));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (_wm->_webservercallback != NULL) {
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("[CB] _webservercallback calling"));
|
||||
#endif
|
||||
_wm->_webservercallback();
|
||||
}
|
||||
|
||||
// Shell: only HTML document. JSON + actions + OTA below (no legacy /wifi, /info, … routes).
|
||||
|
||||
server->on(WM_G(R_root), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleRoot(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_api_bootstrap), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiBootstrap(request);
|
||||
});
|
||||
server->on(WM_G(R_api_wifi_scan_status), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiWifiScanStatus(request);
|
||||
});
|
||||
server->on(WM_G(R_api_wifi_scan), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiWifiScan(request);
|
||||
});
|
||||
server->on(WM_G(R_api_wifi_meta), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiWifiMeta(request);
|
||||
});
|
||||
server->on(WM_G(R_api_wifi_save), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiWifiSave(request);
|
||||
});
|
||||
server->on(WM_G(R_api_wifi_connect_status), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiWifiConnectStatus(request);
|
||||
});
|
||||
server->on(WM_G(R_api_params), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiParamsGet(request);
|
||||
});
|
||||
server->on(WM_G(R_api_params_save), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiParamsSave(request);
|
||||
});
|
||||
server->on(WM_G(R_api_info), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiInfo(request);
|
||||
});
|
||||
server->on(WM_G(R_api_status), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiStatus(request);
|
||||
});
|
||||
server->on(WM_G(R_api_device_restart), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiDeviceRestart(request);
|
||||
});
|
||||
server->on(WM_G(R_api_device_erase), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiDeviceErase(request);
|
||||
});
|
||||
server->on(WM_G(R_api_portal_close), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiPortalClose(request);
|
||||
});
|
||||
server->on(WM_G(R_api_portal_exit), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiPortalExit(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_updatedone), HTTP_POST,
|
||||
[this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleUpdateDone(request);
|
||||
},
|
||||
[this](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len,
|
||||
bool final) {
|
||||
this->_handlers->handleUpdating(request, filename, index, data, len, final);
|
||||
});
|
||||
|
||||
server->onNotFound([this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleNotFound(request);
|
||||
});
|
||||
|
||||
server->begin();
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("HTTP server started"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiManagerServer::setupDNSD() {
|
||||
dnsServer.reset(new DNSServer());
|
||||
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("dns server started with ip: "), WiFi.softAPIP());
|
||||
#endif
|
||||
const uint8_t DNS_PORT = 53;
|
||||
dnsServer->start(DNS_PORT, F("*"), WiFi.softAPIP());
|
||||
}
|
||||
|
||||
void WiFiManagerServer::processDNS() {
|
||||
if (dnsServer) {
|
||||
dnsServer->processNextRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiManagerServer::shutdownServer() {
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
if (_wmOwnsDfteLogSink) {
|
||||
deviceFrameworkTemplateEngineDisableLoggingForOwner(static_cast<const void*>(this));
|
||||
_dfteLogger.reset();
|
||||
_wmOwnsDfteLogSink = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (server) {
|
||||
server.reset();
|
||||
}
|
||||
|
||||
if (dnsServer) {
|
||||
dnsServer->stop();
|
||||
dnsServer.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* WiFiManagerUtils.cpp
|
||||
* Implementation of pure utility functions for WiFiManager
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
#include "WiFiManagerUtils.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#elif defined(ESP32)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
namespace WiFiManagerUtils {
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// WIFI STATUS ARRAYS
|
||||
|
||||
const char * const WIFI_STA_STATUS[] PROGMEM = {
|
||||
"WL_IDLE_STATUS", // 0 STATION_IDLE
|
||||
"WL_NO_SSID_AVAIL", // 1 STATION_NO_AP_FOUND
|
||||
"WL_SCAN_COMPLETED", // 2
|
||||
"WL_CONNECTED", // 3 STATION_GOT_IP
|
||||
"WL_CONNECT_FAILED", // 4 STATION_CONNECT_FAIL, STATION_WRONG_PASSWORD(NI)
|
||||
"WL_CONNECTION_LOST", // 5
|
||||
"WL_DISCONNECTED", // 6
|
||||
"WL_STATION_WRONG_PASSWORD" // 7 KLUDGE
|
||||
};
|
||||
|
||||
#ifdef ESP32
|
||||
const char * const AUTH_MODE_NAMES[] PROGMEM = {
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA_PSK",
|
||||
"WPA2_PSK",
|
||||
"WPA_WPA2_PSK",
|
||||
"WPA2_ENTERPRISE",
|
||||
"MAX"
|
||||
};
|
||||
#elif defined(ESP8266)
|
||||
const char * const AUTH_MODE_NAMES[] PROGMEM = {
|
||||
"",
|
||||
"",
|
||||
"WPA_PSK", // 2 ENC_TYPE_TKIP
|
||||
"",
|
||||
"WPA2_PSK", // 4 ENC_TYPE_CCMP
|
||||
"WEP", // 5 ENC_TYPE_WEP
|
||||
"",
|
||||
"OPEN", //7 ENC_TYPE_NONE
|
||||
"WPA_WPA2_PSK", // 8 ENC_TYPE_AUTO
|
||||
};
|
||||
#endif
|
||||
|
||||
const char* const WIFI_MODES[] PROGMEM = { "NULL", "STA", "AP", "STA+AP" };
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// STRING CONVERSION UTILITIES
|
||||
|
||||
String getStatusString(uint8_t status) {
|
||||
if(status <= 7) return FPSTR(WIFI_STA_STATUS[status]);
|
||||
return F("Unknown");
|
||||
}
|
||||
|
||||
|
||||
String getModeString(uint8_t mode) {
|
||||
if(mode <= 3) return FPSTR(WIFI_MODES[mode]);
|
||||
return F("Unknown");
|
||||
}
|
||||
|
||||
String getEncryptionString(uint8_t authmode) {
|
||||
return FPSTR(AUTH_MODE_NAMES[authmode]);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// HTML UTILITIES
|
||||
|
||||
String htmlEntities(const String& str, bool whitespace) {
|
||||
String result = str;
|
||||
result.replace("&","&");
|
||||
result.replace("<","<");
|
||||
result.replace(">",">");
|
||||
result.replace("'","'");
|
||||
if(whitespace) result.replace(" "," ");
|
||||
return result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// IP/STRING UTILITIES
|
||||
|
||||
bool isValidIP(const String& str) {
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
int c = str.charAt(i);
|
||||
if (c != '.' && (c < '0' || c > '9')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String ipToString(IPAddress ip) {
|
||||
String res = "";
|
||||
for (int i = 0; i < 3; i++) {
|
||||
res += String((ip >> (8 * i)) & 0xFF) + ".";
|
||||
}
|
||||
res += String(((ip >> 8 * 3)) & 0xFF);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool isValidAPPassword(const String& password) {
|
||||
if (password.length() < 8 || password.length() > 63) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// HELPER CALCULATIONS
|
||||
|
||||
int rssiToQuality(int rssi) {
|
||||
int quality = 0;
|
||||
if (rssi <= -100) {
|
||||
quality = 0;
|
||||
} else if (rssi >= -50) {
|
||||
quality = 100;
|
||||
} else {
|
||||
quality = 2 * (rssi + 100);
|
||||
}
|
||||
return quality;
|
||||
}
|
||||
|
||||
} // namespace WiFiManagerUtils
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "WiFiManager",
|
||||
"version": "3.0.3",
|
||||
"keywords": [
|
||||
"wifi",
|
||||
"wi-fi",
|
||||
"esp",
|
||||
"esp8266",
|
||||
"esp32",
|
||||
"espressif8266",
|
||||
"espressif32",
|
||||
"nodemcu",
|
||||
"wemos",
|
||||
"arduino"
|
||||
],
|
||||
"description": "Maintained ESP8266/ESP32 WiFi configuration portal with asynchronous web support.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "tzapu",
|
||||
"url": "https://github.com/tzapu"
|
||||
},
|
||||
{
|
||||
"name": "tablatronix",
|
||||
"url": "https://github.com/tablatronix",
|
||||
"maintainer": true
|
||||
},
|
||||
{
|
||||
"name": "Alex Hope-O'Connor",
|
||||
"url": "https://github.com/alexhopeoconnor"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alexhopeoconnor/WiFiManager.git"
|
||||
},
|
||||
"frameworks": "arduino",
|
||||
"platforms": [
|
||||
"espressif8266",
|
||||
"espressif32"
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"owner": "ESP32Async",
|
||||
"name": "ESPAsyncWebServer",
|
||||
"version": "3.9.1"
|
||||
},
|
||||
{
|
||||
"owner": "ESP32Async",
|
||||
"name": "ESPAsyncTCP",
|
||||
"version": "2.0.0",
|
||||
"platforms": "espressif8266"
|
||||
},
|
||||
{
|
||||
"owner": "ESP32Async",
|
||||
"name": "AsyncTCP",
|
||||
"version": "3.4.9",
|
||||
"platforms": "espressif32"
|
||||
},
|
||||
{
|
||||
"name": "DeviceFrameworkTemplateEngine",
|
||||
"version": "https://github.com/alexhopeoconnor/DFTE.git#v1.0.2"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"includeDir": "lib/WiFiManager/include",
|
||||
"srcDir": "lib/WiFiManager/src"
|
||||
},
|
||||
"export": {
|
||||
"include": [
|
||||
"lib/WiFiManager/include",
|
||||
"lib/WiFiManager/src",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"library.json"
|
||||
]
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/schema/library.json",
|
||||
"homepage": "https://github.com/alexhopeoconnor/WiFiManager"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
; PlatformIO Project Configuration File for WiFiManager Library
|
||||
; This file is used for building tests
|
||||
|
||||
[env:esp8266]
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
framework = arduino
|
||||
; Postmortem jump relocation fix for large binaries (see ESP8266-LINKER-WORKAROUND.md in arduino-home-assistant)
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
monitor_speed = 115200
|
||||
test_framework = unity
|
||||
test_build_src = no
|
||||
build_flags =
|
||||
-DWM_LOG_LEVEL=5
|
||||
-DUNIT_TEST
|
||||
lib_deps =
|
||||
ESP32Async/ESPAsyncWebServer@3.9.1
|
||||
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.0.2
|
||||
ESP32Async/ESPAsyncTCP@2.0.0
|
||||
|
||||
[env:esp32]
|
||||
platform = espressif32@6.13.0
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
test_framework = unity
|
||||
test_build_src = no
|
||||
build_unflags =
|
||||
-std=gnu++11
|
||||
build_flags =
|
||||
-std=gnu++14
|
||||
-DWM_LOG_LEVEL=5
|
||||
-DUNIT_TEST
|
||||
lib_deps =
|
||||
ESP32Async/ESPAsyncWebServer@3.9.1
|
||||
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.0.2
|
||||
ESP32Async/AsyncTCP@3.4.9
|
||||
|
||||
; Optional: compile tests with DFTE logs bridged into WiFiManager::log (see README)
|
||||
[env:esp8266_dfte_log]
|
||||
extends = env:esp8266
|
||||
build_flags =
|
||||
${env:esp8266.build_flags}
|
||||
-DWM_DFTE_LOGGING
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 --platform esp8266|esp32" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "--platform" ]] || usage
|
||||
platform="${2:-}"
|
||||
[[ "$platform" == "esp8266" || "$platform" == "esp32" ]] || usage
|
||||
[[ $# -eq 2 ]] || usage
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
pio run -d "$root/test/compile-project" -e "$platform"
|
||||
|
||||
echo "WiFiManager consumer compile check passed for $platform"
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 vMAJOR.MINOR.PATCH [--tag]"
|
||||
exit 2
|
||||
}
|
||||
|
||||
tag="${1:-}"
|
||||
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage
|
||||
[[ "${2:-}" == "" || "${2:-}" == "--tag" ]] || usage
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="${tag#v}"
|
||||
manifest_version="$(sed -n 's/.*"version": "\([^"]*\)".*/\1/p' "$root/library.json" | head -n 1)"
|
||||
|
||||
if [[ "$manifest_version" != "$version" ]]; then
|
||||
echo "library.json is $manifest_version; expected $version for $tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$root/library.properties" ]]; then
|
||||
properties_version="$(sed -n 's/^version=//p' "$root/library.properties" | head -n 1)"
|
||||
if [[ "$properties_version" != "$version" ]]; then
|
||||
echo "library.properties is $properties_version; expected $version for $tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
package_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$package_dir"' EXIT
|
||||
pio pkg pack "$root" --output "$package_dir/package.tar.gz" >/dev/null
|
||||
echo "Validated PlatformIO package for $tag"
|
||||
|
||||
if [[ "${2:-}" == "--tag" ]]; then
|
||||
git -C "$root" diff --quiet
|
||||
git -C "$root" diff --cached --quiet
|
||||
git -C "$root" tag -a "$tag" -m "Release $tag"
|
||||
echo "Created $tag. Push the branch and tag; GitHub Actions will publish the release."
|
||||
fi
|
||||
@@ -0,0 +1,33 @@
|
||||
[platformio]
|
||||
default_envs = esp8266
|
||||
|
||||
[common]
|
||||
framework = arduino
|
||||
lib_ldf_mode = chain
|
||||
lib_deps =
|
||||
WiFiManager=symlink://../..
|
||||
ESP32Async/ESPAsyncWebServer@3.9.1
|
||||
|
||||
[env:esp8266]
|
||||
extends = common
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
build_flags =
|
||||
-DWM_LOG_LEVEL=3
|
||||
lib_deps =
|
||||
${common.lib_deps}
|
||||
ESP32Async/ESPAsyncTCP@2.0.0
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = espressif32@6.13.0
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
-std=gnu++14
|
||||
-DWM_LOG_LEVEL=3
|
||||
lib_deps =
|
||||
${common.lib_deps}
|
||||
ESP32Async/AsyncTCP@3.4.9
|
||||
@@ -0,0 +1,10 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
WiFiManager wifiManager;
|
||||
|
||||
void setup() {
|
||||
wifiManager.setConfigPortalTimeout(1);
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
@@ -0,0 +1,180 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include "test_main.h"
|
||||
|
||||
// Test case array
|
||||
TestCase tests[] = {
|
||||
// Basic tests
|
||||
TEST_ENTRY(test_basic_wifimanager_instantiation),
|
||||
TEST_ENTRY(test_reset_settings),
|
||||
// JSON API shape (buildApiInfoJson is large — run with fresh heap, before stress tests)
|
||||
TEST_ENTRY(test_captive_redirect_host_rule),
|
||||
TEST_ENTRY(test_api_wifi_meta_json_shape),
|
||||
TEST_ENTRY(test_api_wifi_meta_password_field_type),
|
||||
TEST_ENTRY(test_api_wifi_connect_status_success_redirect),
|
||||
TEST_ENTRY(test_api_info_json_shape),
|
||||
TEST_ENTRY(test_api_params_json_shape),
|
||||
TEST_ENTRY(test_api_status_json_shape),
|
||||
|
||||
// Configuration tests
|
||||
TEST_ENTRY(test_set_and_get_hostname),
|
||||
TEST_ENTRY(test_get_default_ap_name),
|
||||
TEST_ENTRY(test_get_wifi_status_string),
|
||||
TEST_ENTRY(test_get_mode_string),
|
||||
TEST_ENTRY(test_get_rssi_as_quality),
|
||||
|
||||
// Portal lifecycle tests
|
||||
TEST_ENTRY(test_start_config_portal),
|
||||
TEST_ENTRY(test_start_config_portal_with_password),
|
||||
TEST_ENTRY(test_start_config_portal_auto_name),
|
||||
TEST_ENTRY(test_stop_config_portal),
|
||||
TEST_ENTRY(test_config_portal_infrastructure),
|
||||
TEST_ENTRY(test_start_web_portal),
|
||||
TEST_ENTRY(test_stop_web_portal),
|
||||
TEST_ENTRY(test_config_portal_multiple_start_stop),
|
||||
TEST_ENTRY(test_config_portal_already_active),
|
||||
TEST_ENTRY(test_get_config_portal_ssid),
|
||||
TEST_ENTRY(test_bootstrap_json_portal_feature_flags),
|
||||
TEST_ENTRY(test_bootstrap_json_contract_v2),
|
||||
TEST_ENTRY(test_bootstrap_json_snapshot_consistency),
|
||||
TEST_ENTRY(test_root_render_interleaved_context_isolation),
|
||||
|
||||
// Non-blocking tests
|
||||
TEST_ENTRY(test_nonblocking_process),
|
||||
TEST_ENTRY(test_client_check_setters),
|
||||
TEST_ENTRY(test_process_required_for_timeout),
|
||||
|
||||
// Callback tests
|
||||
TEST_ENTRY(test_ap_callback),
|
||||
TEST_ENTRY(test_web_server_callback),
|
||||
TEST_ENTRY(test_multiple_callbacks),
|
||||
|
||||
// Parameter tests
|
||||
TEST_ENTRY(test_create_parameter),
|
||||
TEST_ENTRY(test_add_parameter),
|
||||
TEST_ENTRY(test_get_parameters),
|
||||
TEST_ENTRY(test_parameter_with_custom_html),
|
||||
TEST_ENTRY(test_parameter_value_length),
|
||||
TEST_ENTRY(test_parameter_label_placement),
|
||||
TEST_ENTRY(test_multiple_parameters),
|
||||
TEST_ENTRY(test_parameter_placeholder),
|
||||
|
||||
// Static IP tests
|
||||
TEST_ENTRY(test_set_ap_static_ip_config),
|
||||
TEST_ENTRY(test_ap_static_ip_application),
|
||||
|
||||
// WiFi connection tests
|
||||
TEST_ENTRY(test_autoconnect_fallback_to_portal),
|
||||
TEST_ENTRY(test_connectwifi_ssid_not_found),
|
||||
TEST_ENTRY(test_connection_state_transitions),
|
||||
TEST_ENTRY(test_autoconnect_with_timeout),
|
||||
|
||||
// Callback firing tests
|
||||
TEST_ENTRY(test_timeout_callback_fires),
|
||||
TEST_ENTRY(test_config_reset_callback_fires),
|
||||
TEST_ENTRY(test_ap_callback_fires_improved),
|
||||
TEST_ENTRY(test_web_server_callback_fires_improved),
|
||||
|
||||
// DNS lifecycle tests
|
||||
TEST_ENTRY(test_dns_server_created),
|
||||
TEST_ENTRY(test_dns_server_cleanup),
|
||||
TEST_ENTRY(test_dns_server_lifecycle_cycles),
|
||||
|
||||
// Error condition tests
|
||||
TEST_ENTRY(test_connection_failure_handling),
|
||||
TEST_ENTRY(test_resource_cleanup_after_error),
|
||||
TEST_ENTRY(test_multiple_rapid_start_stop),
|
||||
|
||||
// Parameter value tests
|
||||
TEST_ENTRY(test_parameter_value_set_directly),
|
||||
TEST_ENTRY(test_parameter_value_length_validation),
|
||||
TEST_ENTRY(test_parameter_value_persistence),
|
||||
TEST_ENTRY(test_parameter_value_update),
|
||||
TEST_ENTRY(test_multiple_parameters_different_values),
|
||||
|
||||
// WiFi scanning tests
|
||||
TEST_ENTRY(test_wifi_scan_initiates),
|
||||
TEST_ENTRY(test_async_scan_behavior),
|
||||
TEST_ENTRY(test_scan_status_checking),
|
||||
TEST_ENTRY(test_scan_completion_wait),
|
||||
TEST_ENTRY(test_scan_cancels_when_connect_pending),
|
||||
TEST_ENTRY(test_scan_cancels_when_lifecycle_blocked),
|
||||
TEST_ENTRY(test_scan_generation_invalidated_on_reset),
|
||||
|
||||
// Template rendering tests
|
||||
TEST_ENTRY(test_shell_template_renders_core_placeholders),
|
||||
TEST_ENTRY(test_shell_template_renders_dynamic_styles_with_percent_values),
|
||||
|
||||
// State transition tests
|
||||
TEST_ENTRY(test_portal_to_connected_transition),
|
||||
TEST_ENTRY(test_concurrent_operations),
|
||||
TEST_ENTRY(test_state_consistency_during_portal),
|
||||
TEST_ENTRY(test_state_transitions_multiple_cycles),
|
||||
|
||||
// Integration tests
|
||||
TEST_ENTRY(test_autoconnect_fallback_flow),
|
||||
TEST_ENTRY(test_portal_lifecycle_with_connection_attempt),
|
||||
TEST_ENTRY(test_parameter_add_and_retrieve),
|
||||
TEST_ENTRY(test_complete_flow_reset_autoconnect_portal),
|
||||
TEST_ENTRY(test_portal_with_parameters_and_infrastructure),
|
||||
|
||||
// Stress tests
|
||||
TEST_ENTRY(test_many_start_stop_cycles),
|
||||
TEST_ENTRY(test_long_running_portal),
|
||||
TEST_ENTRY(test_rapid_portal_start_stop),
|
||||
TEST_ENTRY(test_multiple_parameters_stress),
|
||||
TEST_ENTRY(test_portal_with_timeout_stress),
|
||||
};
|
||||
|
||||
const size_t TEST_COUNT = sizeof(tests) / sizeof(TestCase);
|
||||
|
||||
// Test state variables
|
||||
size_t next_index = 0;
|
||||
bool begun = false;
|
||||
|
||||
// Test setup and teardown
|
||||
void setUp(void) {
|
||||
// No per-test setup needed
|
||||
}
|
||||
|
||||
void tearDown(void) {
|
||||
// No per-test teardown needed
|
||||
}
|
||||
|
||||
// Arduino setup and loop functions (required by framework)
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(2000); // Give time for serial to initialize
|
||||
|
||||
Serial.println("\n[TEST] =============================================");
|
||||
Serial.println("[TEST] === WiFiManager Test Setup ===");
|
||||
Serial.println("[TEST] Starting WiFiManager tests...");
|
||||
Serial.println("[TEST] =============================================");
|
||||
|
||||
UNITY_BEGIN(); // Start Unity test framework
|
||||
begun = true; // Start tests immediately
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Run one test and return immediately
|
||||
if (begun && next_index < TEST_COUNT) {
|
||||
TestCase& t = tests[next_index];
|
||||
Serial.print("\n[TEST] ==== Running test: ");
|
||||
Serial.print(t.name);
|
||||
Serial.println(" ====");
|
||||
UnityDefaultTestRun(t.fn, t.name, t.line);
|
||||
next_index++;
|
||||
return; // yield quickly
|
||||
}
|
||||
|
||||
// All tests completed
|
||||
if (begun && next_index >= TEST_COUNT) {
|
||||
Serial.println("\n[TEST] =============================================");
|
||||
Serial.println("[TEST] === All tests completed ===");
|
||||
Serial.println("[TEST] =============================================");
|
||||
UNITY_END(); // prints Unity summary
|
||||
begun = false; // avoid repeating
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#ifndef TEST_MAIN_H
|
||||
#define TEST_MAIN_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Test function type
|
||||
using TestFn = void(*)();
|
||||
|
||||
// Test case structure
|
||||
struct TestCase {
|
||||
const char* name;
|
||||
TestFn fn;
|
||||
uint16_t line;
|
||||
};
|
||||
|
||||
#define TEST_ENTRY(fn) { #fn, fn, __LINE__ }
|
||||
|
||||
// Basic tests
|
||||
void test_basic_wifimanager_instantiation();
|
||||
void test_reset_settings();
|
||||
|
||||
// Configuration tests
|
||||
void test_set_and_get_hostname();
|
||||
void test_get_default_ap_name();
|
||||
void test_get_wifi_status_string();
|
||||
void test_get_mode_string();
|
||||
void test_get_rssi_as_quality();
|
||||
|
||||
// Portal lifecycle tests
|
||||
void test_start_config_portal();
|
||||
void test_start_config_portal_with_password();
|
||||
void test_start_config_portal_auto_name();
|
||||
void test_stop_config_portal();
|
||||
void test_config_portal_infrastructure();
|
||||
void test_start_web_portal();
|
||||
void test_stop_web_portal();
|
||||
void test_config_portal_multiple_start_stop();
|
||||
void test_config_portal_already_active();
|
||||
void test_get_config_portal_ssid();
|
||||
void test_bootstrap_json_portal_feature_flags();
|
||||
void test_bootstrap_json_contract_v2();
|
||||
void test_bootstrap_json_snapshot_consistency();
|
||||
void test_root_render_interleaved_context_isolation();
|
||||
|
||||
// Non-blocking tests
|
||||
void test_nonblocking_process();
|
||||
void test_client_check_setters();
|
||||
void test_process_required_for_timeout();
|
||||
|
||||
// Callback tests
|
||||
void test_ap_callback();
|
||||
void test_web_server_callback();
|
||||
void test_multiple_callbacks();
|
||||
|
||||
// Parameter tests
|
||||
void test_create_parameter();
|
||||
void test_add_parameter();
|
||||
void test_get_parameters();
|
||||
void test_parameter_with_custom_html();
|
||||
void test_parameter_value_length();
|
||||
void test_parameter_label_placement();
|
||||
void test_multiple_parameters();
|
||||
void test_parameter_placeholder();
|
||||
|
||||
// Static IP tests
|
||||
void test_set_ap_static_ip_config();
|
||||
void test_ap_static_ip_application();
|
||||
|
||||
// Advanced options tests
|
||||
|
||||
// WiFi connection tests
|
||||
void test_autoconnect_fallback_to_portal();
|
||||
void test_connectwifi_ssid_not_found();
|
||||
void test_connection_state_transitions();
|
||||
void test_autoconnect_with_timeout();
|
||||
|
||||
// Callback firing tests
|
||||
void test_timeout_callback_fires();
|
||||
void test_config_reset_callback_fires();
|
||||
void test_ap_callback_fires_improved();
|
||||
void test_web_server_callback_fires_improved();
|
||||
|
||||
// DNS lifecycle tests
|
||||
void test_dns_server_created();
|
||||
void test_dns_server_cleanup();
|
||||
void test_dns_server_lifecycle_cycles();
|
||||
|
||||
// Error condition tests
|
||||
void test_connection_failure_handling();
|
||||
void test_resource_cleanup_after_error();
|
||||
void test_multiple_rapid_start_stop();
|
||||
|
||||
// Parameter value tests
|
||||
void test_parameter_value_set_directly();
|
||||
void test_parameter_value_length_validation();
|
||||
void test_parameter_value_persistence();
|
||||
void test_parameter_value_update();
|
||||
void test_multiple_parameters_different_values();
|
||||
|
||||
// WiFi scanning tests
|
||||
void test_wifi_scan_initiates();
|
||||
void test_async_scan_behavior();
|
||||
void test_scan_status_checking();
|
||||
void test_scan_completion_wait();
|
||||
void test_scan_cancels_when_connect_pending();
|
||||
void test_scan_cancels_when_lifecycle_blocked();
|
||||
void test_scan_generation_invalidated_on_reset();
|
||||
|
||||
// Template rendering tests
|
||||
void test_shell_template_renders_core_placeholders();
|
||||
void test_shell_template_renders_dynamic_styles_with_percent_values();
|
||||
|
||||
// API JSON + captive helper tests
|
||||
void test_captive_redirect_host_rule();
|
||||
void test_api_wifi_meta_json_shape();
|
||||
void test_api_wifi_meta_password_field_type();
|
||||
void test_api_wifi_connect_status_success_redirect();
|
||||
void test_api_info_json_shape();
|
||||
void test_api_params_json_shape();
|
||||
void test_api_status_json_shape();
|
||||
|
||||
// State transition tests
|
||||
void test_portal_to_connected_transition();
|
||||
void test_concurrent_operations();
|
||||
void test_state_consistency_during_portal();
|
||||
void test_state_transitions_multiple_cycles();
|
||||
|
||||
// Integration tests
|
||||
void test_autoconnect_fallback_flow();
|
||||
void test_portal_lifecycle_with_connection_attempt();
|
||||
void test_parameter_add_and_retrieve();
|
||||
void test_complete_flow_reset_autoconnect_portal();
|
||||
void test_portal_with_parameters_and_infrastructure();
|
||||
|
||||
// Stress tests
|
||||
void test_many_start_stop_cycles();
|
||||
void test_long_running_portal();
|
||||
void test_rapid_portal_start_stop();
|
||||
void test_multiple_parameters_stress();
|
||||
void test_portal_with_timeout_stress();
|
||||
|
||||
#endif // TEST_MAIN_H
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
// Test advanced configuration options - consolidated into logical groups
|
||||
|
||||
// Test connection behavior options
|
||||
void test_connection_behavior_options() {
|
||||
Serial.println("[TEST] Testing connection behavior options...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Test connection options
|
||||
wm.setBreakAfterConfig(true);
|
||||
wm.setBreakAfterConfig(false);
|
||||
wm.setSaveConnect(true);
|
||||
wm.setSaveConnect(false);
|
||||
wm.setRestorePersistent(true);
|
||||
wm.setRestorePersistent(false);
|
||||
wm.setConnectRetries(1);
|
||||
wm.setConnectRetries(3);
|
||||
wm.setConnectRetries(5);
|
||||
wm.setSaveConnectTimeout(0);
|
||||
wm.setSaveConnectTimeout(10);
|
||||
wm.setSaveConnectTimeout(30);
|
||||
wm.setEnableConfigPortal(true);
|
||||
wm.setEnableConfigPortal(false);
|
||||
wm.setDisableConfigPortal(true);
|
||||
wm.setDisableConfigPortal(false);
|
||||
wm.portalSetBehaviorConnectOnSave(true);
|
||||
wm.portalSetBehaviorConnectOnSave(false);
|
||||
wm.portalSetBehaviorExitAllowed(true);
|
||||
wm.portalSetBehaviorExitAllowed(false);
|
||||
wm.portalSetBehaviorConnectTimeoutSeconds(15);
|
||||
wm.portalSetBehaviorPortalTimeoutSeconds(20);
|
||||
wm.portalSetBehaviorAutoReconnect(true);
|
||||
wm.portalSetBehaviorAutoReconnect(false);
|
||||
wm.portalSetBehaviorApClientCheck(true);
|
||||
wm.portalSetBehaviorApClientCheck(false);
|
||||
wm.portalSetBehaviorWebClientCheck(true);
|
||||
wm.portalSetBehaviorWebClientCheck(false);
|
||||
wm.preloadWiFi("TestSSID", "TestPassword");
|
||||
|
||||
// All setters executed without crash
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "Connection behavior options executed without crash");
|
||||
|
||||
Serial.println("[TEST] Connection behavior options test completed successfully");
|
||||
}
|
||||
|
||||
// Test UI display options
|
||||
void test_ui_display_options() {
|
||||
Serial.println("[TEST] Testing UI display options...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Test display toggles
|
||||
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Actual);
|
||||
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Masked);
|
||||
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Hidden);
|
||||
wm.setScanDispPerc(true);
|
||||
wm.setScanDispPerc(false);
|
||||
wm.portalSetActionEraseVisible(true);
|
||||
wm.portalSetActionEraseVisible(false);
|
||||
wm.portalSetActionRestartVisible(true);
|
||||
wm.portalSetActionRestartVisible(false);
|
||||
wm.portalSetActionExitVisible(true);
|
||||
wm.portalSetActionExitVisible(false);
|
||||
wm.portalSetActionCloseCaptiveVisible(true);
|
||||
wm.portalSetActionCloseCaptiveVisible(false);
|
||||
wm.portalSetPageUpdateVisible(true);
|
||||
wm.portalSetPageUpdateVisible(false);
|
||||
wm.portalSetPageSetupVisible(true);
|
||||
wm.portalSetPageSetupVisible(false);
|
||||
wm.portalSetLayoutParamsLocation(PortalParamsLocation::WiFiPage);
|
||||
wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
|
||||
wm.portalSetActionBackVisible(true);
|
||||
wm.portalSetActionBackVisible(false);
|
||||
|
||||
// All setters executed without crash
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "UI display options executed without crash");
|
||||
|
||||
Serial.println("[TEST] UI display options test completed successfully");
|
||||
}
|
||||
|
||||
// Test custom HTML/UI customization
|
||||
void test_ui_customization() {
|
||||
Serial.println("[TEST] Testing UI customization...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Minimal customization retained: title
|
||||
wm.portalSetBrandTitle("MyDevice");
|
||||
|
||||
// All setters executed without crash
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "UI customization options executed without crash");
|
||||
|
||||
Serial.println("[TEST] UI customization test completed successfully");
|
||||
}
|
||||
|
||||
void test_debug_soft_ap_config() {
|
||||
Serial.println("[TEST] Testing debugSoftAPConfig()...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
wm.startConfigPortal("TestAP");
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
// Debug output should not crash
|
||||
wm.debugSoftAPConfig();
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] debugSoftAPConfig() test completed successfully");
|
||||
}
|
||||
|
||||
void test_debug_platform_info() {
|
||||
Serial.println("[TEST] Testing debugPlatformInfo()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Debug output should not crash
|
||||
wm.debugPlatformInfo();
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "debugPlatformInfo() executed without crash");
|
||||
|
||||
Serial.println("[TEST] debugPlatformInfo() test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerHandlers.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
void test_captive_redirect_host_rule() {
|
||||
Serial.println("[TEST] Testing captive redirect host comparison...");
|
||||
|
||||
TEST_ASSERT_TRUE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("captive.apple.com", "192.168.4.1"));
|
||||
TEST_ASSERT_TRUE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("192.168.4.2", "192.168.4.1"));
|
||||
TEST_ASSERT_FALSE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("192.168.4.1", "192.168.4.1"));
|
||||
TEST_ASSERT_FALSE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("example.com", ""));
|
||||
|
||||
Serial.println("[TEST] Captive redirect host rule test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_wifi_meta_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/wifi/meta JSON shape...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
String j = handlers.buildApiWifiMetaJson();
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"wifiFields\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"staticFields\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"canRefreshScan\":true")));
|
||||
|
||||
WiFiManagerParameter fieldParam("mqtt_host", "MQTT host", "broker.local", 64, "placeholder='broker.local'");
|
||||
WiFiManagerParameter htmlParam("<div class='wm-callout wm-callout--info'><p>GPS settings...</p></div>");
|
||||
wm.portalAddParameter(&fieldParam);
|
||||
wm.portalAddParameter(&htmlParam);
|
||||
j = handlers.buildApiWifiMetaJson();
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"kind\":\"field\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"kind\":\"html\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"html\":\"<div class='wm-callout wm-callout--info'><p>GPS settings...</p></div>\"}")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"wifiFields\":[{\"id\":\"s\"")));
|
||||
|
||||
Serial.println("[TEST] WiFi meta JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_wifi_meta_password_field_type() {
|
||||
Serial.println("[TEST] Testing /api/wifi/meta password field type from custom attrs...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
WiFiManagerParameter pwd("mqttpass", "MQTT password", "secret", 24, " type='password' autocorrect='off'");
|
||||
wm.portalAddParameter(&pwd);
|
||||
|
||||
String j = handlers.buildApiWifiMetaJson();
|
||||
const char* p = j.c_str();
|
||||
const char* chunk = strstr(p, "\"id\":\"mqttpass\"");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(chunk, "mqttpass param missing from meta JSON");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(chunk, "\"type\":\"password\""),
|
||||
"customAttrs type=password must map to JSON type password");
|
||||
|
||||
Serial.println("[TEST] WiFi meta password field type test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_wifi_connect_status_success_redirect() {
|
||||
Serial.println("[TEST] Testing /api/wifi/connect-status success redirect payload...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
#ifdef UNIT_TEST
|
||||
wm.wmTestSetPortalConnectSuccess("WiFi connected. Redirecting to 192.168.1.42", "192.168.1.42");
|
||||
#endif
|
||||
String j = handlers.buildApiWifiConnectStatusJson();
|
||||
const char* p = j.c_str();
|
||||
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"state\":\"success\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"stationIp\":\"192.168.1.42\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"redirectUrl\":\"http://192.168.1.42/\""));
|
||||
|
||||
Serial.println("[TEST] WiFi connect-status success redirect payload test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_info_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/info JSON shape...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
PortalInfoSection battery;
|
||||
battery.id = "battery";
|
||||
battery.title = "Battery";
|
||||
battery.items.push_back({"soc", "State of charge", "84%"});
|
||||
battery.items.push_back({"voltage", "Voltage", "13.2V"});
|
||||
wm.portalAddInfoSection(battery);
|
||||
String j = handlers.buildApiInfoJson();
|
||||
const char* p = j.c_str();
|
||||
// Shape checks only: "status" is a nested object (},\n"device" not ],"device"); require
|
||||
// a tail marker so we know the full String was built (heap is OK). Middle sections
|
||||
// (wifi / about) are not asserted — long runs fragment heap and can truncate long JSONs.
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
TEST_ASSERT_GREATER_THAN_INT_MESSAGE(200, (int)j.length(),
|
||||
"api/info body unexpectedly short; likely heap/fragmentation");
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"status\":"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"connected\":"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "},\"device\":["));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"extraSections\":"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"id\":\"battery\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"title\":\"Battery\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"label\":\"State of charge\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"showUpdate\":"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"key\":\"aboutver\",\"label\":\"WiFiManager\",\"value\":\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"},{\"key\":\"aboutarduinover\""));
|
||||
|
||||
Serial.println("[TEST] API info JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_params_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/params JSON shape...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
String j = handlers.buildApiParamsGetJson();
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"showBack\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\":[")));
|
||||
|
||||
Serial.println("[TEST] API params JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_status_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/status JSON shape...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
String j = handlers.buildApiStatusJson();
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"text\"")));
|
||||
TEST_ASSERT_EQUAL('{', j.charAt(0));
|
||||
TEST_ASSERT_EQUAL('}', j.charAt(j.length() - 1));
|
||||
|
||||
Serial.println("[TEST] API status JSON shape test completed successfully");
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Test basic WiFiManager instantiation
|
||||
void test_basic_wifimanager_instantiation() {
|
||||
Serial.println("[TEST] Testing basic WiFiManager instantiation...");
|
||||
|
||||
// Test default constructor
|
||||
WiFiManager wm;
|
||||
|
||||
// Test that we can call basic methods without crashing
|
||||
String defaultName = wm.getDefaultAPName();
|
||||
TEST_ASSERT_GREATER_THAN(0, defaultName.length());
|
||||
|
||||
// Test default state - portal should not be active
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
TEST_ASSERT_FALSE(wm.getWebPortalActive());
|
||||
|
||||
Serial.println("[TEST] Basic WiFiManager instantiation test completed successfully");
|
||||
}
|
||||
|
||||
// Test resetSettings clears credentials
|
||||
void test_reset_settings() {
|
||||
Serial.println("[TEST] Testing resetSettings()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Ensure WiFi is in a stable state before resetting
|
||||
delay(100);
|
||||
|
||||
// Reset settings (should not crash)
|
||||
wm.resetSettings();
|
||||
|
||||
// Small delay to allow WiFi operations to complete
|
||||
delay(100);
|
||||
|
||||
// Verify WiFi is not saved after reset
|
||||
// Note: ESP32 uses NVS for WiFi credentials which behaves differently than ESP8266's EEPROM
|
||||
// On ESP32, getWiFiIsSaved() may still return TRUE due to NVS persistence differences
|
||||
#ifdef ESP32
|
||||
// ESP32: Due to NVS behavior, getWiFiIsSaved() may return TRUE even after resetSettings()
|
||||
// This is a known platform difference - the reset is still effective, just detected differently
|
||||
bool wifiIsSaved = wm.getWiFiIsSaved();
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "resetSettings() executed on ESP32 (NVS behavior differs from ESP8266)");
|
||||
Serial.print("[TEST] ESP32: getWiFiIsSaved() = ");
|
||||
Serial.println(wifiIsSaved ? "TRUE" : "FALSE");
|
||||
#else
|
||||
// ESP8266: Should return FALSE after resetSettings()
|
||||
TEST_ASSERT_FALSE(wm.getWiFiIsSaved());
|
||||
#endif
|
||||
|
||||
Serial.println("[TEST] resetSettings() test completed successfully");
|
||||
}
|
||||
|
||||
// Test disconnect - verify it doesn't crash and can be called multiple times
|
||||
void test_disconnect() {
|
||||
Serial.println("[TEST] Testing disconnect()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Disconnect should not crash even if not connected
|
||||
(void)wm.disconnect();
|
||||
|
||||
// Can be called multiple times safely
|
||||
(void)wm.disconnect();
|
||||
|
||||
Serial.println("[TEST] disconnect() test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Callback flags (static to avoid duplicate definitions)
|
||||
static bool timeout_callback_fired = false;
|
||||
static bool config_reset_callback_fired = false;
|
||||
|
||||
// Callback functions (static to avoid duplicate definitions)
|
||||
static void timeout_callback() {
|
||||
timeout_callback_fired = true;
|
||||
}
|
||||
|
||||
static void config_reset_callback() {
|
||||
config_reset_callback_fired = true;
|
||||
}
|
||||
|
||||
// Reset callback flags (static to avoid duplicate definitions)
|
||||
static void reset_callback_flags() {
|
||||
timeout_callback_fired = false;
|
||||
config_reset_callback_fired = false;
|
||||
}
|
||||
|
||||
// Test timeout callback fires
|
||||
void test_timeout_callback_fires() {
|
||||
Serial.println("[TEST] Testing timeout callback firing...");
|
||||
|
||||
reset_callback_flags();
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeoutCallback(timeout_callback);
|
||||
|
||||
wm.setConfigPortalTimeout(2); // 2 second timeout
|
||||
wm.startConfigPortal("TestAP");
|
||||
|
||||
// Process until timeout
|
||||
unsigned long start = millis();
|
||||
while (wm.getConfigPortalActive() && (millis() - start < 5000)) {
|
||||
wm.process();
|
||||
delay(50);
|
||||
}
|
||||
|
||||
// Callback should have fired when timeout occurred
|
||||
TEST_ASSERT_TRUE(timeout_callback_fired);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Timeout callback firing test completed successfully");
|
||||
}
|
||||
|
||||
// Test config reset callback fires
|
||||
void test_config_reset_callback_fires() {
|
||||
Serial.println("[TEST] Testing config reset callback firing...");
|
||||
|
||||
reset_callback_flags();
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigResetCallback(config_reset_callback);
|
||||
|
||||
// Ensure WiFi is in a stable state before resetting
|
||||
delay(100);
|
||||
|
||||
// Call resetSettings which should trigger callback
|
||||
wm.resetSettings();
|
||||
|
||||
// Allow WiFi operations to complete
|
||||
delay(100);
|
||||
|
||||
// Callback should have fired
|
||||
TEST_ASSERT_TRUE(config_reset_callback_fired);
|
||||
|
||||
Serial.println("[TEST] Config reset callback firing test completed successfully");
|
||||
}
|
||||
|
||||
// Test AP callback fires (improve existing test)
|
||||
void test_ap_callback_fires_improved() {
|
||||
Serial.println("[TEST] Testing AP callback firing (improved)...");
|
||||
|
||||
bool ap_callback_fired = false;
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setAPCallback([&](WiFiManager* wm) {
|
||||
ap_callback_fired = true;
|
||||
(void)wm; // Suppress unused parameter warning
|
||||
});
|
||||
|
||||
wm.setConfigPortalTimeout(5);
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Callback should have fired when AP starts
|
||||
TEST_ASSERT_TRUE(ap_callback_fired);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] AP callback firing (improved) test completed successfully");
|
||||
}
|
||||
|
||||
// Test web server callback fires (improve existing test)
|
||||
void test_web_server_callback_fires_improved() {
|
||||
Serial.println("[TEST] Testing web server callback firing (improved)...");
|
||||
|
||||
bool web_server_callback_fired = false;
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setWebServerCallback([&]() {
|
||||
web_server_callback_fired = true;
|
||||
});
|
||||
|
||||
wm.setConfigPortalTimeout(5);
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Wait for server to initialize
|
||||
unsigned long start = millis();
|
||||
while (!web_server_callback_fired && (millis() - start < 1000)) {
|
||||
wm.process();
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// Callback should have fired when server starts
|
||||
TEST_ASSERT_TRUE(web_server_callback_fired);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Web server callback firing (improved) test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
// Callback flags
|
||||
bool ap_callback_fired = false;
|
||||
bool web_server_callback_fired = false;
|
||||
bool save_config_callback_fired = false;
|
||||
bool pre_save_config_callback_fired = false;
|
||||
bool save_params_callback_fired = false;
|
||||
bool pre_save_params_callback_fired = false;
|
||||
bool config_reset_callback_fired = false;
|
||||
bool config_portal_timeout_callback_fired = false;
|
||||
|
||||
// Callback functions
|
||||
void ap_callback(WiFiManager* wm) {
|
||||
ap_callback_fired = true;
|
||||
}
|
||||
|
||||
void web_server_callback() {
|
||||
web_server_callback_fired = true;
|
||||
}
|
||||
|
||||
void save_config_callback() {
|
||||
save_config_callback_fired = true;
|
||||
}
|
||||
|
||||
void pre_save_config_callback() {
|
||||
pre_save_config_callback_fired = true;
|
||||
}
|
||||
|
||||
void save_params_callback(WiFiManager::WiFiManagerRequestArgs requestArgs) {
|
||||
save_params_callback_fired = true;
|
||||
// Test that we can access request args
|
||||
(void)requestArgs; // Suppress unused variable warning
|
||||
}
|
||||
|
||||
void pre_save_params_callback() {
|
||||
pre_save_params_callback_fired = true;
|
||||
}
|
||||
|
||||
void config_reset_callback() {
|
||||
config_reset_callback_fired = true;
|
||||
}
|
||||
|
||||
void config_portal_timeout_callback() {
|
||||
config_portal_timeout_callback_fired = true;
|
||||
}
|
||||
|
||||
// Reset all callback flags
|
||||
void reset_callback_flags() {
|
||||
ap_callback_fired = false;
|
||||
web_server_callback_fired = false;
|
||||
save_config_callback_fired = false;
|
||||
pre_save_config_callback_fired = false;
|
||||
save_params_callback_fired = false;
|
||||
pre_save_params_callback_fired = false;
|
||||
config_reset_callback_fired = false;
|
||||
config_portal_timeout_callback_fired = false;
|
||||
}
|
||||
|
||||
void test_ap_callback() {
|
||||
Serial.println("[TEST] Testing AP callback...");
|
||||
|
||||
reset_callback_flags();
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setAPCallback(ap_callback);
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
wm.startConfigPortal("TestAP");
|
||||
|
||||
// Call process() to ensure portal is fully initialized
|
||||
wm.process();
|
||||
|
||||
// Callback should have fired when AP starts
|
||||
TEST_ASSERT_TRUE(ap_callback_fired);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] AP callback test completed successfully");
|
||||
}
|
||||
|
||||
void test_web_server_callback() {
|
||||
Serial.println("[TEST] Testing web server callback...");
|
||||
|
||||
reset_callback_flags();
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setWebServerCallback(web_server_callback);
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
wm.startConfigPortal("TestAP");
|
||||
|
||||
// Call process() to ensure server is initialized
|
||||
wm.process();
|
||||
delay(100); // Give time for server to start
|
||||
|
||||
// Callback should have fired when server starts
|
||||
TEST_ASSERT_TRUE(web_server_callback_fired);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Web server callback test completed successfully");
|
||||
}
|
||||
|
||||
// Test callback registration - verify all callbacks can be registered without crash
|
||||
void test_callback_registration() {
|
||||
Serial.println("[TEST] Testing callback registration...");
|
||||
|
||||
reset_callback_flags();
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Register all callbacks - verify registration doesn't crash
|
||||
wm.setConfigResetCallback(config_reset_callback);
|
||||
wm.setSaveConfigCallback(save_config_callback);
|
||||
wm.setPreSaveConfigCallback(pre_save_config_callback);
|
||||
wm.setSaveParamsCallback(save_params_callback);
|
||||
wm.setPreSaveParamsCallback(pre_save_params_callback);
|
||||
wm.setConfigPortalTimeoutCallback(config_portal_timeout_callback);
|
||||
|
||||
// Call resetSettings to verify callback registration doesn't interfere
|
||||
wm.resetSettings();
|
||||
|
||||
// Verify registration doesn't crash
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "All callbacks registered without crash");
|
||||
|
||||
Serial.println("[TEST] Callback registration test completed successfully");
|
||||
}
|
||||
|
||||
void test_multiple_callbacks() {
|
||||
Serial.println("[TEST] Testing multiple callbacks together...");
|
||||
|
||||
reset_callback_flags();
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Register all callbacks
|
||||
wm.setAPCallback(ap_callback);
|
||||
wm.setWebServerCallback(web_server_callback);
|
||||
wm.setSaveConfigCallback(save_config_callback);
|
||||
wm.setPreSaveConfigCallback(pre_save_config_callback);
|
||||
wm.setSaveParamsCallback(save_params_callback);
|
||||
wm.setPreSaveParamsCallback(pre_save_params_callback);
|
||||
wm.setConfigResetCallback(config_reset_callback);
|
||||
wm.setConfigPortalTimeoutCallback(config_portal_timeout_callback);
|
||||
|
||||
// Start portal - should fire AP and web server callbacks
|
||||
wm.setConfigPortalTimeout(5);
|
||||
wm.startConfigPortal("TestAP");
|
||||
|
||||
// Call process() to ensure portal is fully initialized
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
// Verify AP callback fired
|
||||
TEST_ASSERT_TRUE(ap_callback_fired);
|
||||
TEST_ASSERT_TRUE(web_server_callback_fired);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Multiple callbacks test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
// Test configuration API methods
|
||||
// Consolidated setter tests - verify setters don't crash and can be called multiple times
|
||||
void test_configuration_setters() {
|
||||
Serial.println("[TEST] Testing configuration setters...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Test timeout setters
|
||||
wm.setConfigPortalTimeout(0);
|
||||
wm.setConfigPortalTimeout(30);
|
||||
wm.setConfigPortalTimeout(300);
|
||||
wm.setConnectTimeout(0);
|
||||
wm.setConnectTimeout(20);
|
||||
wm.setConnectTimeout(60);
|
||||
|
||||
// Test HTTP port
|
||||
wm.setHttpPort(80);
|
||||
wm.setHttpPort(8080);
|
||||
|
||||
// Test WiFi quality and filtering
|
||||
wm.setMinimumSignalQuality(-1); // Disable filter
|
||||
wm.setMinimumSignalQuality(0);
|
||||
wm.setMinimumSignalQuality(50);
|
||||
wm.setMinimumSignalQuality(100);
|
||||
wm.setRemoveDuplicateAPs(true);
|
||||
wm.setRemoveDuplicateAPs(false);
|
||||
|
||||
// Test display options
|
||||
wm.setShowStaticFields(true);
|
||||
wm.setShowStaticFields(false);
|
||||
wm.setShowDnsFields(true);
|
||||
wm.setShowDnsFields(false);
|
||||
|
||||
// Test portal behavior
|
||||
wm.portalSetBehaviorCaptivePortalEnabled(true);
|
||||
wm.portalSetBehaviorCaptivePortalEnabled(false);
|
||||
|
||||
// Test custom title path
|
||||
wm.portalSetBrandTitle("TestTitle");
|
||||
|
||||
// Call setters multiple times with different values.
|
||||
wm.setConfigPortalTimeout(60);
|
||||
wm.setConnectTimeout(30);
|
||||
wm.setHttpPort(8080);
|
||||
wm.setMinimumSignalQuality(50);
|
||||
|
||||
Serial.println("[TEST] Configuration setters test completed successfully");
|
||||
}
|
||||
|
||||
// Test hostname setter/getter - can verify behavior
|
||||
void test_set_and_get_hostname() {
|
||||
Serial.println("[TEST] Testing setHostname() and getWiFiHostname()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(wm.setHostname("test-hostname"), "Valid char* hostname should be accepted");
|
||||
TEST_ASSERT_TRUE_MESSAGE(wm.setHostname(String(" trimmed-hostname ")), "Valid String hostname should be trimmed and accepted");
|
||||
TEST_ASSERT_TRUE_MESSAGE(wm.setHostname(""), "Empty hostname should clear the configured hostname");
|
||||
|
||||
String tooLongHostname = "123456789012345678901234567890123";
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.setHostname(tooLongHostname), "Hostnames longer than 32 chars should be rejected");
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("bad host"), "Hostnames with spaces should be rejected");
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("-leading"), "Hostnames cannot start with a hyphen");
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("trailing-"), "Hostnames cannot end with a hyphen");
|
||||
|
||||
(void)wm.getWiFiHostname();
|
||||
|
||||
Serial.println("[TEST] setHostname() and getWiFiHostname() test completed successfully");
|
||||
}
|
||||
|
||||
// Test WiFi AP configuration setters - consolidated
|
||||
void test_wifi_ap_configuration_setters() {
|
||||
Serial.println("[TEST] Testing WiFi AP configuration setters...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Test AP channel
|
||||
wm.setWiFiAPChannel(0); // Auto
|
||||
wm.setWiFiAPChannel(1);
|
||||
wm.setWiFiAPChannel(11);
|
||||
|
||||
// Test AP hidden
|
||||
wm.setWiFiAPHidden(true);
|
||||
wm.setWiFiAPHidden(false);
|
||||
|
||||
// Test country code
|
||||
wm.setCountry("US");
|
||||
wm.setCountry("CN");
|
||||
|
||||
// Test clean connect
|
||||
wm.setCleanConnect(true);
|
||||
wm.setCleanConnect(false);
|
||||
|
||||
// Test auto reconnect
|
||||
wm.setWiFiAutoReconnect(true);
|
||||
wm.setWiFiAutoReconnect(false);
|
||||
|
||||
Serial.println("[TEST] WiFi AP configuration setters test completed successfully");
|
||||
}
|
||||
|
||||
void test_get_default_ap_name() {
|
||||
Serial.println("[TEST] Testing getDefaultAPName()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
String apName = wm.getDefaultAPName();
|
||||
|
||||
// Should return a non-empty string (chip ID based)
|
||||
TEST_ASSERT_GREATER_THAN(0, apName.length());
|
||||
|
||||
Serial.println("[TEST] getDefaultAPName() test completed successfully");
|
||||
}
|
||||
|
||||
void test_get_wifi_status_string() {
|
||||
Serial.println("[TEST] Testing getWLStatusString()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Test with status code
|
||||
String status1 = wm.getWLStatusString(WL_IDLE_STATUS);
|
||||
String status2 = wm.getWLStatusString(WL_CONNECTED);
|
||||
String status3 = wm.getWLStatusString(WL_DISCONNECTED);
|
||||
|
||||
// All should return non-empty strings
|
||||
TEST_ASSERT_GREATER_THAN(0, status1.length());
|
||||
TEST_ASSERT_GREATER_THAN(0, status2.length());
|
||||
TEST_ASSERT_GREATER_THAN(0, status3.length());
|
||||
|
||||
// Test without parameter (uses current WiFi.status())
|
||||
String status4 = wm.getWLStatusString();
|
||||
TEST_ASSERT_GREATER_THAN(0, status4.length());
|
||||
|
||||
Serial.println("[TEST] getWLStatusString() test completed successfully");
|
||||
}
|
||||
|
||||
void test_get_mode_string() {
|
||||
Serial.println("[TEST] Testing getModeString()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
String mode1 = wm.getModeString(WIFI_OFF);
|
||||
String mode2 = wm.getModeString(WIFI_STA);
|
||||
String mode3 = wm.getModeString(WIFI_AP);
|
||||
String mode4 = wm.getModeString(WIFI_AP_STA);
|
||||
|
||||
// All should return non-empty strings
|
||||
TEST_ASSERT_GREATER_THAN(0, mode1.length());
|
||||
TEST_ASSERT_GREATER_THAN(0, mode2.length());
|
||||
TEST_ASSERT_GREATER_THAN(0, mode3.length());
|
||||
TEST_ASSERT_GREATER_THAN(0, mode4.length());
|
||||
|
||||
Serial.println("[TEST] getModeString() test completed successfully");
|
||||
}
|
||||
|
||||
void test_get_rssi_as_quality() {
|
||||
Serial.println("[TEST] Testing getRSSIasQuality()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Test various RSSI values
|
||||
int quality1 = wm.getRSSIasQuality(-30); // Very strong
|
||||
int quality2 = wm.getRSSIasQuality(-70); // Good
|
||||
int quality3 = wm.getRSSIasQuality(-90); // Weak
|
||||
|
||||
// Quality should be 0-100
|
||||
TEST_ASSERT_GREATER_OR_EQUAL(0, quality1);
|
||||
TEST_ASSERT_LESS_OR_EQUAL(100, quality1);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL(0, quality2);
|
||||
TEST_ASSERT_LESS_OR_EQUAL(100, quality2);
|
||||
|
||||
// Stronger signal should have higher quality
|
||||
TEST_ASSERT_GREATER_THAN(quality3, quality1);
|
||||
|
||||
Serial.println("[TEST] getRSSIasQuality() test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Test DNS server is created when portal starts
|
||||
void test_dns_server_created() {
|
||||
Serial.println("[TEST] Testing DNS server creation...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Wait for DNS server to be created
|
||||
unsigned long start = millis();
|
||||
while (!wm.getDNSServer() && (millis() - start < 1000)) {
|
||||
wm.process();
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// DNS server should be created when portal starts
|
||||
TEST_ASSERT_NOT_NULL(wm.getDNSServer());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] DNS server creation test completed successfully");
|
||||
}
|
||||
|
||||
// Test DNS server cleanup when portal stops
|
||||
void test_dns_server_cleanup() {
|
||||
Serial.println("[TEST] Testing DNS server cleanup...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Wait for DNS server to be created
|
||||
unsigned long start = millis();
|
||||
while (!wm.getDNSServer() && (millis() - start < 1000)) {
|
||||
wm.process();
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// Verify DNS server exists
|
||||
TEST_ASSERT_NOT_NULL(wm.getDNSServer());
|
||||
|
||||
// Stop portal
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
|
||||
// DNS server should be cleaned up (set to nullptr or deleted)
|
||||
// Note: Actual cleanup verification depends on implementation
|
||||
// For now, verify portal is stopped
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] DNS server cleanup test completed successfully");
|
||||
}
|
||||
|
||||
// Test DNS server lifecycle with multiple start/stop cycles
|
||||
void test_dns_server_lifecycle_cycles() {
|
||||
Serial.println("[TEST] Testing DNS server lifecycle with multiple cycles...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Cycle 1
|
||||
wm.startConfigPortal("TestAP1");
|
||||
wm.process();
|
||||
|
||||
unsigned long start = millis();
|
||||
while (!wm.getDNSServer() && (millis() - start < 1000)) {
|
||||
wm.process();
|
||||
delay(10);
|
||||
}
|
||||
TEST_ASSERT_NOT_NULL(wm.getDNSServer());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
delay(200);
|
||||
|
||||
// Cycle 2
|
||||
wm.startConfigPortal("TestAP2");
|
||||
wm.process();
|
||||
|
||||
start = millis();
|
||||
while (!wm.getDNSServer() && (millis() - start < 1000)) {
|
||||
wm.process();
|
||||
delay(10);
|
||||
}
|
||||
TEST_ASSERT_NOT_NULL(wm.getDNSServer());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
|
||||
Serial.println("[TEST] DNS server lifecycle cycles test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Test invalid AP password (too short)
|
||||
void test_invalid_ap_password_too_short() {
|
||||
Serial.println("[TEST] Testing invalid AP password (too short)...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
// Password < 8 chars - should handle gracefully
|
||||
wm.startConfigPortal("TestAP", "short"); // < 8 chars
|
||||
|
||||
// Portal should either not start or use default behavior
|
||||
// Verify it doesn't crash
|
||||
wm.process();
|
||||
|
||||
// Portal may or may not be active (implementation dependent)
|
||||
// Just verify no crash
|
||||
if (wm.getConfigPortalActive()) {
|
||||
wm.stopConfigPortal();
|
||||
}
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "Invalid AP password (too short) handled without crash");
|
||||
|
||||
Serial.println("[TEST] Invalid AP password (too short) test completed successfully");
|
||||
}
|
||||
|
||||
// Test invalid AP password (too long)
|
||||
void test_invalid_ap_password_too_long() {
|
||||
Serial.println("[TEST] Testing invalid AP password (too long)...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
// Password > 63 chars - should handle gracefully
|
||||
String longPassword = String('a', 64); // 64 chars
|
||||
wm.startConfigPortal("TestAP", longPassword.c_str());
|
||||
|
||||
// Portal should handle gracefully
|
||||
wm.process();
|
||||
|
||||
// Portal may or may not be active (implementation dependent)
|
||||
// Just verify no crash
|
||||
if (wm.getConfigPortalActive()) {
|
||||
wm.stopConfigPortal();
|
||||
}
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "Invalid AP password (too long) handled without crash");
|
||||
|
||||
Serial.println("[TEST] Invalid AP password (too long) test completed successfully");
|
||||
}
|
||||
|
||||
// Test empty SSID
|
||||
void test_empty_ssid() {
|
||||
Serial.println("[TEST] Testing empty SSID...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
// Empty SSID - should use default AP name
|
||||
wm.startConfigPortal(""); // Empty SSID
|
||||
|
||||
wm.process();
|
||||
|
||||
// Portal should start with default name or handle gracefully
|
||||
if (wm.getConfigPortalActive()) {
|
||||
String ssid = wm.getConfigPortalSSID();
|
||||
// Should have a valid SSID (either default or empty handled)
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "Empty SSID handled gracefully");
|
||||
wm.stopConfigPortal();
|
||||
} else {
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "Empty SSID handled gracefully (portal not started)");
|
||||
}
|
||||
|
||||
Serial.println("[TEST] Empty SSID test completed successfully");
|
||||
}
|
||||
|
||||
// Test very long SSID
|
||||
void test_very_long_ssid() {
|
||||
Serial.println("[TEST] Testing very long SSID...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
// Very long SSID (> 32 chars) - should handle gracefully
|
||||
String longSSID = String('A', 64); // 64 chars
|
||||
wm.startConfigPortal(longSSID.c_str());
|
||||
|
||||
wm.process();
|
||||
|
||||
// Portal should handle gracefully (may truncate or reject)
|
||||
if (wm.getConfigPortalActive()) {
|
||||
wm.stopConfigPortal();
|
||||
}
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "Very long SSID handled without crash");
|
||||
|
||||
Serial.println("[TEST] Very long SSID test completed successfully");
|
||||
}
|
||||
|
||||
// Test connection failure handling
|
||||
void test_connection_failure_handling() {
|
||||
Serial.println("[TEST] Testing connection failure handling...");
|
||||
|
||||
WiFiManager wm;
|
||||
delay(100); // Ensure WiFi is in a stable state
|
||||
wm.resetSettings(); // Clear saved credentials
|
||||
delay(100); // Allow WiFi operations to complete
|
||||
|
||||
// Attempt connection via autoConnect (will fail)
|
||||
wm.setConfigPortalTimeout(5);
|
||||
bool result = wm.autoConnect("TestAP");
|
||||
|
||||
// Should handle failure gracefully
|
||||
TEST_ASSERT_FALSE(result);
|
||||
|
||||
// Verify WiFi state is consistent
|
||||
wl_status_t status = WiFi.status();
|
||||
TEST_ASSERT_NOT_EQUAL(WL_CONNECTED, status);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Connection failure handling test completed successfully");
|
||||
}
|
||||
|
||||
// Test resource cleanup after error
|
||||
void test_resource_cleanup_after_error() {
|
||||
Serial.println("[TEST] Testing resource cleanup after error...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Stop portal (simulating cleanup after potential error)
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
|
||||
// Verify portal is stopped
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
// Verify we can start again (resources cleaned up)
|
||||
wm.startConfigPortal("TestAP2");
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Resource cleanup after error test completed successfully");
|
||||
}
|
||||
|
||||
// Test multiple rapid start/stop calls
|
||||
void test_multiple_rapid_start_stop() {
|
||||
Serial.println("[TEST] Testing multiple rapid start/stop calls...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
// Rapid start/stop cycles
|
||||
for (int i = 0; i < 5; i++) {
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
delay(10);
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// Final state should be stopped
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] Multiple rapid start/stop calls test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerParameter.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Test autoConnect fallback flow
|
||||
void test_autoconnect_fallback_flow() {
|
||||
Serial.println("[TEST] Testing autoConnect fallback flow...");
|
||||
|
||||
WiFiManager wm;
|
||||
delay(100); // Ensure WiFi is in a stable state
|
||||
wm.resetSettings(); // Clear saved credentials
|
||||
delay(100); // Allow WiFi operations to complete
|
||||
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
// 1. Device starts
|
||||
// 2. autoConnect() called with no saved credentials
|
||||
bool result = wm.autoConnect("TestAP");
|
||||
|
||||
// 3. Should fail to connect (no saved credentials)
|
||||
TEST_ASSERT_FALSE(result);
|
||||
|
||||
// 4. Portal should start automatically as fallback
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] autoConnect fallback flow test completed successfully");
|
||||
}
|
||||
|
||||
// Test portal lifecycle with connection attempt
|
||||
void test_portal_lifecycle_with_connection_attempt() {
|
||||
Serial.println("[TEST] Testing portal lifecycle with connection attempt...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// 1. Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// 2. Attempt connection (while portal active) via preloadWiFi
|
||||
wm.preloadWiFi("NonExistentSSID_12345", "password");
|
||||
|
||||
// 3. Verify state handling - portal should still be active
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Connection should fail (verify WiFi is not connected)
|
||||
wl_status_t status = WiFi.status();
|
||||
TEST_ASSERT_NOT_EQUAL(WL_CONNECTED, status);
|
||||
|
||||
// 4. Stop portal
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] Portal lifecycle with connection attempt test completed successfully");
|
||||
}
|
||||
|
||||
// Test parameter add and retrieve in portal
|
||||
void test_parameter_add_and_retrieve() {
|
||||
Serial.println("[TEST] Testing parameter add and retrieve in portal...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// 1. Add parameters
|
||||
WiFiManagerParameter param("server", "Server", "default", 40);
|
||||
wm.portalAddParameter(¶m);
|
||||
|
||||
TEST_ASSERT_EQUAL(1, wm.getParametersCount());
|
||||
|
||||
// 2. Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// 3. Verify parameters are accessible
|
||||
TEST_ASSERT_EQUAL(1, wm.getParametersCount());
|
||||
|
||||
WiFiManagerParameter** params = wm.getParameters();
|
||||
TEST_ASSERT_NOT_NULL(params);
|
||||
TEST_ASSERT_EQUAL_STRING("server", params[0]->getID());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Parameter add and retrieve in portal test completed successfully");
|
||||
}
|
||||
|
||||
// Test complete flow: reset -> autoConnect -> portal
|
||||
void test_complete_flow_reset_autoconnect_portal() {
|
||||
Serial.println("[TEST] Testing complete flow: reset -> autoConnect -> portal...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// 1. Reset settings
|
||||
delay(100); // Ensure WiFi is in a stable state
|
||||
wm.resetSettings();
|
||||
delay(100); // Allow WiFi operations to complete
|
||||
|
||||
// 2. autoConnect (should fail and start portal)
|
||||
wm.setConfigPortalTimeout(5);
|
||||
bool result = wm.autoConnect("TestAP");
|
||||
|
||||
// 3. Verify portal started
|
||||
TEST_ASSERT_FALSE(result); // Connection failed
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive()); // Portal started
|
||||
|
||||
// 4. Process portal
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
// 5. Verify portal is still active
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// 6. Stop portal
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] Complete flow: reset -> autoConnect -> portal test completed successfully");
|
||||
}
|
||||
|
||||
// Test portal with parameters and infrastructure
|
||||
void test_portal_with_parameters_and_infrastructure() {
|
||||
Serial.println("[TEST] Testing portal with parameters and infrastructure...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Add parameters
|
||||
WiFiManagerParameter p1("server", "Server", "192.168.1.1", 40);
|
||||
WiFiManagerParameter p2("port", "Port", "1883", 6);
|
||||
wm.portalAddParameter(&p1);
|
||||
wm.portalAddParameter(&p2);
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Verify portal is active
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Verify infrastructure
|
||||
TEST_ASSERT_NOT_NULL(wm.getServer());
|
||||
TEST_ASSERT_NOT_NULL(wm.getDNSServer());
|
||||
|
||||
// Verify parameters
|
||||
TEST_ASSERT_EQUAL(2, wm.getParametersCount());
|
||||
|
||||
// Verify WiFi mode
|
||||
WiFiMode_t mode = WiFi.getMode();
|
||||
TEST_ASSERT_TRUE((mode & WIFI_AP) != 0);
|
||||
|
||||
// Verify AP IP
|
||||
IPAddress apIP = WiFi.softAPIP();
|
||||
TEST_ASSERT_NOT_EQUAL(IPAddress(0,0,0,0), apIP);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Portal with parameters and infrastructure test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
// Test process() doesn't block - verify it must be called periodically
|
||||
void test_nonblocking_process() {
|
||||
Serial.println("[TEST] Testing process() calls (non-blocking behavior)...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
wm.startConfigPortal("TestAP");
|
||||
|
||||
// Verify portal is active
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process multiple times - should not block
|
||||
for (int i = 0; i < 10; i++) {
|
||||
unsigned long start = millis();
|
||||
wm.process();
|
||||
unsigned long elapsed = millis() - start;
|
||||
|
||||
// Each call should be fast (< 100ms) - verifies non-blocking behavior
|
||||
TEST_ASSERT_LESS_THAN(100, elapsed);
|
||||
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// Portal should still be active after multiple process() calls
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Non-blocking process() test completed successfully");
|
||||
}
|
||||
|
||||
// Test client check setters - verify they don't interfere with portal operation
|
||||
void test_client_check_setters() {
|
||||
Serial.println("[TEST] Testing client check setters...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Test AP client check
|
||||
wm.setAPClientCheck(true);
|
||||
wm.setAPClientCheck(false);
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal and verify it works with client check settings
|
||||
wm.startConfigPortal("TestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
wm.process();
|
||||
delay(100);
|
||||
wm.stopConfigPortal();
|
||||
|
||||
// Test web portal client check
|
||||
wm.setWebPortalClientCheck(true);
|
||||
wm.setWebPortalClientCheck(false);
|
||||
|
||||
// Start web portal and verify it works
|
||||
wm.setConfigPortalTimeout(5);
|
||||
wm.startWebPortal();
|
||||
TEST_ASSERT_TRUE(wm.getWebPortalActive());
|
||||
wm.process();
|
||||
wm.stopWebPortal();
|
||||
|
||||
Serial.println("[TEST] Client check setters test completed successfully");
|
||||
}
|
||||
|
||||
void test_nonblocking_timeout_behavior() {
|
||||
Serial.println("[TEST] Testing timeout behavior (requires process() calls)...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(2); // 2 second timeout
|
||||
wm.startConfigPortal("TestAP");
|
||||
|
||||
unsigned long start = millis();
|
||||
bool stillActive = true;
|
||||
|
||||
// Process until timeout or max wait
|
||||
while (stillActive && (millis() - start < 5000)) {
|
||||
wm.process();
|
||||
|
||||
// Check if portal is still active (may become inactive after timeout)
|
||||
if (!wm.getConfigPortalActive()) {
|
||||
stillActive = false;
|
||||
break; // Exit loop if portal has closed
|
||||
}
|
||||
|
||||
delay(50);
|
||||
}
|
||||
|
||||
// After timeout, portal should be inactive (may have auto-closed)
|
||||
// Just verify we can check the state without crashing
|
||||
bool portalState = wm.getConfigPortalActive();
|
||||
|
||||
// Only call stopConfigPortal if portal is still active
|
||||
// If it auto-closed on timeout, calling stopConfigPortal may crash
|
||||
if (portalState) {
|
||||
wm.stopConfigPortal();
|
||||
}
|
||||
|
||||
// Test completed without crash
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "Timeout test completed without crash");
|
||||
|
||||
Serial.println("[TEST] Timeout behavior test completed successfully");
|
||||
}
|
||||
|
||||
// Test that process() is required for timeouts to work
|
||||
void test_process_required_for_timeout() {
|
||||
Serial.println("[TEST] Testing that process() is required for timeouts...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(1); // 1 second timeout
|
||||
wm.startConfigPortal("TestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Wait 2 seconds WITHOUT calling process()
|
||||
delay(2000);
|
||||
|
||||
// Portal should still be active because process() wasn't called
|
||||
// (timeout checking only happens in process())
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Now call process() - timeout should be checked
|
||||
unsigned long start = millis();
|
||||
bool stillActive = true;
|
||||
|
||||
// Process until timeout or max wait
|
||||
while (stillActive && (millis() - start < 3000)) {
|
||||
wm.process();
|
||||
|
||||
if (!wm.getConfigPortalActive()) {
|
||||
stillActive = false;
|
||||
break;
|
||||
}
|
||||
|
||||
delay(50);
|
||||
}
|
||||
|
||||
// Portal should have timed out after process() was called
|
||||
// (may have auto-closed, but at least we verified process() is needed)
|
||||
bool portalState = wm.getConfigPortalActive();
|
||||
|
||||
if (portalState) {
|
||||
wm.stopConfigPortal();
|
||||
}
|
||||
|
||||
Serial.println("[TEST] process() required for timeout test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerParameter.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Test parameter value set directly
|
||||
void test_parameter_value_set_directly() {
|
||||
Serial.println("[TEST] Testing parameter value set directly...");
|
||||
|
||||
WiFiManagerParameter param("server", "Server", "default", 40);
|
||||
|
||||
// Set value directly
|
||||
param.setValue("newvalue", 40);
|
||||
|
||||
// Verify value was set
|
||||
TEST_ASSERT_EQUAL_STRING("newvalue", param.getValue());
|
||||
|
||||
Serial.println("[TEST] Parameter value set directly test completed successfully");
|
||||
}
|
||||
|
||||
// Test parameter value length validation
|
||||
void test_parameter_value_length_validation() {
|
||||
Serial.println("[TEST] Testing parameter value length validation...");
|
||||
|
||||
WiFiManagerParameter param("server", "Server", "", 10);
|
||||
|
||||
// Set value within length
|
||||
param.setValue("short", 10);
|
||||
TEST_ASSERT_EQUAL_STRING("short", param.getValue());
|
||||
|
||||
// Set value at length limit
|
||||
param.setValue("1234567890", 10); // Exactly 10 chars
|
||||
String value = param.getValue();
|
||||
TEST_ASSERT_LESS_OR_EQUAL(10, value.length());
|
||||
|
||||
Serial.println("[TEST] Parameter value length validation test completed successfully");
|
||||
}
|
||||
|
||||
// Test parameter value persistence
|
||||
void test_parameter_value_persistence() {
|
||||
Serial.println("[TEST] Testing parameter value persistence...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
WiFiManagerParameter param("server", "Server", "default", 40);
|
||||
wm.portalAddParameter(¶m);
|
||||
|
||||
// Set value
|
||||
param.setValue("persisted_value", 40);
|
||||
TEST_ASSERT_EQUAL_STRING("persisted_value", param.getValue());
|
||||
|
||||
// Verify parameter is still accessible through WiFiManager
|
||||
TEST_ASSERT_EQUAL(1, wm.getParametersCount());
|
||||
|
||||
WiFiManagerParameter** params = wm.getParameters();
|
||||
TEST_ASSERT_NOT_NULL(params);
|
||||
TEST_ASSERT_EQUAL_STRING("persisted_value", params[0]->getValue());
|
||||
|
||||
Serial.println("[TEST] Parameter value persistence test completed successfully");
|
||||
}
|
||||
|
||||
// Test parameter value update
|
||||
void test_parameter_value_update() {
|
||||
Serial.println("[TEST] Testing parameter value update...");
|
||||
|
||||
WiFiManagerParameter param("server", "Server", "default", 40);
|
||||
|
||||
// Set initial value
|
||||
param.setValue("initial", 40);
|
||||
TEST_ASSERT_EQUAL_STRING("initial", param.getValue());
|
||||
|
||||
// Update value
|
||||
param.setValue("updated", 40);
|
||||
TEST_ASSERT_EQUAL_STRING("updated", param.getValue());
|
||||
|
||||
Serial.println("[TEST] Parameter value update test completed successfully");
|
||||
}
|
||||
|
||||
// Test multiple parameters with different values
|
||||
void test_multiple_parameters_different_values() {
|
||||
Serial.println("[TEST] Testing multiple parameters with different values...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
WiFiManagerParameter p1("server", "Server", "192.168.1.1", 40);
|
||||
WiFiManagerParameter p2("port", "Port", "1883", 6);
|
||||
WiFiManagerParameter p3("key", "Key", "default_key", 32);
|
||||
|
||||
wm.portalAddParameter(&p1);
|
||||
wm.portalAddParameter(&p2);
|
||||
wm.portalAddParameter(&p3);
|
||||
|
||||
// Set different values
|
||||
p1.setValue("10.0.0.1", 40);
|
||||
p2.setValue("8883", 6);
|
||||
p3.setValue("new_key", 32);
|
||||
|
||||
// Verify all values are correct
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.0.1", p1.getValue());
|
||||
TEST_ASSERT_EQUAL_STRING("8883", p2.getValue());
|
||||
TEST_ASSERT_EQUAL_STRING("new_key", p3.getValue());
|
||||
|
||||
// Verify through WiFiManager
|
||||
WiFiManagerParameter** params = wm.getParameters();
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.0.1", params[0]->getValue());
|
||||
TEST_ASSERT_EQUAL_STRING("8883", params[1]->getValue());
|
||||
TEST_ASSERT_EQUAL_STRING("new_key", params[2]->getValue());
|
||||
|
||||
Serial.println("[TEST] Multiple parameters with different values test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerParameter.h>
|
||||
|
||||
// Test custom parameters
|
||||
void test_create_parameter() {
|
||||
Serial.println("[TEST] Testing WiFiManagerParameter creation...");
|
||||
|
||||
// Create parameter with basic constructor
|
||||
WiFiManagerParameter param("test_id", "Test Label", "default_value", 40);
|
||||
|
||||
// Verify basic getters work
|
||||
TEST_ASSERT_EQUAL_STRING("test_id", param.getID());
|
||||
TEST_ASSERT_EQUAL_STRING("Test Label", param.getLabel());
|
||||
TEST_ASSERT_EQUAL_STRING("default_value", param.getValue());
|
||||
TEST_ASSERT_EQUAL(40, param.getValueLength());
|
||||
|
||||
Serial.println("[TEST] WiFiManagerParameter creation test completed successfully");
|
||||
}
|
||||
|
||||
void test_add_parameter() {
|
||||
Serial.println("[TEST] Testing portalAddParameter()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
WiFiManagerParameter param1("param1", "Parameter 1", "value1", 40);
|
||||
WiFiManagerParameter param2("param2", "Parameter 2", "value2", 40);
|
||||
|
||||
// Add parameters
|
||||
bool result1 = wm.portalAddParameter(¶m1);
|
||||
bool result2 = wm.portalAddParameter(¶m2);
|
||||
|
||||
TEST_ASSERT_TRUE(result1);
|
||||
TEST_ASSERT_TRUE(result2);
|
||||
|
||||
// Verify parameters are stored
|
||||
TEST_ASSERT_EQUAL(2, wm.getParametersCount());
|
||||
wm.portalClearParameters();
|
||||
TEST_ASSERT_EQUAL(0, wm.getParametersCount());
|
||||
|
||||
Serial.println("[TEST] portalAddParameter() test completed successfully");
|
||||
}
|
||||
|
||||
void test_get_parameters() {
|
||||
Serial.println("[TEST] Testing getParameters() and getParametersCount()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
WiFiManagerParameter param1("param1", "Parameter 1", "value1", 40);
|
||||
WiFiManagerParameter param2("param2", "Parameter 2", "value2", 40);
|
||||
WiFiManagerParameter param3("param3", "Parameter 3", "value3", 40);
|
||||
|
||||
wm.portalAddParameter(¶m1);
|
||||
wm.portalAddParameter(¶m2);
|
||||
wm.portalAddParameter(¶m3);
|
||||
|
||||
// Get count
|
||||
int count = wm.getParametersCount();
|
||||
TEST_ASSERT_EQUAL(3, count);
|
||||
|
||||
// Get parameters array
|
||||
WiFiManagerParameter** params = wm.getParameters();
|
||||
TEST_ASSERT_NOT_NULL(params);
|
||||
|
||||
// Verify we can access parameters
|
||||
TEST_ASSERT_EQUAL_STRING("param1", params[0]->getID());
|
||||
TEST_ASSERT_EQUAL_STRING("param2", params[1]->getID());
|
||||
TEST_ASSERT_EQUAL_STRING("param3", params[2]->getID());
|
||||
|
||||
Serial.println("[TEST] getParameters() test completed successfully");
|
||||
}
|
||||
|
||||
void test_parameter_with_custom_html() {
|
||||
Serial.println("[TEST] Testing WiFiManagerParameter with custom HTML...");
|
||||
|
||||
// Create parameter with custom HTML
|
||||
const char* custom_html = "<input type='text' name='custom' value='test'>";
|
||||
WiFiManagerParameter param(custom_html);
|
||||
|
||||
// Verify custom HTML is accessible
|
||||
const char* html = param.getCustomHTML();
|
||||
TEST_ASSERT_NOT_NULL(html);
|
||||
|
||||
Serial.println("[TEST] Parameter with custom HTML test completed successfully");
|
||||
}
|
||||
|
||||
void test_parameter_value_length() {
|
||||
Serial.println("[TEST] Testing WiFiManagerParameter value length...");
|
||||
|
||||
WiFiManagerParameter param("test_id", "Test Label", "default", 10);
|
||||
|
||||
TEST_ASSERT_EQUAL(10, param.getValueLength());
|
||||
|
||||
// Try to set value within length
|
||||
param.setValue("short", 10);
|
||||
TEST_ASSERT_EQUAL_STRING("short", param.getValue());
|
||||
|
||||
Serial.println("[TEST] Parameter value length test completed successfully");
|
||||
}
|
||||
|
||||
void test_parameter_label_placement() {
|
||||
Serial.println("[TEST] Testing WiFiManagerParameter label placement...");
|
||||
|
||||
WiFiManagerParameter param("test_id", "Test Label", "default", 40, "", WFM_LABEL_BEFORE);
|
||||
|
||||
TEST_ASSERT_EQUAL(WFM_LABEL_BEFORE, param.getLabelPlacement());
|
||||
|
||||
WiFiManagerParameter param2("test_id2", "Test Label 2", "default", 40, "", WFM_LABEL_AFTER);
|
||||
|
||||
TEST_ASSERT_EQUAL(WFM_LABEL_AFTER, param2.getLabelPlacement());
|
||||
|
||||
Serial.println("[TEST] Parameter label placement test completed successfully");
|
||||
}
|
||||
|
||||
void test_multiple_parameters() {
|
||||
Serial.println("[TEST] Testing multiple parameters...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Add several parameters
|
||||
WiFiManagerParameter p1("mqtt_server", "MQTT Server", "192.168.1.1", 40);
|
||||
WiFiManagerParameter p2("mqtt_port", "MQTT Port", "1883", 6);
|
||||
WiFiManagerParameter p3("api_key", "API Key", "default_key", 32);
|
||||
|
||||
wm.portalAddParameter(&p1);
|
||||
wm.portalAddParameter(&p2);
|
||||
wm.portalAddParameter(&p3);
|
||||
|
||||
TEST_ASSERT_EQUAL(3, wm.getParametersCount());
|
||||
|
||||
WiFiManagerParameter** params = wm.getParameters();
|
||||
|
||||
// Verify all parameters are accessible
|
||||
TEST_ASSERT_EQUAL_STRING("mqtt_server", params[0]->getID());
|
||||
TEST_ASSERT_EQUAL_STRING("mqtt_port", params[1]->getID());
|
||||
TEST_ASSERT_EQUAL_STRING("api_key", params[2]->getID());
|
||||
|
||||
Serial.println("[TEST] Multiple parameters test completed successfully");
|
||||
}
|
||||
|
||||
void test_parameter_placeholder() {
|
||||
Serial.println("[TEST] Testing WiFiManagerParameter placeholder (deprecated)...");
|
||||
|
||||
WiFiManagerParameter param("test_id", "Test Label", "default", 40);
|
||||
|
||||
// getPlaceholder() is deprecated but should still work
|
||||
const char* placeholder = param.getPlaceholder();
|
||||
// May return label or empty, just verify no crash
|
||||
TEST_ASSERT_NOT_NULL(placeholder);
|
||||
|
||||
Serial.println("[TEST] Parameter placeholder test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
// Test config portal lifecycle
|
||||
void test_start_config_portal() {
|
||||
Serial.println("[TEST] Testing startConfigPortal()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal with SSID only
|
||||
wm.startConfigPortal("TestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] startConfigPortal() test completed successfully");
|
||||
}
|
||||
|
||||
void test_start_config_portal_with_password() {
|
||||
Serial.println("[TEST] Testing startConfigPortal() with password...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal with SSID and password
|
||||
wm.startConfigPortal("TestAP2", "password123");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] startConfigPortal() with password test completed successfully");
|
||||
}
|
||||
|
||||
void test_start_config_portal_auto_name() {
|
||||
Serial.println("[TEST] Testing startConfigPortal() with auto-generated name...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal without SSID (uses chip ID)
|
||||
wm.startConfigPortal();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(100); // Give it time to initialize
|
||||
|
||||
// Verify we can get the SSID
|
||||
String ssid = wm.getConfigPortalSSID();
|
||||
TEST_ASSERT_GREATER_THAN(0, ssid.length());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] startConfigPortal() auto-name test completed successfully");
|
||||
}
|
||||
|
||||
void test_stop_config_portal() {
|
||||
Serial.println("[TEST] Testing stopConfigPortal()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
// Stop portal
|
||||
wm.stopConfigPortal();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] stopConfigPortal() test completed successfully");
|
||||
}
|
||||
|
||||
void test_config_portal_infrastructure() {
|
||||
Serial.println("[TEST] Testing config portal infrastructure...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
wm.startConfigPortal("TestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(200); // Give time for initialization
|
||||
|
||||
// Verify server object exists (must check while portal is active)
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(wm.getServer(), "Server object should exist when portal is active");
|
||||
|
||||
// Verify DNS server exists
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(wm.getDNSServer(), "DNS server object should exist when portal is active");
|
||||
|
||||
// Verify WiFi is in AP mode
|
||||
WiFiMode_t mode = WiFi.getMode();
|
||||
TEST_ASSERT_TRUE_MESSAGE((mode & WIFI_AP) != 0, "WiFi should be in AP mode");
|
||||
|
||||
// Verify AP has an IP
|
||||
IPAddress apIP = WiFi.softAPIP();
|
||||
TEST_ASSERT_NOT_EQUAL(IPAddress(0,0,0,0), apIP);
|
||||
|
||||
// Verify AP SSID matches
|
||||
String apSSID = WiFi.softAPSSID();
|
||||
TEST_ASSERT_EQUAL_STRING("TestAP", apSSID.c_str());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Config portal infrastructure test completed successfully");
|
||||
}
|
||||
|
||||
void test_start_web_portal() {
|
||||
Serial.println("[TEST] Testing startWebPortal()...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
// Note: Web portal requires STA mode, but for testing we can just verify it starts
|
||||
// In real usage, WiFi would need to be connected first
|
||||
wm.startWebPortal();
|
||||
|
||||
TEST_ASSERT_TRUE(wm.getWebPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
|
||||
// Verify server object exists
|
||||
TEST_ASSERT_NOT_NULL(wm.getServer());
|
||||
|
||||
wm.stopWebPortal();
|
||||
|
||||
Serial.println("[TEST] startWebPortal() test completed successfully");
|
||||
}
|
||||
|
||||
void test_stop_web_portal() {
|
||||
Serial.println("[TEST] Testing stopWebPortal()...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(5);
|
||||
|
||||
wm.startWebPortal();
|
||||
TEST_ASSERT_TRUE(wm.getWebPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
wm.stopWebPortal();
|
||||
TEST_ASSERT_FALSE(wm.getWebPortalActive());
|
||||
|
||||
Serial.println("[TEST] stopWebPortal() test completed successfully");
|
||||
}
|
||||
|
||||
void test_config_portal_multiple_start_stop() {
|
||||
Serial.println("[TEST] Testing multiple config portal start/stop cycles...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// First cycle
|
||||
wm.startConfigPortal("TestAP1");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
wm.process();
|
||||
delay(100);
|
||||
wm.stopConfigPortal();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
delay(200);
|
||||
|
||||
// Second cycle
|
||||
wm.startConfigPortal("TestAP2");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
wm.process();
|
||||
delay(100);
|
||||
wm.stopConfigPortal();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
delay(200);
|
||||
|
||||
// Third cycle
|
||||
wm.startConfigPortal("TestAP3");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
wm.process();
|
||||
delay(100);
|
||||
wm.stopConfigPortal();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] Multiple config portal cycles test completed successfully");
|
||||
}
|
||||
|
||||
void test_config_portal_already_active() {
|
||||
Serial.println("[TEST] Testing startConfigPortal() when already active...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
// Try to start again (should silently fail when already active)
|
||||
wm.startConfigPortal("TestAP2");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive()); // Still active with original AP name
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Config portal already active test completed successfully");
|
||||
}
|
||||
|
||||
void test_get_config_portal_ssid() {
|
||||
Serial.println("[TEST] Testing getConfigPortalSSID()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start with known SSID
|
||||
wm.startConfigPortal("MyTestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
// Verify SSID matches what we set
|
||||
String ssid = wm.getConfigPortalSSID();
|
||||
TEST_ASSERT_EQUAL_STRING("MyTestAP", ssid.c_str());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] getConfigPortalSSID() test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerHandlers.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include "templates/RootShell.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char kEmptyTemplateChunk[] PROGMEM = "";
|
||||
const char kTestTitle[] PROGMEM = "WiFiManager";
|
||||
|
||||
const char* dynamicStringGetter(void* userData) {
|
||||
const auto* value = static_cast<const String*>(userData);
|
||||
return value ? value->c_str() : "";
|
||||
}
|
||||
|
||||
size_t dynamicStringLengthGetter(const char* data, void* /*userData*/) {
|
||||
return data ? strlen(data) : 0;
|
||||
}
|
||||
|
||||
void configureDescriptor(DynamicDataDescriptor& descriptor, String& value) {
|
||||
descriptor.getter = &dynamicStringGetter;
|
||||
descriptor.getLength = &dynamicStringLengthGetter;
|
||||
descriptor.userData = &value;
|
||||
}
|
||||
|
||||
void renderContextsInterleaved(TemplateContext& first, String& firstOutput,
|
||||
TemplateContext& second, String& secondOutput) {
|
||||
uint8_t firstBuffer[31];
|
||||
uint8_t secondBuffer[31];
|
||||
bool firstDone = false;
|
||||
bool secondDone = false;
|
||||
|
||||
while (!firstDone || !secondDone) {
|
||||
if (!firstDone) {
|
||||
size_t written = TemplateRenderer::renderNextChunk(first, firstBuffer, sizeof(firstBuffer));
|
||||
if (written == 0) {
|
||||
firstDone = true;
|
||||
} else {
|
||||
firstOutput.concat(reinterpret_cast<const char*>(firstBuffer), written);
|
||||
}
|
||||
}
|
||||
|
||||
if (!secondDone) {
|
||||
size_t written = TemplateRenderer::renderNextChunk(second, secondBuffer, sizeof(secondBuffer));
|
||||
if (written == 0) {
|
||||
secondDone = true;
|
||||
} else {
|
||||
secondOutput.concat(reinterpret_cast<const char*>(secondBuffer), written);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void test_bootstrap_json_portal_feature_flags() {
|
||||
Serial.println("[TEST] Testing bootstrap JSON portal feature flags...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
|
||||
String idleBootstrap = handlers.buildPortalBootstrapJson();
|
||||
TEST_ASSERT_EQUAL(-1, idleBootstrap.indexOf(F("\"closeCaptive\":{\"visible\":true")));
|
||||
|
||||
wm.wmTestSetPortalActive(true);
|
||||
wm.portalSetBehaviorCaptivePortalEnabled(true);
|
||||
|
||||
String apBootstrap = handlers.buildPortalBootstrapJson();
|
||||
TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"closeCaptive\":{\"visible\":true")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"exitPortal\":{\"visible\":true")));
|
||||
|
||||
wm.wmTestSetPortalActive(false);
|
||||
|
||||
Serial.println("[TEST] Bootstrap portal feature flags test completed successfully");
|
||||
}
|
||||
|
||||
void test_bootstrap_json_contract_v2() {
|
||||
Serial.println("[TEST] Testing bootstrap JSON v2 contract...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
|
||||
wm.portalSetBrandTitle("Solar Battery Monitor Setup");
|
||||
wm.portalSetContextIdentityText("Solar Battery Monitor");
|
||||
wm.portalSetBrandHomeIntro("Connect this device to WiFi and finish setup.");
|
||||
wm.portalSetBrandLogoSvg("<svg viewBox='0 0 24 24'></svg>");
|
||||
wm.portalSetPageInfoVisible(true);
|
||||
wm.portalSetPageUpdateVisible(false);
|
||||
wm.portalSetPageSetupVisible(true);
|
||||
wm.portalSetActionEraseVisible(false);
|
||||
wm.portalSetActionBackVisible(true);
|
||||
wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
|
||||
wm.setConfigPortalTimeout(90);
|
||||
|
||||
PortalHomeCard card;
|
||||
card.id = "solar";
|
||||
card.title = "Solar summary";
|
||||
card.kind = PortalHomeCardKind::KeyValue;
|
||||
card.items.push_back({"pv", "PV input", "420W"});
|
||||
wm.portalAddHomeCard(card);
|
||||
|
||||
String j = handlers.buildPortalBootstrapJson();
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"contractVersion\":2")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"brand\":{")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"title\":\"Solar Battery Monitor Setup\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"homeIntro\":\"Connect this device to WiFi and finish setup.\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"logoSvg\":\"<svg viewBox='0 0 24 24'></svg>\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"context\":{")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"identityText\":\"Solar Battery Monitor\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"pages\":{")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"setup\":{\"visible\":true}")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\":{")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"erase\":{\"visible\":false}")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"back\":{\"visible\":true}")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"layout\":{\"paramsLocation\":\"setup\"}")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"extraHomeCards\":[")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"id\":\"solar\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"kind\":\"kv\"")));
|
||||
|
||||
wm.startWebPortal();
|
||||
j = handlers.buildPortalBootstrapJson();
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"portalActive\":true")));
|
||||
wm.stopWebPortal();
|
||||
|
||||
wm.wmTestSetPortalActive(true);
|
||||
j = handlers.buildPortalBootstrapJson();
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"portalTimeoutSecondsRemaining\":")));
|
||||
TEST_ASSERT_EQUAL(-1, j.indexOf(F("\"portalTimeoutSecondsRemaining\":0")));
|
||||
wm.wmTestSetPortalActive(false);
|
||||
|
||||
Serial.println("[TEST] Bootstrap JSON v2 contract test completed successfully");
|
||||
}
|
||||
|
||||
void test_bootstrap_json_snapshot_consistency() {
|
||||
Serial.println("[TEST] Testing bootstrap JSON snapshot consistency...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
|
||||
wm.setConfigPortalTimeout(5);
|
||||
wm.startConfigPortal("RootSnapshotAP");
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
String snap1 = handlers.buildPortalBootstrapJson();
|
||||
String snap2 = handlers.buildPortalBootstrapJson();
|
||||
|
||||
TEST_ASSERT_GREATER_THAN(32, snap1.length());
|
||||
TEST_ASSERT_EQUAL_STRING(snap1.c_str(), snap2.c_str());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Bootstrap JSON snapshot consistency test completed successfully");
|
||||
}
|
||||
|
||||
void test_root_render_interleaved_context_isolation() {
|
||||
Serial.println("[TEST] Testing root render interleaved context isolation...");
|
||||
|
||||
PlaceholderRegistry registryA(8);
|
||||
PlaceholderRegistry registryB(8);
|
||||
|
||||
registryA.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
|
||||
registryA.registerProgmemData("%PAGE_TITLE%", kTestTitle);
|
||||
registryA.registerProgmemData("%PORTAL_APPEND_JS%", kEmptyTemplateChunk);
|
||||
|
||||
registryB.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
|
||||
registryB.registerProgmemData("%PAGE_TITLE%", kTestTitle);
|
||||
registryB.registerProgmemData("%PORTAL_APPEND_JS%", kEmptyTemplateChunk);
|
||||
|
||||
String bootstrapA = F("{\"ctx\":\"A\"}");
|
||||
String appJsA = F("// shell A");
|
||||
String bootstrapB = F("{\"ctx\":\"B\"}");
|
||||
String appJsB = F("// shell B");
|
||||
|
||||
DynamicDataDescriptor bootstrapDescriptorA{};
|
||||
DynamicDataDescriptor appJsDescriptorA{};
|
||||
DynamicDataDescriptor bootstrapDescriptorB{};
|
||||
DynamicDataDescriptor appJsDescriptorB{};
|
||||
|
||||
configureDescriptor(bootstrapDescriptorA, bootstrapA);
|
||||
configureDescriptor(appJsDescriptorA, appJsA);
|
||||
configureDescriptor(bootstrapDescriptorB, bootstrapB);
|
||||
configureDescriptor(appJsDescriptorB, appJsB);
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicData("%BOOTSTRAP_JSON%", &bootstrapDescriptorA),
|
||||
"Registry A bootstrap placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicData("%PORTAL_APP_JS%", &appJsDescriptorA),
|
||||
"Registry A app JS placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicData("%BOOTSTRAP_JSON%", &bootstrapDescriptorB),
|
||||
"Registry B bootstrap placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicData("%PORTAL_APP_JS%", &appJsDescriptorB),
|
||||
"Registry B app JS placeholder should register");
|
||||
|
||||
TemplateContext contextA;
|
||||
TemplateContext contextB;
|
||||
contextA.setRegistry(®istryA);
|
||||
contextB.setRegistry(®istryB);
|
||||
|
||||
TemplateRenderer::initializeContext(contextA, WM_ROOT_SHELL_TEMPLATE);
|
||||
TemplateRenderer::initializeContext(contextB, WM_ROOT_SHELL_TEMPLATE);
|
||||
|
||||
String outputA;
|
||||
String outputB;
|
||||
renderContextsInterleaved(contextA, outputA, contextB, outputB);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("{\"ctx\":\"A\"}"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("// shell A"));
|
||||
TEST_ASSERT_EQUAL(-1, outputA.indexOf("{\"ctx\":\"B\"}"));
|
||||
TEST_ASSERT_EQUAL(-1, outputA.indexOf("// shell B"));
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("{\"ctx\":\"B\"}"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("// shell B"));
|
||||
TEST_ASSERT_EQUAL(-1, outputB.indexOf("{\"ctx\":\"A\"}"));
|
||||
TEST_ASSERT_EQUAL(-1, outputB.indexOf("// shell A"));
|
||||
|
||||
Serial.println("[TEST] Root render interleaved context isolation test completed successfully");
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Test portal to connected transition (state consistency)
|
||||
void test_portal_to_connected_transition() {
|
||||
Serial.println("[TEST] Testing portal to connected transition...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Verify portal is active
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
TEST_ASSERT_FALSE(wm.getWebPortalActive());
|
||||
|
||||
// Portal state should be consistent
|
||||
// While portal is active, WiFi should be in AP mode
|
||||
WiFiMode_t mode = WiFi.getMode();
|
||||
TEST_ASSERT_TRUE((mode & WIFI_AP) != 0);
|
||||
|
||||
// Stop portal
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
|
||||
// Verify portal is stopped
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] Portal to connected transition test completed successfully");
|
||||
}
|
||||
|
||||
// Test concurrent operations (portal + connection attempt)
|
||||
void test_concurrent_operations() {
|
||||
Serial.println("[TEST] Testing concurrent operations...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Attempt connection while portal is active (via preloadWiFi)
|
||||
// This should either be ignored or handled gracefully
|
||||
wm.preloadWiFi("NonExistentSSID_12345", "password");
|
||||
|
||||
// Portal should still be active (connection attempt shouldn't stop it)
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Verify WiFi is not connected (SSID doesn't exist)
|
||||
wl_status_t status = WiFi.status();
|
||||
TEST_ASSERT_NOT_EQUAL(WL_CONNECTED, status);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Concurrent operations test completed successfully");
|
||||
}
|
||||
|
||||
// Test state consistency during portal operation
|
||||
void test_state_consistency_during_portal() {
|
||||
Serial.println("[TEST] Testing state consistency during portal operation...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Verify state consistency
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
TEST_ASSERT_NOT_NULL(wm.getServer());
|
||||
TEST_ASSERT_NOT_NULL(wm.getDNSServer());
|
||||
|
||||
// Process multiple times
|
||||
for (int i = 0; i < 10; i++) {
|
||||
wm.process();
|
||||
delay(10);
|
||||
|
||||
// State should remain consistent
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
}
|
||||
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
|
||||
// After stop, portal should be inactive
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] State consistency during portal operation test completed successfully");
|
||||
}
|
||||
|
||||
// Test state transitions with multiple start/stop cycles
|
||||
void test_state_transitions_multiple_cycles() {
|
||||
Serial.println("[TEST] Testing state transitions with multiple cycles...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
for (int cycle = 0; cycle < 3; cycle++) {
|
||||
// Start state
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Active state
|
||||
wm.process();
|
||||
delay(100);
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Stop portal
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
delay(100);
|
||||
}
|
||||
|
||||
Serial.println("[TEST] State transitions with multiple cycles test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
// Test static IP configuration
|
||||
// Test AP static IP configuration - verifies IP is actually applied
|
||||
void test_set_ap_static_ip_config() {
|
||||
Serial.println("[TEST] Testing setAPStaticIPConfig()...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
IPAddress ip(192, 168, 4, 1);
|
||||
IPAddress gw(192, 168, 4, 1);
|
||||
IPAddress sn(255, 255, 255, 0);
|
||||
|
||||
// Set AP static IP
|
||||
wm.setAPStaticIPConfig(ip, gw, sn);
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Start portal and verify IP is actually set
|
||||
wm.startConfigPortal("TestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(200); // Give time for IP to be configured
|
||||
|
||||
// Verify AP IP matches what we configured
|
||||
IPAddress apIP = WiFi.softAPIP();
|
||||
TEST_ASSERT_EQUAL(ip, apIP);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] setAPStaticIPConfig() test completed successfully");
|
||||
}
|
||||
|
||||
// Test STA static IP configuration - consolidated
|
||||
void test_sta_static_ip_configuration() {
|
||||
Serial.println("[TEST] Testing STA static IP configuration...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
IPAddress ip(192, 168, 1, 100);
|
||||
IPAddress gw(192, 168, 1, 1);
|
||||
IPAddress sn(255, 255, 255, 0);
|
||||
IPAddress dns(8, 8, 8, 8);
|
||||
|
||||
// Test setSTAStaticIPConfig without DNS
|
||||
wm.setSTAStaticIPConfig(ip, gw, sn);
|
||||
|
||||
// Test setSTAStaticIPConfig with DNS
|
||||
wm.setSTAStaticIPConfig(ip, gw, sn, dns);
|
||||
|
||||
// Verify no crash
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "STA static IP configuration setters executed without crash");
|
||||
|
||||
Serial.println("[TEST] STA static IP configuration test completed successfully");
|
||||
}
|
||||
|
||||
// Note: test_show_static_fields and test_show_dns_fields removed -
|
||||
// already covered in test_configuration_setters() in test_configuration.cpp
|
||||
|
||||
void test_ap_static_ip_application() {
|
||||
Serial.println("[TEST] Testing AP static IP application...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
IPAddress customIP(10, 0, 1, 1);
|
||||
IPAddress customGW(10, 0, 1, 1);
|
||||
IPAddress customSN(255, 255, 255, 0);
|
||||
|
||||
wm.setAPStaticIPConfig(customIP, customGW, customSN);
|
||||
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
wm.startConfigPortal("TestAP");
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Call process() to simulate real usage
|
||||
wm.process();
|
||||
delay(200); // Give time for AP to configure
|
||||
|
||||
IPAddress apIP = WiFi.softAPIP();
|
||||
|
||||
// AP should have the configured IP
|
||||
TEST_ASSERT_EQUAL(customIP, apIP);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] AP static IP application test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerParameter.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Test many start/stop cycles for memory leaks
|
||||
void test_many_start_stop_cycles() {
|
||||
Serial.println("[TEST] Testing many start/stop cycles for memory leaks...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Test 10 start/stop cycles (enough to verify memory leaks and resource cleanup)
|
||||
for (int i = 0; i < 10; i++) {
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Verify portal started
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
|
||||
// Verify portal stopped
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
delay(10); // Small delay between cycles
|
||||
}
|
||||
|
||||
// Verify no crashes or memory leaks (by checking we can still start/stop)
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Many start/stop cycles test completed successfully");
|
||||
}
|
||||
|
||||
// Test long running portal
|
||||
void test_long_running_portal() {
|
||||
Serial.println("[TEST] Testing long running portal...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(30); // Longer timeout for long running test
|
||||
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Process for extended period (enough to verify stability)
|
||||
for (int i = 0; i < 50; i++) {
|
||||
wm.process();
|
||||
|
||||
// Verify portal remains active
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Verify infrastructure remains
|
||||
TEST_ASSERT_NOT_NULL(wm.getServer());
|
||||
TEST_ASSERT_NOT_NULL(wm.getDNSServer());
|
||||
|
||||
delay(10);
|
||||
}
|
||||
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] Long running portal test completed successfully");
|
||||
}
|
||||
|
||||
// Test rapid portal start/stop
|
||||
void test_rapid_portal_start_stop() {
|
||||
Serial.println("[TEST] Testing rapid portal start/stop...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Rapid start/stop cycles (reduced for faster execution)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
wm.stopConfigPortal();
|
||||
wm.process();
|
||||
}
|
||||
|
||||
// Verify final state
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
// Verify we can still start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Rapid portal start/stop test completed successfully");
|
||||
}
|
||||
|
||||
// Test multiple parameters stress
|
||||
void test_multiple_parameters_stress() {
|
||||
Serial.println("[TEST] Testing multiple parameters stress...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(10);
|
||||
|
||||
// Add many parameters
|
||||
WiFiManagerParameter* params[20];
|
||||
for (int i = 0; i < 20; i++) {
|
||||
char id[20];
|
||||
char label[30];
|
||||
sprintf(id, "param%d", i);
|
||||
sprintf(label, "Parameter %d", i);
|
||||
params[i] = new WiFiManagerParameter(id, label, "default", 40);
|
||||
wm.portalAddParameter(params[i]);
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL(20, wm.getParametersCount());
|
||||
|
||||
// Start portal
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
// Verify all parameters accessible
|
||||
WiFiManagerParameter** paramArray = wm.getParameters();
|
||||
for (int i = 0; i < 20; i++) {
|
||||
TEST_ASSERT_NOT_NULL(paramArray[i]);
|
||||
}
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
// Cleanup - Parameters are managed by WiFiManager, but we allocated with new
|
||||
// Note: WiFiManagerParameter doesn't have virtual destructor, but this is just cleanup
|
||||
for (int i = 0; i < 20; i++) {
|
||||
delete params[i];
|
||||
}
|
||||
|
||||
Serial.println("[TEST] Multiple parameters stress test completed successfully");
|
||||
}
|
||||
|
||||
// Test portal with timeout stress
|
||||
void test_portal_with_timeout_stress() {
|
||||
Serial.println("[TEST] Testing portal with timeout stress...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Multiple cycles with different timeouts
|
||||
for (int timeout = 1; timeout <= 5; timeout++) {
|
||||
wm.setConfigPortalTimeout(timeout);
|
||||
wm.startConfigPortal("TestAP");
|
||||
wm.process();
|
||||
|
||||
// Process until timeout or max wait
|
||||
unsigned long start = millis();
|
||||
unsigned long maxWait = (timeout + 1) * 1000UL;
|
||||
while (wm.getConfigPortalActive() && (millis() - start < maxWait)) {
|
||||
wm.process();
|
||||
delay(50);
|
||||
}
|
||||
|
||||
// Portal may have timed out or still be active
|
||||
if (wm.getConfigPortalActive()) {
|
||||
wm.stopConfigPortal();
|
||||
}
|
||||
wm.process();
|
||||
|
||||
delay(100);
|
||||
}
|
||||
|
||||
// Verify final state
|
||||
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
|
||||
|
||||
Serial.println("[TEST] Portal with timeout stress test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include "templates/RootShell.h"
|
||||
#include "templates/PortalAppJS.h"
|
||||
#include "../test_main.h"
|
||||
|
||||
namespace {
|
||||
|
||||
String renderTemplate(const char* templateData, PlaceholderRegistry& registry) {
|
||||
TemplateContext context;
|
||||
context.setRegistry(®istry);
|
||||
TemplateRenderer::initializeContext(context, templateData);
|
||||
|
||||
uint8_t buffer[128];
|
||||
String output;
|
||||
while (true) {
|
||||
size_t written = TemplateRenderer::renderNextChunk(context, buffer, sizeof(buffer));
|
||||
if (written == 0) {
|
||||
break;
|
||||
}
|
||||
output.concat(reinterpret_cast<const char*>(buffer), written);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
const char kEmpty[] PROGMEM = "";
|
||||
const char kDocTitle[] PROGMEM = "Config ESP";
|
||||
const char kBootstrapJson[] PROGMEM = "{\"title\":\"Test\"}";
|
||||
const char kPortalAppJs[] PROGMEM = "console.log('portal');";
|
||||
const char kPortalAppendJs[] PROGMEM = "";
|
||||
|
||||
const char* dynamicStringGetter(void* userData) {
|
||||
const auto* value = static_cast<const String*>(userData);
|
||||
return value ? value->c_str() : "";
|
||||
}
|
||||
|
||||
size_t dynamicStringLengthGetter(const char* data, void* /*userData*/) {
|
||||
return data ? strlen(data) : 0;
|
||||
}
|
||||
|
||||
void configureDescriptor(DynamicDataDescriptor& descriptor, String& value) {
|
||||
descriptor.getter = &dynamicStringGetter;
|
||||
descriptor.getLength = &dynamicStringLengthGetter;
|
||||
descriptor.userData = &value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void test_shell_template_renders_core_placeholders() {
|
||||
Serial.println("[TEST] Testing portal shell template rendering...");
|
||||
|
||||
PlaceholderRegistry registry(8);
|
||||
registry.registerProgmemData("%STYLES%", kEmpty);
|
||||
registry.registerProgmemData("%PAGE_TITLE%", kDocTitle);
|
||||
registry.registerProgmemData("%BOOTSTRAP_JSON%", kBootstrapJson);
|
||||
registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs);
|
||||
registry.registerProgmemData("%PORTAL_APPEND_JS%", kPortalAppendJs);
|
||||
|
||||
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<title>Config ESP</title>"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<div id='app'></div>"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-toast'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-dialog'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-bootstrap'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("{\"title\":\"Test\"}"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("console.log('portal')"));
|
||||
|
||||
Serial.println("[TEST] Portal shell template rendering test completed successfully");
|
||||
}
|
||||
|
||||
void test_shell_template_renders_dynamic_styles_with_percent_values() {
|
||||
Serial.println("[TEST] Testing portal shell dynamic styles placeholder with percent values...");
|
||||
|
||||
PlaceholderRegistry registry(8);
|
||||
String styles = F("<style>body{width:100%;max-width:92%}</style>");
|
||||
String title = F("Config ESP");
|
||||
String bootstrap = F("{\"title\":\"Test\"}");
|
||||
String appendJs = F("");
|
||||
DynamicDataDescriptor stylesDescriptor{};
|
||||
DynamicDataDescriptor titleDescriptor{};
|
||||
DynamicDataDescriptor bootstrapDescriptor{};
|
||||
DynamicDataDescriptor appendJsDescriptor{};
|
||||
|
||||
configureDescriptor(stylesDescriptor, styles);
|
||||
configureDescriptor(titleDescriptor, title);
|
||||
configureDescriptor(bootstrapDescriptor, bootstrap);
|
||||
configureDescriptor(appendJsDescriptor, appendJs);
|
||||
|
||||
TEST_ASSERT_TRUE(registry.registerDynamicData("%STYLES%", &stylesDescriptor));
|
||||
TEST_ASSERT_TRUE(registry.registerDynamicData("%PAGE_TITLE%", &titleDescriptor));
|
||||
TEST_ASSERT_TRUE(registry.registerDynamicData("%BOOTSTRAP_JSON%", &bootstrapDescriptor));
|
||||
TEST_ASSERT_TRUE(registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs));
|
||||
TEST_ASSERT_TRUE(registry.registerDynamicData("%PORTAL_APPEND_JS%", &appendJsDescriptor));
|
||||
|
||||
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<style>body{width:100%;max-width:92%}</style>"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<title>Config ESP</title>"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("{\"title\":\"Test\"}"));
|
||||
|
||||
Serial.println("[TEST] Dynamic styles placeholder percent rendering test completed successfully");
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
// Test autoConnect fallback to portal when no saved credentials
|
||||
void test_autoconnect_fallback_to_portal() {
|
||||
Serial.println("[TEST] Testing autoConnect fallback to portal...");
|
||||
|
||||
WiFiManager wm;
|
||||
delay(100); // Ensure WiFi is in a stable state
|
||||
wm.resetSettings(); // Clear saved credentials
|
||||
delay(100); // Allow WiFi operations to complete
|
||||
|
||||
wm.setConfigPortalTimeout(5);
|
||||
bool result = wm.autoConnect("TestAP");
|
||||
|
||||
// Should fail to connect (no saved credentials)
|
||||
TEST_ASSERT_FALSE(result);
|
||||
|
||||
// Should start portal as fallback
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] autoConnect fallback to portal test completed successfully");
|
||||
}
|
||||
|
||||
// Test connectWifi with non-existent SSID (using autoConnect instead)
|
||||
void test_connectwifi_ssid_not_found() {
|
||||
Serial.println("[TEST] Testing connection with non-existent SSID...");
|
||||
|
||||
WiFiManager wm;
|
||||
delay(100); // Ensure WiFi is in a stable state
|
||||
wm.resetSettings(); // Clear saved credentials
|
||||
delay(100); // Allow WiFi operations to complete
|
||||
|
||||
// Use autoConnect with non-existent SSID (will fail to connect)
|
||||
wm.setConfigPortalTimeout(5);
|
||||
bool result = wm.autoConnect("TestAP");
|
||||
|
||||
// Should fail to connect (no saved credentials or SSID not found)
|
||||
TEST_ASSERT_FALSE(result);
|
||||
|
||||
// Verify WiFi is not connected
|
||||
wl_status_t status = WiFi.status();
|
||||
TEST_ASSERT_NOT_EQUAL(WL_CONNECTED, status);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Connection with non-existent SSID test completed successfully");
|
||||
}
|
||||
|
||||
// Test connect retry count setting
|
||||
void test_connectwifi_retry_count() {
|
||||
Serial.println("[TEST] Testing connect retry count setting...");
|
||||
|
||||
WiFiManager wm;
|
||||
delay(100); // Ensure WiFi is in a stable state
|
||||
wm.resetSettings(); // Clear saved credentials
|
||||
delay(100); // Allow WiFi operations to complete
|
||||
|
||||
// Set retry count
|
||||
wm.setConnectRetries(3);
|
||||
|
||||
// Verify setting is stored (by attempting connection via autoConnect)
|
||||
// The retry logic should be called internally
|
||||
wm.setConfigPortalTimeout(5);
|
||||
bool result = wm.autoConnect("TestAP");
|
||||
|
||||
// Should fail (not connected)
|
||||
TEST_ASSERT_FALSE(result);
|
||||
|
||||
// Verify no crash with retry setting
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "autoConnect with retry count executed without crash");
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Connect retry count test completed successfully");
|
||||
}
|
||||
|
||||
// Test connect timeout setting
|
||||
void test_connectwifi_timeout_setting() {
|
||||
Serial.println("[TEST] Testing connect timeout setting...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
// Ensure WiFi is in a stable state before resetting
|
||||
delay(100);
|
||||
wm.resetSettings(); // Clear saved credentials
|
||||
|
||||
// Allow WiFi operations to complete
|
||||
delay(100);
|
||||
|
||||
// Set timeout
|
||||
wm.setConnectTimeout(5);
|
||||
|
||||
// Verify timeout setting is used (by attempting connection via autoConnect)
|
||||
wm.setConfigPortalTimeout(5);
|
||||
bool result = wm.autoConnect("TestAP");
|
||||
|
||||
// Should fail (not connected)
|
||||
TEST_ASSERT_FALSE(result);
|
||||
|
||||
// Verify no crash with timeout setting
|
||||
TEST_ASSERT_TRUE_MESSAGE(true, "autoConnect with timeout setting executed without crash");
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Connect timeout setting test completed successfully");
|
||||
}
|
||||
|
||||
// Test connection state transitions
|
||||
void test_connection_state_transitions() {
|
||||
Serial.println("[TEST] Testing connection state transitions...");
|
||||
|
||||
WiFiManager wm;
|
||||
delay(100); // Ensure WiFi is in a stable state
|
||||
wm.resetSettings(); // Clear saved credentials
|
||||
delay(100); // Allow WiFi operations to complete
|
||||
|
||||
// Attempt connection via autoConnect (will fail)
|
||||
wm.setConfigPortalTimeout(5);
|
||||
wm.autoConnect("TestAP");
|
||||
|
||||
// Wait a bit for state to potentially change
|
||||
delay(100);
|
||||
|
||||
// Verify state transition (may be IDLE, DISCONNECTED, or NO_SSID_AVAIL)
|
||||
wl_status_t finalStatus = WiFi.status();
|
||||
|
||||
// Should not be CONNECTED (since no saved credentials)
|
||||
TEST_ASSERT_NOT_EQUAL(WL_CONNECTED, finalStatus);
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Connection state transitions test completed successfully");
|
||||
}
|
||||
|
||||
// Test autoConnect with timeout
|
||||
void test_autoconnect_with_timeout() {
|
||||
Serial.println("[TEST] Testing autoConnect with timeout...");
|
||||
|
||||
WiFiManager wm;
|
||||
delay(100); // Ensure WiFi is in a stable state
|
||||
wm.resetSettings(); // Clear saved credentials
|
||||
delay(100); // Allow WiFi operations to complete
|
||||
|
||||
wm.setConfigPortalTimeout(5);
|
||||
wm.setConnectTimeout(3);
|
||||
|
||||
bool result = wm.autoConnect("TestAP");
|
||||
|
||||
// Should fail to connect (no saved credentials)
|
||||
TEST_ASSERT_FALSE(result);
|
||||
|
||||
// Should start portal as fallback
|
||||
TEST_ASSERT_TRUE(wm.getConfigPortalActive());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] autoConnect with timeout test completed successfully");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
void test_wifi_scan_initiates() {
|
||||
Serial.println("[TEST] Testing async scan request queues...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.requestAsyncScan(true);
|
||||
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_QUEUED, wm.getScanState());
|
||||
TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().schedulePending,
|
||||
"Explicit refresh should mark scan scheduling as pending");
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_SCHEDULE_USER_REFRESH,
|
||||
wm.getScanSnapshot().scheduledReason);
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(),
|
||||
"Queueing a scan should invalidate previous cached results");
|
||||
|
||||
Serial.println("[TEST] Async scan request queues test completed successfully");
|
||||
}
|
||||
|
||||
void test_async_scan_behavior() {
|
||||
Serial.println("[TEST] Testing repeated scan requests coalesce...");
|
||||
|
||||
WiFiManager wm;
|
||||
wm.requestAsyncScan(true);
|
||||
wm.requestAsyncScan(true);
|
||||
wm.requestAsyncScan(true);
|
||||
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_QUEUED, wm.getScanState());
|
||||
TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().schedulePending,
|
||||
"Repeated refresh clicks should still leave one pending schedule");
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_SCHEDULE_USER_REFRESH,
|
||||
wm.getScanSnapshot().scheduledReason);
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(),
|
||||
"Repeated refresh requests should keep a single queued scan lifecycle");
|
||||
|
||||
Serial.println("[TEST] Repeated scan requests coalesce test completed successfully");
|
||||
}
|
||||
|
||||
void test_scan_status_checking() {
|
||||
Serial.println("[TEST] Testing cached scan snapshot state...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
#ifdef UNIT_TEST
|
||||
std::vector<WiFiManager::WiFiScanNetwork> results = {
|
||||
{ "Office", -48, 1 },
|
||||
{ "Guest", -70, 0 }
|
||||
};
|
||||
wm.wmTestInjectScanResults(results);
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(wm.hasValidScanResults(),
|
||||
"Injected scan snapshot should be marked valid");
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_COMPLETE, wm.getScanState());
|
||||
TEST_ASSERT_EQUAL(static_cast<size_t>(2), wm.getScanResults().size());
|
||||
TEST_ASSERT_EQUAL_STRING("Office", wm.getScanResults()[0].ssid.c_str());
|
||||
#else
|
||||
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
|
||||
#endif
|
||||
|
||||
Serial.println("[TEST] Cached scan snapshot state test completed successfully");
|
||||
}
|
||||
|
||||
void test_scan_completion_wait() {
|
||||
Serial.println("[TEST] Testing scan timeout transition...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
#ifdef UNIT_TEST
|
||||
wm.wmTestSetPortalActive(true);
|
||||
wm.wmTestForceScanState(WiFiManager::WM_SCAN_RUNNING);
|
||||
wm.wmTestSetScanStartedAt(millis() - 20000);
|
||||
wm.wmTestSetScanTimeoutMs(1000);
|
||||
wm.process();
|
||||
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_TIMEOUT, wm.getScanState());
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(),
|
||||
"Timed out scans should not leave cached results marked valid");
|
||||
#else
|
||||
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
|
||||
#endif
|
||||
|
||||
Serial.println("[TEST] Scan timeout transition test completed successfully");
|
||||
}
|
||||
|
||||
void test_scan_cancels_when_connect_pending() {
|
||||
Serial.println("[TEST] Testing scan cancellation during connect...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
#ifdef UNIT_TEST
|
||||
wm.wmTestSetPortalActive(true);
|
||||
wm.wmTestForceScanState(WiFiManager::WM_SCAN_RUNNING);
|
||||
wm.wmTestSetScanStartedAt(millis());
|
||||
wm.wmTestSetConnectPending(true);
|
||||
wm.process();
|
||||
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState());
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().schedulePending,
|
||||
"Connect flow should cancel any in-flight async scan");
|
||||
#else
|
||||
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
|
||||
#endif
|
||||
|
||||
Serial.println("[TEST] Scan cancellation during connect test completed successfully");
|
||||
}
|
||||
|
||||
void test_scan_cancels_when_lifecycle_blocked() {
|
||||
Serial.println("[TEST] Testing scan cancellation during lifecycle block...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
#ifdef UNIT_TEST
|
||||
wm.wmTestSetPortalActive(true);
|
||||
wm.requestAsyncScan(true);
|
||||
wm.wmTestSetScanLifecycleBlocked(true);
|
||||
wm.process();
|
||||
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState());
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().schedulePending,
|
||||
"Lifecycle blocking should leave scan engine idle");
|
||||
#else
|
||||
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
|
||||
#endif
|
||||
|
||||
Serial.println("[TEST] Scan cancellation during lifecycle block test completed successfully");
|
||||
}
|
||||
|
||||
void test_scan_generation_invalidated_on_reset() {
|
||||
Serial.println("[TEST] Testing scan generation invalidation on reset...");
|
||||
|
||||
WiFiManager wm;
|
||||
|
||||
#ifdef UNIT_TEST
|
||||
wm.wmTestSetScanGenerations(4, 4, 4);
|
||||
wm.wmTestSetScanCompletionPending(3);
|
||||
wm.wmTestClearScanResults();
|
||||
|
||||
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState());
|
||||
TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().generation > 4,
|
||||
"Resetting scan state should invalidate older completion generations");
|
||||
TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().completionPending,
|
||||
"Resetting scan state should clear pending completion callbacks");
|
||||
#else
|
||||
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
|
||||
#endif
|
||||
|
||||
Serial.println("[TEST] Scan generation invalidation on reset test completed successfully");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user