mirror of
https://github.com/wled/WLED.git
synced 2026-06-24 05:41:28 +00:00
Compare commits
656 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5adaaa44e | |||
| 78c1051cbb | |||
| 121250c0a0 | |||
| 2302863386 | |||
| 7560811480 | |||
| 252007d37f | |||
| 6e76a69417 | |||
| 24e2e3c195 | |||
| f1c58ee75f | |||
| c8fc1c89b6 | |||
| 12f6fbc005 | |||
| c819814904 | |||
| 0ef5ee74d4 | |||
| 51862e3572 | |||
| aaf51927a6 | |||
| 72a43c6431 | |||
| 7c6828d443 | |||
| f8f46736ec | |||
| e4f8534cf0 | |||
| 79ecffe0f3 | |||
| 1fb636d8ea | |||
| e04c855e6c | |||
| 6a627a86b7 | |||
| 746df24011 | |||
| da64f71ce2 | |||
| 8972b6bfe2 | |||
| 8241468fe2 | |||
| e9b740a915 | |||
| 548bb6ffd5 | |||
| a5f28d0fcb | |||
| fb8f8f0b08 | |||
| 53fdf9a89c | |||
| a1316034c1 | |||
| 75df4affa8 | |||
| 820c841376 | |||
| 34d50710b3 | |||
| d0d62d9493 | |||
| 89b2bc2992 | |||
| 472fca7486 | |||
| 42844e4fa8 | |||
| 4a6ff64519 | |||
| f6a43f4dfa | |||
| b7d2c3cd85 | |||
| 20c021d9b8 | |||
| 9c93cb8cf0 | |||
| ad5043f7e0 | |||
| 5ac282f8b9 | |||
| e4351c8979 | |||
| e019d36221 | |||
| 6a16593be5 | |||
| 3bcb0c902e | |||
| 78ecd3805e | |||
| 4c7fa0303b | |||
| 75b679814b | |||
| d8cb20a9e7 | |||
| 710f897a1d | |||
| abb4124ab4 | |||
| 3c7fbabd23 | |||
| bda74bafb8 | |||
| 9cf3a7d184 | |||
| 5cef850c47 | |||
| 74763040ad | |||
| 7a635c88d6 | |||
| dc5a7350d2 | |||
| 81f251c125 | |||
| ff9067f10b | |||
| 144c0f9dd6 | |||
| a721264205 | |||
| f28cbea2c2 | |||
| 3b256eed1e | |||
| 610c80a0ae | |||
| aa6cd883c2 | |||
| 7006285856 | |||
| 7e490fcd28 | |||
| 6f030e540f | |||
| a63a307c6e | |||
| b57d51ef19 | |||
| 34722aa371 | |||
| 19c178d0f9 | |||
| 3244d0f593 | |||
| 64f3aa96dd | |||
| f816b5fa98 | |||
| e4cd730654 | |||
| c7fa496fda | |||
| 40442551ef | |||
| cfa93c9778 | |||
| c35358c005 | |||
| 2ab9659332 | |||
| 3bfdb736e3 | |||
| 53d6c4b059 | |||
| 5beafe0a7d | |||
| 7f44396f7f | |||
| 5e1ae99767 | |||
| 1da5dc7d18 | |||
| 05498f2ae4 | |||
| 9ae1ef506c | |||
| cd1c5ca6bd | |||
| b07cd45642 | |||
| 278a1fb6c1 | |||
| 0e1da4f004 | |||
| 5f5f468983 | |||
| c604b99a4c | |||
| 5f28406f42 | |||
| bc229b8cb6 | |||
| 5790309371 | |||
| 210b4d8f54 | |||
| 19292675d8 | |||
| ac1a4dfbfd | |||
| 7fa15761ae | |||
| 35d267109f | |||
| 3bfbbab807 | |||
| b97b46ae12 | |||
| 26d2cc971a | |||
| 3972b6e8fb | |||
| 3116e88cdc | |||
| 1c2f70598a | |||
| 6f65b46b14 | |||
| 1c8dd8ecd2 | |||
| 55c4288cb6 | |||
| 231fb5a7ca | |||
| 536b40c6f1 | |||
| d3a8da212c | |||
| 522927ea94 | |||
| 4178d05c6a | |||
| 2d1315f9dc | |||
| 822e07d9f5 | |||
| 439d4a0e98 | |||
| 1f102ca832 | |||
| 43e86d0f4d | |||
| 29e9c73274 | |||
| 6240ee69fc | |||
| 14bd5d615b | |||
| e0441c8876 | |||
| 1ee42f0206 | |||
| d1ed547a7c | |||
| f830ea498c | |||
| ce31d802d5 | |||
| f09c449ad5 | |||
| 6bebb8b4a8 | |||
| e3bc32a823 | |||
| 6b70c6ae91 | |||
| b9138b4300 | |||
| 7a157a8c91 | |||
| 7387baace4 | |||
| 80e75139c6 | |||
| e9ced6ddf1 | |||
| 6d1d494cf5 | |||
| 65daf26aee | |||
| 591f65fcee | |||
| 8b6d4134bd | |||
| 2102bb12f0 | |||
| 747bc2bacd | |||
| 4d63b3c8a0 | |||
| 147384074d | |||
| 129137d96c | |||
| 0120b1ab79 | |||
| c64b1e3e5d | |||
| 0c08c27df4 | |||
| 2676ac771d | |||
| 680ef26f4d | |||
| eb4bd6fbb1 | |||
| e78b4a245d | |||
| 73ae3de5b8 | |||
| a6b107e8df | |||
| 6b953d96eb | |||
| 3d33bae2b8 | |||
| f2df029f26 | |||
| 761eb99e53 | |||
| 2434a9624e | |||
| 03d0522cf1 | |||
| 642c99a617 | |||
| 76c25da58e | |||
| b51e7b65f9 | |||
| 78166090bc | |||
| 6d788a27b6 | |||
| 32cda7d793 | |||
| c180a3fe6f | |||
| 1ca55e42af | |||
| ba677d1a32 | |||
| 354da8fdc0 | |||
| 2c4ed4249d | |||
| 39bf31d3a1 | |||
| 50ef8db595 | |||
| d4f365e7e5 | |||
| f19d29cd64 | |||
| 1031e70d70 | |||
| c9f47d4b5c | |||
| 857e73ab25 | |||
| 4e072962c0 | |||
| 81af160be6 | |||
| a64334c32e | |||
| 8d39dac654 | |||
| e867fcab1a | |||
| 9683896a21 | |||
| ca1d6614b2 | |||
| 96f423438b | |||
| df94a8d5af | |||
| d9cc751db4 | |||
| 99c3f68f80 | |||
| be900737d2 | |||
| af8db57f02 | |||
| 1773f61ded | |||
| a024935c39 | |||
| 10df03e564 | |||
| 7a9e7f9c41 | |||
| 45acb44a36 | |||
| 8a3cb46007 | |||
| ba5cf9cd3c | |||
| d1d9dec402 | |||
| 6e9dc181e1 | |||
| fe3a158264 | |||
| e2de1af6f4 | |||
| 22ab62d090 | |||
| 254e0099ca | |||
| 8433fd24c3 | |||
| 32daa03941 | |||
| 4749247389 | |||
| a870474b49 | |||
| 97493b1af8 | |||
| 8e27fe4c0c | |||
| 407c9fd72f | |||
| e95450b318 | |||
| b556da8b36 | |||
| 5cfb6f984b | |||
| 60e1698ed2 | |||
| 60b2c3bb54 | |||
| 979e3fd1f7 | |||
| 600b9b4d8e | |||
| 787d8a7342 | |||
| 46e77ea203 | |||
| fa868568af | |||
| 1c2cacf185 | |||
| f1f067e93a | |||
| 8a2a7054ab | |||
| e4b0ee0672 | |||
| b821e20fd6 | |||
| 51a14ede8b | |||
| 788c09972e | |||
| ba21ab55f8 | |||
| 1585cab3ba | |||
| 304c59e09b | |||
| fdb85d82da | |||
| c8a03817ed | |||
| dc76ff669b | |||
| eca002b655 | |||
| 71c8a30f42 | |||
| 65f1d8d836 | |||
| 624763cbc8 | |||
| af7c91057e | |||
| dd3edf1a3c | |||
| 7f4e0f74ba | |||
| b452370be7 | |||
| 913c7316f2 | |||
| bb6114e8aa | |||
| c097cb1f27 | |||
| 9094b3130d | |||
| 32b104e1a9 | |||
| d1260ccf8b | |||
| c35140e763 | |||
| 6632a35339 | |||
| 6388b8f4bb | |||
| f9a8b3021f | |||
| 6a8c6c1f58 | |||
| 19bc3c513a | |||
| fa3a94e33e | |||
| 5c2177e8d5 | |||
| 6e39969cdc | |||
| 4b0cf874c9 | |||
| 2ff4ee0e1b | |||
| f2a3502445 | |||
| 6ca8ed65e8 | |||
| 6a2b7995e9 | |||
| 247a7a51d7 | |||
| 1fee9d4c29 | |||
| b4d3a279e3 | |||
| 4684e092a8 | |||
| 2a53f415ea | |||
| e074d19593 | |||
| 14a728084c | |||
| ead1d6b5f8 | |||
| a421cfeabe | |||
| 6f6ac066c9 | |||
| a55a32cc7e | |||
| 474c84c9e6 | |||
| a2b64ad332 | |||
| cc5b504771 | |||
| fe33709eb0 | |||
| 41b51edbdd | |||
| e6b5429873 | |||
| 8cbc76540f | |||
| de01c5e61f | |||
| c114ea6b30 | |||
| bdea3d4959 | |||
| e403f4e0d0 | |||
| 3bac2ddae2 | |||
| b98b1b4c78 | |||
| 5885a9cc63 | |||
| e306e14b22 | |||
| 7b9d643dcd | |||
| f70b359631 | |||
| ae37f4268c | |||
| 7c6a1d717d | |||
| a2c1ad01da | |||
| a947e8f35e | |||
| 653e03921e | |||
| a0eec81c8a | |||
| 9eda32b93a | |||
| e1f5bbf895 | |||
| 5d4fdb171e | |||
| 4fa4bc8d4b | |||
| b5f13e4331 | |||
| a897271a03 | |||
| 75a7ed132a | |||
| 379343278d | |||
| 0b965ea431 | |||
| d6cd65e108 | |||
| fc25eb2c90 | |||
| dc5732a5f5 | |||
| a9811c2020 | |||
| 33411f0300 | |||
| 8bc434b614 | |||
| ce6577ee35 | |||
| 579021f5fc | |||
| 17e91a7d2a | |||
| 61f5737df2 | |||
| 49a25af1f2 | |||
| b6f3cb6394 | |||
| 571ab674c3 | |||
| 6b607fb545 | |||
| e761418531 | |||
| fca921ee82 | |||
| fc7993f4a7 | |||
| f12e3e03ac | |||
| eb87fbf8e4 | |||
| c534328cc5 | |||
| 28d8a1c25c | |||
| d1c4de2499 | |||
| 1e081a7f0d | |||
| 730205ded5 | |||
| 90ca6ccf8b | |||
| d7fd49cc4c | |||
| eb03520aa9 | |||
| d8e2ceecf7 | |||
| 7dfed581b7 | |||
| 49a1ae54cf | |||
| 5b3cc753e2 | |||
| 4615eb8258 | |||
| 9b787e13d1 | |||
| 3dbcd79b3c | |||
| a1aac452de | |||
| b90fbe6b1a | |||
| 1860258deb | |||
| a2935b87c2 | |||
| c1ce1d8aba | |||
| 54b7dfe04b | |||
| 4a33809d66 | |||
| 336e074b4a | |||
| aaad450175 | |||
| 85b3c5d91b | |||
| 4db86ebf7f | |||
| 65c43b5224 | |||
| d1ed5844e3 | |||
| c649ec1d8c | |||
| 271e9ac7b7 | |||
| 8348089b50 | |||
| 4f968861d6 | |||
| 66ffd65476 | |||
| 194829336f | |||
| f1d708ca43 | |||
| 4f93661865 | |||
| cd2dc437a3 | |||
| f95dae1b1b | |||
| 6ae4b1fc38 | |||
| fc776eeb16 | |||
| 79376bbc58 | |||
| 3b14c31e00 | |||
| a666f07340 | |||
| bd933ff230 | |||
| 7addae9c24 | |||
| a73a2aaa33 | |||
| a96e88043d | |||
| 29d2f7fc1b | |||
| 7aedf77d83 | |||
| 1324d49098 | |||
| 79a52a60ff | |||
| 6581dd6ff9 | |||
| 4659939547 | |||
| 50d33c5bf4 | |||
| c7c379f962 | |||
| af8c851cc6 | |||
| 88466c7d1f | |||
| a36638ee6d | |||
| ff93a48926 | |||
| 9474c29946 | |||
| 8097c7c86d | |||
| abfe91d47b | |||
| a4109c7ea8 | |||
| 34445dbe0f | |||
| c5631b8fe3 | |||
| 4cddd3face | |||
| 5250a0fe2c | |||
| 95611f19c0 | |||
| b98ee3e7b6 | |||
| 90d4dd79de | |||
| 00904d8862 | |||
| 2e7b6b79bf | |||
| 104d2ae7e8 | |||
| f1242bfb7a | |||
| aef5e9691c | |||
| 3d9012b43a | |||
| 3c5df5ae66 | |||
| 520f1f884b | |||
| 8fc33fd7b1 | |||
| 9e0f7ec4e9 | |||
| 00ca694eea | |||
| c91a39f55c | |||
| ffc7b66c20 | |||
| 94bea4405a | |||
| 2963c1b761 | |||
| 013ecfb189 | |||
| 670f74d589 | |||
| d475d21a79 | |||
| 91349234a0 | |||
| 601bb6f0ca | |||
| 07e26d31f4 | |||
| 151acb249e | |||
| 25d5295d5d | |||
| 474a995845 | |||
| f0f12e77ad | |||
| 5bf1fc38b1 | |||
| d55a3f078a | |||
| 46125773d9 | |||
| ec61a35042 | |||
| 1afd72cb83 | |||
| c623b82698 | |||
| acd415c522 | |||
| 5fb37130f8 | |||
| 8e00e7175c | |||
| 0f06535932 | |||
| 46ff43889b | |||
| 7e1992fc5c | |||
| 0391488cef | |||
| eb80fdf733 | |||
| 4973fd5a39 | |||
| 1dd338c5e0 | |||
| 186c4a7724 | |||
| f0182eb1b2 | |||
| 2acf731baf | |||
| e2b8f91417 | |||
| 5f33c69dd0 | |||
| 76bb3f7d77 | |||
| da1d53c3b0 | |||
| f4b98c43de | |||
| 62c78fc5ac | |||
| 9c4cf78a52 | |||
| 790be35ab8 | |||
| 0eef321f88 | |||
| 69dfe6c8a1 | |||
| 80c97076ae | |||
| c3f394489f | |||
| ce172df91a | |||
| 91baa34071 | |||
| 0e043b2a1b | |||
| 01c84b0140 | |||
| 1da2692c34 | |||
| d538736411 | |||
| b268aea0ab | |||
| a04d70293d | |||
| c66d67dd19 | |||
| 0c22163fd9 | |||
| 50c0f41508 | |||
| b60313e1f8 | |||
| 1fff61b726 | |||
| c4850aed08 | |||
| 43eb2d6f47 | |||
| 0ec4488dd1 | |||
| ca5debef32 | |||
| cc0230f83f | |||
| 3bc728e068 | |||
| 7f1f986f13 | |||
| 65b91762fd | |||
| 91fdb5822b | |||
| 5ca10f35d1 | |||
| a073bf32e4 | |||
| d79b02379e | |||
| e4cabf8de6 | |||
| f5f3fc338f | |||
| f034601512 | |||
| 151a974607 | |||
| 042ed39464 | |||
| 9f583f16f8 | |||
| 4c4436f48c | |||
| c3e18905c1 | |||
| a18a661c73 | |||
| 93908e758f | |||
| 30fbf55b9a | |||
| 3562fa264e | |||
| 359d46c3e1 | |||
| 2b73a349dd | |||
| d86ae7db40 | |||
| f096320e63 | |||
| e23751bd1d | |||
| d5002cce25 | |||
| daa833f33d | |||
| 7fe831c5e3 | |||
| 66069245a1 | |||
| 8b3975752c | |||
| c6b4c77387 | |||
| 9152d9d2ed | |||
| 5a4a50415e | |||
| 640d0ee133 | |||
| 529edfc39b | |||
| 4b1b0fe045 | |||
| 4bfc9a9514 | |||
| 4d39dd0a5e | |||
| 15ba01a1c6 | |||
| 2593b11aba | |||
| e7652e389f | |||
| bd4a7e748d | |||
| 77f3426867 | |||
| 45615c07ee | |||
| ee5a70a63e | |||
| deac50409c | |||
| c5119c8aa6 | |||
| 6c718c3558 | |||
| 75481d3251 | |||
| 7d6f47755c | |||
| eb5d548ba7 | |||
| 3a7de8275f | |||
| 33d79e048c | |||
| ed2b170e1b | |||
| c73636d96d | |||
| 3410b785db | |||
| 762d4433d8 | |||
| e69bf4eceb | |||
| 741fd8d9d3 | |||
| 684224c614 | |||
| 9826197083 | |||
| 2ba84b12f8 | |||
| efeb791807 | |||
| 43e3578d50 | |||
| a79ae25621 | |||
| 05c481c5bb | |||
| 7ce0d69563 | |||
| 011e72c3c1 | |||
| e44bdf6193 | |||
| 29dcdf8e85 | |||
| e37d4cd8a8 | |||
| 46e60b4d0a | |||
| 649d43b581 | |||
| bc5d4fed3c | |||
| 199bc45ae2 | |||
| 127c700a99 | |||
| a67a2cbf5c | |||
| f9a6a3d36f | |||
| 9c38843747 | |||
| 3fc653bbff | |||
| 600603149e | |||
| 2b42049998 | |||
| 5f70fe57e3 | |||
| a2fc1a4d88 | |||
| beee4e9293 | |||
| 17f7b158fa | |||
| 697ef4bdb7 | |||
| ce323bed7a | |||
| db8b378ee0 | |||
| 85214f1f2b | |||
| 63c3d5c89d | |||
| b43f85c305 | |||
| f12840218e | |||
| 5f224fa5f9 | |||
| 263150aeb3 | |||
| 164f213094 | |||
| 06b4c05f73 | |||
| 38975dbfd4 | |||
| a6ce136843 | |||
| a678bd09e9 | |||
| 8215fefc2d | |||
| 86137a669e | |||
| 33650e3886 | |||
| d090b11a47 | |||
| e44456fc0b | |||
| 59f50a569a | |||
| 9fda639e00 | |||
| 0ff2b5d081 | |||
| 134ce5d42d | |||
| 0a9ed37244 | |||
| 6246e41b55 | |||
| 6b6d26b8f0 | |||
| 62c87e206e | |||
| 4905cdd2e3 | |||
| ce3f88428e | |||
| 74644aa4a7 | |||
| 2ccb7312d0 | |||
| bd7735af3d | |||
| 0c2ab7ef45 | |||
| fab724a703 | |||
| ea86973548 | |||
| 9eb0c3c8e5 | |||
| f20b4c9e81 | |||
| 6540deb462 | |||
| c80c9bd8b9 | |||
| 3dc45b80ba | |||
| 020eca8292 | |||
| fb469d7b24 | |||
| 0f5d297bfb | |||
| a56bd3cb91 | |||
| ddd5d4152b | |||
| edf6cb146d | |||
| 4707a50fbb | |||
| 91bd6c3f35 | |||
| 009950e28f | |||
| 8ee12620f0 | |||
| 65edc50563 | |||
| f51783f039 | |||
| c203ef8bcd | |||
| e6b145412b | |||
| d2f8f99683 | |||
| 1c146baeeb | |||
| f447df9873 | |||
| d320c4650d | |||
| de8a3666ec | |||
| f1b9952bf9 | |||
| 6f03854eda | |||
| c356846d90 | |||
| f7b8828deb | |||
| 4276671538 | |||
| 5b86c67a98 | |||
| 6ce6b9576d | |||
| e74eb7d3fc | |||
| fbeead0c74 | |||
| 9a9c65ac8e | |||
| 0a8d86cfc3 | |||
| 8632a0a6ec | |||
| e111b6e1b7 | |||
| b7aba15d58 | |||
| 713cbb81b8 | |||
| fc0739703b | |||
| 382d7e8ac3 | |||
| 23e578bfbf | |||
| ad402adf7a | |||
| 21c582ee1a | |||
| e0d78d5328 | |||
| f96acd6263 | |||
| e185f2eaf6 | |||
| 78fb9dcc59 | |||
| e066b502c3 | |||
| e94943d505 | |||
| aae9446ce0 | |||
| ecd46f2f06 | |||
| 74f77a7e8a | |||
| 07a15883bd | |||
| 2bd1e81917 | |||
| 755f91f5ab | |||
| 7603b5a56c | |||
| 7ef84cfbfe |
@@ -0,0 +1,82 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
#
|
||||
# CodeRabbit configuration — references existing guideline files to avoid
|
||||
# duplicating conventions. See:
|
||||
# .github/copilot-instructions.md — project overview & general rules
|
||||
# docs/cpp.instructions.md — C++ coding conventions
|
||||
# docs/web.instructions.md — Web UI coding conventions
|
||||
# docs/cicd.instructions.md — GitHub Actions / CI-CD conventions
|
||||
#
|
||||
# NOTE: This file must be committed (tracked by git) for CodeRabbit to read
|
||||
# it from the repository. If it is listed in .gitignore, CodeRabbit will
|
||||
# not see it and these settings will have no effect.
|
||||
|
||||
language: en-US
|
||||
|
||||
reviews:
|
||||
path_instructions:
|
||||
- path: "**/*.{cpp,h,hpp,ino}"
|
||||
instructions: >
|
||||
Follow the C++ coding conventions documented in docs/cpp.instructions.md
|
||||
and the general project guidelines in .github/copilot-instructions.md.
|
||||
|
||||
Key rules: 2-space indentation (no tabs), camelCase functions/variables,
|
||||
PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros.
|
||||
|
||||
Hot-path optimization guidelines (attributes, uint_fast types, caching,
|
||||
unsigned range checks) apply from pixel set/get operations and strip.show() downward —
|
||||
NOT to effect functions in FX.cpp, which have diverse contributor styles.
|
||||
|
||||
- path: "wled00/data/**"
|
||||
instructions: >
|
||||
Follow the web UI conventions documented in docs/web.instructions.md.
|
||||
|
||||
Key rules: indent HTML and JavaScript with tabs, CSS with tabs.
|
||||
Files here are built into wled00/html_*.h and wled00/js_*.h by tools/cdata.js — never
|
||||
edit those generated headers directly.
|
||||
|
||||
- path: "wled00/html_*.h"
|
||||
instructions: >
|
||||
These files are auto-generated from wled00/data/ by tools/cdata.js.
|
||||
They must never be manually edited or committed. Flag any PR that
|
||||
includes changes to these files.
|
||||
|
||||
- path: "wled00/js_*.h"
|
||||
instructions: >
|
||||
These files are auto-generated from wled00/data/ by tools/cdata.js.
|
||||
They must never be manually edited or committed. Flag any PR that
|
||||
includes changes to these files.
|
||||
|
||||
- path: "usermods/**"
|
||||
instructions: >
|
||||
Usermods are community add-ons.
|
||||
Each usermod lives in its own directory under usermods/ and is implemented
|
||||
as a .cpp file with a dedicated library.json file to manage dependencies.
|
||||
Follow the same C++ conventions as the core firmware (docs/cpp.instructions.md).
|
||||
|
||||
- path: ".github/workflows/*.{yml,yaml}"
|
||||
instructions: >
|
||||
Follow the CI/CD conventions documented in docs/cicd.instructions.md.
|
||||
|
||||
Key rules: 2-space indentation, descriptive name: on every workflow/job/step.
|
||||
Third-party actions must be pinned to a specific version tag — branch pins
|
||||
such as @main or @master are not allowed. Declare explicit permissions: blocks
|
||||
scoped to least privilege. Never interpolate github.event.* values directly
|
||||
into run: steps — pass them through an env: variable to prevent script
|
||||
injection. Do not use pull_request_target unless fully justified.
|
||||
|
||||
- path: "**/*.instructions.md"
|
||||
instructions: |
|
||||
This file contains both AI-facing rules and human-only reference sections.
|
||||
Human-only sections are enclosed in `<!-- HUMAN_ONLY_START -->` /
|
||||
`<!-- HUMAN_ONLY_END -->` HTML comment markers and should not be used as
|
||||
actionable review criteria.
|
||||
|
||||
When this file is modified in a PR, perform the following alignment check:
|
||||
1. For each `<!-- HUMAN_ONLY_START --> ... <!-- HUMAN_ONLY_END -->` block,
|
||||
verify that its examples and guidance are consistent with (and do not
|
||||
contradict) the AI-facing rules stated in the same file.
|
||||
2. Flag any HUMAN_ONLY section whose content has drifted from the surrounding
|
||||
AI-facing rules due to edits introduced in this PR.
|
||||
3. If new AI-facing rules were added without updating a related HUMAN_ONLY
|
||||
reference section, note this as a suggestion (not a required fix).
|
||||
+1
-2
@@ -1,3 +1,2 @@
|
||||
github: [Aircoookie,blazoncek,DedeHai,lost-hope,willmmiles]
|
||||
github: [DedeHai,lost-hope,willmmiles,netmindz,softhack007]
|
||||
custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek']
|
||||
thanks_dev: u/gh/netmindz
|
||||
|
||||
@@ -44,7 +44,10 @@ body:
|
||||
id: version
|
||||
attributes:
|
||||
label: What version of WLED?
|
||||
description: You can find this in by going to Config -> Security & Updates -> Scroll to Bottom. Copy and paste the entire line after "Server message"
|
||||
description: |-
|
||||
Find this by going to <kbd><samp>⚙️ Config</samp></kbd> → <kbd><samp>Security & Updates</samp></kbd> → Scroll to Bottom.
|
||||
Copy and paste the rest of the line that begins “<samp>Installed version: </samp>”,
|
||||
or, for older versions, the entire line after “<samp>Server message</samp>”.
|
||||
placeholder: "e.g. WLED 0.13.1 (build 2203150)"
|
||||
validations:
|
||||
required: true
|
||||
@@ -60,6 +63,8 @@ body:
|
||||
- ESP32-S2
|
||||
- ESP32-C3
|
||||
- Other
|
||||
- ESP32-C6 (experimental)
|
||||
- ESP32-C5 (experimental)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
applyTo: "**"
|
||||
---
|
||||
# Agent-Mode Build & Test Instructions
|
||||
|
||||
Detailed build workflow, timeouts, and troubleshooting for making code changes in agent mode. Always reference these instructions first when running builds or validating changes.
|
||||
|
||||
## Build Timing and Timeouts
|
||||
|
||||
Use these timeout values when running builds:
|
||||
|
||||
| Command | Typical Time | Minimum Timeout | Notes |
|
||||
|---|---|---|---|
|
||||
| `npm run build` | ~3 s | 30 s | Web UI → `wled00/html_*.h` `wled00/js_*.h` headers |
|
||||
| `npm test` | ~40 s | 2 min | Validates build system |
|
||||
| `npm run dev` | continuous | — | Watch mode, auto-rebuilds on changes |
|
||||
| `pio run -e <env>` | 15–20 min | 30 min | First build downloads toolchains; subsequent builds are faster |
|
||||
|
||||
**NEVER cancel long-running builds.** PlatformIO downloads and compilation require patience.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Style Summary
|
||||
- **C++** files in `wled00/` and `usermods/`: 2-space indentation (no tabs), camelCase functions/variables, PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros.
|
||||
- **Web UI** files in `wled00/data`: indent HTML and JavaScript with tabs, CSS with tabs.
|
||||
- **CI/CD workflows** in `.github/workflows`: 2-space indentation, descriptive `name:` on every workflow/job/step. Third-party actions must be pinned to a specific version tag — branch pins such as `@main` or `@master` are not allowed. SHA pinning recommended.
|
||||
|
||||
### Web UI Changes
|
||||
|
||||
1. Edit files in `wled00/data/`
|
||||
2. Run `npm run build` to regenerate `wled00/html_*.h` `wled00/js_*.h` headers
|
||||
3. Test with local HTTP server (see Manual Testing below)
|
||||
4. Run `npm test` to validate
|
||||
|
||||
### Firmware Changes
|
||||
|
||||
1. Edit files in `wled00/` (but **never** `html_*.h` and `js_*.h` files)
|
||||
2. Ensure web UI is built first: `npm run build`
|
||||
3. Build firmware: `pio run -e esp32dev` (set timeout ≥ 30 min)
|
||||
4. Flash to device: `pio run -e [target] --target upload`
|
||||
|
||||
### Combined Web + Firmware Changes
|
||||
|
||||
1. Always build web UI first
|
||||
2. Test web interface manually
|
||||
3. Then build and test firmware
|
||||
|
||||
|
||||
## Before Finishing Work - Testing
|
||||
|
||||
**You MUST complete ALL of these before marking work as done:**
|
||||
|
||||
1. **Run tests**: `npm test` — must pass
|
||||
2. **Build firmware**: `pio run -e esp32dev` — must succeed after source code changes, **never skip this step**.
|
||||
- Set timeout to 30+ minutes, **never cancel**
|
||||
- Choose `esp32dev` as a common, representative environment
|
||||
- If the build fails, fix the issue before proceeding
|
||||
3. **For web UI changes**: manually test the interface (see below)
|
||||
|
||||
If any step fails, fix the issue. **Do NOT mark work complete with failing builds or tests.**
|
||||
|
||||
## Manual Web UI Testing
|
||||
|
||||
Start a local server:
|
||||
|
||||
```sh
|
||||
cd wled00/data && python3 -m http.server 8080
|
||||
# Open http://localhost:8080/index.htm
|
||||
```
|
||||
|
||||
Test these scenarios after every web UI change:
|
||||
|
||||
- **Load**: `index.htm` loads without JavaScript errors (check browser console)
|
||||
- **Navigation**: switching between main page and settings pages works
|
||||
- **Color controls**: color picker and brightness controls function correctly
|
||||
- **Effects**: effect selection and parameter changes work
|
||||
- **Settings**: form submission and validation work
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Build Issues
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| Missing `html_*.h` | Run `npm ci; npm run build` |
|
||||
| Web UI looks broken | Check browser console for JS errors |
|
||||
| PlatformIO network errors | Retry — downloads can be flaky |
|
||||
| Node.js version mismatch | Ensure Node.js 20+ (check `.nvmrc`) |
|
||||
|
||||
### Recovery Steps
|
||||
|
||||
- **Force web UI rebuild**: `npm run build -- -f`
|
||||
- **Clear generated files**: `rm -f wled00/html_*.h wled00/js_*.h` then `npm run build`
|
||||
- **Clean PlatformIO build artifacts**: `pio run --target clean`
|
||||
- **Reinstall Node deps**: `rm -rf node_modules && npm ci`
|
||||
|
||||
## CI/CD Validation
|
||||
|
||||
The GitHub Actions CI workflow will:
|
||||
1. Install Node.js and Python dependencies
|
||||
2. Run `npm test`
|
||||
3. Build web UI (automatic via PlatformIO)
|
||||
4. Compile firmware for **all** `default_envs` targets
|
||||
|
||||
**To ensure CI success, always validate locally:**
|
||||
- Run `npm test` and ensure it passes
|
||||
- Run `pio run -e esp32dev` (or another common firmware environment, see next section) and ensure it completes successfully
|
||||
- If either fails locally, it WILL fail in CI
|
||||
|
||||
Match this workflow in local development to catch failures before pushing.
|
||||
|
||||
## Important Reminders
|
||||
|
||||
- Always **commit source code**
|
||||
- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`
|
||||
- Web UI rebuild is part of the PlatformIO firmware compilation pipeline
|
||||
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi`
|
||||
- List all PlatformIO targets: `pio run --list-targets`
|
||||
+105
-108
@@ -4,135 +4,132 @@ WLED is a fast and feature-rich implementation of an ESP32 and ESP8266 webserver
|
||||
|
||||
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||
|
||||
## Working Effectively
|
||||
> **Note for AI review tools**: sections enclosed in
|
||||
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
|
||||
> contributor reference material. Do **not** use that content as actionable review
|
||||
> criteria — treat it as background context only.
|
||||
|
||||
### Initial Setup
|
||||
- Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version`
|
||||
- Install dependencies: `npm ci` (takes ~5 seconds)
|
||||
- Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds)
|
||||
## Setup
|
||||
|
||||
### Build and Test Workflow
|
||||
- **ALWAYS build web UI first**: `npm run build` -- takes 3 seconds. NEVER CANCEL.
|
||||
- **Run tests**: `npm test` -- takes 40 seconds. NEVER CANCEL. Set timeout to 2+ minutes.
|
||||
- **Development mode**: `npm run dev` -- monitors file changes and auto-rebuilds web UI
|
||||
- **Hardware firmware build**: `pio run -e [environment]` -- takes 15+ minutes. NEVER CANCEL. Set timeout to 30+ minutes.
|
||||
- Node.js 20+ (see `.nvmrc`)
|
||||
- Install dependencies: `npm ci`
|
||||
- PlatformIO (required only for firmware compilation): `pip install -r requirements.txt`
|
||||
|
||||
### Build Process Details
|
||||
The build has two main phases:
|
||||
1. **Web UI Generation** (`npm run build`):
|
||||
- Processes files in `wled00/data/` (HTML, CSS, JS)
|
||||
- Minifies and compresses web content
|
||||
- Generates `wled00/html_*.h` files with embedded web content
|
||||
- **CRITICAL**: Must be done before any hardware build
|
||||
## Build and Test
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
2. **Hardware Compilation** (`pio run`):
|
||||
- Compiles C++ firmware for various ESP32/ESP8266 targets
|
||||
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`
|
||||
- List all targets: `pio run --list-targets`
|
||||
| Command | Purpose | Typical Time |
|
||||
|---|---|---|
|
||||
| `npm run build` | Build web UI → generates `wled00/html_*.h` and `wled00/js_*.h` headers | ~3 s |
|
||||
| `npm test` | Run test suite | ~40 s |
|
||||
| `npm run dev` | Watch mode — auto-rebuilds web UI on file changes | — |
|
||||
| `pio run -e <env>` | Build firmware for a hardware target | 15–20 min |
|
||||
|
||||
## Validation and Testing
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Web UI Testing
|
||||
- **ALWAYS validate web UI changes manually**:
|
||||
- Start local server: `cd wled00/data && python3 -m http.server 8080`
|
||||
- Open `http://localhost:8080/index.htm` in browser
|
||||
- Test basic functionality: color picker, effects, settings pages
|
||||
- **Check for JavaScript errors** in browser console
|
||||
- **Always run `npm run build` before any `pio run`** (and run `npm ci` first on fresh clones or when lockfile/dependencies change).
|
||||
- The web UI build generates required `wled00/html_*.h` and `wled00/js_*.h` headers for firmware compilation.
|
||||
- **Build firmware to validate code changes**: `pio run -e esp32dev` — must succeed, never skip this step.
|
||||
- Common firmware environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi`
|
||||
|
||||
### Code Validation
|
||||
- **No automated linting configured** - follow existing code style in files you edit
|
||||
- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files
|
||||
- **C++ formatting available**: `clang-format` is installed but not in CI
|
||||
- **Always run tests before finishing**: `npm test`
|
||||
For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build.instructions.md](agent-build.instructions.md).
|
||||
|
||||
### Manual Testing Scenarios
|
||||
After making changes to web UI, always test:
|
||||
- **Load main interface**: Verify index.htm loads without errors
|
||||
- **Navigation**: Test switching between main page and settings pages
|
||||
- **Color controls**: Verify color picker and brightness controls work
|
||||
- **Effects**: Test effect selection and parameter changes
|
||||
- **Settings**: Test form submission and validation
|
||||
### Usermod Guidelines
|
||||
|
||||
## Common Tasks
|
||||
- New custom effects can be added into the user_fx usermod. Read the [user_fx documentation](https://github.com/wled/WLED/blob/main/usermods/user_fx/README.md) for guidance.
|
||||
- Other usermods may be based on the [EXAMPLE usermod](https://github.com/wled/WLED/tree/main/usermods/EXAMPLE). Never edit the example, always create a copy!
|
||||
- New usermod IDs can be added into [wled00/const.h](https://github.com/wled/WLED/blob/main/wled00/const.h#L160).
|
||||
- To activate a usermod, a custom build configuration should be used. Add the usermod name to `custom_usermods`.
|
||||
|
||||
## Project Structure Overview
|
||||
|
||||
### Project Branch / Release Structure
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
```text
|
||||
main # Main development trunk (daily/nightly) 17.0.0-dev
|
||||
├── V5 # special branch: code rework for esp-idf 5.5.x (unstable)
|
||||
├── V5-C6 # special branch: integration of new MCU types: esp32-c5, esp32-c6, esp32-p4 (unstable)
|
||||
16_x # current beta, preparations for next release 16.0.0
|
||||
0_15_x # maintenance (bugfixes only) for current release 0.15.4
|
||||
(tag) v0.14.4 # previous version 0.14.4 (no maintenance)
|
||||
(tag) v0.13.3 # old version 0.13.3 (no maintenance)
|
||||
(tag) v0. ... . ... # historical versions 0.12.x and before
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
- ``main``: development trunk (daily/nightly)
|
||||
- ``V5`` and ``V5-C6``: code rework for esp-idf 5.5.x (unstable) - branched from ``main``.
|
||||
- ``0_15_x``: bugfixing / maintenance for release 0.15.x
|
||||
|
||||
### Repository Structure
|
||||
```
|
||||
wled00/ # Main firmware source (C++)
|
||||
├── data/ # Web interface files
|
||||
│ ├── index.htm # Main UI
|
||||
|
||||
tl;dr:
|
||||
* Firmware source: `wled00/` (C++). Web UI source: `wled00/data/`. Build targets: `platformio.ini`.
|
||||
* Auto-generated headers: `wled00/html_*.h` and `wled00/js_*.h` — **never edit or commit**.
|
||||
* ArduinoJSON + AsyncJSON: `wled00/src/dependencies/json` (included via `wled.h`). CI/CD: `.github/workflows/`.
|
||||
* Usermods: `usermods/` (C++, with individual library.json).
|
||||
* Contributor docs: `docs/` (coding guidelines, etc).
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
Detailed overview:
|
||||
|
||||
```text
|
||||
wled00/ # Main firmware source (C++) "WLED core"
|
||||
├── data/ # Web interface files
|
||||
│ ├── index.htm # Main UI
|
||||
│ ├── settings*.htm # Settings pages
|
||||
│ └── *.js/*.css # Frontend resources
|
||||
├── *.cpp/*.h # Firmware source files
|
||||
└── html_*.h # Generated embedded web files (DO NOT EDIT)
|
||||
tools/ # Build tools (Node.js)
|
||||
│ └── *.js/*.css # Frontend resources
|
||||
├── *.cpp/*.h # Firmware source files
|
||||
├── html_*.h # Auto-generated embedded web files (DO NOT EDIT, DO NOT COMMIT)
|
||||
├── src/ # Modules used by the WLED core (C++)
|
||||
│ ├── fonts/ # Font libraries for scrolling text effect
|
||||
└ └── dependencies/ # Utility functions - some of them have their own licensing terms
|
||||
lib/ # Project specific custom libraries. PlatformIO will compile them to separate static libraries and link them
|
||||
platformio.ini # Hardware build configuration
|
||||
|
||||
platformio_override.sample.ini # examples for custom build configurations - entries must be copied into platformio_override.ini to use them.
|
||||
# platformio_override.ini is _not_ stored in the WLED repository!
|
||||
usermods/ # User-contributed addons to the WLED core, maintained by individual contributors (C++, with individual library.json)
|
||||
package.json # Node.js dependencies and scripts, release identification
|
||||
pio-scripts/ # Build tools (PlatformIO)
|
||||
tools/ # Build tools (Node.js), partition files, and generic utilities
|
||||
├── cdata.js # Web UI build script
|
||||
└── cdata-test.js # Test suite
|
||||
platformio.ini # Hardware build configuration
|
||||
package.json # Node.js dependencies and scripts
|
||||
docs/ # Contributor docs, coding guidelines
|
||||
.github/workflows/ # CI/CD pipelines
|
||||
```
|
||||
|
||||
### Key Files and Their Purpose
|
||||
- `wled00/data/index.htm` - Main web interface
|
||||
- `wled00/data/settings*.htm` - Configuration pages
|
||||
- `tools/cdata.js` - Converts web files to C++ headers
|
||||
- `wled00/wled.h` - Main firmware configuration
|
||||
- `platformio.ini` - Hardware build targets and settings
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
## General Guidelines
|
||||
|
||||
### Development Workflow
|
||||
1. **For web UI changes**:
|
||||
- Edit files in `wled00/data/`
|
||||
- Run `npm run build` to regenerate headers
|
||||
- Test with local HTTP server
|
||||
- Run `npm test` to validate build system
|
||||
- **Repository language is English.** Suggest translations for non-English content.
|
||||
- **Use VS Code with PlatformIO extension** for best development experience.
|
||||
- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`.
|
||||
- If updating Web UI files in `wled00/data/`, **make use of common functions in `wled00/data/common.js` whenever possible**.
|
||||
- **When unsure, say so.** Gather more information rather than guessing.
|
||||
- **Acknowledge good patterns** when you see them. Summarize good practices as part of your review - positive feedback always helps.
|
||||
- **Provide references** when making analyses or recommendations. Base them on the correct branch or PR.
|
||||
- **Highlight user-visible breaking changes and ripple effects**. Ask for confirmation that these were introduced intentionally.
|
||||
- **Unused / dead code must be justified or removed**. This helps to keep the codebase clean, maintainable and readable.
|
||||
- **C++ formatting available**: `clang-format` is installed but not in CI
|
||||
- No automated linting is configured — match existing code style in files you edit.
|
||||
|
||||
2. **For firmware changes**:
|
||||
- Edit files in `wled00/` (but NOT `html_*.h` files)
|
||||
- Ensure web UI is built first (`npm run build`)
|
||||
- Build firmware: `pio run -e [target]`
|
||||
- Flash to device: `pio run -e [target] --target upload`
|
||||
Refer to `docs/cpp.instructions.md` and `docs/web.instructions.md` for language-specific conventions, and `docs/cicd.instructions.md` for GitHub Actions workflows.
|
||||
|
||||
3. **For both web and firmware**:
|
||||
- Always build web UI first
|
||||
- Test web interface manually
|
||||
- Build and test firmware if making firmware changes
|
||||
### Attribution for AI-generated code
|
||||
Using AI-generated code can hide the source of the inspiration / knowledge / sources it used.
|
||||
- Document attribution of inspiration / knowledge / sources used in the code, e.g. link to GitHub repositories or other websites describing the principles / algorithms used.
|
||||
- When a larger block of code is generated by an AI tool, mark it with an `// AI: below section was generated by an AI` comment (see C++ guidelines).
|
||||
- Every non-trivial AI-generated function should have a brief comment describing what it does. Explain parameters when their names alone are not self-explanatory.
|
||||
- AI-generated code must be well documented with meaningful comments that explain intent, assumptions, and non-obvious logic. Do not rephrase source code; explain concepts and reasoning.
|
||||
|
||||
## Build Timing and Timeouts
|
||||
### Pull Request Expectations
|
||||
|
||||
- **Web UI build**: 3 seconds - Set timeout to 30 seconds minimum
|
||||
- **Test suite**: 40 seconds - Set timeout to 2 minutes minimum
|
||||
- **Hardware builds**: 15+ minutes - Set timeout to 30+ minutes minimum
|
||||
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation can take significant time
|
||||
- **No force-push on open PRs.** Once a pull request is open and being reviewed, do not force-push (`git push --force`) to the branch. Force-pushing rewrites history that reviewers may have already commented on, making it impossible to track incremental changes. Use regular commits or `git merge` to incorporate feedback; the branch will be squash-merged when it is accepted.
|
||||
- **Document your changes in the PR.** Every pull request should include a clear description of *what* changed and *why*. If the change affects user-visible behavior, describe the expected impact. Link to related issues where applicable. Provide screenshots to showcase new features.
|
||||
|
||||
## Troubleshooting
|
||||
### Supporting Reviews and Discussions
|
||||
- **For "is it worth doing?" debates** about proposed reliability, safety, or data-integrity mechanisms (CRC checks, backups, power-loss protection): suggest a software **FMEA** (Failure Mode and Effects Analysis).
|
||||
Clarify the main feared events, enumerate failure modes, assess each mitigation's effectiveness per failure mode, note common-cause failures, and rate credibility for the typical WLED use case.
|
||||
|
||||
### Common Issues
|
||||
- **Build fails with missing html_*.h**: Run `npm run build` first
|
||||
- **Web UI looks broken**: Check browser console for JavaScript errors
|
||||
- **PlatformIO network errors**: Try again, downloads can be flaky
|
||||
- **Node.js version issues**: Ensure Node.js 20+ is installed (check `.nvmrc`)
|
||||
|
||||
### When Things Go Wrong
|
||||
- **Clear generated files**: `rm -f wled00/html_*.h` then rebuild
|
||||
- **Force web UI rebuild**: `npm run build -- --force` or `npm run build -- -f`
|
||||
- **Clean PlatformIO cache**: `pio run --target clean`
|
||||
- **Reinstall dependencies**: `rm -rf node_modules && npm install`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **DO NOT edit `wled00/html_*.h` files** - they are auto-generated
|
||||
- **Always commit both source files AND generated html_*.h files**
|
||||
- **Web UI must be built before firmware compilation**
|
||||
- **Test web interface manually after any web UI changes**
|
||||
- **Use VS Code with PlatformIO extension for best development experience**
|
||||
- **Hardware builds require appropriate ESP32/ESP8266 development board**
|
||||
|
||||
## CI/CD Pipeline
|
||||
The GitHub Actions workflow:
|
||||
1. Installs Node.js and Python dependencies
|
||||
2. Runs `npm test` to validate build system
|
||||
3. Builds web UI with `npm run build`
|
||||
4. Compiles firmware for multiple hardware targets
|
||||
5. Uploads build artifacts
|
||||
|
||||
Match this workflow in your local development to ensure CI success.
|
||||
|
||||
@@ -23,12 +23,13 @@ jobs:
|
||||
run: ls -la
|
||||
- name: "✏️ Generate release changelog"
|
||||
id: changelog
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.3
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
sinceTag: v0.15.0
|
||||
output: CHANGELOG_NIGHTLY.md
|
||||
# Exclude issues that were closed without resolution from changelog
|
||||
exclude-labels: 'stale,wontfix,duplicate,invalid'
|
||||
excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'
|
||||
- name: Update Nightly Release
|
||||
uses: andelf/nightly-release@main
|
||||
env:
|
||||
@@ -37,7 +38,7 @@ jobs:
|
||||
tag_name: nightly
|
||||
name: 'Nightly Release $$'
|
||||
prerelease: true
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
body_path: CHANGELOG_NIGHTLY.md
|
||||
files: |
|
||||
*.bin
|
||||
*.bin.gz
|
||||
|
||||
@@ -33,6 +33,6 @@
|
||||
run: |
|
||||
jq -n \
|
||||
--arg content "Pull Request #${PR_NUMBER} \"${PR_TITLE}\" merged by ${ACTOR}
|
||||
${PR_URL}. It will be included in the next nightly builds, please test" \
|
||||
${PR_URL} . It will be included in the next nightly builds, please test" \
|
||||
'{content: $content}' \
|
||||
| curl -H "Content-Type: application/json" -d @- ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }}
|
||||
|
||||
@@ -20,13 +20,13 @@ jobs:
|
||||
merge-multiple: true
|
||||
- name: "✏️ Generate release changelog"
|
||||
id: changelog
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.3
|
||||
uses: janheinrichmerker/action-github-changelog-generator@v2.4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
sinceTag: v0.15.0
|
||||
maxIssues: 500
|
||||
# Exclude issues that were closed without resolution from changelog
|
||||
exclude-labels: 'stale,wontfix,duplicate,invalid'
|
||||
excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
name: Usermod CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- usermods/**
|
||||
- .github/workflows/usermods.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- usermods/**
|
||||
@@ -12,6 +8,8 @@ on:
|
||||
jobs:
|
||||
|
||||
get_usermod_envs:
|
||||
# Only run for pull requests from forks (not from branches within wled/WLED)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
name: Gather Usermods
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -31,6 +29,8 @@ jobs:
|
||||
|
||||
|
||||
build:
|
||||
# Only run for pull requests from forks (not from branches within wled/WLED)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
name: Build Enviornments
|
||||
runs-on: ubuntu-latest
|
||||
needs: get_usermod_envs
|
||||
|
||||
+7
-1
@@ -7,6 +7,12 @@
|
||||
.pioenvs
|
||||
.piolibdeps
|
||||
.vscode
|
||||
compile_commands.json
|
||||
__pycache__/
|
||||
|
||||
/.dummy
|
||||
/dependencies.lock
|
||||
/managed_components
|
||||
|
||||
esp01-update.sh
|
||||
platformio_override.ini
|
||||
@@ -23,4 +29,4 @@ wled-update.sh
|
||||
/wled00/Release
|
||||
/wled00/wled00.ino.cpp
|
||||
/wled00/html_*.h
|
||||
compile_commands.json
|
||||
/wled00/js_*.h
|
||||
|
||||
+175
-27
@@ -1,43 +1,189 @@
|
||||
## Thank you for making WLED better!
|
||||
# Thank you for making WLED better!
|
||||
|
||||
Here are a few suggestions to make it easier for you to contribute!
|
||||
WLED is a community-driven project, and every contribution matters! We appreciate your time and effort.
|
||||
|
||||
### Describe your PR
|
||||
Our maintainers are here for two things: **helping you** improve your code, and **keeping WLED** lean, efficient, and maintainable.
|
||||
We'll work with you to refine your contribution, but we'll also push back if something might create technical debt or add features without clear value. Don't take it personally - we're just protecting WLED's architecture while helping your contribution succeed!
|
||||
|
||||
Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing.
|
||||
## Getting Started
|
||||
|
||||
A good description helps us to review and understand your proposed changes. For example, you could say a few words about
|
||||
* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.)
|
||||
* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code)
|
||||
* testing you performed, known limitations, open ends you possibly could not solve.
|
||||
* any areas where you like to get help from an experienced maintainer (yes WLED has become big 😉)
|
||||
Here are a few suggestions to make it easier for you to contribute:
|
||||
|
||||
### Important Developer Infos
|
||||
* [Project Structure, Files and Directories](.github/copilot-instructions.md#project-structure-overview) (in our AI instructions)
|
||||
* [Instructions for creating usermods](.github/copilot-instructions.md#usermod-guidelines) (in our AI instructions)
|
||||
* KB: [Compiling WLED](https://kno.wled.ge/advanced/compiling-wled/) - slightly outdated but still helpful :-)
|
||||
* Arduino IDE is not supported any more. Use VSCode with the PlatformIO extension.
|
||||
* [Compiling in VSCode/Platformio](https://github.com/wled/WLED-Docs/issues/161) - modern way without command line or platformio.ini changes.
|
||||
* If you add a new feature, consider making a PR to [``wled-docs``](https://github.com/wled/WLED-Docs) for updating our official documentation.
|
||||
|
||||
### PR from a branch in your own fork
|
||||
Start your pull request (PR) in a branch of your own fork. Don't make a PR directly from your main branch.
|
||||
This lets you update your PR if needed, while you can work on other tasks in 'main' or in other branches.
|
||||
|
||||
> [!TIP]
|
||||
> **The easiest way to start your first PR**
|
||||
> When viewing a file in `wled/WLED`, click on the "pen" icon and start making changes.
|
||||
> When you choose to 'Commit changes', GitHub will automatically create a PR from your fork.
|
||||
>
|
||||
> <img width="295" height="134" alt="image: fork and edit" src="https://github.com/user-attachments/assets/f0dc7567-edcb-4409-a530-cd621ae9661f" />
|
||||
|
||||
### Target branch for pull requests
|
||||
|
||||
Please make all PRs against the `main` branch.
|
||||
|
||||
### Describing your PR
|
||||
|
||||
Please add a description of your proposed code changes.
|
||||
A PR with no description or just a few words might not get accepted, simply because very basic information is missing.
|
||||
No need to write an essay!
|
||||
|
||||
A good description helps us to review and understand your proposed changes. For example, you could say a few words about
|
||||
* What you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.)
|
||||
* How your code works (short technical summary - focus on important aspects that might not be obvious when reading the code)
|
||||
* Testing you performed, known limitations, anything you couldn't quite solve.
|
||||
* Let us know if you'd like guidance from a maintainer (WLED is a big project 😉)
|
||||
|
||||
### Testing Your Changes
|
||||
|
||||
Before submitting:
|
||||
|
||||
- ✅ Does it compile?
|
||||
- ✅ Does your feature/fix actually work?
|
||||
- ✅ Did you break anything else?
|
||||
- ✅ Tested on actual hardware if possible?
|
||||
|
||||
Mention your testing in the PR description (e.g., "Tested on ESP32 + WS2812B").
|
||||
|
||||
## During Review
|
||||
|
||||
We're all volunteers, so reviews can take some time (longer during busy times).
|
||||
Don't worry - we haven't forgotten you! Feel free to ping after a week if there's no activity.
|
||||
|
||||
### Updating your code
|
||||
While the PR is open - and under review by maintainers - you may be asked to modify your PR source code.
|
||||
You can simply update your own branch, and push changes in response to reviewer recommendations.
|
||||
Github will pick up the changes so your PR stays up-to-date.
|
||||
While the PR is open, you can keep updating your branch - just push more commits! GitHub will automatically update your PR.
|
||||
|
||||
> [!CAUTION]
|
||||
You don't need to squash commits or clean up history - we'll handle that when merging.
|
||||
|
||||
> [!CAUTION]
|
||||
> Do not use "force-push" while your PR is open!
|
||||
> It has many subtle and unexpected consequences on our github reposistory.
|
||||
> For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push.
|
||||
> It has many subtle and unexpected consequences on our GitHub repository.
|
||||
> For example, we regularly lose review comments when the PR author force-pushes code changes. Our review bot (coderabbit) may become unable to properly track changes, it gets confused or stops responding to questions.
|
||||
> So, pretty please, do not force-push.
|
||||
|
||||
> [!TIP]
|
||||
> Use [cherry-picking](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop) to copy commits from one branch to another.
|
||||
|
||||
|
||||
You can find a collection of very useful tips and tricks here: https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR
|
||||
### Responding to Reviews
|
||||
|
||||
When we ask for changes:
|
||||
|
||||
- **Add new commits** - please don't amend or force-push
|
||||
- **Reply in the PR** - let us know when you've addressed comments
|
||||
- **Ask questions** - if something's unclear, just ask!
|
||||
- **Be patient** - we're all volunteers here 😊
|
||||
|
||||
You can reference feedback in commit messages:
|
||||
> ```text
|
||||
> Fix naming per @Aircoookie's suggestion
|
||||
> ```
|
||||
|
||||
### Dealing with Merge Conflicts
|
||||
|
||||
Got conflicts with `main`? No worries - here's how to fix them:
|
||||
|
||||
**Using GitHub Desktop** (easier for beginners):
|
||||
|
||||
1. Click **Fetch origin**, then **Pull origin**
|
||||
2. If conflicts exist, GitHub Desktop will warn you - click **View conflicts**
|
||||
3. Open the conflicted files in your editor (VS Code, etc.)
|
||||
4. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and keep the correct code
|
||||
5. Save the files
|
||||
6. Back in GitHub Desktop, commit the merge (it'll suggest a message)
|
||||
7. Click **Push origin**
|
||||
|
||||
**Using command line**:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git merge origin/main
|
||||
# Fix conflicts in your editor
|
||||
git add .
|
||||
git commit
|
||||
git push
|
||||
```
|
||||
|
||||
Either way works fine - pick what you're comfortable with! Merging is simpler than rebasing and keeps everything connected.
|
||||
|
||||
#### When you MUST rebase (really rare!)
|
||||
|
||||
Sometimes you might hit merge conflicts with `main` that are harder to solve. Here's what to try:
|
||||
|
||||
1. **Merge instead of rebase** (safest option):
|
||||
```bash
|
||||
git fetch origin
|
||||
git merge origin/main
|
||||
git push
|
||||
```
|
||||
Keeps review comments attached and CI results visible!
|
||||
|
||||
2. **Use cherry-picking** to copy commits between branches without rewriting history - [here's how](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop).
|
||||
|
||||
3. **If all else fails, use `--force-with-lease`** (not plain `--force`):
|
||||
```bash
|
||||
git rebase origin/main
|
||||
git push --force-with-lease
|
||||
```
|
||||
Then **leave a comment** explaining why you had to force-push, and be ready to re-address some feedback.
|
||||
|
||||
### Additional Resources
|
||||
Want to know more? Check out:
|
||||
- 📚 [GitHub Desktop documentation](https://docs.github.com/en/desktop) - if you prefer GUI tools
|
||||
- 🎓 [How to properly submit a PR](https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR) - detailed tips and tricks
|
||||
|
||||
|
||||
## After Approval
|
||||
Once approved, a maintainer will merge your PR (possibly squashing commits).
|
||||
Your contribution will be in the next WLED release - thank you! 🎉
|
||||
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
### Source Code from an AI agent or bot
|
||||
> [!IMPORTANT]
|
||||
> It's OK if you took help from an AI for writing your source code.
|
||||
>
|
||||
> AI tools can be very helpful, but as the contributor, **you're responsible for the code**.
|
||||
|
||||
* Make sure you really understand the AI-generated code, don't just accept it because it "seems to work".
|
||||
* Don't let the AI change existing code without double-checking by you as the contributor. Often, the result will not be complete. For example, previous source code comments may be lost.
|
||||
* Remember that AI is still "Often-Wrong" ;-)
|
||||
* If you don't feel confident using English, you can use AI for translating code comments and descriptions into English. AI bots are very good at understanding language. However, always check if the results are correct. The translation might still have wrong technical terms, or errors in some details.
|
||||
|
||||
#### Best Practice with AI
|
||||
|
||||
AI tools are powerful but "often wrong" - your judgment is essential! 😊
|
||||
|
||||
- ✅ **Understand the code** - As the person contributing to WLED, make sure you understand exactly what the AI-generated source code does
|
||||
- ✅ **Review carefully** - AI can lose comments, introduce bugs, or make unnecessary changes
|
||||
- ✅ **Be transparent** - Add a comment like `// This section was AI-generated` for larger chunks
|
||||
- ✅ **Use AI for translation** - AI is great for translating comments to English (but verify technical terms!)
|
||||
|
||||
### Code style
|
||||
|
||||
When in doubt, it is easiest to replicate the code style you find in the files you want to edit :)
|
||||
Below are the guidelines we use in the WLED repository.
|
||||
Don't stress too much about style! When in doubt, just match the style in the files you're editing. 😊
|
||||
|
||||
Our review bot (coderabbit) has learned lots of detailed guides and hints - it will suggest them automatically when you submit a PR for review.
|
||||
If you are curious, these are the detailed guides:
|
||||
* [C++ Coding](docs/cpp.instructions.md)
|
||||
* [WebUi: HTML, JS, CSS](docs/web.instructions.md)
|
||||
|
||||
Below are the main rules used in the WLED repository:
|
||||
|
||||
#### Indentation
|
||||
|
||||
We use tabs for Indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files.
|
||||
We use tabs for indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files.
|
||||
You are all set if you have enabled `Editor: Detect Indentation` in VS Code.
|
||||
|
||||
#### Blocks
|
||||
@@ -55,7 +201,7 @@ if (a == b) {
|
||||
if (a == b) doStuff(a);
|
||||
```
|
||||
|
||||
Acceptable - however the first variant is usually easier to read:
|
||||
Also acceptable (though the first style is usually easier to read):
|
||||
```cpp
|
||||
if (a == b)
|
||||
{
|
||||
@@ -86,20 +232,22 @@ if( a==b ){
|
||||
#### Comments
|
||||
|
||||
Comments should have a space between the delimiting characters (e.g. `//`) and the comment text.
|
||||
Note: This is a recent change, the majority of the codebase still has comments without spaces.
|
||||
We're gradually adopting this style - don't worry if you see older code without spaces!
|
||||
|
||||
Good:
|
||||
```
|
||||
// This is a comment.
|
||||
|
||||
/* This is a CSS inline comment */
|
||||
```cpp
|
||||
// This is a short inline comment.
|
||||
|
||||
/*
|
||||
* This is a comment
|
||||
* This is a longer comment
|
||||
* wrapping over multiple lines,
|
||||
* used in WLED for file headers and function explanations
|
||||
*/
|
||||
|
||||
```
|
||||
```css
|
||||
/* This is a CSS inline comment */
|
||||
```
|
||||
```html
|
||||
<!-- This is an HTML comment -->
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_8MB.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-DBOARD_HAS_PSRAM"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [
|
||||
[
|
||||
"0x239A",
|
||||
"0x8125"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x0125"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x8126"
|
||||
]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "adafruit_matrixportal_esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"bluetooth",
|
||||
"wifi"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Adafruit MatrixPortal ESP32-S3 for WLED",
|
||||
"upload": {
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 460800
|
||||
},
|
||||
"url": "https://www.adafruit.com/product/5778",
|
||||
"vendor": "Adafruit"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi",
|
||||
"partitions": "default_16MB.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_TTGO_T7_S3",
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [
|
||||
[
|
||||
"0X303A",
|
||||
"0x1001"
|
||||
]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"wifi",
|
||||
"bluetooth"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "LILYGO T3-S3",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.aliexpress.us/item/3256804591247074.html",
|
||||
"vendor": "LILYGO"
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
applyTo: ".github/workflows/*.yml,.github/workflows/*.yaml"
|
||||
---
|
||||
# CI/CD Conventions — GitHub Actions Workflows
|
||||
|
||||
> **Note for AI review tools**: sections enclosed in
|
||||
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
|
||||
> contributor reference material. Do **not** use that content as actionable review
|
||||
> criteria — treat it as background context only.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## YAML Style
|
||||
|
||||
- Indent with **2 spaces** (no tabs)
|
||||
- Every workflow, job, and step must have a `name:` field that clearly describes its purpose
|
||||
- Group related steps logically; separate unrelated groups with a blank line
|
||||
- Comments (`#`) are encouraged for non-obvious decisions (e.g., why `fail-fast: false` is set, what a cron expression means)
|
||||
|
||||
## Workflow Structure
|
||||
|
||||
### Triggers
|
||||
|
||||
- Declare `on:` triggers explicitly; avoid bare `on: push` without branch filters on long-running or expensive jobs
|
||||
- Prefer `workflow_call` for shared build logic (see `build.yml`) to avoid duplicating steps across workflows
|
||||
- Document scheduled triggers (`cron:`) with a human-readable comment:
|
||||
|
||||
```yaml
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # run at 2 AM UTC daily
|
||||
```
|
||||
|
||||
### Jobs
|
||||
|
||||
- Express all inter-job dependencies with `needs:` — never rely on implicit ordering
|
||||
- Use job `outputs:` + step `id:` to pass structured data between jobs (see `get_default_envs` in `build.yml`)
|
||||
- Set `fail-fast: false` on matrix builds so that a single failing environment does not cancel others
|
||||
|
||||
### Runners
|
||||
|
||||
- Pin to a specific Ubuntu version (`ubuntu-22.04`, `ubuntu-24.04`) rather than `ubuntu-latest` for reproducible builds
|
||||
- Only use `ubuntu-latest` in jobs where exact environment reproducibility is not required (e.g., trivial download/publish steps)
|
||||
|
||||
### Tool and Language Versions
|
||||
|
||||
- Pin tool versions explicitly:
|
||||
```yaml
|
||||
python-version: '3.12'
|
||||
```
|
||||
- Do not rely on the runner's pre-installed tool versions — always install via a versioned setup action
|
||||
|
||||
### Caching
|
||||
|
||||
- Always cache package managers and build tool directories when the job installs dependencies:
|
||||
```yaml
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
```
|
||||
- Include the environment name or a relevant identifier in cache keys when building multiple targets
|
||||
|
||||
### Artifacts
|
||||
|
||||
- Name artifacts with enough context to be unambiguous (e.g., `firmware-${{ matrix.environment }}`)
|
||||
- Avoid uploading artifacts that will never be consumed downstream
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
Important: Several current workflows still violate parts of the baseline below - migration is in progress.
|
||||
|
||||
### Permissions — Least Privilege
|
||||
|
||||
Declare explicit `permissions:` blocks. The default token permissions are broad; scope them to the minimum required:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read # for checkout
|
||||
```
|
||||
|
||||
For jobs that publish releases or write to the repository:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: write # create/update releases
|
||||
```
|
||||
|
||||
A common safe baseline for build-only jobs:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
```
|
||||
|
||||
### Supply Chain — Action Pinning
|
||||
|
||||
**Third-party actions** (anything outside the `actions/` and `github/` namespaces) should be pinned to a specific release tag. Branch pins (`@main`, `@master`) are **not allowed** — they can be updated by the action author at any time without notice:
|
||||
|
||||
```yaml
|
||||
# ✅ Acceptable — specific version tag. SHA pinning recommended for more security, as @v2 is still a mutable tag.
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
||||
# ❌ Not acceptable — mutable branch reference
|
||||
uses: andelf/nightly-release@main
|
||||
```
|
||||
|
||||
SHA pinning (e.g., `uses: someorg/some-action@abc1234`) is the most secure option for third-party actions; it is recommended when auditing supply-chain risk is a priority. At minimum, always use a specific version tag.
|
||||
|
||||
**First-party actions** (`actions/checkout`, `actions/cache`, `actions/upload-artifact`, etc.) pinned to a major version tag (e.g., `@v4`) are acceptable because GitHub maintains and audits these.
|
||||
|
||||
When adding a new third-party action:
|
||||
1. Check that the action's repository is actively maintained
|
||||
2. Review the action's source before adding it
|
||||
3. Prefer well-known, widely-used actions over obscure ones
|
||||
|
||||
### Credentials and Secrets
|
||||
|
||||
- Use `${{ secrets.GITHUB_TOKEN }}` for operations within the same repository — it is automatically scoped and rotated
|
||||
- Never commit secrets, tokens, or passwords into workflow files or any tracked file
|
||||
- Never print secrets in `run:` steps, even with `echo` — GitHub masks known secrets but derived values are not automatically masked
|
||||
- Scope secrets to the narrowest step that needs them using `env:` at the step level, not at the workflow level:
|
||||
|
||||
```yaml
|
||||
# ✅ Scoped to the step that needs it
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# ❌ Unnecessarily broad
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
- Personal Access Tokens (PATs, stored as repository secrets) should have the minimum required scopes and should be rotated periodically
|
||||
|
||||
### Script Injection
|
||||
|
||||
`${{ }}` expressions are evaluated before the shell script runs. If an expression comes from untrusted input (PR titles, issue bodies, branch names from forks), it can inject arbitrary shell commands.
|
||||
|
||||
**Never** interpolate `github.event.*` values directly into a `run:` step:
|
||||
|
||||
```yaml
|
||||
# ❌ Injection risk — PR title is attacker-controlled
|
||||
- run: echo "${{ github.event.pull_request.title }}"
|
||||
|
||||
# ✅ Safe — value passed through an environment variable
|
||||
- env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
run: echo "$PR_TITLE"
|
||||
```
|
||||
|
||||
This rule applies to any value that originates outside the repository (issue bodies, labels, comments, commit messages from forks).
|
||||
|
||||
### Pull Request Workflows
|
||||
|
||||
- Workflows triggered by `pull_request` from a fork run with **read-only** token permissions and no access to repository secrets — this is intentional and correct
|
||||
- Do not use `pull_request_target` unless you fully understand the security implications; it runs in the context of the base branch and *does* have secret access, making it a common attack surface
|
||||
@@ -0,0 +1,524 @@
|
||||
---
|
||||
applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino"
|
||||
---
|
||||
# C++ Coding Conventions
|
||||
|
||||
> **Note for AI review tools**: sections enclosed in
|
||||
> `<!-- HUMAN_ONLY_START -->` / `<!-- HUMAN_ONLY_END -->` HTML comments contain
|
||||
> contributor reference material. Do **not** use that content as actionable review
|
||||
> criteria — treat it as background context only.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
<!-- hiding this reference, to avoid cyclic "include" loops -->
|
||||
See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines that apply to all contributors.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
## Formatting
|
||||
|
||||
- Indent with **2 spaces** (no tabs in C++ files)
|
||||
- Opening braces on the same line is preferred (K&R style). Brace on a separate line (Allman style) is acceptable
|
||||
- Single-statement `if` bodies may omit braces: `if (a == b) doStuff(a);`
|
||||
- Space between keyword and parenthesis: `if (...)`, `for (...)`. No space between function name and parenthesis: `doStuff(a)`
|
||||
- No enforced line-length limit; wrap when a line exceeds your editor width
|
||||
|
||||
## Naming
|
||||
|
||||
- **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent`
|
||||
- **PascalCase** for classes and structs: `PinManagerClass`, `BusConfig`
|
||||
- **UPPER_CASE** for macros and constants: `WLED_MAX_USERMODS`, `DEFAULT_CLIENT_SSID`
|
||||
|
||||
## General
|
||||
|
||||
- Follow the existing style in the file you are editing
|
||||
- If possible, use `static` for local (C-style) variables and functions (keeps the global namespace clean)
|
||||
- Avoid unexplained "magic numbers". Prefer named constants (`constexpr`) or C-style `#define` constants for repeated numbers that have the same meaning
|
||||
- Include `"wled.h"` as the primary project header where needed
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
## Header Guards
|
||||
|
||||
Most headers use `#ifndef` / `#define` guards. Some newer headers add `#pragma once` before the guard:
|
||||
|
||||
```cpp
|
||||
#ifndef WLED_EXAMPLE_H
|
||||
#define WLED_EXAMPLE_H
|
||||
// ...
|
||||
#endif // WLED_EXAMPLE_H
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
## Comments
|
||||
|
||||
- `//` for inline comments, `/* ... */` for block comments. Always put a space after `//`
|
||||
- **AI attribution:** When a larger block of code is generated by an AI tool, mark it with an `// AI:` comment so reviewers know to scrutinize it:
|
||||
|
||||
```cpp
|
||||
// AI: below section was generated by an AI
|
||||
void calculateCRC(const uint8_t* data, size_t len) {
|
||||
...
|
||||
}
|
||||
// AI: end of AI-generated section
|
||||
```
|
||||
|
||||
Single-line AI-assisted edits do not need the marker — use it when the AI produced a contiguous block that a human did not write line-by-line.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
<!-- hidden from AI for now, as it created too many "please add a description" review findings in my first tests -->
|
||||
- **Function & feature comments:** Every non-trivial function should have a brief comment above it describing what it does. Include a note about each parameter when the names alone are not self-explanatory:
|
||||
|
||||
```cpp
|
||||
/* *****
|
||||
* Apply gamma correction to a single color channel.
|
||||
* @param value raw 8-bit channel value (0–255)
|
||||
* @param gamma gamma exponent (typically 2.8)
|
||||
* @return corrected 8-bit value
|
||||
***** */
|
||||
uint8_t gammaCorrect(uint8_t value, float gamma);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
Short accessor-style functions (getters/setters, one-liners) may skip this if their purpose is obvious from the name.
|
||||
|
||||
## Preprocessor & Feature Flags
|
||||
|
||||
- Prefer compile-time feature flags (`#ifdef` / `#ifndef`) over runtime checks where possible
|
||||
- Platform differentiation: `ARDUINO_ARCH_ESP32` vs `ESP8266`
|
||||
- PSRAM availability: `BOARD_HAS_PSRAM`
|
||||
|
||||
## Error Handling
|
||||
|
||||
- `DEBUG_PRINTF()` / `DEBUG_PRINTLN()` for developer diagnostics (compiled out unless `-D WLED_DEBUG`)
|
||||
- Don't rely on C++ exceptions — use return codes (`-1` / `false` for errors) and global flags (e.g. `errorFlag = ERR_LOW_MEM`). Some builds don't support C++ exceptions.
|
||||
|
||||
## Strings
|
||||
|
||||
- Use `const char*` for temporary/parsed strings
|
||||
- Avoid `String` (Arduino heap-allocated string) in hot paths; acceptable in config/setup code
|
||||
- Use `F("string")` for string constants (major RAM win on ESP8266; mostly overload/type compatibility on ESP32)
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
On **ESP8266** this explicitly stores the string in flash (PROGMEM), saving precious RAM — every byte counts on that platform.
|
||||
On **ESP32**, `PROGMEM` is a no-op and string literals already reside in flash/rodata, so `F()` yields little RAM benefit but remains harmless (it satisfies `__FlashStringHelper*` overloads that some APIs expect).
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
```cpp
|
||||
DEBUG_PRINTLN(F("WS client connected.")); // string stays in flash, not RAM
|
||||
DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); // format string stays in flash
|
||||
```
|
||||
|
||||
## Memory
|
||||
|
||||
- **PSRAM-aware allocation**: use `d_malloc()` (prefer DRAM), `p_malloc()` (prefer PSRAM) from `fcn_declare.h`
|
||||
- **Avoid Variable Length Arrays (VLAs)**: FreeRTOS task stacks are typically 2–8 KB. A runtime-sized VLA can silently exhaust the stack. Use fixed-size arrays or heap allocation (`d_malloc` / `p_malloc`). Any VLA must be explicitly justified in source or PR.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
GCC/Clang support VLAs as an extension (they are not part of the C++ standard), so they look like a legitimate feature — but they are allocated on the stack at runtime. On ESP32/ESP8266, a VLA whose size depends on a runtime parameter (segment dimensions, pixel counts, etc.) can silently exhaust the stack and cause the program to behave in unexpected ways or crash.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
- **Larger buffers** (LED data, JSON documents) should use PSRAM when available and technically feasible
|
||||
- **Hot-path**: some data should stay in DRAM or IRAM for performance reasons
|
||||
- Memory efficiency matters, but is less critical on boards with PSRAM
|
||||
|
||||
Heap fragmentation is a concern:
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
- Fragmentation can lead to crashes, even when the overall amount of available heap is still good. The C++ runtime doesn't do any "garbage collection".
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
- Avoid frequent `d_malloc` and `d_free` inside a function, especially for small sizes.
|
||||
- Avoid frequent creation / destruction of objects.
|
||||
- Allocate buffers early, and try to re-use them.
|
||||
- Instead of incrementally appending to a `String`, reserve the expected max buffer upfront by using the `reserve()` method.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
```cpp
|
||||
String result;
|
||||
result.reserve(65); // pre-allocate to avoid realloc fragmentation
|
||||
```
|
||||
|
||||
```cpp
|
||||
// prefer DRAM; falls back gracefully and enforces MIN_HEAP_SIZE guard
|
||||
_ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len));
|
||||
```
|
||||
|
||||
```cpp
|
||||
_mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation - does not increase size()
|
||||
_modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation - does not increase size()
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
## `const` and `constexpr`
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
`const` is a promise to the compiler that a value (or object) will not change - a function declared with a `const char* message` parameter is not allowed to modify the content of `message`.
|
||||
This pattern enables optimizations and makes intent clear to reviewers.
|
||||
|
||||
`constexpr` allows to define constants that are *guaranteed* to be evaluated by the compiler (zero run-time costs).
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
- For function parameters that are read-only, prefer `const &` or `const`.
|
||||
|
||||
### `const` locals
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
* Adding `const` to a local variable that is only assigned once is optional, but *not* strictly necessary.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
* In hot-path code, `const` on cached locals may help the compiler keep values in registers.
|
||||
```cpp
|
||||
const uint_fast16_t cols = vWidth();
|
||||
const uint_fast16_t rows = vHeight();
|
||||
```
|
||||
|
||||
### `const` references to avoid copies
|
||||
- Pass objects by `const &` (or `&`) instead of copying them implicitly.
|
||||
- Use `const &` (or `&`) inside loops - This avoids constructing temporary objects on every access.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
const auto &m = _mappings[i]; // reference, not a copy (bus_manager.cpp)
|
||||
Segment& sourcesegment = strip.getSegment(sourceid); // alias — avoids creating a temporary Segment instance
|
||||
```
|
||||
|
||||
For function parameters that are read-only, prefer `const &`:
|
||||
```cpp
|
||||
BusManager::add(const BusConfig &bc, bool placeholder) {
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
- Class **Data Members:** Avoid reference data members (`T&` or `const T&`) in a class.
|
||||
A reference member can outlive the object it refers to, causing **dangling reference** bugs that are hard to diagnose. Prefer value storage or use a pointer and document the expected lifetime.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
<!-- hidden from AI for now - codebase is not compliant to this rule (slowly migrating) -->
|
||||
### `constexpr` over `#define`
|
||||
|
||||
- Prefer `constexpr` for compile-time constants. Unlike `#define`, `constexpr` respects scope and type safety, keeping the global namespace clean.
|
||||
|
||||
```cpp
|
||||
// Prefer:
|
||||
constexpr uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
|
||||
constexpr int WLED_MAX_BUSSES = WLED_MAX_DIGITAL_CHANNELS + WLED_MAX_ANALOG_CHANNELS;
|
||||
|
||||
// Avoid (when possible):
|
||||
#define TWO_CHANNEL_MASK 0x00FF00FF
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### `static_assert` over `#error`
|
||||
|
||||
- Use `static_assert` instead of the C-style `#if … #error … #endif` pattern when validating compile-time constants. It provides a clear message and works with `constexpr` values.
|
||||
- `#define` and `#if ... #else ... #endif` is still needed for conditional-compilation guards and build-flag-overridable values.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
// Prefer:
|
||||
constexpr int WLED_MAX_BUSSES = WLED_MAX_DIGITAL_CHANNELS + WLED_MAX_ANALOG_CHANNELS;
|
||||
static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
|
||||
// Avoid:
|
||||
#if (WLED_MAX_BUSSES > 32)
|
||||
#error "WLED_MAX_BUSSES exceeds hard limit"
|
||||
#endif
|
||||
```
|
||||
|
||||
```cpp
|
||||
// using static_assert() to validate enumerated types (zero cost at runtime)
|
||||
static_assert(0u == static_cast<uint8_t>(PinOwner::None),
|
||||
"PinOwner::None must be zero, so default array initialization works as expected");
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### `static` and `const` class methods
|
||||
|
||||
#### `const` member functions
|
||||
|
||||
Marking a member function `const` tells the compiler that it does not modify the object's state:
|
||||
|
||||
```cpp
|
||||
uint16_t length() const { return _len; }
|
||||
bool isActive() const { return _active; }
|
||||
```
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
Benefits for GCC/Xtensa/RISC-V:
|
||||
- The compiler knows the method cannot write to `this`, so it is free to **keep member values in registers** across the call and avoid reload barriers.
|
||||
- `const` methods can be called on `const` objects and `const` references — essential when passing large objects as `const &` to avoid copying.
|
||||
- `const` allows the compiler to **eliminate redundant loads**: if a caller already has a member value cached, the compiler can prove the `const` call cannot invalidate it.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
Declare getter, query, or inspection methods `const`. If you need to mark a member `mutable` to work around this (e.g. for a cache or counter), document the reason.
|
||||
|
||||
#### `static` member functions
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
A `static` member function has no implicit `this` pointer. This has two distinct advantages:
|
||||
|
||||
1. **Smaller code, faster calls**: no `this` is passed in a register. On Xtensa and RISC-V, this removes one register argument from every call site and prevents the compiler from emitting `this`-preservation code around inlined blocks.
|
||||
2. **Better inlining**: GCC can inline a `static` method with more certainty because it cannot be overridden by a derived class (no virtual dispatch ambiguity) and has no aliasing concern through `this`.
|
||||
|
||||
Use `static` for any method that does not need access to instance members:
|
||||
|
||||
```cpp
|
||||
// Factory / utility — no instance needed:
|
||||
static BusConfig fromJson(JsonObject obj);
|
||||
|
||||
// Pure computation helpers:
|
||||
static uint8_t gamma8(uint8_t val);
|
||||
static uint32_t colorBalance(uint32_t color, uint8_t r, uint8_t g, uint8_t b);
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
`static` communicates intent clearly: a reviewer immediately knows the method is stateless and safe to call without a fully constructed object.
|
||||
|
||||
> **Rule of thumb**: if a method does not read or write any member variable, make it `static`. If it only reads member variables, make it `const`. Note: `static` methods cannot also be `const`-qualified because there is no implicit `this` pointer to be const — just use `static`. Both qualifiers reduce coupling and improve generated code on all ESP32 targets.
|
||||
|
||||
---
|
||||
|
||||
## Hot-Path Optimization
|
||||
|
||||
The hot path is the per-frame pixel pipeline: **Segment → Strip → BusManager → Bus(Digital,HUB75,Network) or PolyBus → LED driver, plus ``WS2812FX::show()`` and below**.
|
||||
Speed is the priority here. The patterns below are taken from existing hot-path code (`FX_fcn.cpp`, `FX_2Dfcn.cpp`, `bus_manager.cpp`, `colors.cpp`) and should be followed when modifying these files.
|
||||
|
||||
Note: `FX.cpp` (effect functions) is written by many contributors and has diverse styles — that is acceptable.
|
||||
|
||||
### Function Attributes
|
||||
|
||||
Stack the appropriate attributes on hot-path functions. Defined in `const.h`:
|
||||
|
||||
| Attribute | Meaning | When to use |
|
||||
|---|---|---|
|
||||
| `__attribute__((hot))` | Branch-prediction hint | hot-path functions with complex logic |
|
||||
| `IRAM_ATTR` | Place in fast IRAM (ESP32) | Critical per-pixel functions (e.g. `BusDigital::setPixelColor`) |
|
||||
| `IRAM_ATTR_YN` | IRAM on ESP32, no-op on ESP8266 | Hot functions that ESP8266 can't fit in IRAM |
|
||||
| `WLED_O2_ATTR` | Force `-O2` optimization | Most hot-path functions |
|
||||
| `WLED_O3_ATTR` | Force `-O3,fast-math` | Innermost color math (e.g. `color_blend`) |
|
||||
| `[[gnu::hot]] inline` | Modern C++ attribute + inline | Header-defined accessors (e.g. `progress()`, `currentBri()`) |
|
||||
|
||||
Note: `WLED_O3_ATTR` sometimes causes performance loss compared to `WLED_O2_ATTR`. Choose optimization levels based on test results.
|
||||
|
||||
Example signature:
|
||||
|
||||
```cpp
|
||||
void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(unsigned i, uint32_t c)
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Cache Members to Locals Before Loops
|
||||
|
||||
Copy class members and virtual-call results to local variables before entering a loop:
|
||||
|
||||
```cpp
|
||||
uint_fast8_t count = numBusses; // avoid repeated member access
|
||||
for (uint_fast8_t i = 0; i < count; i++) {
|
||||
Bus* const b = busses[i]; // const pointer hints to compiler
|
||||
uint_fast16_t bstart = b->getStart();
|
||||
uint_fast16_t blen = b->getLength();
|
||||
...
|
||||
}
|
||||
```
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
### Unsigned Range Check
|
||||
|
||||
Replace two-comparison range tests with a single unsigned subtraction:
|
||||
|
||||
```cpp
|
||||
// Instead of: if (pix >= bstart && pix < bstart + blen)
|
||||
if ((uint_fast16_t)(pix - bstart) < blen) // also catches negative pix via unsigned underflow
|
||||
```
|
||||
|
||||
### Early Returns
|
||||
|
||||
Guard every hot-path function with the cheapest necessary checks first:
|
||||
|
||||
```cpp
|
||||
if (!isActive()) return; // inactive segment
|
||||
if (unsigned(i) >= vLength()) return; // bounds check (catches negative i too)
|
||||
```
|
||||
|
||||
### Avoid Nested Calls — Fast Path / Complex Path
|
||||
|
||||
Avoid calling non-inline functions or making complex decisions inside per-pixel hot-path code. When a function has both a common simple case and a rare complex case, split it into two variants and choose once per frame rather than per pixel.
|
||||
|
||||
General rules:
|
||||
- Keep fast-path functions free of non-inline calls, multi-way branches, and complex switch-case decisions.
|
||||
- Hoist per-frame decisions (e.g. simple vs. complex segment) out of the per-pixel loop.
|
||||
- Code duplication between fast/slow variants is acceptable to keep the fast path lean.
|
||||
|
||||
### Function Pointers to Eliminate Repeated Decisions
|
||||
|
||||
When the same decision (e.g. "which drawing routine?") would be evaluated for every pixel, assign the chosen variant to a function pointer once and let the inner loop call through the pointer. This removes the branch entirely — the calling code (e.g. the GIF decoder loop) only ever invokes one function per frame, with no per-pixel decision.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
`image_loader.cpp` demonstrates the pattern: `calculateScaling()` picks the best drawing callback once per frame based on segment dimensions and GIF size, then passes it to the decoder via `setDrawPixelCallback()`:
|
||||
|
||||
```cpp
|
||||
// calculateScaling() — called once per frame
|
||||
if ((perPixelX < 2) && (perPixelY < 2))
|
||||
decoder.setDrawPixelCallback(drawPixelCallbackDownScale2D); // downscale-only variant
|
||||
else
|
||||
decoder.setDrawPixelCallback(drawPixelCallback2D); // full-scaling variant
|
||||
```
|
||||
|
||||
Each callback is a small, single-purpose function with no internal branching — the decoder's per-pixel loop never re-evaluates which strategy to use.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
### Template Specialization (Advanced)
|
||||
|
||||
Templates can eliminate runtime decisions by generating separate code paths at compile time. For example, a pixel setter could be templated on color order or channel count so the compiler removes dead branches and produces tight, specialized machine code:
|
||||
|
||||
```cpp
|
||||
template<bool hasWhite>
|
||||
void setChannel(uint8_t* out, uint32_t col) {
|
||||
out[0] = R(col); out[1] = G(col); out[2] = B(col);
|
||||
if constexpr (hasWhite) out[3] = W(col); // compiled out when hasWhite==false
|
||||
}
|
||||
```
|
||||
|
||||
Use sparingly — each instantiation duplicates code in flash. On ESP8266 and small-flash ESP32 boards this can exhaust IRAM/flash. Prefer templates only when the hot path is measurably faster and the number of instantiations is small (2–4).
|
||||
|
||||
### RAII Lock-Free Synchronization (Advanced)
|
||||
|
||||
Where contention is rare and the critical section is short, consider replacing mutex-based locking with lock-free techniques using `std::atomic` and RAII scoped guards. A scoped guard sets a flag on construction and clears it on destruction, guaranteeing cleanup even on early return:
|
||||
|
||||
```cpp
|
||||
struct ScopedBusyFlag {
|
||||
std::atomic<bool>& flag;
|
||||
bool acquired;
|
||||
ScopedBusyFlag(std::atomic<bool>& f) : flag(f), acquired(false) {
|
||||
bool expected = false;
|
||||
acquired = flag.compare_exchange_strong(expected, true);
|
||||
}
|
||||
~ScopedBusyFlag() { if (acquired) flag.store(false); }
|
||||
explicit operator bool() const { return acquired; }
|
||||
};
|
||||
|
||||
// Usage
|
||||
static std::atomic<bool> busySending{false};
|
||||
ScopedBusyFlag guard(busySending);
|
||||
if (!guard) return; // another task is already sending
|
||||
// ... do work — flag auto-clears when guard goes out of scope
|
||||
```
|
||||
|
||||
This avoids FreeRTOS semaphore overhead and the risk of forgetting to return a semaphore. There are no current examples of this pattern in the codebase — consult with maintainers before introducing it in new code, to ensure it aligns with the project's synchronization conventions.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Pre-Compute Outside Loops
|
||||
|
||||
Move invariant calculations before the loop. Pre-compute reciprocals to replace division with multiplication.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
```cpp
|
||||
const uint_fast16_t cols = virtualWidth();
|
||||
const uint_fast16_t rows = virtualHeight();
|
||||
uint_fast8_t fadeRate = (255U - rate) >> 1;
|
||||
float mappedRate_r = 1.0f / (float(fadeRate) + 1.1f); // reciprocal — avoid division inside loop
|
||||
```
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Parallel Channel Processing
|
||||
|
||||
Process R+B and W+G channels simultaneously using the two-channel mask pattern:
|
||||
|
||||
```cpp
|
||||
constexpr uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
|
||||
uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK;
|
||||
uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK;
|
||||
return rb | wg;
|
||||
```
|
||||
|
||||
### Bit Shifts Over Division (mainly for RISC-V boards)
|
||||
|
||||
ESP32 and ESP32-S3 (Xtensa core) have a fast "integer divide" instruction, so manual shifts rarely help.
|
||||
On RISC-V targets (ESP32-C3/C6/P4) and ESP8266, prefer explicit bit-shifts for power-of-two arithmetic — the compiler does **not** always convert divisions to shifts.
|
||||
Always use unsigned operands for right shifts; signed right-shift is implementation-defined.
|
||||
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
On RISC-V-based boards (ESP32-C3, ESP32-C6, ESP32-C5) explicit shifts can be beneficial.
|
||||
```cpp
|
||||
position >> 3 // instead of position / 8
|
||||
(255U - rate) >> 1 // instead of (255 - rate) / 2
|
||||
i & 0x0007 // instead of i % 8
|
||||
```
|
||||
|
||||
**Important**: The bit-shifted expression should be unsigned. On some MCUs, "signed right-shift" is implemented by an "arithmetic shift right" that duplicates the sign bit: ``0b1010 >> 1 = 0b1101``.
|
||||
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
### Static Caching for Expensive Computations
|
||||
|
||||
Cache results in static locals when the input rarely changes between calls:
|
||||
|
||||
```cpp
|
||||
static uint16_t lastKelvin = 0;
|
||||
static byte correctionRGB[4] = {255,255,255,0};
|
||||
if (lastKelvin != kelvin) {
|
||||
colorKtoRGB(kelvin, correctionRGB); // expensive — only recalculate when input changes
|
||||
lastKelvin = kelvin;
|
||||
}
|
||||
```
|
||||
|
||||
### Inlining Strategy
|
||||
|
||||
- Move frequently-called small functions to headers for inlining (e.g. `Segment::setPixelColorRaw` is in `FX.h`)
|
||||
- Use `static inline` for file-local helpers
|
||||
|
||||
### Math & Trigonometric Functions
|
||||
|
||||
- WLED uses a custom `fastled_slim` library. The old FastLED trig aliases (`sin8`, `cos8`, `sin16`, `cos16`) **no longer exist and cause a compile error** — use `sin8_t()`, `cos8_t()`, `sin16_t()`, `cos16_t()` instead. For float approximations use `sin_approx()` / `cos_approx()` instead of `sinf()` / `cosf()`. Replace FastLED noise aliases (`inoise8`, `inoise16`) with `perlin8`, `perlin16`.
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
|
||||
| ❌ Do not use (compile error) | ✅ Use instead | Source |
|
||||
|---|---|---|
|
||||
| `sin8()`, `cos8()` | `sin8_t()`, `cos8_t()` | `fastled_slim.h` → `wled_math.cpp` |
|
||||
| `sin16()`, `cos16()` | `sin16_t()`, `cos16_t()` | `fastled_slim.h` → `wled_math.cpp` |
|
||||
| `sinf()`, `cosf()` | `sin_approx()`, `cos_approx()` | `wled_math.cpp` |
|
||||
| `atan2f()`, `atan2()` | `atan2_t()` | `wled_math.cpp` |
|
||||
| `sqrt()` on integers | `sqrt32_bw()` | `fcn_declare.h` → `wled_math.cpp` |
|
||||
| `sqrtf()` on floats | `sqrtf()` (acceptable) | — no WLED replacement |
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
---
|
||||
|
||||
## `delay()` vs `yield()` in ESP32 Tasks
|
||||
<!-- HUMAN_ONLY_START -->
|
||||
* On ESP32, `delay(ms)` calls `vTaskDelay(ms / portTICK_PERIOD_MS)`, which **suspends only the calling task**. The FreeRTOS scheduler immediately runs all other ready tasks.
|
||||
* The Arduino `loop()` function runs inside `loopTask`. Calling `delay()` there does *not* block the network stack, audio FFT, LED DMA, nor any other FreeRTOS task.
|
||||
* This differs from ESP8266, where `delay()` stalls the entire system unless `yield()` was called inside.
|
||||
<!-- HUMAN_ONLY_END -->
|
||||
|
||||
- On ESP32, `delay()` is generally allowed, as it helps to efficiently manage CPU usage of all tasks.
|
||||
- On ESP8266, only use `delay()` and `yield()` in the main `loop()` context. If not sure, protect with `if (can_yield()) ...`.
|
||||
- Do *not* use `delay()` in effects (FX.cpp) or in the hot pixel path.
|
||||
- `delay()` on the bus-level is allowed, it might be needed to achieve exact timing in LED drivers.
|
||||
|
||||
### IDLE Watchdog and Custom Tasks on ESP32
|
||||
|
||||
- In arduino-esp32, `yield()` calls `vTaskDelay(0)`, which only switches to tasks at equal or higher priority — the IDLE task (priority 0) is never reached.
|
||||
- **Do not use `yield()` to pace ESP32 tasks or assume it feeds any watchdog**.
|
||||
- **Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved. The IDLE watchdog panic is the first visible symptom — but the damage starts earlier: deleted task memory leaks, software timers stop firing, light sleep is disabled, and Wi-Fi/BT idle hooks don't run. Structure custom tasks like this:
|
||||
```cpp
|
||||
// WRONG — IDLE task is never scheduled; yield() does not feed the idle task watchdog.
|
||||
void myTask(void*) {
|
||||
for (;;) {
|
||||
doWork();
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT — delay(1) suspends the task for ≥1 ms, IDLE task runs, IDLE watchdog is fed
|
||||
void myTask(void*) {
|
||||
for (;;) {
|
||||
doWork();
|
||||
delay(1); // DO NOT REMOVE — lets IDLE(0) run and feeds its watchdog
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed.
|
||||
- **Watchdog note.** WLED disables the Task Watchdog by default (`WLED_WATCHDOG_TIMEOUT 0` in `wled.h`). When enabled, `esp_task_wdt_reset()` is called at the end of each `loop()` iteration. Long blocking operations inside `loop()` — such as OTA downloads or slow file I/O — must call `esp_task_wdt_reset()` periodically, or be restructured so the main loop is not blocked for longer than the configured timeout.
|
||||
|
||||
## Caveats and Pitfalls
|
||||
|
||||
- **LittleFS filenames**: File paths passed to `file.open()` must not exceed 255 bytes (`LFS_NAME_MAX`). Validate constructed paths (e.g., `/ledmap_` + segment name + `.json`) stay within this limit (assume standard configurations, like WLED_MAX_SEGNAME_LEN = 64).
|
||||
|
||||
- **Float-to-unsigned conversion is undefined behavior when the value is out of range.** Converting a negative `float` directly to an unsigned integer type (`uint8_t`, `uint16_t`, …) is UB per the C++ standard — the Xtensa (ESP32) toolchain may silently wrap, but RISC-V (ESP32-C3/C5/C6/P4) can produce different results due to clamping. Cast through a signed integer first:
|
||||
```cpp
|
||||
// Undefined behavior — avoid:
|
||||
uint8_t angle = 40.74f * atan2f(dy, dx); // negative float → uint8_t is UB
|
||||
|
||||
// Correct — cast through int first:
|
||||
// atan2f returns [-π..+π], scaled ≈ [-128..+128] as int; uint8_t wraps negative ints via 2's complement (e.g. -1 → 255)
|
||||
uint8_t angle = int(40.74f * atan2f(dy, dx)); // float→int (defined), int→uint8_t (defined)
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
applyTo: "wled00/data/**"
|
||||
---
|
||||
# Web UI Coding Conventions
|
||||
|
||||
## Formatting
|
||||
|
||||
- Indent **HTML and JavaScript** with **tabs**
|
||||
- Indent **CSS** with **tabs**
|
||||
|
||||
## JavaScript Style
|
||||
|
||||
- **camelCase** for functions and variables: `gId()`, `selectedFx`, `currentPreset`
|
||||
- Abbreviated helpers are common: `d` for `document`, `gId()` for `getElementById()`
|
||||
|
||||
## Key Files
|
||||
|
||||
- `index.htm` — main interface
|
||||
- `index.js` — functions that manage / update the main interface
|
||||
- `settings*.htm` — configuration pages
|
||||
- `*.css` — stylesheets (inlined during build)
|
||||
- `common.js` — helper functions
|
||||
|
||||
**Reuse shared helpers from `common.js` whenever possible** instead of duplicating utilities in page-local scripts.
|
||||
|
||||
## Build Integration
|
||||
|
||||
Files in this directory are processed by `tools/cdata.js` into generated headers
|
||||
(`wled00/html_*.h`, `wled00/js_*.h`).
|
||||
Run `npm run build` after any change. **Never edit generated headers directly.**
|
||||
Generated
+33
-31
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "wled",
|
||||
"version": "0.16.0-alpha",
|
||||
"version": "17.0.0-dev",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wled",
|
||||
"version": "0.16.0-alpha",
|
||||
"version": "17.0.0-dev",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"nodemon": "^3.1.14",
|
||||
"web-resource-inliner": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -111,10 +111,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
@@ -129,13 +132,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@@ -211,12 +216,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@@ -524,15 +523,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
@@ -552,15 +554,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
|
||||
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
|
||||
"version": "3.1.14",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
||||
"integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^4",
|
||||
"ignore-by-default": "^1.0.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^10.2.1",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"semver": "^7.5.3",
|
||||
"simple-update-notifier": "^2.0.0",
|
||||
@@ -609,9 +611,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wled",
|
||||
"version": "0.16.0-alpha",
|
||||
"version": "17.0.0-dev",
|
||||
"description": "Tools for WLED project",
|
||||
"main": "tools/cdata.js",
|
||||
"directories": {
|
||||
@@ -14,21 +14,21 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wled-dev/WLED.git"
|
||||
"url": "git+https://github.com/wled/WLED.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wled-dev/WLED/issues"
|
||||
"url": "https://github.com/wled/WLED/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wled-dev/WLED#readme",
|
||||
"homepage": "https://github.com/wled/WLED#readme",
|
||||
"dependencies": {
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"web-resource-inliner": "^7.0.0",
|
||||
"nodemon": "^3.1.9"
|
||||
"nodemon": "^3.1.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Add a section to the linker script to store our dynamic arrays
|
||||
# This is implemented as a pio post-script to ensure that we can
|
||||
# place our linker script at the correct point in the command arguments.
|
||||
Import("env")
|
||||
from pathlib import Path
|
||||
|
||||
platform = env.get("PIOPLATFORM")
|
||||
script_file = Path(f"tools/dynarray_{platform}.ld")
|
||||
if script_file.is_file():
|
||||
linker_script = f"-T{script_file}"
|
||||
if platform == "espressif32":
|
||||
# For ESP32, the script must be added at the right point in the list
|
||||
linkflags = env.get("LINKFLAGS", [])
|
||||
idx = linkflags.index("memory.ld")
|
||||
linkflags.insert(idx+1, linker_script)
|
||||
env.Replace(LINKFLAGS=linkflags)
|
||||
else:
|
||||
# For other platforms, put it in last
|
||||
env.Append(LINKFLAGS=[linker_script])
|
||||
+121
-18
@@ -1,6 +1,8 @@
|
||||
Import('env')
|
||||
from collections import deque
|
||||
from pathlib import Path # For OS-agnostic path manipulation
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from click import secho
|
||||
from SCons.Script import Exit
|
||||
from platformio.builder.tools.piolib import LibBuilderBase
|
||||
@@ -25,25 +27,117 @@ def find_usermod(mod: str) -> Path:
|
||||
return mp
|
||||
raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!")
|
||||
|
||||
def is_wled_module(dep: LibBuilderBase) -> bool:
|
||||
"""Returns true if the specified library is a wled module
|
||||
# Names of external/registry deps listed in custom_usermods.
|
||||
# Populated during parsing below; read by is_wled_module() at configure time.
|
||||
_custom_usermod_names: set[str] = set()
|
||||
|
||||
# Matches any RFC-valid URL scheme (http, https, git, git+https, symlink, file, hg+ssh, etc.)
|
||||
_URL_SCHEME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9+.-]*://')
|
||||
# SSH git URL: user@host:path (e.g. git@github.com:user/repo.git#tag)
|
||||
_SSH_URL_RE = re.compile(r'^[^@\s]+@[^@:\s]+:[^:\s]')
|
||||
# Explicit custom name: "LibName = <spec>" (PlatformIO [<name>=]<spec> form)
|
||||
_NAME_EQ_RE = re.compile(r'^([A-Za-z0-9_.-]+)\s*=\s*(\S.*)')
|
||||
|
||||
|
||||
def _is_external_entry(line: str) -> bool:
|
||||
"""Return True if line is a lib_deps-style external/registry entry."""
|
||||
if _NAME_EQ_RE.match(line): # "LibName = <spec>"
|
||||
return True
|
||||
if _URL_SCHEME_RE.match(line): # https://, git://, symlink://, etc.
|
||||
return True
|
||||
if _SSH_URL_RE.match(line): # git@github.com:user/repo.git
|
||||
return True
|
||||
if '@' in line: # "owner/Name @ ^1.0.0"
|
||||
return True
|
||||
if re.match(r'^[^/\s]+/[^/\s]+$', line): # "owner/Name"
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _predict_dep_name(entry: str) -> str | None:
|
||||
"""Predict the library name PlatformIO will assign to this dep (best-effort).
|
||||
|
||||
Accuracy relies on the library's manifest "name" matching the repo/package
|
||||
name in the spec. This holds for well-authored libraries; the libArchive
|
||||
check (which requires library.json) provides an early-failure safety net.
|
||||
"""
|
||||
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
|
||||
entry = entry.strip()
|
||||
# "LibName = <spec>" — name is given explicitly; always use it
|
||||
m = _NAME_EQ_RE.match(entry)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
# URL scheme: extract name from path
|
||||
if _URL_SCHEME_RE.match(entry):
|
||||
parsed = urlparse(entry)
|
||||
if parsed.netloc in ('github.com', 'gitlab.com', 'bitbucket.com'):
|
||||
parts = [p for p in parsed.path.split('/') if p]
|
||||
if len(parts) >= 2:
|
||||
name = parts[1]
|
||||
else:
|
||||
name = Path(parsed.path.rstrip('/')).name.strip()
|
||||
if name.endswith('.git'):
|
||||
name = name[:-4]
|
||||
return name or None
|
||||
# SSH git URL: git@github.com:user/repo.git#tag → repo
|
||||
if _SSH_URL_RE.match(entry):
|
||||
path_part = entry.split(':', 1)[1].split('#')[0].rstrip('/')
|
||||
name = Path(path_part).name
|
||||
return (name[:-4] if name.endswith('.git') else name) or None
|
||||
# Versioned registry: "owner/Name @ version" → Name
|
||||
if '@' in entry:
|
||||
name_part = entry.split('@')[0].strip()
|
||||
return name_part.split('/')[-1].strip() if '/' in name_part else name_part
|
||||
# Plain registry: "owner/Name" → Name
|
||||
if re.match(r'^[^/\s]+/[^/\s]+$', entry):
|
||||
return entry.split('/')[-1].strip()
|
||||
return None
|
||||
|
||||
## Script starts here
|
||||
# Process usermod option
|
||||
usermods = env.GetProjectOption("custom_usermods","")
|
||||
|
||||
# Handle "all usermods" case
|
||||
if usermods == '*':
|
||||
usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()]
|
||||
else:
|
||||
usermods = usermods.split()
|
||||
def is_wled_module(dep: LibBuilderBase) -> bool:
|
||||
"""Returns true if the specified library is a wled module."""
|
||||
return (
|
||||
usermod_dir in Path(dep.src_dir).parents
|
||||
or str(dep.name).startswith("wled-")
|
||||
or dep.name in _custom_usermod_names
|
||||
)
|
||||
|
||||
if usermods:
|
||||
# Inject usermods in to project lib_deps
|
||||
symlinks = [f"symlink://{find_usermod(mod).resolve()}" for mod in usermods]
|
||||
env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + symlinks)
|
||||
|
||||
## Script starts here — parse custom_usermods
|
||||
raw_usermods = env.GetProjectOption("custom_usermods", "")
|
||||
usermods_libdeps: list[str] = []
|
||||
|
||||
for line in raw_usermods.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or line.startswith(';'):
|
||||
continue
|
||||
|
||||
if _is_external_entry(line):
|
||||
# External URL or registry entry: pass through to lib_deps unchanged.
|
||||
predicted = _predict_dep_name(line)
|
||||
if predicted:
|
||||
_custom_usermod_names.add(predicted)
|
||||
else:
|
||||
secho(
|
||||
f"WARNING: Cannot determine library name for custom_usermods entry "
|
||||
f"{line!r}. If it is not recognised as a WLED module at build time, "
|
||||
f"ensure its library.json 'name' matches the repo name.",
|
||||
fg="yellow", err=True)
|
||||
usermods_libdeps.append(line)
|
||||
else:
|
||||
# Bare name(s): split on whitespace for backwards compatibility.
|
||||
for token in line.split():
|
||||
if token == '*':
|
||||
for mod_path in sorted(usermod_dir.iterdir()):
|
||||
if mod_path.is_dir() and (mod_path / 'library.json').exists():
|
||||
_custom_usermod_names.add(mod_path.name)
|
||||
usermods_libdeps.append(f"symlink://{mod_path.resolve()}")
|
||||
else:
|
||||
resolved = find_usermod(token)
|
||||
_custom_usermod_names.add(resolved.name)
|
||||
usermods_libdeps.append(f"symlink://{resolved.resolve()}")
|
||||
|
||||
if usermods_libdeps:
|
||||
env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + usermods_libdeps)
|
||||
|
||||
# Utility function for assembling usermod include paths
|
||||
def cached_add_includes(dep, dep_cache: set, includes: deque):
|
||||
@@ -86,6 +180,14 @@ def wrapped_ConfigureProjectLibBuilder(xenv):
|
||||
# Add WLED's own dependencies
|
||||
for dir in extra_include_dirs:
|
||||
dep.env.PrependUnique(CPPPATH=str(dir))
|
||||
# Ensure debug info is emitted for this module's source files.
|
||||
# validate_modules.py uses `nm --defined-only -l` on the final ELF to check
|
||||
# that each module has at least one symbol placed in the binary. The -l flag
|
||||
# reads DWARF debug sections to map placed symbols back to their original source
|
||||
# files; without -g those sections are absent and the check cannot attribute any
|
||||
# symbol to a specific module. We scope this to usermods only — the main WLED
|
||||
# build and other libraries are unaffected.
|
||||
dep.env.AppendUnique(CCFLAGS=["-g"])
|
||||
# Enforce that libArchive is not set; we must link them directly to the executable
|
||||
if dep.lib_archive:
|
||||
broken_usermods.append(dep)
|
||||
@@ -93,9 +195,10 @@ def wrapped_ConfigureProjectLibBuilder(xenv):
|
||||
if broken_usermods:
|
||||
broken_usermods = [usermod.name for usermod in broken_usermods]
|
||||
secho(
|
||||
f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- modules will not compile in correctly",
|
||||
fg="red",
|
||||
err=True)
|
||||
f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- "
|
||||
f"modules will not compile in correctly. Add '\"build\": {{\"libArchive\": false}}' "
|
||||
f"to their library.json.",
|
||||
fg="red", err=True)
|
||||
Exit(1)
|
||||
|
||||
# Save the depbuilders list for later validation
|
||||
|
||||
@@ -2,6 +2,7 @@ Import('env')
|
||||
import os
|
||||
import shutil
|
||||
import gzip
|
||||
import json
|
||||
|
||||
OUTPUT_DIR = "build_output{}".format(os.path.sep)
|
||||
#OUTPUT_DIR = os.path.join("build_output")
|
||||
@@ -22,7 +23,8 @@ def create_release(source):
|
||||
release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME")
|
||||
if release_name_def:
|
||||
release_name = release_name_def.replace("\\\"", "")
|
||||
version = _get_cpp_define_value(env, "WLED_VERSION")
|
||||
with open("package.json", "r") as package:
|
||||
version = json.load(package)["version"]
|
||||
release_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.bin")
|
||||
release_gz_file = release_file + ".gz"
|
||||
print(f"Copying {source} to {release_file}")
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
Import('env')
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
|
||||
def get_github_repo():
|
||||
"""Extract GitHub repository name from git remote URL.
|
||||
|
||||
Uses the remote that the current branch tracks, falling back to 'origin'.
|
||||
This handles cases where repositories have multiple remotes or where the
|
||||
main remote is not named 'origin'.
|
||||
|
||||
Returns:
|
||||
str: Repository name in 'owner/repo' format for GitHub repos,
|
||||
'unknown' for non-GitHub repos, missing git CLI, or any errors.
|
||||
"""
|
||||
try:
|
||||
remote_name = 'origin' # Default fallback
|
||||
|
||||
# Try to get the remote for the current branch
|
||||
try:
|
||||
# Get current branch name
|
||||
branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
||||
capture_output=True, text=True, check=True)
|
||||
current_branch = branch_result.stdout.strip()
|
||||
|
||||
# Get the remote for the current branch
|
||||
remote_result = subprocess.run(['git', 'config', f'branch.{current_branch}.remote'],
|
||||
capture_output=True, text=True, check=True)
|
||||
tracked_remote = remote_result.stdout.strip()
|
||||
|
||||
# Use the tracked remote if we found one
|
||||
if tracked_remote:
|
||||
remote_name = tracked_remote
|
||||
except subprocess.CalledProcessError:
|
||||
# If branch config lookup fails, continue with 'origin' as fallback
|
||||
pass
|
||||
|
||||
# Get the remote URL for the determined remote
|
||||
result = subprocess.run(['git', 'remote', 'get-url', remote_name],
|
||||
capture_output=True, text=True, check=True)
|
||||
remote_url = result.stdout.strip()
|
||||
|
||||
# Check if it's a GitHub URL
|
||||
if 'github.com' not in remote_url.lower():
|
||||
return None
|
||||
|
||||
# Parse GitHub URL patterns:
|
||||
# https://github.com/owner/repo.git
|
||||
# git@github.com:owner/repo.git
|
||||
# https://github.com/owner/repo
|
||||
|
||||
# Remove .git suffix if present
|
||||
if remote_url.endswith('.git'):
|
||||
remote_url = remote_url[:-4]
|
||||
|
||||
# Handle HTTPS URLs
|
||||
https_match = re.search(r'github\.com/([^/]+/[^/]+)', remote_url, re.IGNORECASE)
|
||||
if https_match:
|
||||
return https_match.group(1)
|
||||
|
||||
# Handle SSH URLs
|
||||
ssh_match = re.search(r'github\.com:([^/]+/[^/]+)', remote_url, re.IGNORECASE)
|
||||
if ssh_match:
|
||||
return ssh_match.group(1)
|
||||
|
||||
return None
|
||||
|
||||
except FileNotFoundError:
|
||||
# Git CLI is not installed or not in PATH
|
||||
return None
|
||||
except subprocess.CalledProcessError:
|
||||
# Git command failed (e.g., not a git repo, no remote, etc.)
|
||||
return None
|
||||
except Exception:
|
||||
# Any other unexpected error
|
||||
return None
|
||||
|
||||
# WLED version is managed by package.json; this is picked up in several places
|
||||
# - It's integrated in to the UI code
|
||||
# - Here, for wled_metadata.cpp
|
||||
# - The output_bins script
|
||||
# We always take it from package.json to ensure consistency
|
||||
with open("package.json", "r") as package:
|
||||
WLED_VERSION = json.load(package)["version"]
|
||||
|
||||
def has_def(cppdefs, name):
|
||||
""" Returns true if a given name is set in a CPPDEFINES collection """
|
||||
for f in cppdefs:
|
||||
if isinstance(f, tuple):
|
||||
f = f[0]
|
||||
if f == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_wled_metadata_flags(env, node):
|
||||
cdefs = env["CPPDEFINES"].copy()
|
||||
|
||||
if not has_def(cdefs, "WLED_REPO"):
|
||||
repo = get_github_repo()
|
||||
if repo:
|
||||
cdefs.append(("WLED_REPO", f"\\\"{repo}\\\""))
|
||||
|
||||
cdefs.append(("WLED_VERSION", WLED_VERSION))
|
||||
|
||||
# This transforms the node in to a Builder; it cannot be modified again
|
||||
return env.Object(
|
||||
node,
|
||||
CPPDEFINES=cdefs
|
||||
)
|
||||
|
||||
env.AddBuildMiddleware(
|
||||
add_wled_metadata_flags,
|
||||
"*/wled_metadata.cpp"
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
Import('env')
|
||||
import json
|
||||
|
||||
PACKAGE_FILE = "package.json"
|
||||
|
||||
with open(PACKAGE_FILE, "r") as package:
|
||||
version = json.load(package)["version"]
|
||||
env.Append(BUILD_FLAGS=[f"-DWLED_VERSION={version}"])
|
||||
@@ -1,16 +1,10 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path # For OS-agnostic path manipulation
|
||||
from typing import Iterable
|
||||
from click import secho
|
||||
from SCons.Script import Action, Exit
|
||||
from platformio.builder.tools.piolib import LibBuilderBase
|
||||
|
||||
|
||||
def is_wled_module(env, dep: LibBuilderBase) -> bool:
|
||||
"""Returns true if the specified library is a wled module
|
||||
"""
|
||||
usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods"
|
||||
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
|
||||
Import("env")
|
||||
|
||||
|
||||
def read_lines(p: Path):
|
||||
@@ -19,29 +13,80 @@ def read_lines(p: Path):
|
||||
return f.readlines()
|
||||
|
||||
|
||||
def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]:
|
||||
""" Identify which dirs contributed to the final build
|
||||
def _get_nm_path(env) -> str:
|
||||
""" Derive the nm tool path from the build environment """
|
||||
if "NM" in env:
|
||||
return env.subst("$NM")
|
||||
# Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm
|
||||
cc = env.subst("$CC")
|
||||
nm = re.sub(r'(gcc|g\+\+)$', 'nm', os.path.basename(cc))
|
||||
return os.path.join(os.path.dirname(cc), nm)
|
||||
|
||||
Returns the (sub)set of dirs that are found in the output ELF
|
||||
|
||||
def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]:
|
||||
""" Check which modules have at least one defined symbol placed in the ELF.
|
||||
|
||||
The map file is not a reliable source for this: with LTO, original object
|
||||
file paths are replaced by temporary ltrans.o partitions in all output
|
||||
sections, making per-module attribution impossible from the map alone.
|
||||
Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug
|
||||
info to attribute each placed symbol to its original source file.
|
||||
|
||||
Requires usermod libraries to be compiled with -g so that DWARF sections
|
||||
are present in the ELF. load_usermods.py injects -g for all WLED modules
|
||||
via dep.env.AppendUnique(CCFLAGS=["-g"]).
|
||||
|
||||
Returns the set of build_dir basenames for confirmed modules.
|
||||
"""
|
||||
# Pattern to match symbols in object directories
|
||||
# Join directories into alternation
|
||||
usermod_dir_regex = "|".join([re.escape(dir) for dir in dirs])
|
||||
# Matches nonzero address, any size, and any path in a matching directory
|
||||
object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o")
|
||||
nm_path = _get_nm_path(env)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[nm_path, "--defined-only", "-l", str(elf_path)],
|
||||
capture_output=True, text=True, errors="ignore", timeout=120,
|
||||
)
|
||||
nm_output = result.stdout
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
secho(f"WARNING: nm failed ({e}); skipping per-module validation", fg="yellow", err=True)
|
||||
return {Path(b.build_dir).name for b in module_lib_builders} # conservative pass
|
||||
|
||||
# Match placed symbols against builders as we parse nm output, exiting early
|
||||
# once all builders are accounted for.
|
||||
# nm --defined-only still includes debugging symbols (type 'N') such as the
|
||||
# per-CU markers GCC emits in .debug_info (e.g. "usermod_example_cpp_6734d48d").
|
||||
# These live at address 0x00000000 in their debug section — not in any load
|
||||
# segment — so filtering them out leaves only genuinely placed symbols.
|
||||
# nm -l appends a tab-separated "file:lineno" location to each symbol line.
|
||||
remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders}
|
||||
found = set()
|
||||
for line in map_file:
|
||||
matches = object_path_regex.findall(line)
|
||||
for m in matches:
|
||||
found.add(m)
|
||||
|
||||
for line in nm_output.splitlines():
|
||||
if not remaining:
|
||||
break # all builders matched
|
||||
addr, _, _ = line.partition(' ')
|
||||
if not addr.lstrip('0'):
|
||||
continue # zero address — skip debug-section marker
|
||||
if '\t' not in line:
|
||||
continue
|
||||
loc = line.rsplit('\t', 1)[1]
|
||||
# Strip trailing :lineno (e.g. "/path/to/foo.cpp:42" → "/path/to/foo.cpp")
|
||||
src_path = Path(loc.rsplit(':', 1)[0])
|
||||
# Path.is_relative_to() handles OS-specific separators correctly without
|
||||
# any regex, avoiding Windows path escaping issues.
|
||||
for src_dir in list(remaining):
|
||||
if src_path.is_relative_to(src_dir):
|
||||
found.add(remaining.pop(src_dir))
|
||||
break
|
||||
|
||||
return found
|
||||
|
||||
|
||||
DYNARRAY_SECTION = ".dtors" if env.get("PIOPLATFORM") == "espressif8266" else ".dynarray"
|
||||
USERMODS_SECTION = f"{DYNARRAY_SECTION}.usermods.1"
|
||||
|
||||
def count_usermod_objects(map_file: list[str]) -> int:
|
||||
""" Returns the number of usermod objects in the usermod list """
|
||||
# Count the number of entries in the usermods table section
|
||||
return len([x for x in map_file if ".dtors.tbl.usermods.1" in x])
|
||||
return len([x for x in map_file if USERMODS_SECTION in x])
|
||||
|
||||
|
||||
def validate_map_file(source, target, env):
|
||||
@@ -65,16 +110,17 @@ def validate_map_file(source, target, env):
|
||||
usermod_object_count = count_usermod_objects(map_file_contents)
|
||||
secho(f"INFO: {usermod_object_count} usermod object entries")
|
||||
|
||||
confirmed_modules = check_map_file_objects(map_file_contents, modules.keys())
|
||||
elf_path = build_dir / env.subst("${PROGNAME}.elf")
|
||||
confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders)
|
||||
|
||||
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
|
||||
if missing_modules:
|
||||
secho(
|
||||
f"ERROR: No object files from {missing_modules} found in linked output!",
|
||||
f"ERROR: No symbols from {missing_modules} found in linked output!",
|
||||
fg="red",
|
||||
err=True)
|
||||
Exit(1)
|
||||
return None
|
||||
|
||||
Import("env")
|
||||
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))
|
||||
|
||||
+109
-20
@@ -10,7 +10,28 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# CI/release binaries
|
||||
default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods
|
||||
default_envs = nodemcuv2
|
||||
esp8266_2m
|
||||
esp01_1m_full
|
||||
nodemcuv2_160
|
||||
esp8266_2m_160
|
||||
esp01_1m_full_160
|
||||
nodemcuv2_compat
|
||||
esp8266_2m_compat
|
||||
esp01_1m_full_compat
|
||||
esp32dev
|
||||
esp32dev_debug
|
||||
esp32_eth
|
||||
esp32_wrover
|
||||
lolin_s2_mini
|
||||
esp32c3dev
|
||||
esp32c3dev_qio
|
||||
esp32S3_wroom2
|
||||
esp32s3dev_16MB_opi
|
||||
esp32s3dev_8MB_opi
|
||||
esp32s3dev_8MB_qspi
|
||||
esp32s3_4M_qspi
|
||||
usermods
|
||||
|
||||
src_dir = ./wled00
|
||||
data_dir = ./wled00/data
|
||||
@@ -100,6 +121,7 @@ build_flags =
|
||||
-D DECODE_SAMSUNG=true
|
||||
-D DECODE_LG=true
|
||||
-DWLED_USE_MY_CONFIG
|
||||
-D WLED_PS_DONT_REPLACE_FX ; PS replacement FX are purely a flash memory saving feature, do not replace classic FX until we run out of flash
|
||||
|
||||
build_unflags =
|
||||
|
||||
@@ -110,9 +132,10 @@ ldscript_4m1m = eagle.flash.4m1m.ld
|
||||
|
||||
[scripts_defaults]
|
||||
extra_scripts =
|
||||
pre:pio-scripts/set_version.py
|
||||
pre:pio-scripts/set_metadata.py
|
||||
post:pio-scripts/output_bins.py
|
||||
post:pio-scripts/strip-floats.py
|
||||
post:pio-scripts/dynarray.py
|
||||
pre:pio-scripts/user_config_copy.py
|
||||
pre:pio-scripts/load_usermods.py
|
||||
pre:pio-scripts/build_ui.py
|
||||
@@ -138,9 +161,8 @@ upload_speed = 115200
|
||||
# ------------------------------------------------------------------------------
|
||||
lib_compat_mode = strict
|
||||
lib_deps =
|
||||
fastled/FastLED @ 3.6.0
|
||||
IRremoteESP8266 @ 2.8.2
|
||||
makuna/NeoPixelBus @ 2.8.3
|
||||
https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce
|
||||
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
|
||||
marvinroger/AsyncMqttClient @ 0.9.0
|
||||
# for I2C interface
|
||||
@@ -226,7 +248,6 @@ lib_deps_compat =
|
||||
ESPAsyncTCP @ 1.2.2
|
||||
ESPAsyncUDP
|
||||
ESP8266PWM
|
||||
fastled/FastLED @ 3.6.0
|
||||
IRremoteESP8266 @ 2.8.2
|
||||
makuna/NeoPixelBus @ 2.7.9
|
||||
https://github.com/blazoncek/QuickESPNow.git#optional-debug
|
||||
@@ -236,7 +257,7 @@ lib_deps_compat =
|
||||
lib_deps =
|
||||
esp32async/AsyncTCP @ 3.4.7
|
||||
bitbank2/AnimatedGIF@^1.4.7
|
||||
https://github.com/Aircoookie/GifDecoder#bc3af18
|
||||
https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e
|
||||
build_flags =
|
||||
-D CONFIG_ASYNC_TCP_USE_WDT=0
|
||||
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192
|
||||
@@ -264,12 +285,13 @@ AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility
|
||||
|
||||
[esp32_idf_V4]
|
||||
;; build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5
|
||||
;; *** important: build flags from esp32_idf_V4 are inherited by _all_ esp32-based MCUs: esp32, esp32s2, esp32s3, esp32c3
|
||||
;;
|
||||
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
|
||||
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
|
||||
|
||||
;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them)
|
||||
platform = https://github.com/tasmota/platform-espressif32/releases/download/2023.06.02/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.9 with IPv6 support, based on IDF 4.4.4
|
||||
platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.18 with IPv6 support, based on IDF 4.4.8
|
||||
platform_packages =
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
|
||||
@@ -278,12 +300,13 @@ build_flags = -g
|
||||
-D WLED_ENABLE_DMX_INPUT
|
||||
lib_deps =
|
||||
${esp32_all_variants.lib_deps}
|
||||
https://github.com/someweisguy/esp_dmx.git#47db25d
|
||||
https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0
|
||||
${env.lib_deps}
|
||||
|
||||
[esp32s2]
|
||||
;; generic definitions for all ESP32-S2 boards
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-DARDUINO_ARCH_ESP32
|
||||
@@ -302,6 +325,7 @@ board_build.partitions = ${esp32.default_partitions} ;; default partioning for
|
||||
[esp32c3]
|
||||
;; generic definitions for all ESP32-C3 boards
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-DARDUINO_ARCH_ESP32
|
||||
@@ -320,6 +344,7 @@ board_build.flash_mode = qio
|
||||
[esp32s3]
|
||||
;; generic definitions for all ESP32-S3 boards
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-DESP32
|
||||
@@ -405,6 +430,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PIXELFORGE
|
||||
lib_deps = ${esp8266.lib_deps}
|
||||
|
||||
[env:esp01_1m_full_compat]
|
||||
@@ -415,6 +441,7 @@ platform_packages = ${esp8266.platform_packages_compat}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PIXELFORGE
|
||||
|
||||
[env:esp01_1m_full_160]
|
||||
extends = env:esp01_1m_full
|
||||
@@ -423,40 +450,53 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
|
||||
; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM
|
||||
-D WLED_DISABLE_PARTICLESYSTEM1D
|
||||
-D WLED_DISABLE_PARTICLESYSTEM2D
|
||||
-D WLED_DISABLE_PIXELFORGE
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp32dev]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
custom_usermods = audioreactive
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
|
||||
[env:esp32dev_debug]
|
||||
extends = env:esp32dev
|
||||
upload_speed = 921600
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags}
|
||||
-D WLED_DEBUG
|
||||
-D WLED_RELEASE_NAME=\"ESP32_DEBUG\"
|
||||
|
||||
[env:esp32dev_8M]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.large_partitions}
|
||||
board_upload.flash_size = 8MB
|
||||
board_upload.maximum_size = 8388608
|
||||
; board_build.f_flash = 80000000L
|
||||
; board_build.flash_mode = qio
|
||||
board_build.flash_mode = dio
|
||||
|
||||
[env:esp32dev_16M]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.extreme_partitions}
|
||||
@@ -468,11 +508,15 @@ board_build.flash_mode = dio
|
||||
[env:esp32_eth]
|
||||
board = esp32-poe
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1
|
||||
; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
|
||||
-D SR_DMTYPE=-1 -D AUDIOPIN=-1 -D I2S_SDPIN=-1 -D I2S_WSPIN=-1 -D I2S_CKPIN=-1 -D MCLK_PIN=-1 ;; force AR to not allocate any PINs at startup
|
||||
-D DATA_PINS=4 ;; default led pin = 16 conflicts with pins used for ethernet
|
||||
; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only => uncomment if your board uses ETH_CLOCK_GPIO0_OUT, ETH_CLOCK_GPIO16_OUT, ETH_CLOCK_GPIO17_OUT
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
@@ -486,6 +530,7 @@ board_build.partitions = ${esp32.extended_partitions}
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
|
||||
-DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html
|
||||
-D DATA_PINS=25
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
@@ -493,9 +538,11 @@ lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
[env:esp32c3dev]
|
||||
extends = esp32c3
|
||||
platform = ${esp32c3.platform}
|
||||
platform_packages = ${esp32c3.platform_packages}
|
||||
framework = arduino
|
||||
board = esp32-c3-devkitm-1
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
custom_usermods = audioreactive
|
||||
build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this
|
||||
@@ -504,19 +551,26 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=
|
||||
upload_speed = 460800
|
||||
build_unflags = ${common.build_unflags}
|
||||
lib_deps = ${esp32c3.lib_deps}
|
||||
board_build.flash_mode = dio ; safe default, required for OTA updates to 0.16 from older version which used dio (must match the bootloader!)
|
||||
|
||||
[env:esp32c3dev_qio]
|
||||
extends = env:esp32c3dev
|
||||
build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3-QIO\"
|
||||
board_build.flash_mode = qio ; qio is faster and works on almost all boards (some boards may use dio to get 2 extra pins)
|
||||
|
||||
[env:esp32s3dev_16MB_opi]
|
||||
;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi)
|
||||
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
|
||||
board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi\"
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
board_build.partitions = ${esp32.extreme_partitions}
|
||||
@@ -531,13 +585,14 @@ monitor_filters = esp32_exception_decoder
|
||||
board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support
|
||||
board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_opi\"
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
board_build.partitions = ${esp32.large_partitions}
|
||||
@@ -545,23 +600,37 @@ board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32s3dev_8MB_qspi]
|
||||
;; generic ESP32-S3 board with 8MB FLASH and PSRAM (memory_type: qio_qspi). Try this one if esp32s3dev_8MB_opi does not work on your board
|
||||
extends = env:esp32s3dev_8MB_opi
|
||||
board_build.arduino.memory_type = qio_qspi
|
||||
board_build.flash_mode = qio
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_qspi\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
;; -DLOLIN_WIFI_FIX ;; uncomment if you have WiFi connectivity problems
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32S3_wroom2]
|
||||
;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1
|
||||
;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi)
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
board = esp32s3camlcd ;; this is the only standard board with "opi_opi"
|
||||
board_build.arduino.memory_type = opi_opi
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\"
|
||||
-D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED
|
||||
-D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1
|
||||
-D WLED_DEBUG
|
||||
;;-D WLED_DEBUG
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
|
||||
@@ -570,15 +639,33 @@ board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32S3_wroom2_32MB]
|
||||
;; For ESP32-S3 WROOM-2 with 32MB Flash, and >= 8MB PSRAM (memory_type: opi_opi)
|
||||
extends = env:esp32S3_wroom2
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2_32MB\"
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip
|
||||
;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED
|
||||
-D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1
|
||||
;;-D WLED_DEBUG
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic
|
||||
board_build.partitions = tools/WLED_ESP32_32MB.csv
|
||||
board_upload.flash_size = 32MB
|
||||
board_upload.maximum_size = 33554432
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:esp32s3_4M_qspi]
|
||||
;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi)
|
||||
board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages = ${esp32s3.platform_packages}
|
||||
upload_speed = 921600
|
||||
custom_usermods = audioreactive
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
@@ -590,6 +677,7 @@ monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:lolin_s2_mini]
|
||||
platform = ${esp32s2.platform}
|
||||
platform_packages = ${esp32s2.platform_packages}
|
||||
board = lolin_s2_mini
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = qio
|
||||
@@ -616,6 +704,7 @@ lib_deps = ${esp32s2.lib_deps}
|
||||
[env:usermods]
|
||||
board = esp32dev
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages = ${esp32_idf_V4.platform_packages}
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\"
|
||||
-DTOUCH_CS=9
|
||||
|
||||
+134
-16
@@ -28,7 +28,6 @@ lib_deps = ${esp8266.lib_deps}
|
||||
; robtillaart/SHT85@~0.3.3
|
||||
; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug
|
||||
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
|
||||
; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
@@ -48,7 +47,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
; -D WLED_DISABLE_MQTT
|
||||
; -D WLED_DISABLE_ADALIGHT
|
||||
; -D WLED_DISABLE_2D
|
||||
; -D WLED_DISABLE_PXMAGIC
|
||||
; -D WLED_DISABLE_PIXELFORGE
|
||||
; -D WLED_DISABLE_ESPNOW
|
||||
; -D WLED_DISABLE_BROWNOUT_DET
|
||||
;
|
||||
@@ -140,8 +139,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
; -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering)
|
||||
;
|
||||
; Use Audioreactive usermod and configure I2S microphone
|
||||
; ${esp32.AR_build_flags} ;; default flags required to properly configure ArduinoFFT
|
||||
; ;; don't forget to add ArduinoFFT to your libs_deps: ${esp32.AR_lib_deps}
|
||||
; -D AUDIOPIN=-1
|
||||
; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM
|
||||
; -D I2S_SDPIN=36
|
||||
@@ -180,7 +177,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
;
|
||||
; enable IR by setting remote type
|
||||
; -D IRTYPE=0 # 0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote
|
||||
;
|
||||
;
|
||||
; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues)
|
||||
; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue # needed only for classic ESP32 rev.1
|
||||
;
|
||||
@@ -194,6 +191,22 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
|
||||
; -D HW_PIN_MISOSPI=9
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Optional: build flags for speed, instead of optimising for size.
|
||||
# Example of usage: see [env:esp32S3_PSRAM_HUB75]
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
[Speed_Flags]
|
||||
build_unflags = -Os ;; to disable standard optimization for small size
|
||||
build_flags =
|
||||
-O2 ;; optimize for speed
|
||||
-free -fipa-pta ;; very useful, too
|
||||
;;-fsingle-precision-constant ;; makes all floating point literals "float" (default is "double")
|
||||
;;-funsafe-math-optimizations ;; less dangerous than -ffast-math; still allows the compiler to exploit FMA and reciprocals (up to 10% faster on -S3)
|
||||
# Important: we need to explicitly switch off some "-O2" optimizations
|
||||
-fno-jump-tables -fno-tree-switch-conversion ;; needed - firmware may crash otherwise
|
||||
-freorder-blocks -Wwrite-strings -fstrict-volatile-bitfields ;; needed - recommended by espressif
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# PRE-CONFIGURED DEVELOPMENT BOARDS AND CONTROLLERS
|
||||
@@ -241,9 +254,7 @@ lib_deps = ${esp8266.lib_deps}
|
||||
extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options)
|
||||
board = esp32dev
|
||||
build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
|
||||
${esp32.AR_build_flags} ;; optional - includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
@@ -255,9 +266,7 @@ board_build.flash_mode = qio
|
||||
extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment
|
||||
board = esp32dev
|
||||
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET
|
||||
${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
@@ -375,11 +384,9 @@ build_flags = ${common.build_flags} ${esp32.build_flags}
|
||||
-D USERMOD_DALLASTEMPERATURE
|
||||
-D USERMOD_FOUR_LINE_DISPLAY
|
||||
-D TEMPERATURE_PIN=23
|
||||
${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
OneWire@~2.3.5 ;; needed for USERMOD_DALLASTEMPERATURE
|
||||
olikraus/U8g2 @ ^2.28.8 ;; needed for USERMOD_FOUR_LINE_DISPLAY
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
|
||||
[env:esp32_pico-D4]
|
||||
@@ -391,13 +398,11 @@ build_flags = ${common.build_flags} ${esp32.build_flags}
|
||||
-D WLED_DISABLE_ADALIGHT ;; no serial-to-USB chip on this board - better to disable serial protocols
|
||||
-D DATA_PINS=2,18 ;; LED pins
|
||||
-D RLYPIN=19 -D BTNPIN=0 -D IRPIN=-1 ;; no default pin for IR
|
||||
${esp32.AR_build_flags} ;; include USERMOD_AUDIOREACTIVE
|
||||
-D UM_AUDIOREACTIVE_ENABLE ;; enable AR by default
|
||||
;; Audioreactive settings for on-board microphone (ICS-43432)
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14
|
||||
-D SR_SQUELCH=5 -D SR_GAIN=30
|
||||
lib_deps = ${esp32.lib_deps}
|
||||
${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.f_flash = 80000000L
|
||||
|
||||
@@ -525,6 +530,7 @@ build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOU
|
||||
-D USER_SETUP_LOADED
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Usermod examples
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -532,6 +538,118 @@ monitor_filters = esp32_exception_decoder
|
||||
# 433MHz RF remote example for esp32dev
|
||||
[env:esp32dev_usermod_RF433]
|
||||
extends = env:esp32dev
|
||||
build_flags = ${env:esp32dev.build_flags} -D USERMOD_RF433
|
||||
lib_deps = ${env:esp32dev.lib_deps}
|
||||
sui77/rc-switch @ 2.6.4
|
||||
custom_usermods =
|
||||
${env:esp32dev.custom_usermods}
|
||||
RF433
|
||||
|
||||
# External usermod from a git repository.
|
||||
# The library's `library.json` must include `"build": {"libArchive": false}`.
|
||||
# The name PlatformIO assigns is taken from the library's `library.json` "name" field.
|
||||
# If that name doesn't match the repo name in the URL, use the "LibName = URL" form
|
||||
# shown in the commented-out line below to supply the name explicitly.
|
||||
[env:esp32dev_external_usermod]
|
||||
extends = env:esp32dev
|
||||
custom_usermods =
|
||||
${env:esp32dev.custom_usermods}
|
||||
https://github.com/wled/wled-usermod-example.git#main
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Hub75 examples
|
||||
# ------------------------------------------------------------------------------
|
||||
# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it.
|
||||
|
||||
[env:esp32dev_hub75]
|
||||
board = esp32dev
|
||||
upload_speed = 921600
|
||||
platform = ${esp32_idf_V4.platform}
|
||||
platform_packages =
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32_hub75\"
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D WLED_DEBUG_BUS
|
||||
; -D WLED_DEBUG
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
|
||||
lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11
|
||||
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp32dev_hub75_forum_pinout]
|
||||
extends = env:esp32dev_hub75
|
||||
build_flags = ${common.build_flags}
|
||||
-D WLED_RELEASE_NAME=\"ESP32_hub75_forum_pinout\"
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins
|
||||
-D WLED_DEBUG_BUS
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
; -D WLED_DEBUG
|
||||
|
||||
|
||||
[env:adafruit_matrixportal_esp32s3]
|
||||
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
|
||||
board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload
|
||||
;; adafruit recommends to use arduino-esp32 2.0.14
|
||||
;;platform = espressif32@ ~6.5.0
|
||||
;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages =
|
||||
upload_speed = 921600
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\"
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
|
||||
-D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3
|
||||
-D WLED_DEBUG_BUS
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash
|
||||
|
||||
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
|
||||
|
||||
board_build.partitions = ${esp32.large_partitions} ;; standard bootloader and 8MB Flash partitions
|
||||
;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
custom_usermods = audioreactive
|
||||
|
||||
[env:esp32S3_PSRAM_HUB75]
|
||||
;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM)
|
||||
board = lilygo-t7-s3
|
||||
platform = ${esp32s3.platform}
|
||||
platform_packages =
|
||||
upload_speed = 921600
|
||||
build_unflags = ${common.build_unflags}
|
||||
${Speed_Flags.build_unflags} ;; optional: removes "-Os" so we can override with "-O2" in build_flags
|
||||
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"esp32S3_16MB_PSRAM_HUB75\"
|
||||
${Speed_Flags.build_flags} ;; optional: -O2 -> optimize for speed instead of size
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)
|
||||
-D WLED_WATCHDOG_TIMEOUT=0
|
||||
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
|
||||
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
|
||||
-D MOONHUB_S3_PINOUT ;; HUB75 pinout
|
||||
-D WLED_DEBUG_BUS
|
||||
-D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75
|
||||
-D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic
|
||||
|
||||
lib_deps = ${esp32s3.lib_deps}
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix
|
||||
|
||||
;;board_build.partitions = ${esp32.large_partitions} ;; for 8MB flash
|
||||
board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
monitor_filters = esp32_exception_decoder
|
||||
custom_usermods = audioreactive
|
||||
+1
-1
@@ -38,7 +38,7 @@ pyelftools==0.32
|
||||
# via platformio
|
||||
pyserial==3.5
|
||||
# via platformio
|
||||
requests==2.32.4
|
||||
requests==2.33.0
|
||||
# via platformio
|
||||
semantic-version==2.10.0
|
||||
# via platformio
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x300000,
|
||||
app1, app, ota_1, 0x310000,0x300000,
|
||||
spiffs, data, spiffs, 0x610000,0x19E0000,
|
||||
coredump, data, coredump,,64K
|
||||
|
+75
-11
@@ -26,7 +26,7 @@ const packageJson = require("../package.json");
|
||||
// Export functions for testing
|
||||
module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan };
|
||||
|
||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h", "wled00/js_iro.h", "wled00/js_omggif.h"]
|
||||
|
||||
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
|
||||
const wledBanner = `
|
||||
@@ -38,6 +38,11 @@ const wledBanner = `
|
||||
\t\t\x1b[36m build script for web UI
|
||||
\x1b[0m`;
|
||||
|
||||
// Generate build timestamp as UNIX timestamp (seconds since epoch)
|
||||
function generateBuildTime() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const singleHeader = `/*
|
||||
* Binary array for the Web UI.
|
||||
* gzip is used for smaller size and improved speeds.
|
||||
@@ -45,6 +50,9 @@ const singleHeader = `/*
|
||||
* Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui
|
||||
* to find out how to easily modify the web UI source!
|
||||
*/
|
||||
|
||||
// Automatically generated build time for cache busting (UNIX timestamp)
|
||||
#define WEB_BUILD_TIME ${generateBuildTime()}
|
||||
|
||||
`;
|
||||
|
||||
@@ -126,12 +134,13 @@ async function minify(str, type = "plain") {
|
||||
throw new Error("Unknown filter: " + type);
|
||||
}
|
||||
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) {
|
||||
console.info("Reading " + sourceFile);
|
||||
inline.html({
|
||||
fileContent: fs.readFileSync(sourceFile, "utf8"),
|
||||
relativeTo: path.dirname(sourceFile),
|
||||
strict: true,
|
||||
strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files)
|
||||
stylesheets: inlineCss // when true (default), css is inlined
|
||||
},
|
||||
async function (error, html) {
|
||||
if (error) throw error;
|
||||
@@ -143,7 +152,7 @@ async function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes");
|
||||
const array = hexdump(result);
|
||||
let src = singleHeader;
|
||||
src += `const uint16_t PAGE_${page}_L = ${result.length};\n`;
|
||||
src += `const uint16_t PAGE_${page}_length = ${result.length};\n`;
|
||||
src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`;
|
||||
console.info("Writing " + resultFile);
|
||||
fs.writeFileSync(resultFile, src);
|
||||
@@ -244,8 +253,63 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar
|
||||
|
||||
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
|
||||
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
|
||||
writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
||||
writeHtmlGzipped("wled00/data/pixelforge/pixelforge.htm", "wled00/html_pixelforge.h", 'pixelforge', false); // do not inline css
|
||||
//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit');
|
||||
|
||||
writeChunks(
|
||||
"wled00/data/",
|
||||
[
|
||||
{
|
||||
file: "iro.js",
|
||||
name: "JS_iro",
|
||||
method: "gzip",
|
||||
filter: "plain", // no minification, it is already minified
|
||||
mangle: (s) => s.replace(/^\/\*![\s\S]*?\*\//, '') // remove license comment at the top
|
||||
}
|
||||
],
|
||||
"wled00/js_iro.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data/pixelforge",
|
||||
[
|
||||
{
|
||||
file: "omggif.js",
|
||||
name: "JS_omggif",
|
||||
method: "gzip",
|
||||
filter: "js-minify",
|
||||
mangle: (s) => s.replace(/^\/\*![\s\S]*?\*\//, '') // remove license comment at the top
|
||||
}
|
||||
],
|
||||
"wled00/js_omggif.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data",
|
||||
[
|
||||
{
|
||||
file: "edit.htm",
|
||||
name: "PAGE_edit",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_edit.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data/cpal",
|
||||
[
|
||||
{
|
||||
file: "cpal.htm",
|
||||
name: "PAGE_cpal",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_cpal.h"
|
||||
);
|
||||
|
||||
writeChunks(
|
||||
"wled00/data",
|
||||
@@ -330,6 +394,12 @@ writeChunks(
|
||||
name: "PAGE_settings_pin",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
},
|
||||
{
|
||||
file: "settings_pininfo.htm",
|
||||
name: "PAGE_settings_pininfo",
|
||||
method: "gzip",
|
||||
filter: "html-minify"
|
||||
}
|
||||
],
|
||||
"wled00/html_settings.h"
|
||||
@@ -375,12 +445,6 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()=====";
|
||||
name: "PAGE_update",
|
||||
method: "gzip",
|
||||
filter: "html-minify",
|
||||
mangle: (str) =>
|
||||
str
|
||||
.replace(
|
||||
/function GetV().*\<\/script\>/gms,
|
||||
"</script><script src=\"/settings/s.js?p=9\"></script>"
|
||||
)
|
||||
},
|
||||
{
|
||||
file: "welcome.htm",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/* ESP32 linker script fragment to add dynamic array section to binary */
|
||||
SECTIONS
|
||||
{
|
||||
.dynarray :
|
||||
{
|
||||
. = ALIGN(0x10);
|
||||
KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))
|
||||
} > default_rodata_seg
|
||||
}
|
||||
INSERT AFTER .flash.rodata;
|
||||
@@ -0,0 +1,10 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table,, 0x8000, 4K
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, app, ota_0, 0x10000, 2048K,
|
||||
ota_1, app, ota_1, 0x210000, 2048K,
|
||||
uf2, app, factory,0x410000, 256K,
|
||||
spiffs, data, spiffs, 0x450000, 11968K,
|
||||
|
@@ -0,0 +1,11 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table, 0x8000, 4K
|
||||
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, 0, ota_0, 0x10000, 1408K,
|
||||
ota_1, 0, ota_1, 0x170000, 1408K,
|
||||
uf2, app, factory,0x2d0000, 256K,
|
||||
spiffs, data, spiffs, 0x310000, 960K,
|
||||
|
@@ -0,0 +1,10 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# bootloader.bin,, 0x1000, 32K
|
||||
# partition table,, 0x8000, 4K
|
||||
nvs, data, nvs, 0x9000, 20K,
|
||||
otadata, data, ota, 0xe000, 8K,
|
||||
ota_0, app, ota_0, 0x10000, 2048K,
|
||||
ota_1, app, ota_1, 0x210000, 2048K,
|
||||
uf2, app, factory,0x410000, 256K,
|
||||
spiffs, data, spiffs, 0x450000, 3776K,
|
||||
|
@@ -0,0 +1,696 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WBF ↔ C Header Bi-Directional Converter</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.card {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
background: #16213e;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 242, 255, .15);
|
||||
border: 1px solid rgba(0, 242, 255, .1);
|
||||
}
|
||||
h1 {
|
||||
color: #00f2ff;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2rem;
|
||||
text-shadow: 0 0 25px #ffffff;
|
||||
}
|
||||
.mode-selector {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mode-btn {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
border: 2px solid rgba(0, 242, 255, .3);
|
||||
background: rgba(0, 0, 0, .4);
|
||||
color: #888;
|
||||
transition: all .3s;
|
||||
}
|
||||
.mode-btn.active {
|
||||
background: linear-gradient(135deg, #0095b3 0%, #00d4ff 100%);
|
||||
border-color: #00f2ff;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 14px rgba(0, 242, 255, .6);
|
||||
}
|
||||
.mode-btn:hover:not(.active) {
|
||||
border-color: #00f2ff;
|
||||
color: #00f2ff;
|
||||
}
|
||||
.controls {
|
||||
background: rgba(255, 255, 255, .03);
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 242, 255, .1);
|
||||
display: none;
|
||||
}
|
||||
.controls.active {
|
||||
display: block;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: .85rem;
|
||||
font-weight: 600;
|
||||
color: #00d4ff;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
.file-label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 242, 255, .2);
|
||||
background: rgba(0, 0, 0, .4);
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all .3s;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.file-label:hover {
|
||||
border-color: #00f2ff;
|
||||
color: #00f2ff;
|
||||
}
|
||||
.file-label.has-file {
|
||||
color: #00f2ff;
|
||||
}
|
||||
input[type="text"], textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 242, 255, .2);
|
||||
background: rgba(0, 0, 0, .4);
|
||||
color: #fff;
|
||||
margin-bottom: 15px;
|
||||
transition: all .3s;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
textarea {
|
||||
min-height: 300px;
|
||||
resize: vertical;
|
||||
font-size: .85rem;
|
||||
}
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #00f2ff;
|
||||
box-shadow: 0 0 8px rgba(0, 242, 255, .3);
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #0095b3 0%, #00d4ff 100%);
|
||||
box-shadow: 0 0 14px rgba(0, 242, 255, .6);
|
||||
color: #fff;
|
||||
transition: all .3s;
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 18px rgba(0, 242, 255, .9);
|
||||
}
|
||||
button:disabled {
|
||||
opacity: .3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.info {
|
||||
background: rgba(0, 242, 255, .05);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 242, 255, .2);
|
||||
font-size: .85rem;
|
||||
}
|
||||
.output {
|
||||
background: rgba(0, 0, 0, .6);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0, 242, 255, .2);
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
.output.active {
|
||||
display: block;
|
||||
}
|
||||
pre {
|
||||
color: #00f2ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: .85rem;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.copy-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>WBF ↔ C Header Converter</h1>
|
||||
|
||||
<div class="mode-selector">
|
||||
<button class="mode-btn active" onclick="switchMode('wbf-to-header')">WBF → C Header</button>
|
||||
<button class="mode-btn" onclick="switchMode('header-to-wbf')">C Header → WBF</button>
|
||||
</div>
|
||||
|
||||
<!-- WBF to Header Mode -->
|
||||
<div id="wbf-to-header" class="controls active">
|
||||
<div class="info">
|
||||
Load a WLED Bitmap Font (.wbf) file and convert it to a C/C++ header file.
|
||||
</div>
|
||||
|
||||
<label>Select WBF Font File</label>
|
||||
<label for="wbfFile" class="file-label" id="wbfFileLabel">Choose .wbf file</label>
|
||||
<input type="file" id="wbfFile" accept=".wbf">
|
||||
|
||||
<label>Array Name</label>
|
||||
<input type="text" id="arrayName" placeholder="console_font_4x6" value="console_font_4x6">
|
||||
|
||||
<button id="convertToHeaderBtn" disabled>Convert to Header</button>
|
||||
</div>
|
||||
|
||||
<!-- Header to WBF Mode -->
|
||||
<div id="header-to-wbf" class="controls">
|
||||
<div class="info">
|
||||
Paste a C/C++ header file containing a WLED font array and convert it to a .wbf file.
|
||||
</div>
|
||||
|
||||
<label>Paste C Header Code</label>
|
||||
<textarea id="headerInput" placeholder="Paste your C header code here (e.g., static const unsigned char font[] PROGMEM = {...};)"></textarea>
|
||||
|
||||
<label>Output Filename (without .wbf extension)</label>
|
||||
<input type="text" id="wbfFilename" placeholder="console_font_4x6" value="console_font_4x6">
|
||||
|
||||
<button id="convertToWbfBtn" disabled>Convert to WBF</button>
|
||||
</div>
|
||||
|
||||
<div id="output" class="output">
|
||||
<pre id="outputCode"></pre>
|
||||
<button class="copy-btn" onclick="copyToClipboard(event)">Copy to Clipboard</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Elements for WBF to Header
|
||||
const wbfFileInput = document.getElementById('wbfFile');
|
||||
const wbfFileLabel = document.getElementById('wbfFileLabel');
|
||||
const arrayNameInput = document.getElementById('arrayName');
|
||||
const convertToHeaderBtn = document.getElementById('convertToHeaderBtn');
|
||||
|
||||
// Elements for Header to WBF
|
||||
const headerInput = document.getElementById('headerInput');
|
||||
const wbfFilenameInput = document.getElementById('wbfFilename');
|
||||
const convertToWbfBtn = document.getElementById('convertToWbfBtn');
|
||||
|
||||
// Output elements
|
||||
const output = document.getElementById('output');
|
||||
const outputCode = document.getElementById('outputCode');
|
||||
|
||||
let wbfData = null;
|
||||
let fileName = '';
|
||||
|
||||
// Mode switching
|
||||
function switchMode(mode) {
|
||||
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.controls').forEach(ctrl => ctrl.classList.remove('active'));
|
||||
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(mode).classList.add('active');
|
||||
output.classList.remove('active');
|
||||
}
|
||||
|
||||
// ==================== WBF to Header ====================
|
||||
wbfFileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
fileName = file.name.replace(/\.[^.]+$/, '');
|
||||
wbfFileLabel.textContent = file.name;
|
||||
wbfFileLabel.classList.add('has-file');
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
wbfData = new Uint8Array(ev.target.result);
|
||||
convertToHeaderBtn.disabled = false;
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
});
|
||||
|
||||
convertToHeaderBtn.addEventListener('click', () => {
|
||||
if (!wbfData) return;
|
||||
|
||||
const header = parseWBF(wbfData);
|
||||
if (header) {
|
||||
generateHeader(header);
|
||||
}
|
||||
});
|
||||
|
||||
function parseWBF(data) {
|
||||
if (data.length < 12) {
|
||||
alert('Invalid WBF file: too short');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse header
|
||||
if (data[0] !== 0x57) { // 'W'
|
||||
alert('Invalid WBF file: missing magic byte');
|
||||
return null;
|
||||
}
|
||||
|
||||
const height = data[1];
|
||||
const maxWidth = data[2];
|
||||
const spacing = data[3];
|
||||
const flags = data[4];
|
||||
const first = data[5];
|
||||
const last = data[6];
|
||||
const reserved = data[7];
|
||||
|
||||
// Unicode offset is 32-bit little-endian
|
||||
const unicodeOffset = data[8] | (data[9] << 8) | (data[10] << 16) | (data[11] << 24);
|
||||
|
||||
const numChars = last - first + 1;
|
||||
const isVariableWidth = (flags & 0x01) !== 0;
|
||||
|
||||
let offset = 12; // Start after header
|
||||
let widthTable = null;
|
||||
|
||||
// If variable width, read width table
|
||||
if (isVariableWidth) {
|
||||
if (data.length < 12 + numChars) {
|
||||
alert('Invalid WBF file: missing width table');
|
||||
return null;
|
||||
}
|
||||
widthTable = Array.from(data.slice(12, 12 + numChars));
|
||||
offset = 12 + numChars;
|
||||
}
|
||||
|
||||
// Calculate expected data size
|
||||
let expectedDataSize = 0;
|
||||
if (isVariableWidth) {
|
||||
for (let w of widthTable) {
|
||||
expectedDataSize += Math.ceil((w * height) / 8);
|
||||
}
|
||||
} else {
|
||||
expectedDataSize = numChars * Math.ceil((maxWidth * height) / 8);
|
||||
}
|
||||
|
||||
if (data.length < offset + expectedDataSize) {
|
||||
alert(`Invalid WBF file: expected ${offset + expectedDataSize} bytes, got ${data.length}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
height,
|
||||
maxWidth,
|
||||
spacing,
|
||||
flags,
|
||||
first,
|
||||
last,
|
||||
reserved,
|
||||
unicodeOffset,
|
||||
data: data,
|
||||
isVariableWidth,
|
||||
widthTable,
|
||||
dataOffset: offset
|
||||
};
|
||||
}
|
||||
|
||||
function generateHeader(header) {
|
||||
const arrayName = arrayNameInput.value || 'console_font';
|
||||
let code = '';
|
||||
|
||||
// Header comment
|
||||
code += `// Font: ${fileName}\n`;
|
||||
code += `// Height: ${header.height}, Max Width: ${header.maxWidth}, Spacing: ${header.spacing}\n`;
|
||||
code += `// Characters: ${header.first}-${header.last} (${header.last - header.first + 1} glyphs)\n`;
|
||||
code += `// Unicode Offset: 0x${header.unicodeOffset.toString(16).padStart(8, '0').toUpperCase()}\n`;
|
||||
code += `// Variable Width: ${header.isVariableWidth ? 'Yes' : 'No'}\n\n`;
|
||||
|
||||
// Array declaration
|
||||
code += `static const unsigned char ${arrayName}[] PROGMEM = {\n`;
|
||||
|
||||
// Header bytes (12 bytes)
|
||||
code += ' ';
|
||||
code += `0x${header.data[0].toString(16).padStart(2, '0').toUpperCase()}, `; // Magic 'W'
|
||||
code += `0x${header.height.toString(16).padStart(2, '0').toUpperCase()}, `; // Height
|
||||
code += `0x${header.maxWidth.toString(16).padStart(2, '0').toUpperCase()}, `; // Max Width
|
||||
code += `0x${header.spacing.toString(16).padStart(2, '0').toUpperCase()}, `; // Spacing
|
||||
code += `0x${header.flags.toString(16).padStart(2, '0').toUpperCase()}, `; // Flags
|
||||
code += `0x${header.first.toString(16).padStart(2, '0').toUpperCase()}, `; // First char
|
||||
code += `0x${header.last.toString(16).padStart(2, '0').toUpperCase()}, `; // Last char
|
||||
code += `0x${header.reserved.toString(16).padStart(2, '0').toUpperCase()}, `; // Reserved
|
||||
|
||||
// Unicode offset (4 bytes, little-endian)
|
||||
code += `0x${header.data[8].toString(16).padStart(2, '0').toUpperCase()}, `;
|
||||
code += `0x${header.data[9].toString(16).padStart(2, '0').toUpperCase()}, `;
|
||||
code += `0x${header.data[10].toString(16).padStart(2, '0').toUpperCase()}, `;
|
||||
code += `0x${header.data[11].toString(16).padStart(2, '0').toUpperCase()}`;
|
||||
|
||||
code += ', // Header: \'W\', H, W, S, Flags, First, Last, Reserved, UnicodeOffset (32bit)\n\n';
|
||||
|
||||
// Width table (if variable width)
|
||||
if (header.isVariableWidth) {
|
||||
code += ' // Width table\n';
|
||||
const numChars = header.last - header.first + 1;
|
||||
for (let i = 0; i < numChars; i++) {
|
||||
if (i % 16 === 0) {
|
||||
if (i > 0) code += '\n';
|
||||
code += ' ';
|
||||
}
|
||||
code += `0x${header.widthTable[i].toString(16).padStart(2, '0').toUpperCase()}`;
|
||||
if (i < numChars - 1) {
|
||||
code += ', ';
|
||||
}
|
||||
}
|
||||
code += ',\n\n';
|
||||
}
|
||||
|
||||
// Character data
|
||||
const numChars = header.last - header.first + 1;
|
||||
let offset = header.dataOffset;
|
||||
|
||||
// First pass: calculate max byte width for alignment
|
||||
let maxByteWidth = 0;
|
||||
let tempOffset = header.dataOffset;
|
||||
for (let i = 0; i < numChars; i++) {
|
||||
const charWidth = header.isVariableWidth ? header.widthTable[i] : header.maxWidth;
|
||||
const bytesPerChar = Math.ceil((charWidth * header.height) / 8);
|
||||
const byteStr = Array(bytesPerChar).fill('0x00').join(', ');
|
||||
maxByteWidth = Math.max(maxByteWidth, byteStr.length);
|
||||
tempOffset += bytesPerChar;
|
||||
}
|
||||
|
||||
// Second pass: generate output with aligned comments
|
||||
for (let i = 0; i < numChars; i++) {
|
||||
const charCode = header.first + i;
|
||||
const charStr = getAsciiString(charCode);
|
||||
|
||||
// Calculate bytes for this character
|
||||
const charWidth = header.isVariableWidth ? header.widthTable[i] : header.maxWidth;
|
||||
const bytesPerChar = Math.ceil((charWidth * header.height) / 8);
|
||||
|
||||
code += ' ';
|
||||
let byteStr = '';
|
||||
for (let b = 0; b < bytesPerChar; b++) {
|
||||
const byte = header.data[offset++];
|
||||
byteStr += `0x${byte.toString(16).padStart(2, '0').toUpperCase()}`;
|
||||
if (b < bytesPerChar - 1) {
|
||||
byteStr += ', ';
|
||||
}
|
||||
}
|
||||
code += byteStr;
|
||||
code += ',';
|
||||
|
||||
// Pad to align comments
|
||||
const padding = ' '.repeat(maxByteWidth - byteStr.length + 5);
|
||||
|
||||
const widthInfo = header.isVariableWidth ? `, w=${charWidth}` : '';
|
||||
code += `${padding}/* code=${charCode}, hex=0x${charCode.toString(16).padStart(2, '0').toUpperCase()}, ascii="${charStr}"${widthInfo} */\n`;
|
||||
}
|
||||
|
||||
code += '};\n';
|
||||
|
||||
// Display output
|
||||
outputCode.textContent = code;
|
||||
output.classList.add('active');
|
||||
}
|
||||
|
||||
// ==================== Header to WBF ====================
|
||||
headerInput.addEventListener('input', () => {
|
||||
convertToWbfBtn.disabled = headerInput.value.trim().length === 0;
|
||||
});
|
||||
|
||||
convertToWbfBtn.addEventListener('click', () => {
|
||||
const headerText = headerInput.value.trim();
|
||||
if (!headerText) return;
|
||||
|
||||
const wbfData = parseHeaderToWBF(headerText);
|
||||
if (wbfData) {
|
||||
downloadWBF(wbfData);
|
||||
}
|
||||
});
|
||||
|
||||
function parseHeaderToWBF(headerText) {
|
||||
try {
|
||||
// Properly remove C-style comments
|
||||
let cleanedText = removeComments(headerText);
|
||||
|
||||
// Extract all hex values from the cleaned text
|
||||
const hexPattern = /0x[0-9a-fA-F]{2}/g;
|
||||
const hexValues = cleanedText.match(hexPattern);
|
||||
|
||||
if (!hexValues || hexValues.length < 12) {
|
||||
alert('Invalid header: Could not find enough hex values (need at least 12 for header)\nFound: ' + (hexValues ? hexValues.length : 0) + ' bytes');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert hex strings to bytes
|
||||
const allBytes = hexValues.map(hex => parseInt(hex, 16));
|
||||
|
||||
// Parse the header (first 12 bytes)
|
||||
const height = allBytes[1];
|
||||
const maxWidth = allBytes[2];
|
||||
const spacing = allBytes[3];
|
||||
const flags = allBytes[4];
|
||||
const first = allBytes[5];
|
||||
const last = allBytes[6];
|
||||
const reserved = allBytes[7];
|
||||
const unicodeOffset = allBytes[8] | (allBytes[9] << 8) | (allBytes[10] << 16) | (allBytes[11] << 24);
|
||||
|
||||
// Validate magic byte
|
||||
if (allBytes[0] !== 0x57) {
|
||||
alert('Invalid header: First byte should be 0x57 (magic \'W\'), found 0x' + allBytes[0].toString(16).padStart(2, '0').toUpperCase());
|
||||
return null;
|
||||
}
|
||||
|
||||
const numChars = last - first + 1;
|
||||
const isVariableWidth = (flags & 0x01) !== 0;
|
||||
|
||||
// Now we need to separate header, width table (if present), and bitmap data
|
||||
let dataStartIndex = 12; // After the 12-byte header
|
||||
let widthTable = null;
|
||||
|
||||
if (isVariableWidth) {
|
||||
// Extract width table
|
||||
widthTable = allBytes.slice(dataStartIndex, dataStartIndex + numChars);
|
||||
dataStartIndex += numChars;
|
||||
}
|
||||
|
||||
// Calculate expected bitmap data size
|
||||
let expectedBitmapSize = 0;
|
||||
if (isVariableWidth) {
|
||||
for (let w of widthTable) {
|
||||
expectedBitmapSize += Math.ceil((w * height) / 8);
|
||||
}
|
||||
} else {
|
||||
expectedBitmapSize = numChars * Math.ceil((maxWidth * height) / 8);
|
||||
}
|
||||
|
||||
// Extract bitmap data
|
||||
const bitmapData = allBytes.slice(dataStartIndex, dataStartIndex + expectedBitmapSize);
|
||||
|
||||
// Validate we have all the data
|
||||
const totalExpected = dataStartIndex + expectedBitmapSize;
|
||||
if (allBytes.length < totalExpected) {
|
||||
alert(`Invalid header: Expected at least ${totalExpected} bytes, found ${allBytes.length}\n\n` +
|
||||
`Header shows: ${numChars} characters from ${first} to ${last}\n` +
|
||||
`${isVariableWidth ? 'Variable width font' : 'Fixed width font'}\n` +
|
||||
`Height: ${height}, Max Width: ${maxWidth}\n` +
|
||||
`Expected bitmap size: ${expectedBitmapSize} bytes`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reconstruct the WBF file
|
||||
let wbfSize = 12; // Header
|
||||
if (isVariableWidth) {
|
||||
wbfSize += numChars; // Width table
|
||||
}
|
||||
wbfSize += expectedBitmapSize; // Bitmap data
|
||||
|
||||
const wbfData = new Uint8Array(wbfSize);
|
||||
let offset = 0;
|
||||
|
||||
// Write header
|
||||
wbfData[offset++] = 0x57; // Magic 'W'
|
||||
wbfData[offset++] = height;
|
||||
wbfData[offset++] = maxWidth;
|
||||
wbfData[offset++] = spacing;
|
||||
wbfData[offset++] = flags;
|
||||
wbfData[offset++] = first;
|
||||
wbfData[offset++] = last;
|
||||
wbfData[offset++] = reserved;
|
||||
wbfData[offset++] = unicodeOffset & 0xFF;
|
||||
wbfData[offset++] = (unicodeOffset >> 8) & 0xFF;
|
||||
wbfData[offset++] = (unicodeOffset >> 16) & 0xFF;
|
||||
wbfData[offset++] = (unicodeOffset >> 24) & 0xFF;
|
||||
|
||||
// Write width table if variable width
|
||||
if (isVariableWidth) {
|
||||
for (let w of widthTable) {
|
||||
wbfData[offset++] = w;
|
||||
}
|
||||
}
|
||||
|
||||
// Write bitmap data
|
||||
for (let byte of bitmapData) {
|
||||
wbfData[offset++] = byte;
|
||||
}
|
||||
|
||||
// Show success message
|
||||
outputCode.textContent = `Successfully parsed header!\n\n` +
|
||||
`Height: ${height}\n` +
|
||||
`Max Width: ${maxWidth}\n` +
|
||||
`Spacing: ${spacing}\n` +
|
||||
`Flags: 0x${flags.toString(16).padStart(2, '0').toUpperCase()} (${isVariableWidth ? 'Variable Width' : 'Fixed Width'})\n` +
|
||||
`Characters: ${first}-${last} (${numChars} glyphs)\n` +
|
||||
`Unicode Offset: 0x${unicodeOffset.toString(16).padStart(8, '0').toUpperCase()}\n` +
|
||||
`Total Size: ${wbfData.length} bytes\n` +
|
||||
` Header: 12 bytes\n` +
|
||||
(isVariableWidth ? ` Width table: ${numChars} bytes\n` : '') +
|
||||
` Bitmap data: ${expectedBitmapSize} bytes\n` +
|
||||
`Parsed ${allBytes.length} total hex values\n\n` +
|
||||
`Your .wbf file is ready to download!`;
|
||||
output.classList.add('active');
|
||||
|
||||
return wbfData;
|
||||
} catch (error) {
|
||||
alert('Error parsing header: ' + error.message);
|
||||
console.error('Parse error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Proper C-style comment removal
|
||||
function removeComments(code) {
|
||||
let result = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < code.length) {
|
||||
// Check for // comment (single-line)
|
||||
if (code[i] === '/' && code[i + 1] === '/') {
|
||||
// Skip until end of line
|
||||
i += 2;
|
||||
while (i < code.length && code[i] !== '\n') {
|
||||
i++;
|
||||
}
|
||||
// Keep the newline
|
||||
if (i < code.length) {
|
||||
result += '\n';
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// Check for /* comment (multi-line)
|
||||
else if (code[i] === '/' && code[i + 1] === '*') {
|
||||
// Skip until we find */
|
||||
i += 2;
|
||||
while (i < code.length - 1) {
|
||||
if (code[i] === '*' && code[i + 1] === '/') {
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
// Preserve newlines in multi-line comments (for line counting if needed)
|
||||
if (code[i] === '\n') {
|
||||
result += '\n';
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// Regular character
|
||||
else {
|
||||
result += code[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function downloadWBF(data) {
|
||||
const filename = wbfFilenameInput.value.trim() || 'font';
|
||||
const blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${filename}.wbf`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
// ==================== Utility Functions ====================
|
||||
function getAsciiString(code) {
|
||||
if (code < 32) {
|
||||
// Control characters
|
||||
return '^' + String.fromCharCode(64 + code);
|
||||
} else if (code === 127) {
|
||||
return '^?';
|
||||
} else if (code >= 32 && code <= 126) {
|
||||
// Printable ASCII
|
||||
return String.fromCharCode(code);
|
||||
} else {
|
||||
// Extended ASCII
|
||||
return '\\x' + code.toString(16).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(event) {
|
||||
const text = outputCode.textContent;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const btn = event.target;
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Clipboard error:', err);
|
||||
// Fallback method for older browsers
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
const btn = event.target;
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
alert('Failed to copy to clipboard: ' + e.message);
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+78
-35
@@ -28,28 +28,78 @@ log() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Generic curl handler function
|
||||
curl_handler() {
|
||||
local command="$1"
|
||||
local hostname="$2"
|
||||
# Fetch a URL to a destination file, validating status codes.
|
||||
# Usage: fetch "<url>" "<dest or empty>" "200 404"
|
||||
fetch() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local accepted="${3:-200}"
|
||||
|
||||
response=$($command -w "%{http_code}" -o /dev/null)
|
||||
curl_exit_code=$?
|
||||
|
||||
if [ "$response" -ge 200 ] && [ "$response" -lt 300 ]; then
|
||||
return 0
|
||||
elif [ $curl_exit_code -ne 0 ]; then
|
||||
log "ERROR" "$RED" "Connection error during request to $hostname (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
elif [ "$response" -ge 400 ]; then
|
||||
log "ERROR" "$RED" "Server error during request to $hostname (HTTP status code: $response)."
|
||||
return 2
|
||||
# If no dest given, just discard body
|
||||
local out
|
||||
if [ -n "$dest" ]; then
|
||||
# Write to ".tmp" files first, then move when success, to ensure we don't write partial files
|
||||
out="${dest}.tmp"
|
||||
else
|
||||
log "ERROR" "$RED" "Unexpected response from $hostname (HTTP status code: $response)."
|
||||
return 3
|
||||
out="/dev/null"
|
||||
fi
|
||||
|
||||
response=$(curl --connect-timeout 5 --max-time 30 -s -w "%{http_code}" -o "$out" "$url")
|
||||
local curl_exit_code=$?
|
||||
|
||||
if [ $curl_exit_code -ne 0 ]; then
|
||||
[ -n "$dest" ] && rm -f "$out"
|
||||
log "ERROR" "$RED" "Connection error during request to $url (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for code in $accepted; do
|
||||
if [ "$response" = "$code" ]; then
|
||||
# Accepted; only persist body for 2xx responses
|
||||
if [ -n "$dest" ]; then
|
||||
if [[ "$response" =~ ^2 ]]; then
|
||||
mv "$out" "$dest"
|
||||
else
|
||||
rm -f "$out"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# not accepted
|
||||
[ -n "$dest" ] && rm -f "$out"
|
||||
log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)."
|
||||
return 2
|
||||
}
|
||||
|
||||
|
||||
# POST a file to a URL, validating status codes.
|
||||
# Usage: post_file "<url>" "<file>" "200"
|
||||
post_file() {
|
||||
local url="$1"
|
||||
local file="$2"
|
||||
local accepted="${3:-200}"
|
||||
|
||||
response=$(curl --connect-timeout 5 --max-time 300 -s -w "%{http_code}" -o /dev/null -X POST -F "file=@$file" "$url")
|
||||
local curl_exit_code=$?
|
||||
|
||||
if [ $curl_exit_code -ne 0 ]; then
|
||||
log "ERROR" "$RED" "Connection error during POST to $url (curl exit code: $curl_exit_code)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for code in $accepted; do
|
||||
if [ "$response" -eq "$code" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)."
|
||||
return 2
|
||||
}
|
||||
|
||||
|
||||
# Print help message
|
||||
show_help() {
|
||||
cat << EOF
|
||||
@@ -109,33 +159,27 @@ backup_one() {
|
||||
local address="$2"
|
||||
local port="$3"
|
||||
|
||||
log "INFO" "$YELLOW" "Backing up device config/presets: $hostname ($address:$port)"
|
||||
log "INFO" "$YELLOW" "Backing up device config/presets/ir: $hostname ($address:$port)"
|
||||
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
local cfg_url="http://$address:$port/cfg.json"
|
||||
local presets_url="http://$address:$port/presets.json"
|
||||
local cfg_dest="${backup_dir}/${hostname}.cfg.json"
|
||||
local presets_dest="${backup_dir}/${hostname}.presets.json"
|
||||
local file_prefix="${backup_dir}/${hostname}"
|
||||
|
||||
# Write to ".tmp" files first, then move when success, to ensure we don't write partial files
|
||||
local curl_command_cfg="curl -s "$cfg_url" -o "$cfg_dest.tmp""
|
||||
local curl_command_presets="curl -s "$presets_url" -o "$presets_dest.tmp""
|
||||
|
||||
if ! curl_handler "$curl_command_cfg" "$hostname"; then
|
||||
if ! fetch "http://$address:$port/cfg.json" "${file_prefix}.cfg.json"; then
|
||||
log "ERROR" "$RED" "Failed to backup configuration for $hostname"
|
||||
rm -f "$cfg_dest.tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! curl_handler "$curl_command_presets" "$hostname"; then
|
||||
if ! fetch "http://$address:$port/presets.json" "${file_prefix}.presets.json"; then
|
||||
log "ERROR" "$RED" "Failed to backup presets for $hostname"
|
||||
rm -f "$presets_dest.tmp"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ir.json is optional
|
||||
if ! fetch "http://$address:$port/ir.json" "${file_prefix}.ir.json" "200 404"; then
|
||||
log "ERROR" "$RED" "Failed to backup ir configs for $hostname"
|
||||
fi
|
||||
|
||||
mv "$cfg_dest.tmp" "$cfg_dest"
|
||||
mv "$presets_dest.tmp" "$presets_dest"
|
||||
log "INFO" "$GREEN" "Successfully backed up config and presets for $hostname"
|
||||
return 0
|
||||
}
|
||||
@@ -150,9 +194,8 @@ update_one() {
|
||||
log "INFO" "$YELLOW" "Starting firmware update for device: $hostname ($address:$port)"
|
||||
|
||||
local url="http://$address:$port/update"
|
||||
local curl_command="curl -s -X POST -F "file=@$firmware" "$url""
|
||||
|
||||
if ! curl_handler "$curl_command" "$hostname"; then
|
||||
if ! post_file "$url" "$firmware" "200"; then
|
||||
log "ERROR" "$RED" "Failed to update firmware for $hostname"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
/*
|
||||
* Usermod for analog clock
|
||||
*/
|
||||
extern Timezone* tz;
|
||||
|
||||
class AnalogClockUsermod : public Usermod {
|
||||
private:
|
||||
@@ -116,7 +115,7 @@ private:
|
||||
);
|
||||
}
|
||||
|
||||
static inline uint32_t scale32(uint32_t c, fract8 scale) {
|
||||
static inline uint32_t scale32(uint32_t c, uint8_t scale) {
|
||||
return RGBW32(
|
||||
scale8(R(c), scale),
|
||||
scale8(G(c), scale),
|
||||
|
||||
@@ -313,11 +313,11 @@ class MyExampleUsermod : public Usermod {
|
||||
yield();
|
||||
// ignore certain button types as they may have other consequences
|
||||
if (!enabled
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "wled.h"
|
||||
#include "FXparticleSystem.h"
|
||||
|
||||
unsigned long nextCometCreationTime = 0;
|
||||
|
||||
#define FX_FALLBACK_STATIC { SEGMENT.fill(SEGCOLOR(0)); return; }
|
||||
// Use UINT32_MAX - 1 for the "no comet" case so we can add 1 later and not have it overflow
|
||||
#define NULL_INDEX UINT32_MAX - 1
|
||||
|
||||
///////////////////////
|
||||
// Effect Function //
|
||||
///////////////////////
|
||||
|
||||
void mode_pscomet() {
|
||||
ParticleSystem2D *PartSys = nullptr;
|
||||
uint32_t i;
|
||||
|
||||
if (SEGMENT.call == 0) { // Initialization
|
||||
// Try to allocate one comet for every column
|
||||
if (!initParticleSystem2D(PartSys, SEGMENT.vWidth())) {
|
||||
FX_FALLBACK_STATIC; // Allocation failed or not 2D
|
||||
}
|
||||
PartSys->setMotionBlur(170); // Enable motion blur
|
||||
PartSys->setParticleSize(0); // Allow small comets to be a single pixel wide
|
||||
}
|
||||
else {
|
||||
PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // If not first call, use existing data
|
||||
}
|
||||
if (PartSys == nullptr || SEGMENT.vHeight() < 2 || SEGMENT.vWidth() < 2) {
|
||||
FX_FALLBACK_STATIC;
|
||||
}
|
||||
|
||||
PartSys->updateSystem(); // Update system properties (dimensions and data pointers)
|
||||
|
||||
auto has_fallen_off_screen = [PartSys](uint32_t particleIndex) {
|
||||
return particleIndex < PartSys->numSources
|
||||
? PartSys->sources[particleIndex].source.y < PartSys->maxY * -1
|
||||
: true;
|
||||
};
|
||||
|
||||
// This will be SEGMENT.vWidth() unless the particle system had insufficient memory
|
||||
uint32_t numComets = PartSys->numSources;
|
||||
// Pick a random column for a new comet to spawn, but reset it to null if it's not time yet or there's already a
|
||||
// comet nearby
|
||||
uint32_t chosenIndex = hw_random(numComets);
|
||||
if (
|
||||
strip.now < nextCometCreationTime
|
||||
|| !has_fallen_off_screen(chosenIndex - 1)
|
||||
|| !has_fallen_off_screen(chosenIndex)
|
||||
|| !has_fallen_off_screen(chosenIndex + 1)
|
||||
) {
|
||||
chosenIndex = NULL_INDEX;
|
||||
} else {
|
||||
uint16_t cometFrequencyDelay = 2040 - (SEGMENT.intensity << 3);
|
||||
nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay);
|
||||
}
|
||||
uint8_t canLargeCometSpawn =
|
||||
// Slider 3 determines % of large comets with extra particle sources on their sides
|
||||
SEGMENT.custom1 > hw_random8(254)
|
||||
&& chosenIndex != 0
|
||||
&& chosenIndex != numComets - 1;
|
||||
uint8_t fallingSpeed = 1 + (SEGMENT.speed >> 2);
|
||||
|
||||
// Update the comets
|
||||
for (i = 0; i < numComets; i++) {
|
||||
auto& source = PartSys->sources[i];
|
||||
auto& sourceParticle = source.source;
|
||||
|
||||
if (!has_fallen_off_screen(i)) {
|
||||
// Active comets fall downwards and emit flames
|
||||
sourceParticle.y -= fallingSpeed;
|
||||
source.vy = (SEGMENT.speed >> 5) - fallingSpeed; // Emitting speed (upwards)
|
||||
PartSys->flameEmit(PartSys->sources[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isChosenComet = i == chosenIndex;
|
||||
bool isChosenSideComet =
|
||||
canLargeCometSpawn &&
|
||||
(i == chosenIndex - 1 || i == chosenIndex + 1);
|
||||
|
||||
// Chosen comets respawn at the top
|
||||
if (isChosenComet || isChosenSideComet) {
|
||||
// Map the comet index into an output pixel index
|
||||
sourceParticle.x = i * PartSys->maxX / (SEGMENT.vWidth() - 1);
|
||||
// Spawn a bit above the top to avoid popping into view
|
||||
sourceParticle.y = PartSys->maxY + (2 * fallingSpeed);
|
||||
if (isChosenComet) {
|
||||
// Slider 4 controls comet length via particle lifetime and fire intensity adjustments
|
||||
source.maxLife = 16 + (SEGMENT.custom2 >> 2);
|
||||
source.minLife = source.maxLife >> 1;
|
||||
sourceParticle.ttl = 16 - (SEGMENT.custom2 >> 4);
|
||||
} else {
|
||||
// Side comets have fixed length
|
||||
source.maxLife = 18;
|
||||
source.minLife = 14;
|
||||
sourceParticle.ttl = 16;
|
||||
// Shift side comets up by 1 pixel
|
||||
sourceParticle.y += 2 * PartSys->maxY / (SEGMENT.vHeight() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slider 4 controls comet length via particle lifetime and fire intensity adjustments
|
||||
PartSys->updateFire(max(255U - SEGMENT.custom2, 45U));
|
||||
}
|
||||
static const char _data_FX_MODE_PSCOMET[] PROGMEM = "PS Comet@Falling Speed,Comet Frequency,Large Comet Probability,Comet Length;;!;2;pal=35,sx=128,ix=255,c1=32,c2=128";
|
||||
|
||||
/////////////////////
|
||||
// UserMod Class //
|
||||
/////////////////////
|
||||
|
||||
class PSCometUsermod : public Usermod {
|
||||
public:
|
||||
void setup() override {
|
||||
strip.addEffect(255, &mode_pscomet, _data_FX_MODE_PSCOMET);
|
||||
}
|
||||
|
||||
void loop() override {}
|
||||
};
|
||||
|
||||
static PSCometUsermod ps_comet;
|
||||
REGISTER_USERMOD(ps_comet);
|
||||
@@ -0,0 +1,25 @@
|
||||
## Description
|
||||
|
||||
A 2D falling comet effect similar to "Matrix" but with a fire particle simulation to enhance the comet trail visuals. Works with custom color palettes, defaulting to "Fire". Supports "small" and "large" comets which are 1px and 3px wide respectively.
|
||||
|
||||
Demo: [https://imgur.com/a/i1v5WAy](https://imgur.com/a/i1v5WAy)
|
||||
|
||||
## Installation
|
||||
|
||||
To activate the usermod, add the following line to your platformio_override.ini
|
||||
```ini
|
||||
custom_usermods = ps_comet
|
||||
```
|
||||
Or if you are already using a usermod, append ps_comet to the list
|
||||
```ini
|
||||
custom_usermods = audioreactive ps_comet
|
||||
```
|
||||
|
||||
You should now see "PS Comet" appear in your effect list.
|
||||
|
||||
## Parameters
|
||||
|
||||
1. **Falling Speed** sets how fast the comets fall
|
||||
2. **Comet Frequency** determines how many comets are on screen at a time
|
||||
3. **Large Comet Probability** determines how often large 3px wide comets spawn
|
||||
4. **Comet Length** sets how far comet trails stretch vertically
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "PS Comet",
|
||||
"build": { "libArchive": false }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "UsermodTemperature.h"
|
||||
|
||||
static uint16_t mode_temperature();
|
||||
static void mode_temperature();
|
||||
|
||||
//Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013
|
||||
float UsermodTemperature::readDallas() {
|
||||
@@ -20,17 +20,19 @@ float UsermodTemperature::readDallas() {
|
||||
}
|
||||
#endif
|
||||
switch(sensorFound) {
|
||||
case 0x10: // DS18S20 has 9-bit precision
|
||||
case 0x10: // DS18S20 has 9-bit precision - 1-bit fraction part
|
||||
result = (data[1] << 8) | data[0];
|
||||
retVal = float(result) * 0.5f;
|
||||
break;
|
||||
case 0x22: // DS18B20
|
||||
case 0x28: // DS1822
|
||||
case 0x22: // DS1822
|
||||
case 0x28: // DS18B20
|
||||
case 0x3B: // DS1825
|
||||
case 0x42: // DS28EA00
|
||||
result = (data[1]<<4) | (data[0]>>4); // we only need whole part, we will add fraction when returning
|
||||
if (data[1] & 0x80) result |= 0xF000; // fix negative value
|
||||
retVal = float(result) + ((data[0] & 0x08) ? 0.5f : 0.0f);
|
||||
// 12-bit precision - 4-bit fraction part
|
||||
result = (data[1] << 8) | data[0];
|
||||
// Clear LSBs to match desired precision (9/10/11-bit) rounding towards negative infinity
|
||||
result &= 0xFFFF << (3 - (resolution & 3));
|
||||
retVal = float(result) * 0.0625f; // 2^(-4)
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -69,8 +71,8 @@ bool UsermodTemperature::findSensor() {
|
||||
if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) {
|
||||
switch (deviceAddress[0]) {
|
||||
case 0x10: // DS18S20
|
||||
case 0x22: // DS18B20
|
||||
case 0x28: // DS1822
|
||||
case 0x22: // DS1822
|
||||
case 0x28: // DS18B20
|
||||
case 0x3B: // DS1825
|
||||
case 0x42: // DS28EA00
|
||||
DEBUG_PRINTLN(F("Sensor found."));
|
||||
@@ -277,6 +279,7 @@ void UsermodTemperature::addToConfig(JsonObject &root) {
|
||||
top[FPSTR(_parasite)] = parasite;
|
||||
top[FPSTR(_parasitePin)] = parasitePin;
|
||||
top[FPSTR(_domoticzIDX)] = idx;
|
||||
top[FPSTR(_resolution)] = resolution;
|
||||
DEBUG_PRINTLN(F("Temperature config saved."));
|
||||
}
|
||||
|
||||
@@ -304,6 +307,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
|
||||
parasite = top[FPSTR(_parasite)] | parasite;
|
||||
parasitePin = top[FPSTR(_parasitePin)] | parasitePin;
|
||||
idx = top[FPSTR(_domoticzIDX)] | idx;
|
||||
resolution = top[FPSTR(_resolution)] | resolution;
|
||||
|
||||
if (!initDone) {
|
||||
// first run: reading from cfg.json
|
||||
@@ -324,7 +328,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
|
||||
}
|
||||
}
|
||||
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
|
||||
return !top[FPSTR(_domoticzIDX)].isNull();
|
||||
return !top[FPSTR(_resolution)].isNull();
|
||||
}
|
||||
|
||||
void UsermodTemperature::appendConfigData() {
|
||||
@@ -332,6 +336,14 @@ void UsermodTemperature::appendConfigData() {
|
||||
oappend(F("',1,'<i>(if no Vcc connected)</i>');")); // 0 is field type, 1 is actual field
|
||||
oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str());
|
||||
oappend(F("',1,'<i>(for external MOSFET)</i>');")); // 0 is field type, 1 is actual field
|
||||
oappend(F("dd=addDD('")); oappend(String(FPSTR(_name)).c_str());
|
||||
oappend(F("','")); oappend(String(FPSTR(_resolution)).c_str()); oappend(F("');"));
|
||||
oappend(F("addO(dd,'0.5 °C (9-bit)',0);"));
|
||||
oappend(F("addO(dd,'0.25°C (10-bit)',1);"));
|
||||
oappend(F("addO(dd,'0.125°C (11-bit)',2);"));
|
||||
oappend(F("addO(dd,'0.0625°C (12-bit)',3);"));
|
||||
oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_resolution)).c_str());
|
||||
oappend(F("',1,'<i>(ignored on DS18S20)</i>');")); // 0 is field type, 1 is actual field
|
||||
}
|
||||
|
||||
float UsermodTemperature::getTemperature() {
|
||||
@@ -351,18 +363,18 @@ const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s";
|
||||
const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr";
|
||||
const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin";
|
||||
const char UsermodTemperature::_domoticzIDX[] PROGMEM = "domoticz-idx";
|
||||
const char UsermodTemperature::_resolution[] PROGMEM = "resolution";
|
||||
const char UsermodTemperature::_sensor[] PROGMEM = "sensor";
|
||||
const char UsermodTemperature::_temperature[] PROGMEM = "temperature";
|
||||
const char UsermodTemperature::_Temperature[] PROGMEM = "/temperature";
|
||||
const char UsermodTemperature::_data_fx[] PROGMEM = "Temperature@Min,Max;;!;01;pal=54,sx=255,ix=0";
|
||||
|
||||
static uint16_t mode_temperature() {
|
||||
static void mode_temperature() {
|
||||
float low = roundf(mapf((float)SEGMENT.speed, 0.f, 255.f, -150.f, 150.f)); // default: 15°C, range: -15°C to 15°C
|
||||
float high = roundf(mapf((float)SEGMENT.intensity, 0.f, 255.f, 300.f, 600.f)); // default: 30°C, range 30°C to 60°C
|
||||
float temp = constrain(UsermodTemperature::getInstance()->getTemperatureC()*10.f, low, high); // get a little better resolution (*10)
|
||||
unsigned i = map(roundf(temp), (unsigned)low, (unsigned)high, 0, 248);
|
||||
SEGMENT.fill(SEGMENT.color_from_palette(i, false, false, 255));
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class UsermodTemperature : public Usermod {
|
||||
|
||||
bool HApublished = false;
|
||||
int16_t idx = -1; // Domoticz virtual sensor idx
|
||||
uint8_t resolution = 0; // 9bits=0, 10bits=1, 11bits=2, 12bits=3
|
||||
|
||||
// strings to reduce flash memory usage (used more than twice)
|
||||
static const char _name[];
|
||||
@@ -56,6 +57,7 @@ class UsermodTemperature : public Usermod {
|
||||
static const char _parasite[];
|
||||
static const char _parasitePin[];
|
||||
static const char _domoticzIDX[];
|
||||
static const char _resolution[];
|
||||
static const char _sensor[];
|
||||
static const char _temperature[];
|
||||
static const char _Temperature[];
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
#include "tetrisaigame.h"
|
||||
// By: muebau
|
||||
|
||||
bool noFlashOnClear = false;
|
||||
|
||||
typedef struct TetrisAI_data
|
||||
{
|
||||
unsigned long lastTime = 0;
|
||||
unsigned long clearingStartTime = 0;
|
||||
TetrisAIGame tetris;
|
||||
uint8_t intelligence;
|
||||
uint8_t rotate;
|
||||
@@ -31,16 +34,27 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
|
||||
//GRID
|
||||
for (auto index_y = 4; index_y < tetris->grid.height; index_y++)
|
||||
{
|
||||
bool isRowClearing = tetris->grid.gridBW.clearingRows[index_y];
|
||||
for (auto index_x = 0; index_x < tetris->grid.width; index_x++)
|
||||
{
|
||||
CRGB color;
|
||||
if (*tetris->grid.getPixel(index_x, index_y) == 0)
|
||||
{
|
||||
uint8_t gridPixel = *tetris->grid.getPixel(index_x, index_y);
|
||||
if (isRowClearing) {
|
||||
if (noFlashOnClear) {
|
||||
color = CRGB::Gray;
|
||||
} else {
|
||||
//flash color white and black every 200ms
|
||||
color = (strip.now % 200) < 150
|
||||
? CRGB::Gray
|
||||
: CRGB::Black;
|
||||
}
|
||||
}
|
||||
else if (gridPixel == 0) {
|
||||
//BG color
|
||||
color = SEGCOLOR(1);
|
||||
}
|
||||
//game over animation
|
||||
else if(*tetris->grid.getPixel(index_x, index_y) == 254)
|
||||
else if (gridPixel == 254)
|
||||
{
|
||||
//use fg
|
||||
color = SEGCOLOR(0);
|
||||
@@ -48,7 +62,7 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
|
||||
else
|
||||
{
|
||||
//spread the color over the whole palette
|
||||
uint8_t colorIndex = *tetris->grid.getPixel(index_x, index_y) * 32;
|
||||
uint8_t colorIndex = gridPixel * 32;
|
||||
colorIndex += tetrisai_data->colorOffset;
|
||||
color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND);
|
||||
}
|
||||
@@ -98,13 +112,13 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
|
||||
////////////////////////////
|
||||
// 2D Tetris AI //
|
||||
////////////////////////////
|
||||
uint16_t mode_2DTetrisAI()
|
||||
void mode_2DTetrisAI()
|
||||
{
|
||||
if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data)))
|
||||
{
|
||||
// not a 2D set-up
|
||||
SEGMENT.fill(SEGCOLOR(0));
|
||||
return 350;
|
||||
return;
|
||||
}
|
||||
TetrisAI_data* tetrisai_data = reinterpret_cast<TetrisAI_data*>(SEGENV.data);
|
||||
|
||||
@@ -170,6 +184,7 @@ uint16_t mode_2DTetrisAI()
|
||||
|
||||
tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces);
|
||||
tetrisai_data->tetris.state = TetrisAIGame::States::INIT;
|
||||
tetrisai_data->clearingStartTime = 0;
|
||||
SEGMENT.fill(SEGCOLOR(1));
|
||||
}
|
||||
|
||||
@@ -184,7 +199,21 @@ uint16_t mode_2DTetrisAI()
|
||||
tetrisai_data->tetris.ai.bumpiness = -0.184483f + dui;
|
||||
}
|
||||
|
||||
if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE)
|
||||
//end line clearing flashing effect if needed
|
||||
if (tetrisai_data->tetris.grid.gridBW.hasClearingRows())
|
||||
{
|
||||
if (tetrisai_data->clearingStartTime == 0) {
|
||||
tetrisai_data->clearingStartTime = strip.now;
|
||||
}
|
||||
if (strip.now - tetrisai_data->clearingStartTime > 750)
|
||||
{
|
||||
tetrisai_data->tetris.grid.gridBW.clearedLinesReadyForRemoval = true;
|
||||
tetrisai_data->tetris.grid.cleanupFullLines();
|
||||
tetrisai_data->clearingStartTime = 0;
|
||||
}
|
||||
drawGrid(&tetrisai_data->tetris, tetrisai_data);
|
||||
}
|
||||
else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE)
|
||||
{
|
||||
|
||||
if (strip.now - tetrisai_data->lastTime > msDelayMove)
|
||||
@@ -222,8 +251,6 @@ uint16_t mode_2DTetrisAI()
|
||||
{
|
||||
tetrisai_data->tetris.poll();
|
||||
}
|
||||
|
||||
return FRAMETIME;
|
||||
} // mode_2DTetrisAI()
|
||||
static const char _data_FX_MODE_2DTETRISAI[] PROGMEM = "Tetris AI@!,Look ahead,Intelligence,Rotate color,Mistake free,Show next,Border,Mistakes;Game Over,!,Border;!;2;sx=127,ix=64,c1=255,c2=0,c3=31,o1=1,o2=1,o3=0,pal=11";
|
||||
|
||||
@@ -231,6 +258,7 @@ class TetrisAIUsermod : public Usermod
|
||||
{
|
||||
|
||||
private:
|
||||
static const char _name[];
|
||||
|
||||
public:
|
||||
void setup()
|
||||
@@ -238,6 +266,20 @@ public:
|
||||
strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI);
|
||||
}
|
||||
|
||||
void addToConfig(JsonObject& root) override
|
||||
{
|
||||
JsonObject top = root.createNestedObject(FPSTR(_name));
|
||||
top["noFlashOnClear"] = noFlashOnClear;
|
||||
}
|
||||
|
||||
bool readFromConfig(JsonObject& root) override
|
||||
{
|
||||
JsonObject top = root[FPSTR(_name)];
|
||||
bool configComplete = !top.isNull();
|
||||
configComplete &= getJsonValue(top["noFlashOnClear"], noFlashOnClear);
|
||||
return configComplete;
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
|
||||
@@ -249,6 +291,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
const char TetrisAIUsermod::_name[] PROGMEM = "TetrisAI_v2";
|
||||
|
||||
static TetrisAIUsermod tetrisai_v2;
|
||||
REGISTER_USERMOD(tetrisai_v2);
|
||||
@@ -13,7 +13,6 @@
|
||||
#ifndef __GRIDBW_H__
|
||||
#define __GRIDBW_H__
|
||||
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
#include "pieces.h"
|
||||
|
||||
@@ -26,11 +25,18 @@ public:
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
std::vector<uint32_t> pixels;
|
||||
// When a row fills, we mark it here first so it can flash before being
|
||||
// fully removed.
|
||||
std::vector<bool> clearingRows;
|
||||
// True when a line clearing flashing effect is over and we're ready to
|
||||
// fully clean up the lines
|
||||
bool clearedLinesReadyForRemoval = false;
|
||||
|
||||
GridBW(uint8_t width, uint8_t height):
|
||||
width(width),
|
||||
height(height),
|
||||
pixels(height)
|
||||
pixels(height),
|
||||
clearingRows(height)
|
||||
{
|
||||
if (width > 32)
|
||||
{
|
||||
@@ -85,9 +91,26 @@ public:
|
||||
piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0;
|
||||
}
|
||||
|
||||
bool hasClearingRows()
|
||||
{
|
||||
for (bool rowClearing : clearingRows)
|
||||
{
|
||||
if (rowClearing)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void cleanupFullLines()
|
||||
{
|
||||
// Skip cleanup if there are rows clearing
|
||||
if (hasClearingRows() && !clearedLinesReadyForRemoval) {
|
||||
return;
|
||||
}
|
||||
uint8_t offset = 0;
|
||||
bool doneRemovingClearedLines = false;
|
||||
|
||||
//from "height - 1" to "0", so from bottom row to top
|
||||
for (uint8_t row = height; row-- > 0; )
|
||||
@@ -95,8 +118,13 @@ public:
|
||||
//full line?
|
||||
if (isLineFull(row))
|
||||
{
|
||||
offset++;
|
||||
pixels[row] = 0x0;
|
||||
if (clearedLinesReadyForRemoval) {
|
||||
offset++;
|
||||
pixels[row] = 0x0;
|
||||
doneRemovingClearedLines = true;
|
||||
} else {
|
||||
clearingRows[row] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -106,11 +134,20 @@ public:
|
||||
pixels[row] = 0x0;
|
||||
}
|
||||
}
|
||||
if (doneRemovingClearedLines) {
|
||||
clearingRows.assign(height, false);
|
||||
clearedLinesReadyForRemoval = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isLineFull(uint8_t y)
|
||||
{
|
||||
return pixels[y] == (uint32_t)((1 << width) - 1);
|
||||
return pixels[y] == (width >= 32 ? UINT32_MAX : (1U << width) - 1);
|
||||
}
|
||||
|
||||
bool isLineReadyForRemoval(uint8_t y)
|
||||
{
|
||||
return clearedLinesReadyForRemoval && isLineFull(y);
|
||||
}
|
||||
|
||||
void reset()
|
||||
@@ -122,6 +159,8 @@ public:
|
||||
|
||||
pixels.clear();
|
||||
pixels.resize(height);
|
||||
clearingRows.assign(height, false);
|
||||
clearedLinesReadyForRemoval = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ public:
|
||||
//from "height - 1" to "0", so from bottom row to top
|
||||
for (uint8_t y = height; y-- > 0; )
|
||||
{
|
||||
if (gridBW.isLineFull(y))
|
||||
if (gridBW.isLineReadyForRemoval(y))
|
||||
{
|
||||
offset++;
|
||||
for (uint8_t x = 0; x < width; x++)
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <bitset>
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
|
||||
#define numPieces 7
|
||||
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
|
||||
This usermod adds a self-playing Tetris game as an 'effect'. The mod requires version 0.14 or higher as it relies on matrix support. The effect was tested on an ESP32 4MB with a WS2812B 16x16 matrix.
|
||||
|
||||
Version 1.0
|
||||
PHOTOSENSITIVE EPILEPSY WARNING: By default the effect features a flashing animation on line clear. This can be disabled
|
||||
from the usermod settings page in WLED.
|
||||
|
||||
## Installation
|
||||
|
||||
Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)).
|
||||
To activate the usermod, add the following line to your platformio_override.ini
|
||||
`custom_usermods = tetrisai_v2`
|
||||
The effect will then become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)).
|
||||
|
||||
If needed simply add to `platformio_override.ini` (or `platformio_override.ini`):
|
||||
If needed simply add to `platformio_override.ini`:
|
||||
|
||||
```ini
|
||||
board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv
|
||||
|
||||
@@ -68,7 +68,7 @@ public:
|
||||
}
|
||||
|
||||
//line full if all ones in mask :-)
|
||||
if (grid.isLineFull(row))
|
||||
if (grid.isLineReadyForRemoval(row))
|
||||
{
|
||||
rating->fullLines++;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
#include "tetrisbag.h"
|
||||
|
||||
@@ -87,17 +86,12 @@ public:
|
||||
void queuePiece()
|
||||
{
|
||||
//move vector to left
|
||||
std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end());
|
||||
for (uint8_t i = 1; i < piecesQueue.size(); i++) {
|
||||
piecesQueue[i - 1] = piecesQueue[i];
|
||||
}
|
||||
piecesQueue[piecesQueue.size() - 1] = getNextPiece();
|
||||
}
|
||||
|
||||
void queuePiece(uint8_t idx)
|
||||
{
|
||||
//move vector to left
|
||||
std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end());
|
||||
piecesQueue[piecesQueue.size() - 1] = Piece(idx % nPieces);
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
bag.clear();
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
#include <driver/i2s.h>
|
||||
#include <driver/adc.h>
|
||||
|
||||
#ifdef WLED_ENABLE_DMX
|
||||
#error This audio reactive usermod is not compatible with DMX Out.
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG))
|
||||
@@ -24,6 +20,26 @@
|
||||
* ....
|
||||
*/
|
||||
|
||||
#define FFT_PREFER_EXACT_PEAKS // use Blackman-Harris FFT windowing instead of Flat Top -> results in "sharper" peaks and less "leaking" into other frequencies (credits to @softhack)
|
||||
|
||||
/*
|
||||
* Note on FFT variants:
|
||||
* - ArduinoFFT: uses floating point calculations, very slow on S2 and C3 (no FPU)
|
||||
* - ESP-IDF DSP library:
|
||||
- faster but uses ~13k of extra flash on ESP32 and S3
|
||||
* - uses integer math on S2 and C3: slightly less accurate but over 10x faster than ArduinoFFT and uses less flash
|
||||
- not available in IDF < 4.4
|
||||
* - ArduinoFFT is used by default on ESP32 and S3
|
||||
* - ESP-IDF DSP FFT with integer math is used by default on S2 and C3
|
||||
* - defines:
|
||||
* - UM_AUDIOREACTIVE_USE_ARDUINO_FFT: use ArduinoFFT library for FFT
|
||||
* - UM_AUDIOREACTIVE_USE_ESPDSP_FFT: use ESP-IDF DSP for FFT
|
||||
*/
|
||||
|
||||
//#define UM_AUDIOREACTIVE_USE_ESPDSP_FFT // default on S2 and C3
|
||||
//#define UM_AUDIOREACTIVE_USE_INTEGER_FFT // use integer FFT if using ESP-IDF DSP library, always used on S2 and C3 (UM_AUDIOREACTIVE_USE_ARDUINO_FFT takes priority)
|
||||
//#define UM_AUDIOREACTIVE_USE_ARDUINO_FFT // default on ESP32 and S3
|
||||
|
||||
#if !defined(FFTTASK_PRIORITY)
|
||||
#define FFTTASK_PRIORITY 1 // standard: looptask prio
|
||||
//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp
|
||||
@@ -103,6 +119,46 @@ static uint8_t maxVol = 31; // (was 10) Reasonable value for constant v
|
||||
static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated)
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_ESPDSP_FFT) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32))
|
||||
#define UM_AUDIOREACTIVE_USE_ARDUINO_FFT // use ArduinoFFT library for FFT instead of ESP-IDF DSP library by default on ESP32 and S3
|
||||
#endif
|
||||
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0)
|
||||
#define UM_AUDIOREACTIVE_USE_ARDUINO_FFT // DSP FFT library is not available in ESP-IDF < 4.4
|
||||
#endif
|
||||
|
||||
#ifdef UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
#include <arduinoFFT.h> // ArduinoFFT library for FFT and window functions
|
||||
#undef UM_AUDIOREACTIVE_USE_INTEGER_FFT // arduinoFFT has not integer support
|
||||
#else
|
||||
#include "dsps_fft2r.h" // ESP-IDF DSP library for FFT and window functions
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
#include "dsps_wind_blackman_harris.h"
|
||||
#else
|
||||
#include "dsps_wind_flat_top.h"
|
||||
#endif
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#define UM_AUDIOREACTIVE_USE_INTEGER_FFT // always use integer FFT on ESP32-S2 and ESP32-C3
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
using FFTsampleType = float;
|
||||
using FFTmathType = float;
|
||||
#define FFTabs fabsf
|
||||
#else
|
||||
using FFTsampleType = int16_t;
|
||||
using FFTmathType = int32_t;
|
||||
#define FFTabs abs
|
||||
#endif
|
||||
// These are the input and output vectors. Input vectors receive computed results from FFT.
|
||||
static FFTsampleType* valFFT = nullptr;
|
||||
#ifdef UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
static float* vImag = nullptr; // imaginary part of FFT results
|
||||
#endif
|
||||
|
||||
// pre-computed window function
|
||||
static FFTsampleType* windowFFT = nullptr;
|
||||
|
||||
// use audio source class (ESP32 specific)
|
||||
#include "audio_source.h"
|
||||
@@ -112,14 +168,14 @@ constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples)
|
||||
// globals
|
||||
static uint8_t inputLevel = 128; // UI slider value
|
||||
#ifndef SR_SQUELCH
|
||||
uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value)
|
||||
static uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value)
|
||||
#else
|
||||
uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value)
|
||||
static uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value)
|
||||
#endif
|
||||
#ifndef SR_GAIN
|
||||
uint8_t sampleGain = 60; // sample gain (config value)
|
||||
static uint8_t sampleGain = 60; // sample gain (config value)
|
||||
#else
|
||||
uint8_t sampleGain = SR_GAIN; // sample gain (config value)
|
||||
static uint8_t sampleGain = SR_GAIN; // sample gain (config value)
|
||||
#endif
|
||||
// user settable options for FFTResult scaling
|
||||
static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root
|
||||
@@ -144,8 +200,8 @@ const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; //
|
||||
// AGC presets end
|
||||
|
||||
static AudioSource *audioSource = nullptr;
|
||||
static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT.
|
||||
|
||||
static bool useBandPassFilter = false; // if true, enables a hard cutoff bandpass filter. Applies after FFT.
|
||||
static bool useMicFilter = false; // if true, enables a IIR bandpass filter 80Hz-20Khz to remove noise. Applies before FFT.
|
||||
////////////////////
|
||||
// Begin FFT Code //
|
||||
////////////////////
|
||||
@@ -153,7 +209,7 @@ static bool useBandPassFilter = false; // if true, enables a
|
||||
// some prototypes, to ensure consistent interfaces
|
||||
static float fftAddAvg(int from, int to); // average of several FFT result bins
|
||||
void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results
|
||||
static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass)
|
||||
static void runMicFilter(uint16_t numSamples, FFTsampleType *sampleBuffer);
|
||||
static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels
|
||||
|
||||
static TaskHandle_t FFT_Task = nullptr;
|
||||
@@ -189,13 +245,13 @@ constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - Thi
|
||||
constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information.
|
||||
// the following are observed values, supported by a bit of "educated guessing"
|
||||
//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
#define FFT_DOWNSCALE 0.40f // downscaling factor for FFT results, RMS averaging for "Blackman-Harris" Window @22kHz (credit to MM)
|
||||
#else
|
||||
#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels
|
||||
#endif
|
||||
#define LOG_256 5.54517744f // log(256)
|
||||
|
||||
// These are the input and output vectors. Input vectors receive computed results from FFT.
|
||||
static float* vReal = nullptr; // FFT sample inputs / freq output - these are our raw result bins
|
||||
static float* vImag = nullptr; // imaginary parts
|
||||
|
||||
// Create FFT object
|
||||
// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2
|
||||
// these options actually cause slow-downs on all esp32 processors, don't use them.
|
||||
@@ -204,16 +260,20 @@ static float* vImag = nullptr; // imaginary parts
|
||||
// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt()
|
||||
// #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - since v2.0.0 this must be done in build_flags
|
||||
|
||||
#include <arduinoFFT.h> // FFT object is created in FFTcode
|
||||
// Helper functions
|
||||
|
||||
// compute average of several FFT result bins
|
||||
static float fftAddAvg(int from, int to) {
|
||||
float result = 0.0f;
|
||||
FFTmathType result = 0;
|
||||
for (int i = from; i <= to; i++) {
|
||||
result += vReal[i];
|
||||
result += valFFT[i];
|
||||
}
|
||||
return result / float(to - from + 1);
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
result = result * 0.0625; // divide by 16 to reduce magnitude. Want end result to be scaled linear and ~4096 max.
|
||||
#else
|
||||
result *= 32; // scale result to match float values. note: raw scaling value between float and int is 512, float version is scaled down by 16
|
||||
#endif
|
||||
return float(result) / float(to - from + 1); // return average as float
|
||||
}
|
||||
|
||||
//
|
||||
@@ -222,18 +282,61 @@ static float fftAddAvg(int from, int to) {
|
||||
void FFTcode(void * parameter)
|
||||
{
|
||||
DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID());
|
||||
|
||||
#ifdef UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
// allocate FFT buffers on first call
|
||||
if (vReal == nullptr) vReal = (float*) calloc(samplesFFT, sizeof(float));
|
||||
if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float));
|
||||
if ((vReal == nullptr) || (vImag == nullptr)) {
|
||||
if (valFFT == nullptr) valFFT = (float*) calloc(samplesFFT, sizeof(float));
|
||||
if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float));
|
||||
if ((valFFT == nullptr) || (vImag == nullptr)) {
|
||||
// something went wrong
|
||||
if (vReal) free(vReal); vReal = nullptr;
|
||||
if (valFFT) free(valFFT); valFFT = nullptr;
|
||||
if (vImag) free(vImag); vImag = nullptr;
|
||||
return;
|
||||
}
|
||||
// Create FFT object with weighing factor storage
|
||||
ArduinoFFT<float> FFT = ArduinoFFT<float>( vReal, vImag, samplesFFT, SAMPLE_RATE, true);
|
||||
ArduinoFFT<float> FFT = ArduinoFFT<float>(valFFT, vImag, samplesFFT, SAMPLE_RATE, true);
|
||||
#elif !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
// allocate and initialize FFT buffers on first call
|
||||
// note: free() is never used on these pointers. If it ever is implemented, this implementation can cause memory leaks (need to free raw pointers)
|
||||
if (valFFT == nullptr) {
|
||||
float* raw_buffer = (float*)heap_caps_malloc((2 * samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT);
|
||||
if ((raw_buffer == nullptr)) return; // something went wrong
|
||||
valFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
|
||||
}
|
||||
// create window
|
||||
if (windowFFT == nullptr) {
|
||||
float* raw_buffer = (float*)heap_caps_malloc((samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT);
|
||||
if ((raw_buffer == nullptr)) return; // something went wrong
|
||||
windowFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary
|
||||
}
|
||||
if (dsps_fft2r_init_fc32(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
|
||||
// create window function for FFT
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
dsps_wind_blackman_harris_f32(windowFFT, samplesFFT);
|
||||
#else
|
||||
dsps_wind_flat_top_f32(windowFFT, samplesFFT);
|
||||
#endif
|
||||
#else
|
||||
// allocate and initialize integer FFT buffers on first call
|
||||
if (valFFT == nullptr) valFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT * 2);
|
||||
if ((valFFT == nullptr)) return; // something went wrong
|
||||
// create window
|
||||
if (windowFFT == nullptr) windowFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT);
|
||||
if ((windowFFT == nullptr)) return; // something went wrong
|
||||
if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
|
||||
// create window function for FFT
|
||||
float *windowFloat = (float*) calloc(sizeof(float), samplesFFT); // temporary buffer for window function
|
||||
if ((windowFloat == nullptr)) return; // something went wrong
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
dsps_wind_blackman_harris_f32(windowFloat, samplesFFT);
|
||||
#else
|
||||
dsps_wind_flat_top_f32(windowFloat, samplesFFT);
|
||||
#endif
|
||||
// convert float window to 16-bit int
|
||||
for (int i = 0; i < samplesFFT; i++) {
|
||||
windowFFT[i] = (int16_t)(windowFloat[i] * 32767.0f);
|
||||
}
|
||||
free(windowFloat); // free temporary buffer
|
||||
#endif
|
||||
|
||||
// see https://www.freertos.org/vtaskdelayuntil.html
|
||||
const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS;
|
||||
@@ -255,8 +358,7 @@ void FFTcode(void * parameter)
|
||||
#endif
|
||||
|
||||
// get a fresh batch of samples from I2S
|
||||
if (audioSource) audioSource->getSamples(vReal, samplesFFT);
|
||||
memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0
|
||||
if (audioSource) audioSource->getSamples(valFFT, samplesFFT); // note: valFFT is used as a int16_t buffer on C3 and S2, could optimize RAM use by only allocating half the size (but makes code harder to read)
|
||||
|
||||
#if defined(WLED_DEBUG) || defined(SR_DEBUG)
|
||||
if (start < esp_timer_get_time()) { // filter out overflows
|
||||
@@ -268,16 +370,15 @@ void FFTcode(void * parameter)
|
||||
|
||||
xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay
|
||||
|
||||
// band pass filter - can reduce noise floor by a factor of 50
|
||||
// band pass filter - can reduce noise floor by a factor of 50 and avoid aliasing effects to base & high frequency bands
|
||||
// downside: frequencies below 100Hz will be ignored
|
||||
if (useBandPassFilter) runMicFilter(samplesFFT, vReal);
|
||||
|
||||
if (useMicFilter) runMicFilter(samplesFFT, valFFT);
|
||||
// find highest sample in the batch
|
||||
float maxSample = 0.0f; // max sample from FFT batch
|
||||
FFTsampleType maxSample = 0; // max sample from FFT batch
|
||||
for (int i=0; i < samplesFFT; i++) {
|
||||
// pick our our current mic sample - we take the max value from all samples that go into FFT
|
||||
if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts
|
||||
if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]);
|
||||
if ((valFFT[i] <= (INT16_MAX - 1024)) && (valFFT[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts
|
||||
if (FFTabs(valFFT[i]) > maxSample) maxSample = FFTabs(valFFT[i]);
|
||||
}
|
||||
// release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function
|
||||
// early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results.
|
||||
@@ -289,32 +390,97 @@ void FFTcode(void * parameter)
|
||||
if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed.
|
||||
#endif
|
||||
|
||||
// run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2)
|
||||
#ifdef UM_AUDIOREACTIVE_USE_ARDUINO_FFT
|
||||
// run Arduino FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2, ~20ms on ESP32-C3)
|
||||
memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0
|
||||
FFT.dcRemoval(); // remove DC offset
|
||||
#ifdef FFT_PREFER_EXACT_PEAKS
|
||||
FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection
|
||||
#else
|
||||
FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy
|
||||
//FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection
|
||||
#endif
|
||||
FFT.compute( FFTDirection::Forward ); // Compute FFT
|
||||
FFT.complexToMagnitude(); // Compute magnitudes
|
||||
vReal[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues.
|
||||
|
||||
FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant
|
||||
valFFT[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues.
|
||||
FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant
|
||||
// note: scaling is done in fftAddAvg(), so we don't scale here
|
||||
#else
|
||||
// run run float DSP FFT (takes ~x ms on ESP32, ~x ms on ESP32-S2, , ~x ms on ESP32-C3) TODO: test and fill in these values
|
||||
// remove DC offset
|
||||
FFTmathType sum = 0;
|
||||
for (int i = 0; i < samplesFFT; i++) sum += valFFT[i];
|
||||
FFTmathType mean = sum / (FFTmathType)samplesFFT;
|
||||
for (int i = 0; i < samplesFFT; i++) valFFT[i] -= mean;
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
//apply window function to samples and fill buffer with interleaved complex values [Re,Im,Re,Im,...]
|
||||
for (int i = samplesFFT - 1; i >= 0 ; i--) {
|
||||
// fill the buffer back to front to avoid overwriting samples
|
||||
float windowed_sample = valFFT[i] * windowFFT[i];
|
||||
valFFT[i * 2] = windowed_sample;
|
||||
valFFT[i * 2 + 1] = 0.0; // set imaginary part to zero
|
||||
}
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
dsps_fft2r_fc32_aes3(valFFT, samplesFFT); // ESP32 S3 optimized version of FFT
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32)
|
||||
dsps_fft2r_fc32_ae32(valFFT, samplesFFT); // ESP32 optimized version of FFT
|
||||
#else
|
||||
dsps_fft2r_fc32_ansi(valFFT, samplesFFT); // perform FFT using ANSI C implementation
|
||||
#endif
|
||||
dsps_bit_rev_fc32(valFFT, samplesFFT); // bit reverse
|
||||
valFFT[0] = 0; // set DC bin to 0, as it is not needed and can cause issues
|
||||
// convert to magnitude & find FFT_MajorPeak and FFT_Magnitude
|
||||
FFT_MajorPeak = 0;
|
||||
FFT_Magnitude = 0;
|
||||
for (int i = 1; i < samplesFFT_2; i++) { // skip [0] as it is DC offset
|
||||
float real_part = valFFT[i * 2];
|
||||
float imag_part = valFFT[i * 2 + 1];
|
||||
valFFT[i] = sqrtf(real_part * real_part + imag_part * imag_part);
|
||||
if (valFFT[i] > FFT_Magnitude) {
|
||||
FFT_Magnitude = valFFT[i];
|
||||
FFT_MajorPeak = i*(SAMPLE_RATE/samplesFFT);
|
||||
}
|
||||
// note: scaling is done in fftAddAvg(), so we don't scale here
|
||||
}
|
||||
#else
|
||||
// run integer DSP FFT (takes ~x ms on ESP32, ~x ms on ESP32-S2, , ~1.5 ms on ESP32-C3) TODO: test and fill in these values
|
||||
//apply window function to samples and fill buffer with interleaved complex values [Re,Im,Re,Im,...]
|
||||
for (int i = samplesFFT - 1; i >= 0 ; i--) {
|
||||
// fill the buffer back to front to avoid overwriting samples
|
||||
int16_t windowed_sample = ((int32_t)valFFT[i] * (int32_t)windowFFT[i]) >> 15; // both values are ±15bit
|
||||
valFFT[i * 2] = windowed_sample;
|
||||
valFFT[i * 2 + 1] = 0; // set imaginary part to zero
|
||||
}
|
||||
dsps_fft2r_sc16_ansi(valFFT, samplesFFT); // perform FFT on complex value pairs (Re,Im)
|
||||
dsps_bit_rev_sc16_ansi(valFFT, samplesFFT); // bit reverse i.e. "unshuffle" the results
|
||||
valFFT[0] = 0; // set DC bin to 0, as it is not needed and can cause issues
|
||||
// convert to magnitude, FFT returns interleaved complex values [Re,Im,Re,Im,...]
|
||||
int FFT_MajorPeak_int = 0;
|
||||
int FFT_Magnitude_int = 0;
|
||||
for (int i = 1; i < samplesFFT_2; i++) { // skip [0], it is DC offset
|
||||
int32_t real_part = valFFT[i * 2];
|
||||
int32_t imag_part = valFFT[i * 2 + 1];
|
||||
valFFT[i] = sqrt32_bw(real_part * real_part + imag_part * imag_part); // note: this should never overflow as Re and Im form a vector of maximum length 32767
|
||||
if (valFFT[i] > FFT_Magnitude_int) {
|
||||
FFT_Magnitude_int = valFFT[i];
|
||||
FFT_MajorPeak_int = ((i * SAMPLE_RATE)/samplesFFT);
|
||||
}
|
||||
// note: scaling is done in fftAddAvg(), so we don't scale here
|
||||
}
|
||||
FFT_Magnitude = FFT_Magnitude_int * 512; // scale to match raw float value
|
||||
FFT_MajorPeak = FFT_MajorPeak_int;
|
||||
FFT_Magnitude = FFT_Magnitude_int;
|
||||
#endif
|
||||
#endif
|
||||
FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects
|
||||
|
||||
#if defined(WLED_DEBUG) || defined(SR_DEBUG)
|
||||
haveDoneFFT = true;
|
||||
#endif
|
||||
|
||||
} else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this.
|
||||
memset(vReal, 0, samplesFFT * sizeof(float));
|
||||
} else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this -> set all samples to 0
|
||||
memset(valFFT, 0, samplesFFT * sizeof(FFTsampleType));
|
||||
FFT_MajorPeak = 1;
|
||||
FFT_Magnitude = 0.001;
|
||||
}
|
||||
|
||||
for (int i = 0; i < samplesFFT; i++) {
|
||||
float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way
|
||||
vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max.
|
||||
} // for()
|
||||
|
||||
// mapping of FFT result bins to frequency channels
|
||||
if (fabsf(sampleAvg) > 0.5f) { // noise gate open
|
||||
#if 0
|
||||
@@ -345,7 +511,7 @@ void FFTcode(void * parameter)
|
||||
fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate
|
||||
#else
|
||||
/* new mapping, optimized for 22050 Hz by softhack007 */
|
||||
// bins frequency range
|
||||
// bins frequency range
|
||||
if (useBandPassFilter) {
|
||||
// skip frequencies below 100hz
|
||||
fftCalc[ 0] = 0.8f * fftAddAvg(3,4);
|
||||
@@ -407,12 +573,15 @@ void FFTcode(void * parameter)
|
||||
// Pre / Postprocessing //
|
||||
///////////////////////////
|
||||
|
||||
static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass)
|
||||
static void runMicFilter(uint16_t numSamples, FFTsampleType *sampleBuffer) // pre-filtering of raw samples (band-pass)
|
||||
{
|
||||
// low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
// low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency (alpha = 2π × fc / fs)
|
||||
//constexpr float alpha = 0.04f; // 150Hz
|
||||
//constexpr float alpha = 0.03f; // 110Hz
|
||||
constexpr float alpha = 0.0225f; // 80hz
|
||||
//constexpr float alpha = 0.0285f; //100Hz
|
||||
constexpr float alpha = 0.0256f; //90Hz
|
||||
//constexpr float alpha = 0.0225f; // 80hz
|
||||
//constexpr float alpha = 0.01693f;// 60hz
|
||||
// high frequency cutoff parameter
|
||||
//constexpr float beta1 = 0.75f; // 11Khz
|
||||
@@ -436,6 +605,39 @@ static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // p
|
||||
lowfilt += alpha * (sampleBuffer[i] - lowfilt);
|
||||
sampleBuffer[i] = sampleBuffer[i] - lowfilt;
|
||||
}
|
||||
#else
|
||||
// low frequency cutoff parameter 17.15 fixed point format
|
||||
//constexpr int32_t ALPHA_FP = 1311; // 0.04f * (1<<15) (150Hz)
|
||||
//constexpr int32_t ALPHA_FP = 983; // 0.03f * (1<<15) (110Hz)
|
||||
//constexpr int32_t ALPHA_FP = 934; // 0.0285f * (1<<15) (100Hz)
|
||||
constexpr int32_t ALPHA_FP = 840; // 0.0256f * (1<<15) (90Hz)
|
||||
//constexpr int32_t ALPHA_FP = 737; // 0.0225f * (1<<15) (80Hz)
|
||||
//constexpr int32_t ALPHA_FP = 555; // 0.01693f * (1<<15) (60Hz)
|
||||
|
||||
// high frequency cutoff parameters 16.16 fixed point format
|
||||
//constexpr int32_t BETA1_FP = 49152; // 0.75f * (1<<16) (11KHz)
|
||||
//constexpr int32_t BETA1_FP = 53740; // 0.82f * (1<<16) (15KHz)
|
||||
//constexpr int32_t BETA1_FP = 54297; // 0.8285f * (1<<16) (18KHz)
|
||||
constexpr int32_t BETA1_FP = 55706; // 0.85f * (1<<16) (20KHz)
|
||||
constexpr int32_t BETA2_FP = (65536 - BETA1_FP) / 2; // ((1.0f - beta1) / 2.0f) * (1<<16)
|
||||
|
||||
static int32_t last_vals[2] = { 0 }; // FIR high freq cutoff filter (scaled by sample range)
|
||||
static int32_t lowfilt_fp = 0; // IIR low frequency cutoff filter (16.16 fixed point)
|
||||
|
||||
for (int i = 0; i < numSamples; i++) {
|
||||
// FIR lowpass filter to remove high frequency noise
|
||||
int32_t highFilteredSample_fp;
|
||||
|
||||
if (i < (numSamples - 1))
|
||||
highFilteredSample_fp = (BETA1_FP * (int32_t)sampleBuffer[i] + BETA2_FP * last_vals[0] + BETA2_FP * (int32_t)sampleBuffer[i + 1]) >> 16; // smooth out spikes
|
||||
else
|
||||
highFilteredSample_fp = (BETA1_FP * (int32_t)sampleBuffer[i] + BETA2_FP * last_vals[0] + BETA2_FP * last_vals[1]) >> 16; // special handling for last sample in array
|
||||
last_vals[1] = last_vals[0];
|
||||
last_vals[0] = (int32_t)sampleBuffer[i];
|
||||
lowfilt_fp += ALPHA_FP * (highFilteredSample_fp - (lowfilt_fp >> 15)); // low pass filter in 17.15 fixed point format
|
||||
sampleBuffer[i] = highFilteredSample_fp - (lowfilt_fp >> 15);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels
|
||||
@@ -524,7 +726,7 @@ static void detectSamplePeak(void) {
|
||||
// Poor man's beat detection by seeing if sample > Average + some value.
|
||||
// This goes through ALL of the 255 bins - but ignores stupid settings
|
||||
// Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync.
|
||||
if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) {
|
||||
if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (valFFT[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) {
|
||||
havePeak = true;
|
||||
}
|
||||
|
||||
@@ -1169,8 +1371,8 @@ class AudioReactive : public Usermod {
|
||||
periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3
|
||||
#endif
|
||||
delay(100); // Give that poor microphone some time to setup.
|
||||
|
||||
useBandPassFilter = false;
|
||||
useBandPassFilter = false; // filter cuts lowest and highest frequency bands from FFT result (use on very noisy mic inputs)
|
||||
useMicFilter = true; // filter fixes aliasing to base & highest frequency bands and reduces noise floor (recommended for all mic inputs)
|
||||
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone
|
||||
@@ -1205,12 +1407,13 @@ class AudioReactive : public Usermod {
|
||||
case 4:
|
||||
DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT));
|
||||
audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f);
|
||||
useMicFilter = false; // I2S with Master Clock is mostly used for line-in, skip sample filtering
|
||||
delay(100);
|
||||
if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);
|
||||
break;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
case 5:
|
||||
DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT));
|
||||
DEBUGSR_PRINT(F("AR: Generic PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT));
|
||||
audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f);
|
||||
useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5)
|
||||
delay(100);
|
||||
@@ -1220,6 +1423,7 @@ class AudioReactive : public Usermod {
|
||||
case 6:
|
||||
DEBUGSR_PRINTLN(F("AR: ES8388 Source"));
|
||||
audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE);
|
||||
useMicFilter = false;
|
||||
delay(100);
|
||||
if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);
|
||||
break;
|
||||
@@ -1227,7 +1431,6 @@ class AudioReactive : public Usermod {
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// ADC over I2S is only possible on "classic" ESP32
|
||||
case 0:
|
||||
default:
|
||||
DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only)."));
|
||||
audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE);
|
||||
delay(100);
|
||||
@@ -1235,10 +1438,25 @@ class AudioReactive : public Usermod {
|
||||
if (audioSource) audioSource->initialize(audioPin);
|
||||
break;
|
||||
#endif
|
||||
|
||||
case 254: // dummy "network receive only" mode
|
||||
if (audioSource) delete audioSource; audioSource = nullptr;
|
||||
disableSoundProcessing = true;
|
||||
audioSyncEnabled = 2; // force udp sound receive mode
|
||||
enabled = true;
|
||||
break;
|
||||
|
||||
case 255: // 255 = -1 = no audio source
|
||||
// falls through to default
|
||||
default:
|
||||
if (audioSource) delete audioSource; audioSource = nullptr;
|
||||
disableSoundProcessing = true;
|
||||
enabled = false;
|
||||
break;
|
||||
}
|
||||
delay(250); // give microphone enough time to initialise
|
||||
|
||||
if (!audioSource) enabled = false; // audio failed to initialise
|
||||
if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise
|
||||
#endif
|
||||
if (enabled) onUpdateBegin(false); // create FFT task, and initialize network
|
||||
|
||||
@@ -1321,7 +1539,7 @@ class AudioReactive : public Usermod {
|
||||
disableSoundProcessing = true;
|
||||
} else {
|
||||
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
|
||||
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled"
|
||||
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) { // we just switched to "enabled"
|
||||
DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed."));
|
||||
DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride));
|
||||
}
|
||||
@@ -1333,7 +1551,7 @@ class AudioReactive : public Usermod {
|
||||
if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode
|
||||
if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
|
||||
if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
|
||||
|
||||
|
||||
// Only run the sampling code IF we're not in Receive mode or realtime mode
|
||||
@@ -1530,7 +1748,7 @@ class AudioReactive : public Usermod {
|
||||
// better would be for AudioSource to implement getType()
|
||||
if (enabled
|
||||
&& dmType == 0 && audioPin>=0
|
||||
&& (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)
|
||||
&& (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -1614,7 +1832,8 @@ class AudioReactive : public Usermod {
|
||||
if (audioSource->getType() == AudioSource::Type_I2SAdc) {
|
||||
infoArr.add(F("ADC analog"));
|
||||
} else {
|
||||
infoArr.add(F("I2S digital"));
|
||||
if (dmType == 5) infoArr.add(F("PDM digital")); // dmType 5 => generic PDM microphone
|
||||
else infoArr.add(F("I2S digital"));
|
||||
}
|
||||
// input level or "silence"
|
||||
if (maxSample5sec > 1.0f) {
|
||||
@@ -1732,14 +1951,14 @@ class AudioReactive : public Usermod {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as<bool>()) {
|
||||
if (palettes > 0 && root.containsKey(F("rmcpal"))) {
|
||||
// handle removal of custom palettes from JSON call so we don't break things
|
||||
removeAudioPalettes();
|
||||
}
|
||||
}
|
||||
|
||||
void onStateChange(uint8_t callMode) override {
|
||||
if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()<10) {
|
||||
if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()<WLED_MAX_CUSTOM_PALETTES) {
|
||||
// if palettes were removed during JSON call re-add them
|
||||
createAudioPalettes();
|
||||
}
|
||||
@@ -1901,7 +2120,7 @@ class AudioReactive : public Usermod {
|
||||
uiScript.print(F("addOption(dd,'SPH0654',3);"));
|
||||
uiScript.print(F("addOption(dd,'Generic I2S with Mclk',4);"));
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
uiScript.print(F("addOption(dd,'Generic I2S PDM',5);"));
|
||||
uiScript.print(F("addOption(dd,'Generic PDM',5);"));
|
||||
#endif
|
||||
uiScript.print(F("addOption(dd,'ES8388',6);"));
|
||||
|
||||
@@ -1981,7 +2200,7 @@ void AudioReactive::createAudioPalettes(void) {
|
||||
if (palettes) return;
|
||||
DEBUG_PRINTLN(F("Adding audio palettes."));
|
||||
for (int i=0; i<MAX_PALETTES; i++)
|
||||
if (customPalettes.size() < 10) {
|
||||
if (customPalettes.size() < WLED_MAX_CUSTOM_PALETTES) {
|
||||
customPalettes.push_back(CRGBPalette16(CRGB(BLACK)));
|
||||
palettes++;
|
||||
DEBUG_PRINTLN(palettes);
|
||||
@@ -1997,12 +2216,12 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) {
|
||||
case 2:
|
||||
b = map(x, 0, 255, 0, NUM_GEQ_CHANNELS/2); // convert palette position to lower half of freq band
|
||||
hsv = CHSV(fftResult[b], 255, x);
|
||||
hsv2rgb_rainbow(hsv, value); // convert to R,G,B
|
||||
value = hsv; // convert to R,G,B
|
||||
break;
|
||||
case 1:
|
||||
b = map(x, 1, 255, 0, 10); // convert palette position to lower half of freq band
|
||||
hsv = CHSV(fftResult[b], 255, map(fftResult[b], 0, 255, 30, 255)); // pick hue
|
||||
hsv2rgb_rainbow(hsv, value); // convert to R,G,B
|
||||
value = hsv; // convert to R,G,B
|
||||
break;
|
||||
default:
|
||||
if (x == 1) {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
// see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents
|
||||
// and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265)
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265)
|
||||
// there are two things in these MCUs that could lead to problems with audio processing:
|
||||
// * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x)
|
||||
// * single core, so FFT task might slow down other things like LED updates
|
||||
@@ -71,7 +71,7 @@
|
||||
* if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case.
|
||||
*/
|
||||
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 6))
|
||||
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 5, 0))
|
||||
// espressif bug: only_left has no sound, left and right are swapped
|
||||
// https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138)
|
||||
// https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918)
|
||||
@@ -134,7 +134,7 @@ class AudioSource {
|
||||
Read num_samples from the microphone, and store them in the provided
|
||||
buffer
|
||||
*/
|
||||
virtual void getSamples(float *buffer, uint16_t num_samples) = 0;
|
||||
virtual void getSamples(FFTsampleType *buffer, uint16_t num_samples) = 0;
|
||||
|
||||
/* check if the audio source driver was initialized successfully */
|
||||
virtual bool isInitialized(void) {return(_initialized);}
|
||||
@@ -225,15 +225,17 @@ class I2SSource : public AudioSource {
|
||||
|
||||
_config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); // Change mode to pdm if clock pin not provided. PDM is not supported on ESP32-S2. PDM RX not supported on ESP32-C3
|
||||
_config.channel_format =I2S_PDM_MIC_CHANNEL; // seems that PDM mono mode always uses left channel.
|
||||
_config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality
|
||||
_config.use_apll = false; // don't use aPLL clock source (fix for #5391)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)
|
||||
if (mclkPin != I2S_PIN_NO_CHANGE) {
|
||||
#if !defined(WLED_USE_ETHERNET) // fix for #5391 aPLL resource conflict - aPLL is needed for ethernet boards with internal RMII clock
|
||||
_config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality, and to avoid glitches.
|
||||
// //_config.fixed_mclk = 512 * _sampleRate;
|
||||
// //_config.fixed_mclk = 256 * _sampleRate;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(SOC_I2S_SUPPORTS_APLL)
|
||||
@@ -314,7 +316,7 @@ class I2SSource : public AudioSource {
|
||||
if (_mclkPin != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_mclkPin, PinOwner::UM_Audioreactive);
|
||||
}
|
||||
|
||||
virtual void getSamples(float *buffer, uint16_t num_samples) {
|
||||
virtual void getSamples(FFTsampleType *buffer, uint16_t num_samples) {
|
||||
if (_initialized) {
|
||||
esp_err_t err;
|
||||
size_t bytes_read = 0; /* Counter variable to check if we actually got enough data */
|
||||
@@ -332,19 +334,36 @@ class I2SSource : public AudioSource {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store samples in sample buffer and update DC offset
|
||||
for (int i = 0; i < num_samples; i++) {
|
||||
|
||||
newSamples[i] = postProcessSample(newSamples[i]); // perform postprocessing (needed for ADC samples)
|
||||
|
||||
float currSample = 0.0f;
|
||||
#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT
|
||||
currSample = (float) newSamples[i] / 65536.0f; // 32bit input -> 16bit; keeping lower 16bits as decimal places
|
||||
#else
|
||||
currSample = (float) newSamples[i]; // 16bit input -> use as-is
|
||||
// Store samples in sample buffer
|
||||
#if defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
//constexpr int32_t FIXEDSHIFT = 8; // shift by 8 bits for fixed point math (no loss at 24bit input sample resolution)
|
||||
//int32_t intSampleScale = _sampleScale * (1<<FIXEDSHIFT); // _sampleScale <= 1.0f, shift for fixed point math
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < num_samples; i++) {
|
||||
newSamples[i] = postProcessSample(newSamples[i]); // perform postprocessing (needed for ADC samples)
|
||||
|
||||
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
|
||||
#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT
|
||||
float currSample = (float) newSamples[i] / 65536.0f; // 32bit input -> 16bit; keeping lower 16bits as decimal places
|
||||
#else
|
||||
float currSample = (float) newSamples[i]; // 16bit input -> use as-is
|
||||
#endif
|
||||
buffer[i] = currSample;
|
||||
buffer[i] *= _sampleScale; // scale samples
|
||||
buffer[i] *= _sampleScale; // scale samples
|
||||
#else
|
||||
#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT
|
||||
// note on sample scaling: scaling is only used for inputs with master clock and those are better suited for ESP32 or S3
|
||||
// execution speed is critical on single core MCUs
|
||||
//int32_t currSample = newSamples[i] >> FIXEDSHIFT; // shift to avoid overlow in multiplication
|
||||
//currSample = (currSample * intSampleScale) >> 16; // scale samples, shift down to 16bit
|
||||
int16_t currSample = newSamples[i] >> 16; // no sample scaling, just shift down to 16bit (not scaling saves ~0.4ms on C3)
|
||||
#else
|
||||
//int32_t currSample = (newSamples[i] * intSampleScale) >> FIXEDSHIFT; // scale samples, shift back down to 16bit
|
||||
int16_t currSample = newSamples[i]; // 16bit input -> use as-is
|
||||
#endif
|
||||
buffer[i] = (int16_t)currSample;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -622,7 +641,7 @@ class I2SAdcSource : public I2SSource {
|
||||
}
|
||||
|
||||
// see example in https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino
|
||||
adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_11); // configure ADC input amplification
|
||||
adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_12); // configure ADC input amplification
|
||||
|
||||
#if defined(I2S_GRAB_ADC1_COMPLETELY)
|
||||
// according to docs from espressif, the ADC needs to be started explicitly
|
||||
@@ -687,7 +706,7 @@ class I2SAdcSource : public I2SSource {
|
||||
}
|
||||
|
||||
|
||||
void getSamples(float *buffer, uint16_t num_samples) {
|
||||
void getSamples(FFTsampleType *buffer, uint16_t num_samples) {
|
||||
/* Enable ADC. This has to be enabled and disabled directly before and
|
||||
* after sampling, otherwise Wifi dies
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "wled.h"
|
||||
#include "driver/rtc_io.h"
|
||||
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
#include "soc/touch_sensor_periph.h"
|
||||
#endif
|
||||
#ifdef ESP8266
|
||||
#error The "Deep Sleep" usermod does not support ESP8266
|
||||
#endif
|
||||
@@ -21,27 +23,34 @@
|
||||
#define DEEPSLEEP_DELAY 1
|
||||
#endif
|
||||
|
||||
RTC_DATA_ATTR bool powerup = true; // variable in RTC data persists on a reboot
|
||||
#ifndef DEEPSLEEP_WAKEUP_TOUCH_PIN
|
||||
#define DEEPSLEEP_WAKEUP_TOUCH_PIN 1
|
||||
#endif
|
||||
RTC_DATA_ATTR bool powerup = true; // this is first boot after power cycle. note: variable in RTC data persists on a reboot
|
||||
RTC_DATA_ATTR uint8_t wakeupPreset = 0; // preset to apply after deep sleep wakeup (0 = none), set to timer macro preset
|
||||
|
||||
class DeepSleepUsermod : public Usermod {
|
||||
|
||||
private:
|
||||
|
||||
bool enabled = true;
|
||||
bool enabled = false; // do not enable by default
|
||||
bool initDone = false;
|
||||
uint8_t wakeupPin = DEEPSLEEP_WAKEUPPIN;
|
||||
uint8_t wakeWhenHigh = DEEPSLEEP_WAKEWHENHIGH; // wake up when pin goes high if 1, triggers on low if 0
|
||||
bool noPull = true; // use pullup/pulldown resistor
|
||||
bool enableTouchWakeup = false;
|
||||
uint8_t touchPin = DEEPSLEEP_WAKEUP_TOUCH_PIN;
|
||||
int wakeupAfter = DEEPSLEEP_WAKEUPINTERVAL; // in seconds, <=0: button only
|
||||
bool presetWake = true; // wakeup timer for preset
|
||||
int sleepDelay = DEEPSLEEP_DELAY; // in seconds, 0 = immediate
|
||||
int delaycounter = 5; // delay deep sleep at bootup until preset settings are applied
|
||||
int delaycounter = 10; // delay deep sleep at bootup until preset settings are applied, force wake up if offmode persists after bootup
|
||||
uint32_t lastLoopTime = 0;
|
||||
|
||||
// string that are used multiple time (this will save some flash memory)
|
||||
static const char _name[];
|
||||
static const char _enabled[];
|
||||
|
||||
bool pin_is_valid(uint8_t wakePin) {
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up. note: input-only GPIOs 34-39 do not have internal pull resistors
|
||||
if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) {
|
||||
return true;
|
||||
}
|
||||
@@ -60,8 +69,57 @@ class DeepSleepUsermod : public Usermod {
|
||||
return false;
|
||||
}
|
||||
|
||||
public:
|
||||
// functions to calculate time difference between now and next scheduled timer event
|
||||
int calculateTimeDifference(int hour1, int minute1, int hour2, int minute2) {
|
||||
int totalMinutes1 = hour1 * 60 + minute1;
|
||||
int totalMinutes2 = hour2 * 60 + minute2;
|
||||
if (totalMinutes2 < totalMinutes1) {
|
||||
totalMinutes2 += 24 * 60;
|
||||
}
|
||||
return totalMinutes2 - totalMinutes1;
|
||||
}
|
||||
|
||||
int findNextTimerInterval() {
|
||||
if (toki.getTimeSource() == TOKI_TS_NONE) {
|
||||
DEBUG_PRINTLN("DeepSleep: local time not yet synchronized, skipping timer check.");
|
||||
return -1;
|
||||
}
|
||||
int currentHour = hour(localTime);
|
||||
int currentMinute = minute(localTime);
|
||||
int currentWeekday = weekdayMondayFirst(); // 1=Monday ... 7=Sunday
|
||||
int minDifference = INT_MAX;
|
||||
|
||||
for (size_t i = 0; i < timers.size(); i++) {
|
||||
const Timer& t = timers[i];
|
||||
// only regular enabled timers with valid date range can be used for wake scheduling
|
||||
if (!t.isEnabled() || !t.isRegular()) continue;
|
||||
if (!isTodayInDateRange(t.monthStart, t.dayStart, t.monthEnd, t.dayEnd)) continue;
|
||||
|
||||
// check all weekdays for the current timer, starting from today
|
||||
for (int dayOffset = 0; dayOffset < 7; dayOffset++) {
|
||||
int checkWeekday = (currentWeekday + dayOffset) % 7; // 1-7, check all weekdays starting from today
|
||||
if (checkWeekday == 0) {
|
||||
checkWeekday = 7; // sunday is 7 not 0
|
||||
}
|
||||
|
||||
int targetHour = t.hour;
|
||||
int targetMinute = t.minute;
|
||||
if ((t.weekdays >> checkWeekday) & 0x01) {
|
||||
if (dayOffset == 0 && (targetHour < currentHour || (targetHour == currentHour && targetMinute <= currentMinute)))
|
||||
continue; // skip if time has already passed today
|
||||
|
||||
int timeDifference = calculateTimeDifference(currentHour, currentMinute, targetHour + (dayOffset * 24), targetMinute);
|
||||
if (timeDifference < minDifference) {
|
||||
minDifference = timeDifference;
|
||||
wakeupPreset = t.preset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return minDifference;
|
||||
}
|
||||
|
||||
public:
|
||||
inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod
|
||||
inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state
|
||||
|
||||
@@ -69,26 +127,40 @@ class DeepSleepUsermod : public Usermod {
|
||||
void setup() {
|
||||
//TODO: if the de-init of RTC pins is required to do it could be done here
|
||||
//rtc_gpio_deinit(wakeupPin);
|
||||
#ifdef WLED_DEBUG
|
||||
DEBUG_PRINTF("sleep wakeup cause: %d\n", esp_sleep_get_wakeup_cause());
|
||||
#endif
|
||||
if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER)
|
||||
wakeupPreset = 0; // not a timed wakeup, don't apply preset
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (!enabled || !offMode) { // disabled or LEDs are on
|
||||
if (!enabled) return;
|
||||
if (!offMode) { // LEDs are on
|
||||
lastLoopTime = 0; // reset timer
|
||||
if (delaycounter)
|
||||
delaycounter--; // decrease delay counter if LEDs are on (they are always turned on after a wake-up, see below)
|
||||
else if (wakeupPreset)
|
||||
applyPreset(wakeupPreset); // apply preset if set, this ensures macro is applied even if we missed the wake-up time
|
||||
return;
|
||||
}
|
||||
|
||||
if (sleepDelay > 0) {
|
||||
if(lastLoopTime == 0) lastLoopTime = millis(); // initialize
|
||||
if (millis() - lastLoopTime < sleepDelay * 1000) {
|
||||
return; // wait until delay is over
|
||||
}
|
||||
powerup = false; // disable "safety" powerup sleep if delay is set
|
||||
if (lastLoopTime == 0)
|
||||
lastLoopTime = millis(); // initialize
|
||||
if (millis() - lastLoopTime < sleepDelay * 1000)
|
||||
return; // wait until delay is over
|
||||
} else if (powerup && delaycounter) {
|
||||
delaycounter--; // on first boot without sleepDelay set, do not force-turn on
|
||||
delay(1000); // just in case: give user a short ~10s window to turn LEDs on in UI (delaycounter is 10 by default)
|
||||
return;
|
||||
}
|
||||
|
||||
if(powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (handleIO() does enable offMode temporarily in this case)
|
||||
if (powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (beginStrip() / handleIO() does enable offMode temporarily in this case)
|
||||
delaycounter--;
|
||||
if(delaycounter == 2 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false)
|
||||
if (briS == 0) bri = 10; // turn on at low brightness
|
||||
if (delaycounter == 1 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false)
|
||||
if (briS == 0) bri = 10; // turn on and set low brightness to avoid automatic turn off
|
||||
else bri = briS;
|
||||
strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset)
|
||||
offMode = false;
|
||||
@@ -99,57 +171,81 @@ class DeepSleepUsermod : public Usermod {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_PRINTLN(F("DeepSleep UM: entering deep sleep..."));
|
||||
powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot)
|
||||
if(!pin_is_valid(wakeupPin)) return;
|
||||
if (!pin_is_valid(wakeupPin)) return;
|
||||
esp_err_t halerror = ESP_OK;
|
||||
pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled
|
||||
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case)
|
||||
|
||||
if(wakeupAfter)
|
||||
esp_sleep_enable_timer_wakeup((uint64_t)wakeupAfter * (uint64_t)1e6); //sleep for x seconds
|
||||
uint32_t wakeupAfterSec = 0;
|
||||
if (presetWake) {
|
||||
int nextInterval = findNextTimerInterval();
|
||||
if (nextInterval > 1 && nextInterval < INT_MAX)
|
||||
wakeupAfterSec = (nextInterval - 1) * 60; // wakeup before next preset
|
||||
}
|
||||
if (wakeupAfter > 0) { // user-defined interval
|
||||
if (wakeupAfterSec == 0 || (uint32_t)wakeupAfter < wakeupAfterSec) {
|
||||
wakeupAfterSec = wakeupAfter;
|
||||
}
|
||||
}
|
||||
if (wakeupAfterSec > 0) {
|
||||
esp_sleep_enable_timer_wakeup(wakeupAfterSec * (uint64_t)1e6);
|
||||
DEBUG_PRINTF("wakeup after %d seconds\n", wakeupAfterSec);
|
||||
}
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3
|
||||
if(noPull)
|
||||
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_FLOATING);
|
||||
else { // enable pullup/pulldown resistor
|
||||
if(wakeWhenHigh)
|
||||
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLDOWN_ONLY);
|
||||
gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold and configure pin
|
||||
if (wakeWhenHigh)
|
||||
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_HIGH);
|
||||
else
|
||||
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLUP_ONLY);
|
||||
}
|
||||
if(wakeWhenHigh)
|
||||
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_HIGH);
|
||||
else
|
||||
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// note: on C3 calling esp_deep_sleep_enable_gpio_wakeup() automatically enables pullup/pulldown unless we call gpio_hold_en() which overrides that
|
||||
gpio_pullup_dis((gpio_num_t)wakeupPin); // disable pull resistors by default
|
||||
gpio_pulldown_dis((gpio_num_t)wakeupPin);
|
||||
if (!noPull) {
|
||||
if (wakeWhenHigh) {
|
||||
gpio_pulldown_en((gpio_num_t)wakeupPin);
|
||||
} else {
|
||||
gpio_pullup_en((gpio_num_t)wakeupPin);
|
||||
}
|
||||
}
|
||||
gpio_hold_en((gpio_num_t)wakeupPin); // hold the configured GPIO state during deep sleep, overrides the automatic pullup/pulldown, see note above
|
||||
#else // ESP32, S2, S3
|
||||
gpio_pulldown_dis((gpio_num_t)wakeupPin); // disable internal pull resistors for GPIO use
|
||||
gpio_pullup_dis((gpio_num_t)wakeupPin);
|
||||
if(noPull) {
|
||||
rtc_gpio_pullup_dis((gpio_num_t)wakeupPin);
|
||||
rtc_gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold so we can (re)configure pin
|
||||
rtc_gpio_init((gpio_num_t)wakeupPin); // hand the pin over to RTC module
|
||||
rtc_gpio_set_direction((gpio_num_t)wakeupPin, RTC_GPIO_MODE_INPUT_ONLY);
|
||||
rtc_gpio_pullup_dis((gpio_num_t)wakeupPin); // disable pull resistors by default
|
||||
rtc_gpio_pulldown_dis((gpio_num_t)wakeupPin);
|
||||
}
|
||||
else { // enable pullup/pulldown resistor for RTC use
|
||||
if(wakeWhenHigh)
|
||||
rtc_gpio_pulldown_en((gpio_num_t)wakeupPin);
|
||||
if (!noPull) {
|
||||
if (wakeWhenHigh)
|
||||
rtc_gpio_pulldown_en((gpio_num_t)wakeupPin);
|
||||
else
|
||||
rtc_gpio_pullup_en((gpio_num_t)wakeupPin);
|
||||
}
|
||||
if (wakeWhenHigh)
|
||||
halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ANY_HIGH); // use ext1 as ext0 does not work with touch wakeup
|
||||
else
|
||||
rtc_gpio_pullup_en((gpio_num_t)wakeupPin);
|
||||
}
|
||||
if(wakeWhenHigh)
|
||||
halerror = esp_sleep_enable_ext0_wakeup((gpio_num_t)wakeupPin, HIGH); // only RTC pins can be used
|
||||
else
|
||||
halerror = esp_sleep_enable_ext0_wakeup((gpio_num_t)wakeupPin, LOW);
|
||||
#endif
|
||||
halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ALL_LOW);
|
||||
|
||||
delay(1); // wait for pin to be ready
|
||||
if(halerror == ESP_OK) esp_deep_sleep_start(); // go into deep sleep
|
||||
if (enableTouchWakeup) {
|
||||
#ifdef SOC_TOUCH_VERSION_2 // S2 and S3 use much higher thresholds, see notes in pin_manager
|
||||
touchSleepWakeUpEnable(touchPin, touchThreshold << 4); // ESP32 S2 & S3: lower threshold = more sensitive
|
||||
#else
|
||||
touchSleepWakeUpEnable(touchPin, touchThreshold); // ESP32: use normal threshold (higher = more sensitive)
|
||||
#endif
|
||||
}
|
||||
delay(1); // wait for pins to be ready
|
||||
rtc_gpio_hold_en((gpio_num_t)wakeupPin); // latch and hold the configured GPIO state during deep sleep
|
||||
#endif
|
||||
WiFi.mode(WIFI_OFF); // Completely shut down the Wi-Fi module
|
||||
if (halerror == ESP_OK) esp_deep_sleep_start(); // go into deep sleep
|
||||
else DEBUG_PRINTLN(F("sleep failed"));
|
||||
}
|
||||
|
||||
//void connected() {} //unused, this is called every time the WiFi is (re)connected
|
||||
|
||||
void addToConfig(JsonObject& root) override
|
||||
void addToConfig(JsonObject& root) override
|
||||
{
|
||||
JsonObject top = root.createNestedObject(FPSTR(_name));
|
||||
top[FPSTR(_enabled)] = enabled;
|
||||
@@ -157,6 +253,11 @@ void addToConfig(JsonObject& root) override
|
||||
top["gpio"] = wakeupPin;
|
||||
top["wakeWhen"] = wakeWhenHigh;
|
||||
top["pull"] = noPull;
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
top["enableTouchWakeup"] = enableTouchWakeup;
|
||||
top["touchPin"] = touchPin;
|
||||
#endif
|
||||
top["presetWake"] = presetWake;
|
||||
top["wakeAfter"] = wakeupAfter;
|
||||
top["delaySleep"] = sleepDelay;
|
||||
}
|
||||
@@ -176,6 +277,11 @@ void addToConfig(JsonObject& root) override
|
||||
}
|
||||
configComplete &= getJsonValue(top["wakeWhen"], wakeWhenHigh, DEEPSLEEP_WAKEWHENHIGH); // default to wake on low
|
||||
configComplete &= getJsonValue(top["pull"], noPull, DEEPSLEEP_DISABLEPULL); // default to no pullup/pulldown
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
configComplete &= getJsonValue(top["enableTouchWakeup"], enableTouchWakeup);
|
||||
configComplete &= getJsonValue(top["touchPin"], touchPin, DEEPSLEEP_WAKEUP_TOUCH_PIN);
|
||||
#endif
|
||||
configComplete &= getJsonValue(top["presetWake"], presetWake);
|
||||
configComplete &= getJsonValue(top["wakeAfter"], wakeupAfter, DEEPSLEEP_WAKEUPINTERVAL);
|
||||
configComplete &= getJsonValue(top["delaySleep"], sleepDelay, DEEPSLEEP_DELAY);
|
||||
|
||||
@@ -200,6 +306,19 @@ void addToConfig(JsonObject& root) override
|
||||
oappend(SET_F(");"));
|
||||
}
|
||||
}
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
// dropdown for touch wakeupPin
|
||||
oappend(SET_F("dd=addDropdown('DeepSleep','touchPin');"));
|
||||
for (int touchchannel = 0; touchchannel < SOC_TOUCH_SENSOR_NUM; touchchannel++) {
|
||||
if (touch_sensor_channel_io_map[touchchannel] >= 0) {
|
||||
oappend(SET_F("addOption(dd,'"));
|
||||
oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str());
|
||||
oappend(SET_F("',"));
|
||||
oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str());
|
||||
oappend(SET_F(");"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
oappend(SET_F("dd=addDropdown('DeepSleep','wakeWhen');"));
|
||||
oappend(SET_F("addOption(dd,'Low',0);"));
|
||||
@@ -207,6 +326,7 @@ void addToConfig(JsonObject& root) override
|
||||
|
||||
oappend(SET_F("addInfo('DeepSleep:pull',1,'','-up/down disable: ');")); // first string is suffix, second string is prefix
|
||||
oappend(SET_F("addInfo('DeepSleep:wakeAfter',1,'seconds <i>(0 = never)<i>');"));
|
||||
oappend(SET_F("addInfo('DeepSleep:presetWake',1,'<i>(wake up before next preset timer)<i>');"));
|
||||
oappend(SET_F("addInfo('DeepSleep:delaySleep',1,'seconds <i>(0 = sleep at powerup)<i>');")); // first string is suffix, second string is prefix
|
||||
}
|
||||
|
||||
@@ -217,7 +337,6 @@ void addToConfig(JsonObject& root) override
|
||||
uint16_t getId() {
|
||||
return USERMOD_ID_DEEP_SLEEP;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// add more strings here to reduce flash memory usage
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Deep Sleep usermod
|
||||
|
||||
This usermod unleashes the low power capabilities of th ESP: when you power off your LEDs (using the UI power button or a macro) the ESP will be put into deep sleep mode, reducing power consumption to a minimum.
|
||||
During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is to use an external signal or a button. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.***
|
||||
During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is by using an external signal, a button, or an automation timer set to the nearest preset time. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.***
|
||||
|
||||
# A word of warning
|
||||
|
||||
@@ -28,7 +28,7 @@ For lowest power consumption, remove the Power LED and make sure your board does
|
||||
|
||||
The GPIOs that can be used to wake the ESP from deep sleep are limited. Only pins connected to the internal RTC unit can be used:
|
||||
|
||||
- ESP32: GPIO 0, 2, 4, 12-15, 25-39
|
||||
- ESP32: GPIO 0, 2, 4, 12-15, 25-39 note: input-only GPIOs 34-39 do not have internal pull up/down resistors, external resistors are required
|
||||
- ESP32 S3: GPIO 0-21
|
||||
- ESP32 S2: GPIO 0-21
|
||||
- ESP32 C3: GPIO 0-5
|
||||
@@ -61,6 +61,7 @@ To override the default settings, place the `#define` in wled.h or add `-D DEEPS
|
||||
* `DEEPSLEEP_DISABLEPULL` - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled)
|
||||
* `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2%
|
||||
* `DEEPSLEEP_DELAY` - delay between power-off and sleep
|
||||
* `DEEPSLEEP_WAKEUP_TOUCH_PIN` - specify GPIO pin used for touch-based wakeup
|
||||
|
||||
example for env build flags:
|
||||
`-D USERMOD_DEEP_SLEEP`
|
||||
|
||||
@@ -562,11 +562,11 @@ void MultiRelay::loop() {
|
||||
bool MultiRelay::handleButton(uint8_t b) {
|
||||
yield();
|
||||
if (!enabled
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -581,20 +581,20 @@ bool MultiRelay::handleButton(uint8_t b) {
|
||||
unsigned long now = millis();
|
||||
|
||||
//button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
|
||||
if (buttonType[b] == BTN_TYPE_SWITCH) {
|
||||
if (buttons[b].type == BTN_TYPE_SWITCH) {
|
||||
//handleSwitch(b);
|
||||
if (buttonPressedBefore[b] != isButtonPressed(b)) {
|
||||
buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = !buttonPressedBefore[b];
|
||||
if (buttons[b].pressedBefore != isButtonPressed(b)) {
|
||||
buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = !buttons[b].pressedBefore;
|
||||
}
|
||||
|
||||
if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled;
|
||||
if (buttons[b].longPressed == buttons[b].pressedBefore) return handled;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
|
||||
if (_relay[i].button == b) {
|
||||
switchRelay(i, buttonPressedBefore[b]);
|
||||
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
|
||||
switchRelay(i, buttons[b].pressedBefore);
|
||||
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,40 +604,40 @@ bool MultiRelay::handleButton(uint8_t b) {
|
||||
//momentary button logic
|
||||
if (isButtonPressed(b)) { //pressed
|
||||
|
||||
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = true;
|
||||
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > 600) { //long press
|
||||
if (now - buttons[b].pressedTime > 600) { //long press
|
||||
//longPressAction(b); //not exposed
|
||||
//handled = false; //use if you want to pass to default behaviour
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
}
|
||||
|
||||
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
|
||||
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
|
||||
|
||||
long dur = now - buttonPressedTime[b];
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {
|
||||
buttonPressedBefore[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
return handled;
|
||||
} //too short "press", debounce
|
||||
bool doublePress = buttonWaitTime[b]; //did we have short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
bool doublePress = buttons[b].waitTime; //did we have short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (!buttonLongPressed[b]) { //short press
|
||||
if (!buttons[b].longPressed) { //short press
|
||||
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
//doublePressAction(b); //not exposed
|
||||
//handled = false; //use if you want to pass to default behaviour
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
// if 350ms elapsed since last press/release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
//shortPressAction(b); //not exposed
|
||||
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
|
||||
if (_relay[i].button == b) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# pixels_dice_tray Usermod - BLE Requirement Notice
|
||||
|
||||
## Important: This Usermod Requires Special Configuration
|
||||
|
||||
The `pixels_dice_tray` usermod requires **ESP32 BLE (Bluetooth Low Energy)** support, which is not available in all WLED build configurations.
|
||||
|
||||
### Why is library.json disabled?
|
||||
|
||||
The `library.json` file has been renamed to `library.json.disabled` to prevent this usermod from being automatically included in builds that use `custom_usermods = *` (like the `usermods` environment in platformio.ini).
|
||||
|
||||
The Tasmota Arduino ESP32 platform used by WLED does not include Arduino BLE library by default, which causes compilation failures when this usermod is auto-included.
|
||||
|
||||
### How to Use This Usermod
|
||||
|
||||
This usermod **requires a custom build configuration**. You cannot simply enable it with `custom_usermods = *`.
|
||||
|
||||
1. **Copy the sample configuration:**
|
||||
```bash
|
||||
cp platformio_override.ini.sample ../../../platformio_override.ini
|
||||
```
|
||||
|
||||
2. **Edit `platformio_override.ini`** to match your ESP32 board configuration
|
||||
|
||||
3. **Build with the custom environment:**
|
||||
```bash
|
||||
pio run -e t_qt_pro_8MB_dice
|
||||
# or
|
||||
pio run -e esp32s3dev_8MB_qspi_dice
|
||||
```
|
||||
|
||||
### Platform Requirements
|
||||
|
||||
- ESP32-S3 or compatible ESP32 board with BLE support
|
||||
- Custom platformio environment (see `platformio_override.ini.sample`)
|
||||
- Cannot be used with ESP8266 or ESP32-S2
|
||||
|
||||
### Re-enabling for Custom Builds
|
||||
|
||||
If you want to use this usermod in a custom build:
|
||||
|
||||
1. Rename `library.json.disabled` back to `library.json`
|
||||
2. Manually add it to your custom environment's `custom_usermods` list
|
||||
3. Ensure your platform includes BLE support
|
||||
|
||||
### References
|
||||
|
||||
- See `README.md` for full usermod documentation
|
||||
- See `platformio_override.ini.sample` for build configuration examples
|
||||
@@ -8,10 +8,10 @@
|
||||
#include "dice_state.h"
|
||||
|
||||
// Reuse FX display functions.
|
||||
extern uint16_t mode_breath();
|
||||
extern uint16_t mode_blends();
|
||||
extern uint16_t mode_glitter();
|
||||
extern uint16_t mode_gravcenter();
|
||||
extern void mode_breath();
|
||||
extern void mode_blends();
|
||||
extern void mode_glitter();
|
||||
extern void mode_gravcenter();
|
||||
|
||||
static constexpr uint8_t USER_ANY_DIE = 0xFF;
|
||||
/**
|
||||
@@ -40,8 +40,8 @@ static pixels::RollEvent GetLastRollForSegment() {
|
||||
* Alternating pixels running function (copied static function).
|
||||
*/
|
||||
// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)
|
||||
#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3)
|
||||
static uint16_t running_copy(uint32_t color1, uint32_t color2, bool theatre = false) {
|
||||
#define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3)
|
||||
static void running_copy(uint32_t color1, uint32_t color2, bool theatre = false) {
|
||||
int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window
|
||||
uint32_t cycleTime = 50 + (255 - SEGMENT.speed);
|
||||
uint32_t it = strip.now / cycleTime;
|
||||
@@ -63,10 +63,9 @@ static uint16_t running_copy(uint32_t color1, uint32_t color2, bool theatre = fa
|
||||
SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1));
|
||||
SEGENV.step = it;
|
||||
}
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
static uint16_t simple_roll() {
|
||||
static void simple_roll() {
|
||||
auto roll = GetLastRollForSegment();
|
||||
if (roll.state != pixels::RollState::ON_FACE) {
|
||||
SEGMENT.fill(0);
|
||||
@@ -79,7 +78,6 @@ static uint16_t simple_roll() {
|
||||
SEGMENT.setPixelColor(i, SEGCOLOR(1));
|
||||
}
|
||||
}
|
||||
return FRAMETIME;
|
||||
}
|
||||
// See https://kno.wled.ge/interfaces/json-api/#effect-metadata
|
||||
// Name - DieSimple
|
||||
@@ -92,31 +90,34 @@ static uint16_t simple_roll() {
|
||||
static const char _data_FX_MODE_SIMPLE_DIE[] PROGMEM =
|
||||
"DieSimple@,,Selected Die;!,!;;1;c1=255";
|
||||
|
||||
static uint16_t pulse_roll() {
|
||||
static void pulse_roll() {
|
||||
auto roll = GetLastRollForSegment();
|
||||
if (roll.state != pixels::RollState::ON_FACE) {
|
||||
return mode_breath();
|
||||
mode_breath();
|
||||
return;
|
||||
} else {
|
||||
uint16_t ret = mode_blends();
|
||||
mode_blends();
|
||||
uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN;
|
||||
for (int i = num_segments; i < SEGLEN; i++) {
|
||||
SEGMENT.setPixelColor(i, SEGCOLOR(1));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
static const char _data_FX_MODE_PULSE_DIE[] PROGMEM =
|
||||
"DiePulse@!,!,Selected Die;!,!;!;1;sx=24,pal=50,c1=255";
|
||||
|
||||
static uint16_t check_roll() {
|
||||
static void check_roll() {
|
||||
auto roll = GetLastRollForSegment();
|
||||
if (roll.state != pixels::RollState::ON_FACE) {
|
||||
return running_copy(SEGCOLOR(0), SEGCOLOR(2));
|
||||
running_copy(SEGCOLOR(0), SEGCOLOR(2));
|
||||
return;
|
||||
} else {
|
||||
if (roll.current_face + 1 >= SEGMENT.custom2) {
|
||||
return mode_glitter();
|
||||
mode_glitter();
|
||||
return;
|
||||
} else {
|
||||
return mode_gravcenter();
|
||||
mode_gravcenter();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "pixels_dice_tray",
|
||||
"build": { "libArchive": false},
|
||||
"dependencies": {
|
||||
"arduino-pixels-dice":"https://github.com/axlan/arduino-pixels-dice.git",
|
||||
"BLE":"*"
|
||||
}
|
||||
}
|
||||
@@ -461,11 +461,11 @@ class PixelsDiceTrayUsermod : public Usermod {
|
||||
#if USING_TFT_DISPLAY
|
||||
bool handleButton(uint8_t b) override {
|
||||
if (!enabled || b > 1 // buttons 0,1 only
|
||||
|| buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE ||
|
||||
buttonType[b] == BTN_TYPE_RESERVED ||
|
||||
buttonType[b] == BTN_TYPE_PIR_SENSOR ||
|
||||
buttonType[b] == BTN_TYPE_ANALOG ||
|
||||
buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE ||
|
||||
buttons[b].type == BTN_TYPE_RESERVED ||
|
||||
buttons[b].type == BTN_TYPE_PIR_SENSOR ||
|
||||
buttons[b].type == BTN_TYPE_ANALOG ||
|
||||
buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -476,43 +476,43 @@ class PixelsDiceTrayUsermod : public Usermod {
|
||||
static unsigned long buttonWaitTime[2] = {0};
|
||||
|
||||
//momentary button logic
|
||||
if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed
|
||||
if (!buttonPressedBefore[b]) {
|
||||
buttonPressedTime[b] = now;
|
||||
if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed
|
||||
if (!buttons[b].pressedBefore) {
|
||||
buttons[b].pressedTime = now;
|
||||
}
|
||||
buttonPressedBefore[b] = true;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
|
||||
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
|
||||
menu_ctrl.HandleButton(ButtonType::LONG, b);
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
return true;
|
||||
}
|
||||
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
|
||||
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
|
||||
|
||||
long dur = now - buttonPressedTime[b];
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {
|
||||
buttonPressedBefore[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
return true;
|
||||
} //too short "press", debounce
|
||||
|
||||
bool doublePress = buttonWaitTime[b]; //did we have short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
bool doublePress = buttons[b].waitTime; //did we have short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (!buttonLongPressed[b]) { //short press
|
||||
if (!buttons[b].longPressed) { //short press
|
||||
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
menu_ctrl.HandleButton(ButtonType::DOUBLE, b);
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
// if 350ms elapsed since last press/release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS &&
|
||||
!buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS &&
|
||||
!buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
menu_ctrl.HandleButton(ButtonType::SINGLE, b);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,37 +5,37 @@ static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;";
|
||||
|
||||
static POV s_pov;
|
||||
|
||||
uint16_t mode_pov_image(void) {
|
||||
void mode_pov_image(void) {
|
||||
Segment& mainseg = strip.getMainSegment();
|
||||
const char* segName = mainseg.name;
|
||||
if (!segName) {
|
||||
return FRAMETIME;
|
||||
return;
|
||||
}
|
||||
// Only proceed for files ending with .bmp (case-insensitive)
|
||||
size_t segLen = strlen(segName);
|
||||
if (segLen < 4) return FRAMETIME;
|
||||
if (segLen < 4) return;
|
||||
const char* ext = segName + (segLen - 4);
|
||||
// compare case-insensitive to ".bmp"
|
||||
if (!((ext[0]=='.') &&
|
||||
(ext[1]=='b' || ext[1]=='B') &&
|
||||
(ext[2]=='m' || ext[2]=='M') &&
|
||||
(ext[3]=='p' || ext[3]=='P'))) {
|
||||
return FRAMETIME;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* current = s_pov.getFilename();
|
||||
if (current && strcmp(segName, current) == 0) {
|
||||
s_pov.showNextLine();
|
||||
return FRAMETIME;
|
||||
return;
|
||||
}
|
||||
|
||||
static unsigned long s_lastLoadAttemptMs = 0;
|
||||
unsigned long nowMs = millis();
|
||||
// Retry at most twice per second if the image is not yet loaded.
|
||||
if (nowMs - s_lastLoadAttemptMs < 500) return FRAMETIME;
|
||||
if (nowMs - s_lastLoadAttemptMs < 500) return;
|
||||
s_lastLoadAttemptMs = nowMs;
|
||||
s_pov.loadImage(segName);
|
||||
return FRAMETIME;
|
||||
return;
|
||||
}
|
||||
|
||||
class PovDisplayUsermod : public Usermod {
|
||||
|
||||
@@ -34,6 +34,7 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
byte currentColors[3];
|
||||
byte lastKnownBri = 0;
|
||||
|
||||
inline uint32_t colorFromRgb(byte* rgb) { return uint32_t((byte(rgb[0]) << 16) | (byte(rgb[1]) << 8) | (byte(rgb[2]))); }
|
||||
|
||||
void initRotaryEncoder()
|
||||
{
|
||||
@@ -54,10 +55,14 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
|
||||
void initLedBus()
|
||||
{
|
||||
byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255};
|
||||
// Initialize all pins to the sentinel value first…
|
||||
byte _pins[OUTPUT_MAX_PINS];
|
||||
std::fill(std::begin(_pins), std::end(_pins), 255);
|
||||
// …then set only the LED pin
|
||||
_pins[0] = static_cast<byte>(ledIo);
|
||||
BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0);
|
||||
|
||||
ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1);
|
||||
busCfg.iType = BusManager::getI(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver
|
||||
ledBus = new BusDigital(busCfg);
|
||||
if (!ledBus->isOk()) {
|
||||
cleanup();
|
||||
return;
|
||||
@@ -75,7 +80,7 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
for (int i = 0; i < currentPos / incrementPerClick - 1; i++) {
|
||||
ledBus->setPixelColor(i, 0);
|
||||
}
|
||||
ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors));
|
||||
ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgb(currentColors));
|
||||
for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
|
||||
ledBus->setPixelColor(i, 0);
|
||||
}
|
||||
@@ -91,7 +96,7 @@ class RgbRotaryEncoderUsermod : public Usermod
|
||||
if (ledMode == 3) {
|
||||
hsv2rgb((i) / float(numLeds), 1, .25);
|
||||
}
|
||||
ledBus->setPixelColor(i, colorFromRgbw(currentColors));
|
||||
ledBus->setPixelColor(i, colorFromRgb(currentColors));
|
||||
}
|
||||
for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
|
||||
ledBus->setPixelColor(i, 0);
|
||||
|
||||
+502
-1
@@ -1,4 +1,505 @@
|
||||
# Usermod user FX
|
||||
|
||||
This Usermod is a common place to put various user's LED effects.
|
||||
This usermod is a common place to put various users’ WLED effects. It lets you load your own custom effects or bring back deprecated ones—without touching core WLED source code.
|
||||
|
||||
Multiple Effects can be specified inside this single usermod, as we will illustrate below. You will be able to define them with custom names, sliders, etc. as with any other Effect.
|
||||
|
||||
* [Installation](./README.md#installation)
|
||||
* [How The Usermod Works](./README.md#how-the-usermod-works)
|
||||
* [Basic Syntax for WLED Effect Creation](./README.md#basic-syntax-for-wled-effect-creation)
|
||||
* [Understanding 2D WLED Effects](./README.md#understanding-2d-wled-effects)
|
||||
* [The Metadata String](./README.md#the-metadata-string)
|
||||
* [Understanding 1D WLED Effects](./README.md#understanding-1d-wled-effects)
|
||||
* [Combining Multiple Effects in this Usermod](./README.md#combining-multiple-effects-in-this-usermod)
|
||||
* [Compiling](./README.md#compiling)
|
||||
* [Change Log](./README.md#change-log)
|
||||
* [Contact Us](./README.md#contact-us)
|
||||
|
||||
## Installation
|
||||
|
||||
To activate the usermod, add the following line to your platformio_override.ini
|
||||
```ini
|
||||
custom_usermods = user_fx
|
||||
```
|
||||
Or if you are already using a usermod, append user_fx to the list
|
||||
```ini
|
||||
custom_usermods = audioreactive user_fx
|
||||
```
|
||||
|
||||
## How The Usermod Works
|
||||
|
||||
The `user_fx.cpp` file can be broken down into four main parts:
|
||||
* **static effect definition** - This is a static LED setting that is displayed if an effect fails to initialize.
|
||||
* **User FX function definition(s)** - This area is where you place the FX code for all of the custom effects you want to use. This mainly includes the FX code and the static variable containing the [metadata string](https://kno.wled.ge/interfaces/json-api/#effect-metadata).
|
||||
* **Usermod Class definition(s)** - The class definition defines the blueprint from which all your custom Effects (or any usermod, for that matter) are created.
|
||||
* **Usermod registration** - All usermods have to be registered so that they are able to be compiled into your binary.
|
||||
|
||||
We will go into greater detail on how custom effects work in the usermod and how to go about creating your own in the section below.
|
||||
|
||||
|
||||
## Basic Syntax for WLED Effect Creation
|
||||
|
||||
WLED effects generally follow a certain procedure for their operation:
|
||||
1. Determine dimension of segment
|
||||
2. Calculate new state if needed
|
||||
3. Implement a loop that calculates color for each pixel and sets it using `SEGMENT.setPixelColor()`
|
||||
4. The function is called at current frame rate.
|
||||
|
||||
Below are some helpful variables and functions to know as you start your journey towards WLED effect creation:
|
||||
|
||||
| Syntax Element | Size | Description |
|
||||
| :---------------------------------------------- | :----- | :---------- |
|
||||
| [`SEGMENT.speed / intensity / custom1 / custom2`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L450) | 8-bit | These read-only variables help you control aspects of your custom effect using the UI sliders. You can edit these variables through the UI sliders when WLED is running your effect. (These variables can be controlled by the API as well.) Note that while `SEGMENT.intensity` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. The other three bits are used by the boolean parameters `SEGMENT.check1` through `SEGMENT.check3` and are bit-packed to conserve data size and memory. |
|
||||
| [`SEGMENT.custom3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L454) | 5-bit | Another optional UI slider for custom effect control. While `SEGMENT.speed` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. |
|
||||
| [`SEGMENT.check1 / check2 / check3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L455) | 1-bit | These variables are boolean parameters which show up as checkbox options in the User Interface. They are bit-packed along with `SEGMENT.custom3` to conserve data size and memory. |
|
||||
| [`SEGENV.aux0 / aux1`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L467) | 16-bit | These are state variables that persists between function calls, and they are free to be overwritten by the user for any use case. |
|
||||
| [`SEGENV.step`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L465) | 32-bit | This is a timestamp variable that contains the last update time. It is initially set during effect initialization to 0, and then it updates with the elapsed time after each frame runs. |
|
||||
| [`SEGENV.call`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L466) | 32-bit | A counter for how many times this effect function has been invoked since it started. |
|
||||
| [`strip.now`](https://github.com/wled/WLED/blob/main/wled00/FX.h) | 32-bit | Current timestamp in milliseconds. (Equivalent to `millis()`, but use `strip.now()` instead.) `strip.now` respects the timebase, which can be used to advance or reset effects in a preset. This can be useful to sync multiple segments. |
|
||||
| [`SEGLEN / SEG_W / SEG_H`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L116) | 16-bit | These variables are macros that help define the length and width of your LED strip/matrix segment. |
|
||||
| [`SEGPALETTE`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L115) | --- | Macro that gets the currently selected palette for the currently processing segment. |
|
||||
| [`hw_random8()`](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/fcn_declare.h#L548) | 8-bit | One of several functions that generates a random integer. (All of the "hw_" functions are similar to the FastLED library's random functions, but in WLED they use true hardware-based randomness instead of a pseudo random number. In short, they are better and faster.) |
|
||||
| [`SEGCOLOR(x)`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L114) | 32-bit | Macro that gets user-selected colors from UI, where x is an integer 1, 2, or 3 for primary, secondary, and tertiary colors, respectively. |
|
||||
| [`SEGMENT.setPixelColor`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) / [`setPixelColorXY`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_2Dfcn.cpp) | 32-bit | Function that paints one pixel. `setPixelColor` is 1‑D; `setPixelColorXY` expects `(x, y)` and an RGBW color value. |
|
||||
| [`SEGMENT.color_wheel()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1092) | 32-bit | Input 0–255 to get a color. Transitions r→g→b→r. In HSV terms, `pos` is H. Note: only returns palette color unless the Default palette is selected. |
|
||||
| [`SEGMENT.color_from_palette()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1093) | 32-bit | Gets a single color from the currently selected palette for a segment. (This function which should be favoured over `ColorFromPalette()` because this function returns an RGBW color with white from the `SEGCOLOR` passed, while also respecting the setting for palette wrapping. On the other hand, `ColorFromPalette()` simply gets the RGB palette color.) |
|
||||
| [`fade_out()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1012) | --- | fade out function, higher rate = quicker fade. fading is highly dependent on frame rate (higher frame rates, faster fading). each frame will fade at max 9% or as little as 0.8%. |
|
||||
| [`fadeToBlackBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | can be used to fade all pixels to black. |
|
||||
| [`fadeToSecondaryBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | fades all pixels to secondary color. |
|
||||
| [`move()`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) | --- | Moves/shifts pixels in the desired direction. |
|
||||
| [`blur / blur2d`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1053) | --- | Blurs all pixels for the desired segment. Blur also has the boolean option `smear`, which, when activated, does not fade the blurred pixel(s). |
|
||||
|
||||
You will see how these syntax elements work in the examples below.
|
||||
|
||||
|
||||
|
||||
## Understanding 2D WLED Effects
|
||||
|
||||
In this section we give some advice to those who are new to WLED Effect creation. We will illustrate how to load in multiple Effects using this single usermod, and we will do a deep dive into the anatomy of a 1D Effect as well as a 2D Effect.
|
||||
(Special thanks to @mryndzionek for offering this "Diffusion Fire" 2D Effect for this tutorial.)
|
||||
|
||||
### Imports
|
||||
The first line of the code imports the [wled.h](https://github.com/wled/WLED/blob/main/wled00/wled.h) file into this module. Importing `wled.h` brings all of the variables, files, and functions listed in the table above (and more) into your custom effect for you to use.
|
||||
|
||||
```cpp
|
||||
#include "wled.h"
|
||||
```
|
||||
|
||||
### Static Effect Definition
|
||||
The next code block is the `mode_static` definition. This is usually left as `SEGMENT.fill(SEGCOLOR(0));` to leave all pixels off if the effect fails to load, but in theory one could use this as a 'fallback effect' to take on a different behavior, such as displaying some other color instead of leaving the pixels off.
|
||||
|
||||
`FX_FALLBACK_STATIC` is a macro that calls `mode_static()` and then returns.
|
||||
|
||||
### User Effect Definitions
|
||||
Pre-loaded in this template is an example 2D Effect called "Diffusion Fire". (This is the name that would be shown in the UI once the binary is compiled and run on your device, as defined in the metadata string.)
|
||||
The effect starts off by checking to see if the segment that the effect is being applied to is a 2D Matrix, and if it is not, then it runs the static effect which displays no pattern:
|
||||
```cpp
|
||||
if (!strip.isMatrix || !SEGMENT.is2D())
|
||||
FX_FALLBACK_STATIC; // not a 2D set-up
|
||||
```
|
||||
The next code block contains several constant variable definitions which essentially serve to extract the dimensions of the user's 2D matrix and allow WLED to interpret the matrix as a 1D coordinate system (WLED must do this for all 2D animations):
|
||||
```cpp
|
||||
const int cols = SEG_W;
|
||||
const int rows = SEG_H;
|
||||
const auto XY = [&](int x, int y) { return x + y * cols; };
|
||||
```
|
||||
* The first line assigns the number of columns (width) in the active segment to cols.
|
||||
* SEG_W is a macro defined in WLED that expands to SEGMENT.width(). This value is the width of your 2D matrix segment, used to traverse the matrix correctly.
|
||||
* Next, we assign the number of rows (height) in the segment to rows.
|
||||
* SEG_H is a macro for SEGMENT.height(). Combined with cols, this allows pixel addressing in 2D (x, y) space.
|
||||
* The third line declares a lambda function named `XY` to map (x, y) matrix coordinates into a 1D index in the LED array. This assumes row-major order (left to right, top to bottom).
|
||||
* This lambda helps with mapping a local 1D array to a 2D one.
|
||||
|
||||
The next lines of code further the setup process by defining variables that allow the effect's settings to be configurable using the UI sliders (or alternatively, through API calls):
|
||||
```cpp
|
||||
const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80);
|
||||
const unsigned refresh_ms = 1000 / refresh_hz;
|
||||
const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100);
|
||||
const uint8_t spark_rate = SEGMENT.intensity;
|
||||
const uint8_t turbulence = SEGMENT.custom2;
|
||||
```
|
||||
* The first line maps the SEGMENT.speed (user-controllable parameter from 0–255) to a value between 20 and 80 Hz.
|
||||
* This determines how often the effect should refresh per second (Higher speed = more frames per second).
|
||||
* Next we convert refresh rate from Hz to milliseconds. (It’s easier to schedule animation updates in WLED using elapsed time in milliseconds.)
|
||||
* This value is used to time when to update the effect.
|
||||
* The third line utilizes the `custom1` control (0–255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0–100.
|
||||
* This controls how much "heat" spreads to neighboring pixels — more diffusion = smoother flame spread.
|
||||
* Next we assign `SEGMENT.intensity` (user input 0–255) to a variable named `spark_rate`.
|
||||
* This controls how frequently new "spark" pixels appear at the bottom of the matrix.
|
||||
* A higher value means more frequent ignition of flame points.
|
||||
* The final line stores the user-defined `custom2` value to a variable called `turbulence`.
|
||||
* This is used to introduce randomness in spark generation or flow — more turbulence means more chaotic behavior.
|
||||
|
||||
Next we will look at some lines of code that handle memory allocation and effect initialization:
|
||||
|
||||
```cpp
|
||||
unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D
|
||||
```
|
||||
* This part calculates how much memory we need to represent per-pixel state.
|
||||
* `cols * rows` or `(or SEGLEN)` returns the total number of pixels in the current segment.
|
||||
* This fire effect models heat values per pixel (not just colors), so we need persistent storage — one uint8_t per pixel — for the entire effect.
|
||||
> **_NOTE:_** Virtual lengths `vWidth()` and `vHeight()` will be evaluated differently based on your own custom effect, and based on what other settings are active. For example: If you have an LED strip of length = 60 and you enable grouping = 2, then the virtual length will be 30, so the FX will render 30 pixels instead of 60. This is also true for mirroring or adding gaps--it halves the size. For a 1D strip mapped to 2D, the virtual length depends on selected mode. Keep these things in mind during your custom effect's creation.
|
||||
|
||||
```cpp
|
||||
if (!SEGENV.allocateData(dataSize))
|
||||
FX_FALLBACK_STATIC; // allocation failed
|
||||
```
|
||||
* Upon the first call, this section allocates a persistent data buffer tied to the segment environment (`SEGENV.data`). All subsequent calls simply ensure that the data is still valid.
|
||||
* The syntax `SEGENV.allocateData(n)` requests a buffer of size n bytes (1 byte per pixel here).
|
||||
* If allocation fails (e.g., out of memory), it returns false, and the effect can’t proceed.
|
||||
* It calls previously defined `mode_static()` fallback effect, which just fills the segment with a static color. We need to do this because WLED needs a fail-safe behavior if a custom effect can't run properly due to memory constraints.
|
||||
|
||||
|
||||
The next lines of code clear the LEDs and initialize timing:
|
||||
```cpp
|
||||
if (SEGENV.call == 0) {
|
||||
SEGMENT.fill(BLACK);
|
||||
SEGENV.step = 0;
|
||||
}
|
||||
```
|
||||
* The first line checks whether this is the first time the effect is being run; `SEGENV.call` is a counter for how many times this effect function has been invoked since it started.
|
||||
* If `SEGENV.call` equals 0 (which it does on the very first call, making it useful for initialization), then it clears the LED segment by filling it with black (turns off all LEDs).
|
||||
* This gives a clean starting point for the fire animation.
|
||||
* It also initializes `SEGENV.step`, a timing marker, to 0. This value is later used as a timestamp to control when the next animation frame should occur (based on elapsed time).
|
||||
|
||||
|
||||
The next block of code is where the animation update logic starts to kick in:
|
||||
```cpp
|
||||
if ((strip.now - SEGENV.step) >= refresh_ms) {
|
||||
uint8_t tmp_row[cols]; // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.
|
||||
SEGENV.step = strip.now;
|
||||
// scroll up
|
||||
for (unsigned y = 1; y < rows; y++)
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
unsigned src = XY(x, y);
|
||||
unsigned dst = XY(x, y - 1);
|
||||
SEGENV.data[dst] = SEGENV.data[src];
|
||||
}
|
||||
```
|
||||
* The first line checks if it's time to update the effect frame. `strip.now` is the current timestamp in milliseconds; `SEGENV.step` is the last update time (set during initialization or previous frame). `refresh_ms` is how long to wait between frames, computed earlier based on SEGMENT.speed.
|
||||
* The conditional statement in the first line of code ensures the effect updates on a fixed interval — e.g., every 20 ms for 50 Hz.
|
||||
* The second line of code declares a temporary row buffer for intermediate diffusion results that is one byte per column (horizontal position), so this buffer holds one row's worth of heat values.
|
||||
* You'll see later that it writes results here before updating `SEGENV.data`.
|
||||
* Note: this is allocated on the stack each frame. Keep such VLAs ≤ ~1 KiB; for larger sizes, prefer a buffer in `SEGENV.data`.
|
||||
|
||||
> **_IMPORTANT NOTE:_** Creating variable‑length arrays (VLAs) is non‑standard C++, but this practice is used throughout WLED and works in practice. But be aware that VLAs live on the stack, which is limited. If the array scales with segment length (1D), it can overflow the stack and crash. Keep VLAs ≲ ~1 KiB; an array with 4000 LEDs is ~4 KiB and will likely crash. It’s worse with `uint16_t`. Anything larger than ~1 KiB should go into `SEGENV.data`, which has a higher limit.
|
||||
|
||||
|
||||
Now we get to the spark generation portion, where new bursts of heat appear at the bottom of the matrix:
|
||||
```cpp
|
||||
if (hw_random8() > turbulence) {
|
||||
// create new sparks at bottom row
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
uint8_t p = hw_random8();
|
||||
if (p < spark_rate) {
|
||||
unsigned dst = XY(x, rows - 1);
|
||||
SEGENV.data[dst] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
* The first line randomizes whether we even attempt to spawn sparks this frame.
|
||||
* `hw_random8()` gives a random number between 0–255 using a fast hardware RNG.
|
||||
* `turbulence` is a user-controlled parameter (SEGMENT.custom2, set earlier).
|
||||
* Higher turbulence means this block is less likely to run (because `hw_random8()` is less likely to exceed a high threshold).
|
||||
* This adds randomness to when sparks appear — simulating natural flicker and chaotic fire.
|
||||
* The next line loops over all columns in the bottom row (row `rows - 1`).
|
||||
* Another random number, `p`, is used to probabilistically decide whether a spark appears at this (x, `rows-1`) position.
|
||||
* Next is a conditional statement. The lower spark_rate is, the fewer sparks will appear.
|
||||
* `spark_rate` comes from `SEGMENT.intensity` (0–255).
|
||||
* High intensity means more frequent ignition.
|
||||
* `dst` calculates the destination index in the bottom row at column x.
|
||||
* The final line here sets the heat at this pixel to maximum (255).
|
||||
* This simulates a fresh burst of flame, which will diffuse and move upward over time in subsequent frames.
|
||||
|
||||
Next we reach the first part of the core of the fire simulation, which is diffusion (how heat spreads to neighboring pixels):
|
||||
```cpp
|
||||
// diffuse
|
||||
for (unsigned y = 0; y < rows; y++) {
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
unsigned v = SEGENV.data[XY(x, y)];
|
||||
if (x > 0) {
|
||||
v += SEGENV.data[XY(x - 1, y)];
|
||||
}
|
||||
if (x < (cols - 1)) {
|
||||
v += SEGENV.data[XY(x + 1, y)];
|
||||
}
|
||||
tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));
|
||||
}
|
||||
```
|
||||
* This block of code starts by looping over each row from top to bottom. (We will do diffusion for each pixel row.)
|
||||
* Next we start an inner loop which iterates across each column in the current row.
|
||||
* Starting with the current heat value of pixel (x, y) assigned `v`:
|
||||
* if there’s a pixel to the left, add its heat to the total.
|
||||
* If there’s a pixel to the right, add its heat as well.
|
||||
* So essentially, what the two `if` statements accomplish is: `v = center + left + right`.
|
||||
* The final line of code applies diffusion smoothing:
|
||||
* The denominator controls how much the neighboring heat contributes. `300 + diffusion` means that with higher diffusion, you get more smoothing (since the sum is divided more).
|
||||
* The `v * 100` scales things before dividing (preserving some dynamic range).
|
||||
* `min(255, ...)` clamps the result to 8-bit range.
|
||||
* This entire line of code stores the smoothed heat into the temporary row buffer.
|
||||
|
||||
After calculating tmp_row, we now handle rendering the pixels by updating the actual segment data and turning 'heat' into visible colors:
|
||||
```cpp
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
SEGENV.data[XY(x, y)] = tmp_row[x];
|
||||
if (SEGMENT.check1) {
|
||||
uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);
|
||||
SEGMENT.setPixelColorXY(x, y, color);
|
||||
} else {
|
||||
uint32_t base = SEGCOLOR(0);
|
||||
SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
* This next loop starts iterating over each row from top to bottom. (We're now doing this for color-rendering for each pixel row.)
|
||||
* Next we update the main segment data with the smoothed value for this pixel.
|
||||
* The if statement creates a conditional rendering path — the user can toggle this. If `check1` is enabled in the effect metadata, we use a color palette to display the flame.
|
||||
* The next line converts the heat value (`tmp_row[x]`) into a `color` from the current palette with 255 brightness, and no wrapping in palette lookup.
|
||||
* This creates rich gradient flames (e.g., yellow → red → black).
|
||||
* Finally we set the rendered color for the pixel (x, y).
|
||||
* This repeats for each pixel in each row.
|
||||
* If palette use is disabled, we fallback to fading a base color.
|
||||
* `SEGCOLOR(0)` gets the first user-selected color for the segment.
|
||||
* The final line of code fades that base color according to the heat value (acts as brightness multiplier).
|
||||
|
||||
* Even though the effect logic itself controls when to update based on refresh_ms, WLED will still call this function at roughly FRAMETIME intervals (the FPS limit set in config) to check whether an update is needed. If nothing needs to change, the frame still needs to be re-rendered so color or brightness transitions will be smooth.
|
||||
|
||||
If you want to run your effect at a fixed frame rate you can use the following code to not update your effect state, be aware however that transitions for your effect will also run at this frame rate - for example if you limit your effect to say 5 FPS, brightness changes and color changes may not look smooth. Also `SEGMENT.call` is still incremented on each function call.
|
||||
```cpp
|
||||
//limit update rate
|
||||
if (strip.now - SEGENV.step < FRAMETIME_FIXED) return;
|
||||
SEGENV.step = strip.now;
|
||||
```
|
||||
|
||||
### The Metadata String
|
||||
At the end of every effect is an important line of code called the **metadata string**.
|
||||
It defines how the effect is to be interacted with in the UI:
|
||||
```cpp
|
||||
static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35";
|
||||
```
|
||||
This metadata string is passed into `strip.addEffect()` and parsed by WLED to determine how your effect appears and behaves in the UI.
|
||||
The string follows the syntax of `<Effect Parameters>;<Colors>;<Palette>;<Flags>;<Defaults>`, where Effect Parameters are specified by a comma-separated list.
|
||||
The values for Effect Parameters will always follow the convention in the table below:
|
||||
|
||||
| Parameter | Default tooltip label |
|
||||
| :-------- | :-------------------- |
|
||||
| sx | Effect Speed |
|
||||
| ix | Effect Intensity |
|
||||
| c1 | Custom 1 |
|
||||
| c2 | Custom 2 |
|
||||
| c3 | Custom 3 |
|
||||
| o1 | Checkbox 1 |
|
||||
| o2 | Checkbox 2 |
|
||||
| o3 | Checkbox 3 |
|
||||
|
||||
Using this info, let’s split the Metadata string above into logical sections:
|
||||
|
||||
| Syntax Element | Description |
|
||||
| :---------------------------------------------- | :---------- |
|
||||
| "Diffusion Fire@! | Name. (The @ symbol marks the end of the Effect Name, and the beginning of the Parameter String elements.) |
|
||||
| !, | Use default UI entry; for the first space, this will automatically create a slider for Speed |
|
||||
| Spark rate, Diffusion Speed, Turbulence, | UI sliders for Spark Rate, Diffusion Speed, and Turbulence. Defining slider 2 as "Spark Rate" overwrites the default value of Intensity. |
|
||||
| (blank), | unused (empty field with not even a space) |
|
||||
| Use palette; | This occupies the spot for the 6th effect parameter, which automatically makes this a checkbox argument `o1` called Use palette in the UI. When this is enabled, the effect uses `SEGMENT.color_from_palette(...)` (RGBW-aware, respects wrap), otherwise it fades from `SEGCOLOR(0)`. The first semicolon marks the end of the Effect Parameters and the beginning of the `Colors` parameter. |
|
||||
| Color; | Custom color field `(SEGCOLOR(0))` |
|
||||
| (blank); | Empty means the effect does not allow Palettes to be selected by the user. But used in conjunction with the checkbox argument, palette use can be turned on/off by the user. |
|
||||
| 2; | Flag specifying that the effect requires a 2D matrix setup |
|
||||
| pal=35" | Default Palette ID. this is the setting that the effect starts up with. |
|
||||
|
||||
More information on metadata strings can be found [here](https://kno.wled.ge/interfaces/json-api/#effect-metadata).
|
||||
|
||||
|
||||
## Understanding 1D WLED Effects
|
||||
|
||||
Next, we will look at a 1D WLED effect called `Sinelon`. This one is an especially interesting example because it shows how a single effect function can be used to create several different selectable effects in the UI.
|
||||
We will break this effect down step by step.
|
||||
(This effect was originally one of the FastLED example effects; more information on FastLED can be found [here](https://fastled.io/).)
|
||||
|
||||
```cpp
|
||||
static void sinelon_base(bool dual, bool rainbow=false) {
|
||||
```
|
||||
* The first line of code defines `sinelon base` as static helper function. This is how all effects are initially defined.
|
||||
* Notice that it has some optional flags; these parameters will allow us to easily define the effect in different ways in the UI.
|
||||
|
||||
```cpp
|
||||
if (SEGLEN <= 1) FX_FALLBACK_STATIC;
|
||||
```
|
||||
* If segment length ≤ 1, there’s nothing to animate. Just show static mode.
|
||||
|
||||
The line of code helps create the "Fade Out" Trail:
|
||||
```cpp
|
||||
SEGMENT.fade_out(SEGMENT.intensity);
|
||||
```
|
||||
* Gradually dims all LEDs each frame using SEGMENT.intensity as fade amount.
|
||||
* Creates the trailing "comet" effect by leaving a fading path behind the moving dot.
|
||||
|
||||
Next, the effect computes some position information for the actively changing pixel, and the rest of the pixels as well:
|
||||
```cpp
|
||||
unsigned pos = beatsin16_t(SEGMENT.speed/10, 0, SEGLEN-1);
|
||||
if (SEGENV.call == 0) SEGENV.aux0 = pos;
|
||||
```
|
||||
* Calculates a sine-based oscillation to move the dot smoothly back and forth.
|
||||
* `beatsin16_t` is an improved version of FastLED’s beatsin16 function, generating smooth oscillations
|
||||
* SEGMENT.speed / 10: affects oscillation speed. Higher = faster.
|
||||
* 0: minimum position.
|
||||
* SEGLEN-1: maximum position.
|
||||
* On first call `(SEGENV.call == 0)`, stores initial position in `SEGENV.aux0`. (`SEGENV.aux0` is a temporary state variable to keep track of last position.)
|
||||
|
||||
The next lines of code help determine the colors to be used:
|
||||
```cpp
|
||||
uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0);
|
||||
uint32_t color2 = SEGCOLOR(2);
|
||||
```
|
||||
* `color1`: main moving dot color, chosen from palette using the current position as index.
|
||||
* `color2`: secondary color from user-configured color slot 2.
|
||||
|
||||
The next part takes into account the optional argument for if a Rainbow colored palette is in use:
|
||||
```cpp
|
||||
if (rainbow) {
|
||||
color1 = SEGMENT.color_wheel((pos & 0x07) * 32);
|
||||
}
|
||||
```
|
||||
* If `rainbow` is true, override color1 using a rainbow wheel, producing rainbow cycling colors.
|
||||
* `(pos & 0x07) * 32` ensures the color changes gradually with position.
|
||||
|
||||
```cpp
|
||||
SEGMENT.setPixelColor(pos, color1);
|
||||
```
|
||||
* Lights up the computed position with the selected color.
|
||||
|
||||
The next line takes into account another one of the optional arguments for the effect to potentially handle dual mirrored dots which create the animation:
|
||||
```cpp
|
||||
if (dual) {
|
||||
if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0);
|
||||
if (rainbow) color2 = color1; // share rainbow color
|
||||
SEGMENT.setPixelColor(SEGLEN-1-pos, color2);
|
||||
}
|
||||
```
|
||||
* If dual is true:
|
||||
* Uses `color2` for mirrored dot on opposite side.
|
||||
* If `color2` is not set (0), fallback to same palette color as `color1`.
|
||||
* In `rainbow` mode, force both dots to share the rainbow color.
|
||||
* Sets pixel at `SEGLEN-1-pos` to `color2`.
|
||||
|
||||
This final part of the effect function will fill in the 'trailing' pixels to complete the animation:
|
||||
```cpp
|
||||
if (SEGENV.aux0 < pos) {
|
||||
for (unsigned i = SEGENV.aux0; i < pos ; i++) {
|
||||
SEGMENT.setPixelColor(i, color1);
|
||||
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
|
||||
}
|
||||
} else {
|
||||
for (unsigned i = SEGENV.aux0; i > pos ; i--) {
|
||||
SEGMENT.setPixelColor(i, color1);
|
||||
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
|
||||
}
|
||||
}
|
||||
SEGENV.aux0 = pos;
|
||||
}
|
||||
```
|
||||
* The first line checks if current position has changed since last frame. (Prevents holes if the dot moves quickly and "skips" pixels.) If the position has changed, then it will implement the logic to update the rest of the pixels.
|
||||
* Fills in all pixels between previous position (SEGENV.aux0) and new position (pos) to ensure smooth continuous trail.
|
||||
* Works in both directions: Forward (if new pos > old pos), and Backward (if new pos < old pos).
|
||||
* Updates `SEGENV.aux0` to current position at the end.
|
||||
|
||||
The last part of this effect has the Wrapper functions for different Sinelon modes.
|
||||
Notice that there are three different modes that we can define from the single effect definition by leveraging the arguments in the function:
|
||||
```cpp
|
||||
void mode_sinelon(void) {
|
||||
sinelon_base(false);
|
||||
}
|
||||
// Calls sinelon_base with dual = false and rainbow = false
|
||||
|
||||
void mode_sinelon_dual(void) {
|
||||
sinelon_base(true);
|
||||
}
|
||||
// Calls sinelon_base with dual = true and rainbow = false
|
||||
|
||||
void mode_sinelon_rainbow(void) {
|
||||
sinelon_base(false, true);
|
||||
}
|
||||
// Calls sinelon_base with dual = false and rainbow = true
|
||||
```
|
||||
|
||||
And then the last part defines the metadata strings for each effect to specify how it will be portrayed in the UI:
|
||||
```cpp
|
||||
static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!";
|
||||
static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!";
|
||||
static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!";
|
||||
```
|
||||
Refer to the section above for guidance on understanding metadata strings.
|
||||
|
||||
|
||||
### The UserFxUsermod Class
|
||||
|
||||
The `UserFxUsermod` class registers the `mode_diffusionfire` effect with WLED. This section starts right after the effect function and metadata string, and is responsible for making the effect usable in the WLED interface:
|
||||
```cpp
|
||||
class UserFxUsermod : public Usermod {
|
||||
private:
|
||||
public:
|
||||
void setup() override {
|
||||
strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE);
|
||||
|
||||
////////////////////////////////////////
|
||||
// add your effect function(s) here //
|
||||
////////////////////////////////////////
|
||||
|
||||
// use id=255 for all custom user FX (the final id is assigned when adding the effect)
|
||||
|
||||
// strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT);
|
||||
// strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2);
|
||||
// strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3);
|
||||
}
|
||||
void loop() override {} // nothing to do in the loop
|
||||
uint16_t getId() override { return USERMOD_ID_USER_FX; }
|
||||
};
|
||||
```
|
||||
* The first line declares a new class called UserFxUsermod. It inherits from `Usermod`, which is the base class WLED uses for any pluggable user-defined modules.
|
||||
* This makes UserFxUsermod a valid WLED extension that can hook into `setup()`, `loop()`, and other lifecycle events.
|
||||
* The `void setup()` function runs once when WLED initializes the usermod.
|
||||
* It's where you should register your effects, initialize hardware, or do any other setup logic.
|
||||
* `override` ensures that this matches the Usermod base class definition.
|
||||
* The `strip.addEffect` line is an important one that registers the custom effect so WLED knows about it.
|
||||
* 255: Temporary ID — WLED will assign a unique ID automatically. (**Create all custom effects with the 255 ID.**)
|
||||
* `&mode_diffusionfire`: Pointer to the effect function.
|
||||
* `_data_FX_MODE_DIFFUSIONFIRE`: Metadata string stored in PROGMEM, describing the effect name and UI fields (like sliders).
|
||||
* After this, your custom effect shows up in the WLED effects list.
|
||||
* The `loop()` function remains empty because this usermod doesn’t need to do anything continuously. WLED still calls this every main loop, but nothing is done here.
|
||||
* If your usermod had to respond to input or update state, you'd do it here.
|
||||
* The last part returns a unique ID constant used to identify this usermod.
|
||||
* USERMOD_ID_USER_FX is defined in [const.h](https://github.com/wled/WLED/blob/main/wled00/const.h). WLED uses this for tracking, debugging, or referencing usermods internally.
|
||||
|
||||
The final part of this file handles instantiation and initialization:
|
||||
```cpp
|
||||
static UserFxUsermod user_fx;
|
||||
REGISTER_USERMOD(user_fx);
|
||||
```
|
||||
* The first line creates a single, global instance of your usermod class.
|
||||
* The last line is a macro that tells WLED: “This is a valid usermod — load it during startup.”
|
||||
* WLED adds it to the list of active usermods, calls `setup()` and `loop()`, and lets it interact with the system.
|
||||
|
||||
|
||||
|
||||
## Combining Multiple Effects in this Usermod
|
||||
|
||||
So now let's say that you wanted add the effects "Diffusion Fire" and "Sinelon" through this same Usermod file:
|
||||
* Navigate to [the code for Sinelon](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/FX.cpp#L3110).
|
||||
* Copy this code, and place it below the metadata string for Diffusion Fire. Be sure to get the metadata string as well--and to name it something different than what's already inside the core WLED code. (Refer to the metadata String section above for more information.)
|
||||
* Register the effect using the `addEffect` function in the Usermod class.
|
||||
* Compile the code!
|
||||
|
||||
## Compiling
|
||||
Compiling WLED yourself is beyond the scope of this tutorial, but [the complete guide to compiling WLED can be found here](https://kno.wled.ge/advanced/compiling-wled/), on the official WLED documentation website.
|
||||
|
||||
## Change Log
|
||||
|
||||
### Version 1.0.0
|
||||
|
||||
* First version of the custom effect creation guide
|
||||
|
||||
## Contact Us
|
||||
|
||||
This custom effect tutorial guide is still in development.
|
||||
If you have suggestions on what should be added, or if you've found any parts of this guide which seem incorrect, feel free to reach out [here](mailto:aregis1992@gmail.com) and help us improve this guide for future creators.
|
||||
|
||||
+1210
-16
File diff suppressed because it is too large
Load Diff
@@ -96,14 +96,14 @@ public:
|
||||
fastled_col.red = colPri[0];
|
||||
fastled_col.green = colPri[1];
|
||||
fastled_col.blue = colPri[2];
|
||||
prim_hsv = rgb2hsv_approximate(fastled_col);
|
||||
prim_hsv = rgb2hsv(fastled_col);
|
||||
new_val = (int16_t)prim_hsv.h + fadeAmount;
|
||||
if (new_val > 255)
|
||||
new_val -= 255; // roll-over if bigger than 255
|
||||
if (new_val < 0)
|
||||
new_val += 255; // roll-over if smaller than 0
|
||||
prim_hsv.h = (byte)new_val;
|
||||
hsv2rgb_rainbow(prim_hsv, fastled_col);
|
||||
fastled_col = prim_hsv ;
|
||||
colPri[0] = fastled_col.red;
|
||||
colPri[1] = fastled_col.green;
|
||||
colPri[2] = fastled_col.blue;
|
||||
@@ -121,14 +121,14 @@ public:
|
||||
fastled_col.red = colPri[0];
|
||||
fastled_col.green = colPri[1];
|
||||
fastled_col.blue = colPri[2];
|
||||
prim_hsv = rgb2hsv_approximate(fastled_col);
|
||||
prim_hsv = rgb2hsv(fastled_col);
|
||||
new_val = (int16_t)prim_hsv.h - fadeAmount;
|
||||
if (new_val > 255)
|
||||
new_val -= 255; // roll-over if bigger than 255
|
||||
if (new_val < 0)
|
||||
new_val += 255; // roll-over if smaller than 0
|
||||
prim_hsv.h = (byte)new_val;
|
||||
hsv2rgb_rainbow(prim_hsv, fastled_col);
|
||||
fastled_col = prim_hsv;
|
||||
colPri[0] = fastled_col.red;
|
||||
colPri[1] = fastled_col.green;
|
||||
colPri[2] = fastled_col.blue;
|
||||
|
||||
@@ -284,7 +284,6 @@ void HttpPullLightControl::handleResponse(String& responseStr) {
|
||||
if (!requestJSONBufferLock(myLockId)) {
|
||||
DEBUG_PRINT(F("ERROR: Can not request JSON Buffer Lock, number: "));
|
||||
DEBUG_PRINTLN(myLockId);
|
||||
releaseJSONBufferLock(); // Just release in any case, maybe there was already a buffer lock
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ public:
|
||||
char objKey[14];
|
||||
bool parsed = false;
|
||||
|
||||
if (!requestJSONBufferLock(22)) return false;
|
||||
if (!requestJSONBufferLock(JSON_LOCK_REMOTE)) return false;
|
||||
|
||||
sprintf_P(objKey, PSTR("\"%d\":"), button);
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"name": "animartrix",
|
||||
"build": { "libArchive": false },
|
||||
"dependencies": {
|
||||
"Animartrix": "https://github.com/netmindz/animartrix.git#b172586"
|
||||
"Animartrix": "https://github.com/netmindz/animartrix.git#81eb09b91c8c9c8c01f8ea442787f8127d56c72f"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,266 +85,214 @@ class ANIMartRIXMod:public ANIMartRIX {
|
||||
};
|
||||
ANIMartRIXMod anim;
|
||||
|
||||
uint16_t mode_Module_Experiment10() {
|
||||
void mode_Module_Experiment10() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment10();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment9() {
|
||||
void mode_Module_Experiment9() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment9();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment8() {
|
||||
void mode_Module_Experiment8() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment8();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment7() {
|
||||
void mode_Module_Experiment7() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment7();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment6() {
|
||||
void mode_Module_Experiment6() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment6();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment5() {
|
||||
void mode_Module_Experiment5() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment5();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment4() {
|
||||
void mode_Module_Experiment4() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment4();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Zoom2() {
|
||||
void mode_Zoom2() {
|
||||
anim.initEffect();
|
||||
anim.Zoom2();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment3() {
|
||||
void mode_Module_Experiment3() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment3();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment2() {
|
||||
void mode_Module_Experiment2() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment2();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Module_Experiment1() {
|
||||
void mode_Module_Experiment1() {
|
||||
anim.initEffect();
|
||||
anim.Module_Experiment1();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Parametric_Water() {
|
||||
void mode_Parametric_Water() {
|
||||
anim.initEffect();
|
||||
anim.Parametric_Water();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Water() {
|
||||
void mode_Water() {
|
||||
anim.initEffect();
|
||||
anim.Water();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Complex_Kaleido_6() {
|
||||
void mode_Complex_Kaleido_6() {
|
||||
anim.initEffect();
|
||||
anim.Complex_Kaleido_6();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Complex_Kaleido_5() {
|
||||
void mode_Complex_Kaleido_5() {
|
||||
anim.initEffect();
|
||||
anim.Complex_Kaleido_5();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Complex_Kaleido_4() {
|
||||
void mode_Complex_Kaleido_4() {
|
||||
anim.initEffect();
|
||||
anim.Complex_Kaleido_4();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Complex_Kaleido_3() {
|
||||
void mode_Complex_Kaleido_3() {
|
||||
anim.initEffect();
|
||||
anim.Complex_Kaleido_3();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Complex_Kaleido_2() {
|
||||
void mode_Complex_Kaleido_2() {
|
||||
anim.initEffect();
|
||||
anim.Complex_Kaleido_2();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Complex_Kaleido() {
|
||||
void mode_Complex_Kaleido() {
|
||||
anim.initEffect();
|
||||
anim.Complex_Kaleido();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_SM10() {
|
||||
void mode_SM10() {
|
||||
anim.initEffect();
|
||||
anim.SM10();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_SM9() {
|
||||
void mode_SM9() {
|
||||
anim.initEffect();
|
||||
anim.SM9();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_SM8() {
|
||||
void mode_SM8() {
|
||||
anim.initEffect();
|
||||
anim.SM8();
|
||||
return FRAMETIME;
|
||||
}
|
||||
// uint16_t mode_SM7() {
|
||||
// void mode_SM7() {
|
||||
// anim.initEffect();
|
||||
// anim.SM7();
|
||||
//
|
||||
// return FRAMETIME;
|
||||
// }
|
||||
uint16_t mode_SM6() {
|
||||
void mode_SM6() {
|
||||
anim.initEffect();
|
||||
anim.SM6();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_SM5() {
|
||||
void mode_SM5() {
|
||||
anim.initEffect();
|
||||
anim.SM5();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_SM4() {
|
||||
void mode_SM4() {
|
||||
anim.initEffect();
|
||||
anim.SM4();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_SM3() {
|
||||
void mode_SM3() {
|
||||
anim.initEffect();
|
||||
anim.SM3();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_SM2() {
|
||||
void mode_SM2() {
|
||||
anim.initEffect();
|
||||
anim.SM2();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_SM1() {
|
||||
void mode_SM1() {
|
||||
anim.initEffect();
|
||||
anim.SM1();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Big_Caleido() {
|
||||
void mode_Big_Caleido() {
|
||||
anim.initEffect();
|
||||
anim.Big_Caleido();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_RGB_Blobs5() {
|
||||
void mode_RGB_Blobs5() {
|
||||
anim.initEffect();
|
||||
anim.RGB_Blobs5();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_RGB_Blobs4() {
|
||||
void mode_RGB_Blobs4() {
|
||||
anim.initEffect();
|
||||
anim.RGB_Blobs4();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_RGB_Blobs3() {
|
||||
void mode_RGB_Blobs3() {
|
||||
anim.initEffect();
|
||||
anim.RGB_Blobs3();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_RGB_Blobs2() {
|
||||
void mode_RGB_Blobs2() {
|
||||
anim.initEffect();
|
||||
anim.RGB_Blobs2();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_RGB_Blobs() {
|
||||
void mode_RGB_Blobs() {
|
||||
anim.initEffect();
|
||||
anim.RGB_Blobs();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Polar_Waves() {
|
||||
void mode_Polar_Waves() {
|
||||
anim.initEffect();
|
||||
anim.Polar_Waves();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Slow_Fade() {
|
||||
void mode_Slow_Fade() {
|
||||
anim.initEffect();
|
||||
anim.Slow_Fade();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Zoom() {
|
||||
void mode_Zoom() {
|
||||
anim.initEffect();
|
||||
anim.Zoom();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Hot_Blob() {
|
||||
void mode_Hot_Blob() {
|
||||
anim.initEffect();
|
||||
anim.Hot_Blob();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Spiralus2() {
|
||||
void mode_Spiralus2() {
|
||||
anim.initEffect();
|
||||
anim.Spiralus2();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Spiralus() {
|
||||
void mode_Spiralus() {
|
||||
anim.initEffect();
|
||||
anim.Spiralus();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Yves() {
|
||||
void mode_Yves() {
|
||||
anim.initEffect();
|
||||
anim.Yves();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Scaledemo1() {
|
||||
void mode_Scaledemo1() {
|
||||
anim.initEffect();
|
||||
anim.Scaledemo1();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Lava1() {
|
||||
void mode_Lava1() {
|
||||
anim.initEffect();
|
||||
anim.Lava1();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Caleido3() {
|
||||
void mode_Caleido3() {
|
||||
anim.initEffect();
|
||||
anim.Caleido3();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Caleido2() {
|
||||
void mode_Caleido2() {
|
||||
anim.initEffect();
|
||||
anim.Caleido2();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Caleido1() {
|
||||
void mode_Caleido1() {
|
||||
anim.initEffect();
|
||||
anim.Caleido1();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Distance_Experiment() {
|
||||
void mode_Distance_Experiment() {
|
||||
anim.initEffect();
|
||||
anim.Distance_Experiment();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Center_Field() {
|
||||
void mode_Center_Field() {
|
||||
anim.initEffect();
|
||||
anim.Center_Field();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Waves() {
|
||||
void mode_Waves() {
|
||||
anim.initEffect();
|
||||
anim.Waves();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Chasing_Spirals() {
|
||||
void mode_Chasing_Spirals() {
|
||||
anim.initEffect();
|
||||
anim.Chasing_Spirals();
|
||||
return FRAMETIME;
|
||||
}
|
||||
uint16_t mode_Rotating_Blob() {
|
||||
void mode_Rotating_Blob() {
|
||||
anim.initEffect();
|
||||
anim.Rotating_Blob();
|
||||
return FRAMETIME;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
// It can be configured to load auto saved preset at startup,
|
||||
// during the first `loop()`.
|
||||
//
|
||||
// By default it will not save the state if an unmodified preset
|
||||
// is selected (to not duplicate it). You can change this behaviour
|
||||
// by setting autoSaveIgnorePresets=false
|
||||
//
|
||||
// AutoSaveUsermod is standalone, but if FourLineDisplayUsermod
|
||||
// is installed, it will notify the user of the saved changes.
|
||||
|
||||
@@ -49,6 +53,8 @@ class AutoSaveUsermod : public Usermod {
|
||||
bool applyAutoSaveOnBoot = false; // do we load auto-saved preset on boot?
|
||||
#endif
|
||||
|
||||
bool autoSaveIgnorePresets = true; // ignore by default to not duplicate presets
|
||||
|
||||
// If we've detected the need to auto save, this will be non zero.
|
||||
unsigned long autoSaveAfter = 0;
|
||||
|
||||
@@ -68,6 +74,7 @@ class AutoSaveUsermod : public Usermod {
|
||||
static const char _autoSaveAfterSec[];
|
||||
static const char _autoSavePreset[];
|
||||
static const char _autoSaveApplyOnBoot[];
|
||||
static const char _autoSaveIgnorePresets[];
|
||||
|
||||
void inline saveSettings() {
|
||||
char presetNameBuffer[PRESET_NAME_BUFFER_SIZE];
|
||||
@@ -122,7 +129,8 @@ class AutoSaveUsermod : public Usermod {
|
||||
void loop() {
|
||||
static unsigned long lastRun = 0;
|
||||
unsigned long now = millis();
|
||||
if (!autoSaveAfterSec || !enabled || currentPreset>0 || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave
|
||||
if (!autoSaveAfterSec || !enabled || (autoSaveIgnorePresets && currentPreset>0) || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave
|
||||
lastRun = now;
|
||||
uint8_t currentMode = strip.getMainSegment().mode;
|
||||
uint8_t currentPalette = strip.getMainSegment().palette;
|
||||
|
||||
@@ -219,10 +227,11 @@ class AutoSaveUsermod : public Usermod {
|
||||
void addToConfig(JsonObject& root) {
|
||||
// we add JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}}
|
||||
JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
|
||||
top[FPSTR(_autoSaveEnabled)] = enabled;
|
||||
top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam
|
||||
top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam
|
||||
top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot;
|
||||
top[FPSTR(_autoSaveEnabled)] = enabled;
|
||||
top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam
|
||||
top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam
|
||||
top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot;
|
||||
top[FPSTR(_autoSaveIgnorePresets)] = autoSaveIgnorePresets;
|
||||
DEBUG_PRINTLN(F("Autosave config saved."));
|
||||
}
|
||||
|
||||
@@ -245,12 +254,13 @@ class AutoSaveUsermod : public Usermod {
|
||||
return false;
|
||||
}
|
||||
|
||||
enabled = top[FPSTR(_autoSaveEnabled)] | enabled;
|
||||
autoSaveAfterSec = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec;
|
||||
autoSaveAfterSec = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking
|
||||
autoSavePreset = top[FPSTR(_autoSavePreset)] | autoSavePreset;
|
||||
autoSavePreset = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking
|
||||
applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot;
|
||||
enabled = top[FPSTR(_autoSaveEnabled)] | enabled;
|
||||
autoSaveAfterSec = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec;
|
||||
autoSaveAfterSec = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking
|
||||
autoSavePreset = top[FPSTR(_autoSavePreset)] | autoSavePreset;
|
||||
autoSavePreset = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking
|
||||
applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot;
|
||||
autoSaveIgnorePresets = top[FPSTR(_autoSaveIgnorePresets)] | autoSaveIgnorePresets;
|
||||
DEBUG_PRINT(FPSTR(_name));
|
||||
DEBUG_PRINTLN(F(" config (re)loaded."));
|
||||
|
||||
@@ -268,11 +278,12 @@ class AutoSaveUsermod : public Usermod {
|
||||
};
|
||||
|
||||
// strings to reduce flash memory usage (used more than twice)
|
||||
const char AutoSaveUsermod::_name[] PROGMEM = "Autosave";
|
||||
const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled";
|
||||
const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec";
|
||||
const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset";
|
||||
const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot";
|
||||
const char AutoSaveUsermod::_name[] PROGMEM = "Autosave";
|
||||
const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled";
|
||||
const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec";
|
||||
const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset";
|
||||
const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot";
|
||||
const char AutoSaveUsermod::_autoSaveIgnorePresets[] PROGMEM = "autoSaveIgnorePresets";
|
||||
|
||||
static AutoSaveUsermod autosave;
|
||||
REGISTER_USERMOD(autosave);
|
||||
|
||||
@@ -749,12 +749,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) {
|
||||
yield();
|
||||
if (!enabled
|
||||
|| b // button 0 only
|
||||
|| buttonType[b] == BTN_TYPE_SWITCH
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_SWITCH
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -401,19 +401,19 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() {
|
||||
re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT);
|
||||
|
||||
DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(customPalettes.size());
|
||||
palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount());
|
||||
palettes_alpha_indexes = re_initIndexArray(getPaletteCount());
|
||||
palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); // allocates memory for all palette names
|
||||
palettes_alpha_indexes = re_initIndexArray(getPaletteCount()); // allocates memory for all palette indexes
|
||||
if (customPalettes.size()) {
|
||||
for (int i=0; i<customPalettes.size(); i++) {
|
||||
palettes_alpha_indexes[getPaletteCount()-customPalettes.size()+i] = 255-i;
|
||||
palettes_qstrings[getPaletteCount()-customPalettes.size()+i] = PSTR("~Custom~");
|
||||
palettes_alpha_indexes[FIXED_PALETTE_COUNT+i] = 255-i;
|
||||
palettes_qstrings[FIXED_PALETTE_COUNT+i] = PSTR("~Custom~");
|
||||
}
|
||||
}
|
||||
// How many palette names start with '*' and should not be sorted?
|
||||
// (Also skipping the first one, 'Default').
|
||||
int skipPaletteCount = 1;
|
||||
while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') skipPaletteCount++;
|
||||
re_sortModes(palettes_qstrings, palettes_alpha_indexes, getPaletteCount()-customPalettes.size(), skipPaletteCount);
|
||||
int skipPaletteCount = 1; // could use DYNAMIC_PALETTE_COUNT instead
|
||||
while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') skipPaletteCount++; // legacy code
|
||||
re_sortModes(palettes_qstrings, palettes_alpha_indexes, FIXED_PALETTE_COUNT, skipPaletteCount); // only sort fixed palettes (skip dynamic)
|
||||
}
|
||||
|
||||
byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) {
|
||||
|
||||
+1593
-1265
File diff suppressed because it is too large
Load Diff
+61
-68
@@ -18,7 +18,7 @@
|
||||
|
||||
#include <vector>
|
||||
#include "wled.h"
|
||||
|
||||
#include "colors.h"
|
||||
#ifdef WLED_DEBUG
|
||||
// enable additional debug output
|
||||
#if defined(WLED_DEBUG_HOST)
|
||||
@@ -38,10 +38,6 @@
|
||||
#define DEBUGFX_PRINTF_P(x...)
|
||||
#endif
|
||||
|
||||
#define FASTLED_INTERNAL //remove annoying pragma messages
|
||||
#define USE_GET_MILLISECOND_TIMER
|
||||
#include "FastLED.h"
|
||||
|
||||
#define DEFAULT_BRIGHTNESS (uint8_t)127
|
||||
#define DEFAULT_MODE (uint8_t)0
|
||||
#define DEFAULT_SPEED (uint8_t)128
|
||||
@@ -58,11 +54,6 @@
|
||||
#define MAX(a,b) ((a)>(b)?(a):(b))
|
||||
#endif
|
||||
|
||||
//color mangling macros
|
||||
#ifndef RGBW32
|
||||
#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))
|
||||
#endif
|
||||
|
||||
extern bool realtimeRespectLedMaps; // used in getMappedPixelIndex()
|
||||
extern byte realtimeMode; // used in getMappedPixelIndex()
|
||||
|
||||
@@ -310,6 +301,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
|
||||
#define FX_MODE_2DFIRENOISE 149
|
||||
#define FX_MODE_2DSQUAREDSWIRL 150
|
||||
// #define FX_MODE_2DFIRE2012 151
|
||||
#define FX_MODE_PACMAN 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable.
|
||||
#define FX_MODE_2DDNA 152
|
||||
#define FX_MODE_2DMATRIX 153
|
||||
#define FX_MODE_2DMETABALLS 154
|
||||
@@ -320,6 +312,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
|
||||
#define FX_MODE_DJLIGHT 159
|
||||
#define FX_MODE_2DFUNKYPLANK 160
|
||||
//#define FX_MODE_2DCENTERBARS 161
|
||||
#define FX_MODE_SHIMMER 161 // gap fill, non SR 1D effect
|
||||
#define FX_MODE_2DPULSER 162
|
||||
#define FX_MODE_BLURZ 163
|
||||
#define FX_MODE_2DDRIFT 164
|
||||
@@ -377,36 +370,38 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
|
||||
#define FX_MODE_PS1DSONICBOOM 215
|
||||
#define FX_MODE_PS1DSPRINGY 216
|
||||
#define FX_MODE_PARTICLEGALAXY 217
|
||||
#define MODE_COUNT 218
|
||||
#define FX_MODE_COLORCLOUDS 218
|
||||
#define FX_MODE_SLOW_TRANSITION 219
|
||||
#define MODE_COUNT 220
|
||||
|
||||
|
||||
#define BLEND_STYLE_FADE 0x00 // universal
|
||||
#define BLEND_STYLE_FAIRY_DUST 0x01 // universal
|
||||
#define BLEND_STYLE_SWIPE_RIGHT 0x02 // 1D or 2D
|
||||
#define BLEND_STYLE_SWIPE_LEFT 0x03 // 1D or 2D
|
||||
#define BLEND_STYLE_OUTSIDE_IN 0x04 // 1D or 2D
|
||||
#define BLEND_STYLE_INSIDE_OUT 0x05 // 1D or 2D
|
||||
#define BLEND_STYLE_SWIPE_UP 0x06 // 2D
|
||||
#define BLEND_STYLE_SWIPE_DOWN 0x07 // 2D
|
||||
#define BLEND_STYLE_OPEN_H 0x08 // 2D
|
||||
#define BLEND_STYLE_OPEN_V 0x09 // 2D
|
||||
#define BLEND_STYLE_SWIPE_TL 0x0A // 2D
|
||||
#define BLEND_STYLE_SWIPE_TR 0x0B // 2D
|
||||
#define BLEND_STYLE_SWIPE_BR 0x0C // 2D
|
||||
#define BLEND_STYLE_SWIPE_BL 0x0D // 2D
|
||||
#define BLEND_STYLE_CIRCULAR_OUT 0x0E // 2D
|
||||
#define BLEND_STYLE_CIRCULAR_IN 0x0F // 2D
|
||||
#define TRANSITION_FADE 0x00 // universal
|
||||
#define TRANSITION_FAIRY_DUST 0x01 // universal
|
||||
#define TRANSITION_SWIPE_RIGHT 0x02 // 1D or 2D
|
||||
#define TRANSITION_SWIPE_LEFT 0x03 // 1D or 2D
|
||||
#define TRANSITION_OUTSIDE_IN 0x04 // 1D or 2D
|
||||
#define TRANSITION_INSIDE_OUT 0x05 // 1D or 2D
|
||||
#define TRANSITION_SWIPE_UP 0x06 // 2D
|
||||
#define TRANSITION_SWIPE_DOWN 0x07 // 2D
|
||||
#define TRANSITION_OPEN_H 0x08 // 2D
|
||||
#define TRANSITION_OPEN_V 0x09 // 2D
|
||||
#define TRANSITION_SWIPE_TL 0x0A // 2D
|
||||
#define TRANSITION_SWIPE_TR 0x0B // 2D
|
||||
#define TRANSITION_SWIPE_BR 0x0C // 2D
|
||||
#define TRANSITION_SWIPE_BL 0x0D // 2D
|
||||
#define TRANSITION_CIRCULAR_OUT 0x0E // 2D
|
||||
#define TRANSITION_CIRCULAR_IN 0x0F // 2D
|
||||
// as there are many push variants to optimise if statements they are groupped together
|
||||
#define BLEND_STYLE_PUSH_RIGHT 0x10 // 1D or 2D (& 0b00010000)
|
||||
#define BLEND_STYLE_PUSH_LEFT 0x11 // 1D or 2D (& 0b00010000)
|
||||
#define BLEND_STYLE_PUSH_UP 0x12 // 2D (& 0b00010000)
|
||||
#define BLEND_STYLE_PUSH_DOWN 0x13 // 2D (& 0b00010000)
|
||||
#define BLEND_STYLE_PUSH_TL 0x14 // 2D (& 0b00010000)
|
||||
#define BLEND_STYLE_PUSH_TR 0x15 // 2D (& 0b00010000)
|
||||
#define BLEND_STYLE_PUSH_BR 0x16 // 2D (& 0b00010000)
|
||||
#define BLEND_STYLE_PUSH_BL 0x17 // 2D (& 0b00010000)
|
||||
#define BLEND_STYLE_PUSH_MASK 0x10
|
||||
#define BLEND_STYLE_COUNT 18
|
||||
#define TRANSITION_PUSH_RIGHT 0x10 // 1D or 2D (& 0b00010000)
|
||||
#define TRANSITION_PUSH_LEFT 0x11 // 1D or 2D (& 0b00010000)
|
||||
#define TRANSITION_PUSH_UP 0x12 // 2D (& 0b00010000)
|
||||
#define TRANSITION_PUSH_DOWN 0x13 // 2D (& 0b00010000)
|
||||
#define TRANSITION_PUSH_TL 0x14 // 2D (& 0b00010000)
|
||||
#define TRANSITION_PUSH_TR 0x15 // 2D (& 0b00010000)
|
||||
#define TRANSITION_PUSH_BR 0x16 // 2D (& 0b00010000)
|
||||
#define TRANSITION_PUSH_BL 0x17 // 2D (& 0b00010000)
|
||||
#define TRANSITION_PUSH_MASK 0x10
|
||||
#define TRANSITION_COUNT 18
|
||||
|
||||
|
||||
typedef enum mapping1D2D {
|
||||
@@ -418,10 +413,12 @@ typedef enum mapping1D2D {
|
||||
} mapping1D2D_t;
|
||||
|
||||
class WS2812FX;
|
||||
class FontManager;
|
||||
|
||||
// segment, 76 bytes
|
||||
class Segment {
|
||||
public:
|
||||
friend class FontManager; // Allow FontManager to access protected members
|
||||
uint32_t colors[NUM_COLORS];
|
||||
uint16_t start; // start index / start X coordinate 2D (left)
|
||||
uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0
|
||||
@@ -458,13 +455,11 @@ class Segment {
|
||||
bool check1 : 1; // checkmark 1
|
||||
bool check2 : 1; // checkmark 2
|
||||
bool check3 : 1; // checkmark 3
|
||||
//uint8_t blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
|
||||
};
|
||||
uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
|
||||
uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, average, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn, stencil
|
||||
char *name; // segment name
|
||||
|
||||
// runtime data
|
||||
mutable unsigned long next_time; // millis() of next update
|
||||
mutable uint32_t step; // custom "step" var
|
||||
mutable uint32_t call; // call counter
|
||||
mutable uint16_t aux0; // custom var
|
||||
@@ -519,7 +514,7 @@ class Segment {
|
||||
, _start(millis())
|
||||
, _colors{0,0,0}
|
||||
#ifndef WLED_SAVE_RAM
|
||||
, _palT(CRGBPalette16(CRGB::Black))
|
||||
, _palT(CRGBPalette16())
|
||||
#endif
|
||||
, _dur(dur)
|
||||
, _progress(0)
|
||||
@@ -536,7 +531,7 @@ class Segment {
|
||||
|
||||
protected:
|
||||
|
||||
inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; }
|
||||
inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData = max(0, int(Segment::_usedSegmentData) + len); } // clamp negative results to 0
|
||||
|
||||
inline uint32_t *getPixels() const { return pixels; }
|
||||
inline void setPixelColorRaw(unsigned i, uint32_t c) const { pixels[i] = c; }
|
||||
@@ -546,7 +541,7 @@ class Segment {
|
||||
inline uint32_t getPixelColorXYRaw(unsigned x, unsigned y) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; return pixels[XY(x,y)]; };
|
||||
#endif
|
||||
void resetIfRequired(); // sets all SEGENV variables to 0 and clears data buffer
|
||||
CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal);
|
||||
void loadPalette(CRGBPalette16 &tgt, uint8_t pal);
|
||||
|
||||
// transition functions
|
||||
void stopTransition(); // ends transition mode by destroying transition structure (does nothing if not in transition)
|
||||
@@ -558,9 +553,9 @@ class Segment {
|
||||
inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // relies on handleTransition()/updateTransitionProgress() to update progression variable
|
||||
inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; }
|
||||
|
||||
inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; }
|
||||
inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } // for isPreviousMode()
|
||||
inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; };
|
||||
inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-BLEND_STYLE_FADE transition
|
||||
inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-TRANSITION_FADE transition
|
||||
|
||||
static void handleRandomPalette();
|
||||
|
||||
@@ -590,7 +585,6 @@ class Segment {
|
||||
, check3(false)
|
||||
, blendMode(0)
|
||||
, name(nullptr)
|
||||
, next_time(0)
|
||||
, step(0)
|
||||
, call(0)
|
||||
, aux0(0)
|
||||
@@ -624,6 +618,9 @@ class Segment {
|
||||
DEBUGFX_PRINTLN();
|
||||
#endif
|
||||
clearName();
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
}
|
||||
@@ -757,8 +754,8 @@ class Segment {
|
||||
{ addPixelColorXY(x, y, RGBW32(r,g,b,w), preserveCR); }
|
||||
inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) const { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), preserveCR); }
|
||||
inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); }
|
||||
inline void blurCols(fract8 blur_amount, bool smear = false) const { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur)
|
||||
inline void blurRows(fract8 blur_amount, bool smear = false) const { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur)
|
||||
inline void blurCols(uint8_t blur_amount, bool smear = false) const { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur)
|
||||
inline void blurRows(uint8_t blur_amount, bool smear = false) const { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur)
|
||||
//void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur
|
||||
void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) const;
|
||||
void moveX(int delta, bool wrap = false) const;
|
||||
@@ -767,12 +764,10 @@ class Segment {
|
||||
void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const;
|
||||
void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const;
|
||||
void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) const;
|
||||
void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const;
|
||||
void wu_pixel(uint32_t x, uint32_t y, CRGB c) const;
|
||||
inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); }
|
||||
inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); }
|
||||
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) const { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline
|
||||
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline
|
||||
inline void fill_solid(CRGB c) const { fill(RGBW32(c.r,c.g,c.b,0)); }
|
||||
#else
|
||||
inline bool is2D() const { return false; }
|
||||
@@ -786,18 +781,18 @@ class Segment {
|
||||
inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); }
|
||||
inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); }
|
||||
#endif
|
||||
inline bool isPixelXYClipped(int x, int y) const { return isPixelClipped(x); }
|
||||
inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(x); }
|
||||
inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) const { blendPixelColor(x, c, blend); }
|
||||
inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); }
|
||||
inline void addPixelColorXY(int x, int y, uint32_t color, bool saturate = false) const { addPixelColor(x, color, saturate); }
|
||||
inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool saturate = false) const { addPixelColor(x, RGBW32(r,g,b,w), saturate); }
|
||||
inline void addPixelColorXY(int x, int y, CRGB c, bool saturate = false) const { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), saturate); }
|
||||
inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { fadePixelColor(x, fade); }
|
||||
//inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {}
|
||||
inline bool isPixelXYClipped(int x, int y) { return isPixelClipped(x); }
|
||||
inline uint32_t getPixelColorXY(int x, int y) { return getPixelColor(x); }
|
||||
inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); }
|
||||
inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); }
|
||||
inline void addPixelColorXY(int x, int y, uint32_t color, bool preserveCR = true) { addPixelColor(x, color, preserveCR); }
|
||||
inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) { addPixelColor(x, RGBW32(r,g,b,w), preserveCR); }
|
||||
inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), preserveCR); }
|
||||
inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); }
|
||||
//inline void box_blur(unsigned i, bool vertical, uint8_t blur_amount) {}
|
||||
inline void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) {}
|
||||
inline void blurCols(fract8 blur_amount, bool smear = false) { blur(blur_amount, smear); } // blur all columns (50% faster than full 2D blur)
|
||||
inline void blurRows(fract8 blur_amount, bool smear = false) {}
|
||||
inline void blurRows(uint8_t blur_amount, bool smear = false) {}
|
||||
inline void blurCols(uint8_t blur_amount, bool smear = false) {}
|
||||
inline void moveX(int delta, bool wrap = false) {}
|
||||
inline void moveY(int delta, bool wrap = false) {}
|
||||
inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {}
|
||||
@@ -807,8 +802,6 @@ class Segment {
|
||||
inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {}
|
||||
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {}
|
||||
inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {}
|
||||
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {}
|
||||
inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {}
|
||||
inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {}
|
||||
#endif
|
||||
friend class WS2812FX;
|
||||
@@ -818,19 +811,18 @@ class Segment {
|
||||
|
||||
// main "strip" class (108 bytes)
|
||||
class WS2812FX {
|
||||
typedef uint16_t (*mode_ptr)(); // pointer to mode function
|
||||
typedef void (*mode_ptr)(); // pointer to mode function
|
||||
typedef void (*show_callback)(); // pre show callback
|
||||
typedef struct ModeData {
|
||||
uint8_t _id; // mode (effect) id
|
||||
mode_ptr _fcn; // mode (effect) function
|
||||
const char *_data; // mode (effect) name and its UI control data
|
||||
ModeData(uint8_t id, uint16_t (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {}
|
||||
ModeData(uint8_t id, void (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {}
|
||||
} mode_data_t;
|
||||
|
||||
public:
|
||||
|
||||
WS2812FX() :
|
||||
paletteBlend(0),
|
||||
now(millis()),
|
||||
timebase(0),
|
||||
isMatrix(false),
|
||||
@@ -932,7 +924,7 @@ class WS2812FX {
|
||||
inline bool isSuspended() const { return _suspend; } // returns true if strip.service() execution is suspended
|
||||
inline bool needsUpdate() const { return _triggered; } // returns true if strip received a trigger() request
|
||||
|
||||
uint8_t paletteBlend;
|
||||
// uint8_t paletteBlend; // obsolete - use global paletteBlend instead of strip.paletteBlend
|
||||
uint8_t getActiveSegmentsNum() const;
|
||||
uint8_t getFirstSelectedSegId() const;
|
||||
uint8_t getLastActiveSegmentId() const;
|
||||
@@ -961,7 +953,8 @@ class WS2812FX {
|
||||
};
|
||||
|
||||
unsigned long now, timebase;
|
||||
inline uint32_t getPixelColor(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n
|
||||
inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds
|
||||
inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table
|
||||
inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call
|
||||
|
||||
const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); }
|
||||
|
||||
+41
-92
@@ -8,7 +8,6 @@
|
||||
Parts of the code adapted from WLED Sound Reactive
|
||||
*/
|
||||
#include "wled.h"
|
||||
#include "palettes.h"
|
||||
|
||||
// setUpMatrix() - constructs ledmap array from matrix of panels with WxH pixels
|
||||
// this converts physical (possibly irregular) LED arrangement into well defined
|
||||
@@ -18,6 +17,7 @@
|
||||
// note: matrix may be comprised of multiple panels each with different orientation
|
||||
// but ledmap takes care of that. ledmap is constructed upon initialization
|
||||
// so matrix should disable regular ledmap processing
|
||||
// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context
|
||||
void WS2812FX::setUpMatrix() {
|
||||
#ifndef WLED_DISABLE_2D
|
||||
// isMatrix is set in cfg.cpp or set.cpp
|
||||
@@ -46,12 +46,12 @@ void WS2812FX::setUpMatrix() {
|
||||
return;
|
||||
}
|
||||
|
||||
suspend();
|
||||
waitForIt();
|
||||
|
||||
customMappingSize = 0; // prevent use of mapping if anything goes wrong
|
||||
|
||||
d_free(customMappingTable);
|
||||
// Segment::maxWidth and Segment::maxHeight are set according to panel layout
|
||||
// and the product will include at least all leds in matrix
|
||||
// if actual LEDs are more, getLengthTotal() will return correct number of LEDs
|
||||
customMappingTable = static_cast<uint16_t*>(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer to not use SPI RAM
|
||||
|
||||
if (customMappingTable) {
|
||||
@@ -74,7 +74,7 @@ void WS2812FX::setUpMatrix() {
|
||||
size_t gapSize = 0;
|
||||
int8_t *gapTable = nullptr;
|
||||
|
||||
if (isFile && requestJSONBufferLock(20)) {
|
||||
if (isFile && requestJSONBufferLock(JSON_LOCK_LEDGAP)) {
|
||||
DEBUG_PRINT(F("Reading LED gap from "));
|
||||
DEBUG_PRINTLN(fileName);
|
||||
// read the array into global JSON buffer
|
||||
@@ -114,13 +114,15 @@ void WS2812FX::setUpMatrix() {
|
||||
|
||||
// delete gap array as we no longer need it
|
||||
p_free(gapTable);
|
||||
resume();
|
||||
|
||||
#ifdef WLED_DEBUG
|
||||
DEBUG_PRINT(F("Matrix ledmap:"));
|
||||
for (unsigned i=0; i<customMappingSize; i++) {
|
||||
if (!(i%Segment::maxWidth)) DEBUG_PRINTLN();
|
||||
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i]);
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
delay(1); // on S2 the CDC output can crash without a delay
|
||||
#endif
|
||||
}
|
||||
DEBUG_PRINTLN();
|
||||
#endif
|
||||
@@ -146,15 +148,15 @@ void WS2812FX::setUpMatrix() {
|
||||
#ifndef WLED_DISABLE_2D
|
||||
// pixel is clipped if it falls outside clipping range
|
||||
// if clipping start > stop the clipping range is inverted
|
||||
bool IRAM_ATTR_YN Segment::isPixelXYClipped(int x, int y) const {
|
||||
if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) {
|
||||
bool Segment::isPixelXYClipped(int x, int y) const {
|
||||
if (blendingStyle != TRANSITION_FADE && isInTransition() && _clipStart != _clipStop) {
|
||||
const bool invertX = _clipStart > _clipStop;
|
||||
const bool invertY = _clipStartY > _clipStopY;
|
||||
const int cStartX = invertX ? _clipStop : _clipStart;
|
||||
const int cStopX = invertX ? _clipStart : _clipStop;
|
||||
const int cStartY = invertY ? _clipStopY : _clipStartY;
|
||||
const int cStopY = invertY ? _clipStartY : _clipStopY;
|
||||
if (blendingStyle == BLEND_STYLE_FAIRY_DUST) {
|
||||
if (blendingStyle == TRANSITION_FAIRY_DUST) {
|
||||
const unsigned width = cStopX - cStartX; // assumes full segment width (faster than virtualWidth())
|
||||
const unsigned len = width * (cStopY - cStartY); // assumes full segment height (faster than virtualHeight())
|
||||
if (len < 2) return false;
|
||||
@@ -162,10 +164,10 @@ bool IRAM_ATTR_YN Segment::isPixelXYClipped(int x, int y) const {
|
||||
const unsigned pos = (shuffled * 0xFFFFU) / len;
|
||||
return progress() <= pos;
|
||||
}
|
||||
if (blendingStyle == BLEND_STYLE_CIRCULAR_IN || blendingStyle == BLEND_STYLE_CIRCULAR_OUT) {
|
||||
if (blendingStyle == TRANSITION_CIRCULAR_IN || blendingStyle == TRANSITION_CIRCULAR_OUT) {
|
||||
const int cx = (cStopX-cStartX+1) / 2;
|
||||
const int cy = (cStopY-cStartY+1) / 2;
|
||||
const bool out = (blendingStyle == BLEND_STYLE_CIRCULAR_OUT);
|
||||
const bool out = (blendingStyle == TRANSITION_CIRCULAR_OUT);
|
||||
const unsigned prog = out ? progress() : 0xFFFFU - progress();
|
||||
int radius2 = max(cx, cy) * prog / 0xFFFF;
|
||||
radius2 = 2 * radius2 * radius2;
|
||||
@@ -177,7 +179,7 @@ bool IRAM_ATTR_YN Segment::isPixelXYClipped(int x, int y) const {
|
||||
}
|
||||
bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside;
|
||||
bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside;
|
||||
const bool clip = blendingStyle == BLEND_STYLE_OUTSIDE_IN ? xInside || yInside : xInside && yInside;
|
||||
const bool clip = blendingStyle == TRANSITION_OUTSIDE_IN ? xInside || yInside : xInside && yInside;
|
||||
return !clip;
|
||||
}
|
||||
return false;
|
||||
@@ -186,7 +188,7 @@ bool IRAM_ATTR_YN Segment::isPixelXYClipped(int x, int y) const {
|
||||
void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) const
|
||||
{
|
||||
if (!isActive()) return; // not active
|
||||
if (x >= (int)vWidth() || y >= (int)vHeight() || x < 0 || y < 0) return; // if pixel would fall out of virtual segment just exit
|
||||
if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return; // if pixel would fall out of virtual segment just exit
|
||||
setPixelColorXYRaw(x, y, col);
|
||||
}
|
||||
|
||||
@@ -236,7 +238,7 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) const
|
||||
// returns RGBW values of pixel
|
||||
uint32_t IRAM_ATTR_YN Segment::getPixelColorXY(int x, int y) const {
|
||||
if (!isActive()) return 0; // not active
|
||||
if (x >= (int)vWidth() || y >= (int)vHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit
|
||||
if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return 0; // if pixel would fall out of virtual segment just exit
|
||||
return getPixelColorXYRaw(x,y);
|
||||
}
|
||||
|
||||
@@ -246,52 +248,42 @@ void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const {
|
||||
const unsigned cols = vWidth();
|
||||
const unsigned rows = vHeight();
|
||||
const auto XY = [&](unsigned x, unsigned y){ return x + y*cols; };
|
||||
uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration
|
||||
uint32_t last;
|
||||
if (blur_x) {
|
||||
const uint8_t keepx = smear ? 255 : 255 - blur_x;
|
||||
const uint8_t seepx = blur_x >> 1;
|
||||
for (unsigned row = 0; row < rows; row++) { // blur rows (x direction)
|
||||
uint32_t carryover = BLACK;
|
||||
uint32_t curnew = BLACK;
|
||||
for (unsigned x = 0; x < cols; x++) {
|
||||
uint32_t cur = getPixelColorRaw(XY(x, row));
|
||||
uint32_t part = color_fade(cur, seepx);
|
||||
curnew = color_fade(cur, keepx);
|
||||
if (x > 0) {
|
||||
if (carryover) curnew = color_add(curnew, carryover);
|
||||
uint32_t prev = color_add(lastnew, part);
|
||||
// optimization: only set pixel if color has changed
|
||||
if (last != prev) setPixelColorRaw(XY(x - 1, row), prev);
|
||||
} else setPixelColorRaw(XY(x, row), curnew); // first pixel
|
||||
lastnew = curnew;
|
||||
last = cur; // save original value for comparison on next iteration
|
||||
// handle first pixel in row to avoid conditional in loop (faster)
|
||||
uint32_t cur = getPixelColorRaw(XY(0, row));
|
||||
uint32_t carryover = fast_color_scale(cur, seepx);
|
||||
setPixelColorRaw(XY(0, row), fast_color_scale(cur, keepx));
|
||||
for (unsigned x = 1; x < cols; x++) {
|
||||
cur = getPixelColorRaw(XY(x, row));
|
||||
uint32_t part = fast_color_scale(cur, seepx);
|
||||
cur = fast_color_scale(cur, keepx);
|
||||
cur = color_add(cur, carryover);
|
||||
setPixelColorRaw(XY(x - 1, row), color_add(getPixelColorRaw(XY(x-1, row)), part)); // previous pixel
|
||||
setPixelColorRaw(XY(x, row), cur); // current pixel
|
||||
carryover = part;
|
||||
}
|
||||
setPixelColorRaw(XY(cols-1, row), curnew); // set last pixel
|
||||
}
|
||||
}
|
||||
if (blur_y) {
|
||||
const uint8_t keepy = smear ? 255 : 255 - blur_y;
|
||||
const uint8_t seepy = blur_y >> 1;
|
||||
for (unsigned col = 0; col < cols; col++) {
|
||||
uint32_t carryover = BLACK;
|
||||
uint32_t curnew = BLACK;
|
||||
for (unsigned y = 0; y < rows; y++) {
|
||||
uint32_t cur = getPixelColorRaw(XY(col, y));
|
||||
uint32_t part = color_fade(cur, seepy);
|
||||
curnew = color_fade(cur, keepy);
|
||||
if (y > 0) {
|
||||
if (carryover) curnew = color_add(curnew, carryover);
|
||||
uint32_t prev = color_add(lastnew, part);
|
||||
// optimization: only set pixel if color has changed
|
||||
if (last != prev) setPixelColorRaw(XY(col, y - 1), prev);
|
||||
} else setPixelColorRaw(XY(col, y), curnew); // first pixel
|
||||
lastnew = curnew;
|
||||
last = cur; //save original value for comparison on next iteration
|
||||
// handle first pixel in column
|
||||
uint32_t cur = getPixelColorRaw(XY(col, 0));
|
||||
uint32_t carryover = fast_color_scale(cur, seepy);
|
||||
setPixelColorRaw(XY(col, 0), fast_color_scale(cur, keepy));
|
||||
for (unsigned y = 1; y < rows; y++) {
|
||||
cur = getPixelColorRaw(XY(col, y));
|
||||
uint32_t part = fast_color_scale(cur, seepy);
|
||||
cur = fast_color_scale(cur, keepy);
|
||||
cur = color_add(cur, carryover);
|
||||
setPixelColorRaw(XY(col, y - 1), color_add(getPixelColorRaw(XY(col, y-1)), part)); // previous pixel
|
||||
setPixelColorRaw(XY(col, y), cur); // current pixel
|
||||
carryover = part;
|
||||
}
|
||||
setPixelColorRaw(XY(col, rows - 1), curnew);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,51 +562,6 @@ void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint3
|
||||
}
|
||||
}
|
||||
|
||||
#include "src/font/console_font_4x6.h"
|
||||
#include "src/font/console_font_5x8.h"
|
||||
#include "src/font/console_font_5x12.h"
|
||||
#include "src/font/console_font_6x8.h"
|
||||
#include "src/font/console_font_7x9.h"
|
||||
|
||||
// draws a raster font character on canvas
|
||||
// only supports: 4x6=24, 5x8=40, 5x12=60, 6x8=48 and 7x9=63 fonts ATM
|
||||
void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2, int8_t rotate) const {
|
||||
if (!isActive()) return; // not active
|
||||
if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported
|
||||
chr -= 32; // align with font table entries
|
||||
const int font = w*h;
|
||||
|
||||
// if col2 == BLACK then use currently selected palette for gradient otherwise create gradient from color and col2
|
||||
CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient
|
||||
|
||||
for (int i = 0; i<h; i++) { // character height
|
||||
uint8_t bits = 0;
|
||||
switch (font) {
|
||||
case 24: bits = pgm_read_byte_near(&console_font_4x6[(chr * h) + i]); break; // 4x6 font
|
||||
case 40: bits = pgm_read_byte_near(&console_font_5x8[(chr * h) + i]); break; // 5x8 font
|
||||
case 48: bits = pgm_read_byte_near(&console_font_6x8[(chr * h) + i]); break; // 6x8 font
|
||||
case 63: bits = pgm_read_byte_near(&console_font_7x9[(chr * h) + i]); break; // 7x9 font
|
||||
case 60: bits = pgm_read_byte_near(&console_font_5x12[(chr * h) + i]); break; // 5x12 font
|
||||
default: return;
|
||||
}
|
||||
CRGBW c = ColorFromPalette(grad, (i+1)*255/h, 255, LINEARBLEND_NOWRAP); // NOBLEND is faster
|
||||
for (int j = 0; j<w; j++) { // character width
|
||||
int x0, y0;
|
||||
switch (rotate) {
|
||||
case -1: x0 = x + (h-1) - i; y0 = y + (w-1) - j; break; // -90 deg
|
||||
case -2:
|
||||
case 2: x0 = x + j; y0 = y + (h-1) - i; break; // 180 deg
|
||||
case 1: x0 = x + i; y0 = y + j; break; // +90 deg
|
||||
default: x0 = x + (w-1) - j; y0 = y + i; break; // no rotation
|
||||
}
|
||||
if (x0 < 0 || x0 >= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen
|
||||
if (((bits>>(j+(8-w))) & 0x01)) { // bit set
|
||||
setPixelColorXYRaw(x0, y0, c.color32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8))
|
||||
void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu_pixel procedure by reddit u/sutaburosu
|
||||
if (!isActive()) return; // not active
|
||||
@@ -637,4 +584,6 @@ void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu
|
||||
}
|
||||
#undef WU_WEIGHT
|
||||
|
||||
#endif // WLED_DISABLE_2D
|
||||
|
||||
|
||||
#endif // WLED_DISABLE_2D
|
||||
Executable → Regular
+256
-183
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
#include "wled.h"
|
||||
#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h?
|
||||
#include "palettes.h"
|
||||
#include "colors.h"
|
||||
|
||||
/*
|
||||
Custom per-LED mapping has moved!
|
||||
@@ -44,7 +44,7 @@ unsigned Segment::_vLength = 0;
|
||||
unsigned Segment::_vWidth = 0;
|
||||
unsigned Segment::_vHeight = 0;
|
||||
uint32_t Segment::_currentColors[NUM_COLORS] = {0,0,0};
|
||||
CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black);
|
||||
CRGBPalette16 Segment::_currentPalette = CRGBPalette16();
|
||||
CRGBPalette16 Segment::_randomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR);
|
||||
CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR);
|
||||
uint16_t Segment::_lastPaletteChange = 0; // in seconds; perhaps it should be per segment
|
||||
@@ -101,12 +101,12 @@ Segment& Segment::operator= (const Segment &orig) {
|
||||
if (_t) stopTransition(); // also erases _t
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
pixels = nullptr;
|
||||
// copy source
|
||||
memcpy((void*)this, (void*)&orig, sizeof(Segment));
|
||||
// erase pointers to allocated data
|
||||
data = nullptr;
|
||||
_dataLen = 0;
|
||||
pixels = nullptr;
|
||||
if (!stop) return *this; // nothing to do if segment is inactive/invalid
|
||||
// copy source data
|
||||
if (orig.pixels) {
|
||||
@@ -218,21 +218,25 @@ void Segment::resetIfRequired() {
|
||||
DEBUG_PRINTF_P(PSTR("-- Segment %p reset, data cleared\n"), this);
|
||||
}
|
||||
if (pixels) for (size_t i = 0; i < length(); i++) pixels[i] = BLACK; // clear pixel buffer
|
||||
next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0;
|
||||
step = 0; call = 0; aux0 = 0; aux1 = 0;
|
||||
reset = false;
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0;
|
||||
if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0;
|
||||
void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
// there is one randomly generated palette (1) followed by 4 palettes created from segment colors (2-5)
|
||||
// those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71)
|
||||
// then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)
|
||||
// palette 0 is a varying palette depending on effect and may be replaced by segment's color if so
|
||||
// instructed in color_from_palette()
|
||||
if (pal >= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette
|
||||
//default palette. Differs depending on effect
|
||||
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()
|
||||
switch (pal) {
|
||||
case 0: //default palette. Exceptions for specific effects above
|
||||
targetPalette = PartyColors_p;
|
||||
targetPalette = PartyColors_gc22;
|
||||
break;
|
||||
case 1: //randomly generated palette
|
||||
targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette()
|
||||
@@ -263,18 +267,17 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
|
||||
}
|
||||
break;}
|
||||
default: //progmem palettes
|
||||
if (pal>245) {
|
||||
if (pal > 255 - customPalettes.size()) {
|
||||
targetPalette = customPalettes[255-pal]; // we checked bounds above
|
||||
} else if (pal < 13) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal-6];
|
||||
} else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes
|
||||
targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT];
|
||||
} else {
|
||||
byte tcp[72];
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72);
|
||||
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));
|
||||
targetPalette.loadDynamicGradientPalette(tcp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return targetPalette;
|
||||
}
|
||||
|
||||
// starting a transition has to occur before change so we get current values 1st
|
||||
@@ -340,7 +343,7 @@ void Segment::updateTransitionProgress() const {
|
||||
uint8_t Segment::currentCCT() const {
|
||||
unsigned prog = progress();
|
||||
if (prog < 0xFFFFU) {
|
||||
if (blendingStyle == BLEND_STYLE_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU;
|
||||
if (blendingStyle == TRANSITION_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU;
|
||||
//else return Segment::isPreviousMode() ? _t->_cct : cct;
|
||||
}
|
||||
return cct;
|
||||
@@ -352,7 +355,7 @@ uint8_t Segment::currentBri() const {
|
||||
unsigned curBri = on ? opacity : 0;
|
||||
if (prog < 0xFFFFU) {
|
||||
// this will blend opacity in new mode if style is FADE (single effect call)
|
||||
if (blendingStyle == BLEND_STYLE_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU;
|
||||
if (blendingStyle == TRANSITION_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU;
|
||||
else curBri = Segment::isPreviousMode() ? _t->_bri : curBri;
|
||||
}
|
||||
return curBri;
|
||||
@@ -368,7 +371,7 @@ void Segment::beginDraw(uint16_t prog) {
|
||||
for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i];
|
||||
// load palette into _currentPalette
|
||||
loadPalette(Segment::_currentPalette, palette);
|
||||
if (isInTransition() && prog < 0xFFFFU && blendingStyle == BLEND_STYLE_FADE) {
|
||||
if (isInTransition() && prog < 0xFFFFU && blendingStyle == TRANSITION_FADE) {
|
||||
// blend colors
|
||||
for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog);
|
||||
// blend palettes
|
||||
@@ -376,7 +379,7 @@ void Segment::beginDraw(uint16_t prog) {
|
||||
// minimum blend time is 100ms maximum is 65535ms
|
||||
#ifndef WLED_SAVE_RAM
|
||||
unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends;
|
||||
if(noOfBlends > 255) noOfBlends = 255; // safety check
|
||||
if (noOfBlends > 255) noOfBlends = 255; // safety check
|
||||
for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48);
|
||||
Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette
|
||||
#else
|
||||
@@ -445,6 +448,9 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
|
||||
// apply change immediately
|
||||
if (i2 <= i1) { //disable segment
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
pixels = nullptr;
|
||||
@@ -452,7 +458,7 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
return;
|
||||
}
|
||||
if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D
|
||||
stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth);
|
||||
stop = i2 > Segment::maxWidth*Segment::maxHeight && i1 >= Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); // check for 2D trailing strip
|
||||
startY = 0;
|
||||
stopY = 1;
|
||||
#ifndef WLED_DISABLE_2D
|
||||
@@ -463,6 +469,9 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
#endif
|
||||
// safety check
|
||||
if (start >= stop || startY >= stopY) {
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
pixels = nullptr;
|
||||
@@ -476,6 +485,9 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
pixels = static_cast<uint32_t*>(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS));
|
||||
if (!pixels) {
|
||||
DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
errorFlag = ERR_NORAM_PX;
|
||||
stop = 0;
|
||||
@@ -494,7 +506,7 @@ Segment &Segment::setColor(uint8_t slot, uint32_t c) {
|
||||
if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black
|
||||
}
|
||||
//DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c);
|
||||
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change
|
||||
startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change
|
||||
colors[slot] = c;
|
||||
stateChanged = true; // send UDP/WS broadcast
|
||||
return *this;
|
||||
@@ -518,7 +530,7 @@ Segment &Segment::setCCT(uint16_t k) {
|
||||
Segment &Segment::setOpacity(uint8_t o) {
|
||||
if (opacity != o) {
|
||||
//DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o);
|
||||
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change
|
||||
startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change
|
||||
opacity = o;
|
||||
stateChanged = true; // send UDP/WS broadcast
|
||||
}
|
||||
@@ -529,7 +541,7 @@ Segment &Segment::setOption(uint8_t n, bool val) {
|
||||
bool prev = (options >> n) & 0x01;
|
||||
if (val == prev) return *this;
|
||||
//DEBUG_PRINTF_P(PSTR("- Starting option transition: %d\n"), n);
|
||||
if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change
|
||||
if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change
|
||||
if (val) options |= 0x01 << n;
|
||||
else options &= ~(0x01 << n);
|
||||
stateChanged = true; // send UDP/WS broadcast
|
||||
@@ -565,7 +577,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
|
||||
sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette
|
||||
if (sOpt >= 0 && loadDefaults) setPalette(sOpt);
|
||||
if (sOpt <= 0) sOpt = 6; // partycolors if zero or not set
|
||||
_default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected)
|
||||
_default_palette = sOpt; // _default_palette is loaded into pal0 in loadPalette() (if selected)
|
||||
markForReset();
|
||||
stateChanged = true; // send UDP/WS broadcast
|
||||
}
|
||||
@@ -573,11 +585,10 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
|
||||
}
|
||||
|
||||
Segment &Segment::setPalette(uint8_t pal) {
|
||||
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes
|
||||
if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0; // custom palettes
|
||||
if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette
|
||||
if (pal != palette) {
|
||||
//DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal);
|
||||
startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment)
|
||||
startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change (no need to copy segment)
|
||||
palette = pal;
|
||||
stateChanged = true; // send UDP/WS broadcast
|
||||
}
|
||||
@@ -680,12 +691,12 @@ uint16_t Segment::maxMappingLength() const {
|
||||
#endif
|
||||
// pixel is clipped if it falls outside clipping range
|
||||
// if clipping start > stop the clipping range is inverted
|
||||
bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const {
|
||||
if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) {
|
||||
bool Segment::isPixelClipped(int i) const {
|
||||
if (blendingStyle != TRANSITION_FADE && isInTransition() && _clipStart != _clipStop) {
|
||||
bool invert = _clipStart > _clipStop; // ineverted start & stop
|
||||
int start = invert ? _clipStop : _clipStart;
|
||||
int stop = invert ? _clipStart : _clipStop;
|
||||
if (blendingStyle == BLEND_STYLE_FAIRY_DUST) {
|
||||
if (blendingStyle == TRANSITION_FAIRY_DUST) {
|
||||
unsigned len = stop - start;
|
||||
if (len < 2) return false;
|
||||
unsigned shuffled = hashInt(i) % len;
|
||||
@@ -698,7 +709,7 @@ bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const
|
||||
void WLED_O2_ATTR Segment::setPixelColor(int i, uint32_t col) const
|
||||
{
|
||||
if (!isActive() || i < 0) return; // not active or invalid index
|
||||
#ifndef WLED_DISABLE_2D
|
||||
@@ -911,7 +922,7 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) const
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const
|
||||
uint32_t WLED_O2_ATTR Segment::getPixelColor(int i) const
|
||||
{
|
||||
if (!isActive() || i < 0) return 0; // not active or invalid index
|
||||
|
||||
@@ -1050,7 +1061,7 @@ void Segment::fadeToSecondaryBy(uint8_t fadeBy) const {
|
||||
void Segment::fadeToBlackBy(uint8_t fadeBy) const {
|
||||
if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply
|
||||
const size_t rlength = rawLength(); // calculate only once
|
||||
for (unsigned i = 0; i < rlength; i++) setPixelColorRaw(i, color_fade(getPixelColorRaw(i), 255-fadeBy));
|
||||
for (unsigned i = 0; i < rlength; i++) setPixelColorRaw(i, fast_color_scale(getPixelColorRaw(i), 255-fadeBy));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1070,25 +1081,19 @@ void Segment::blur(uint8_t blur_amount, bool smear) const {
|
||||
uint8_t keep = smear ? 255 : 255 - blur_amount;
|
||||
uint8_t seep = blur_amount >> 1;
|
||||
unsigned vlength = vLength();
|
||||
uint32_t carryover = BLACK;
|
||||
uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration
|
||||
uint32_t last;
|
||||
uint32_t curnew = BLACK;
|
||||
for (unsigned i = 0; i < vlength; i++) {
|
||||
uint32_t cur = getPixelColorRaw(i);
|
||||
uint32_t part = color_fade(cur, seep);
|
||||
curnew = color_fade(cur, keep);
|
||||
if (i > 0) {
|
||||
if (carryover) curnew = color_add(curnew, carryover);
|
||||
uint32_t prev = color_add(lastnew, part);
|
||||
// optimization: only set pixel if color has changed
|
||||
if (last != prev) setPixelColorRaw(i - 1, prev);
|
||||
} else setPixelColorRaw(i, curnew); // first pixel
|
||||
lastnew = curnew;
|
||||
last = cur; // save original value for comparison on next iteration
|
||||
// handle first pixel to avoid conditional in loop (faster)
|
||||
uint32_t cur = getPixelColorRaw(0);
|
||||
uint32_t carryover = fast_color_scale(cur, seep);
|
||||
setPixelColorRaw(0, fast_color_scale(cur, keep));
|
||||
for (unsigned i = 1; i < vlength; i++) {
|
||||
cur = getPixelColorRaw(i);
|
||||
uint32_t part = fast_color_scale(cur, seep);
|
||||
cur = fast_color_scale(cur, keep);
|
||||
cur = color_add(cur, carryover);
|
||||
setPixelColorRaw(i - 1, color_add(getPixelColorRaw(i - 1), part)); // previous pixel
|
||||
setPixelColorRaw(i, cur); // current pixel
|
||||
carryover = part;
|
||||
}
|
||||
setPixelColorRaw(vlength - 1, curnew);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1099,9 +1104,9 @@ void Segment::blur(uint8_t blur_amount, bool smear) const {
|
||||
uint32_t Segment::color_wheel(uint8_t pos) const {
|
||||
if (palette) return color_from_palette(pos, false, false, 0); // only wrap if "always wrap" is set
|
||||
uint8_t w = W(getCurrentColor(0));
|
||||
uint32_t rgb;
|
||||
hsv2rgb(CHSV32(static_cast<uint16_t>(pos << 8), 255, 255), rgb);
|
||||
return rgb | (w << 24); // add white channel
|
||||
CRGBW rgb;
|
||||
rgb = CHSV32(static_cast<uint16_t>(pos << 8), 255, 255);
|
||||
return rgb.color32 | (w << 24); // add white channel
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1154,41 +1159,74 @@ void WS2812FX::finalizeInit() {
|
||||
|
||||
_hasWhiteChannel = _isOffRefreshRequired = false;
|
||||
BusManager::removeAll();
|
||||
|
||||
// TODO: ideally we would free everything segment related here to reduce fragmentation (pixel buffers, ledamp, segments, etc) but that somehow leads to heap corruption if touchig any of the buffers.
|
||||
unsigned digitalCount = 0;
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
// determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT)
|
||||
unsigned maxLedsOnBus = 0;
|
||||
unsigned busType = 0;
|
||||
// validate the bus config: count I2S buses and check if they meet requirements
|
||||
unsigned i2sBusCount = 0;
|
||||
|
||||
for (const auto &bus : busConfigs) {
|
||||
if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) {
|
||||
digitalCount++;
|
||||
if (busType == 0) busType = bus.type; // remember first bus type
|
||||
if (busType != bus.type) {
|
||||
DEBUG_PRINTF_P(PSTR("Mixed digital bus types detected! Forcing single I2S output.\n"));
|
||||
useParallelI2S = false; // mixed bus types, no parallel I2S
|
||||
}
|
||||
if (bus.count > maxLedsOnBus) maxLedsOnBus = bus.count;
|
||||
if (bus.driverType == 1)
|
||||
i2sBusCount++;
|
||||
}
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount);
|
||||
// we may remove 600 LEDs per bus limit when NeoPixelBus is updated beyond 2.8.3
|
||||
if (maxLedsOnBus <= 600 && useParallelI2S) BusManager::useParallelOutput(); // must call before creating buses
|
||||
else useParallelI2S = false; // enforce single I2S
|
||||
digitalCount = 0;
|
||||
DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u\n"), digitalCount, i2sBusCount);
|
||||
|
||||
// Determine parallel vs single I2S usage (used for memory calculation only)
|
||||
bool useParallelI2S = false;
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// ESP32-S3 always uses parallel LCD driver for I2S
|
||||
if (i2sBusCount > 0) {
|
||||
useParallelI2S = true;
|
||||
}
|
||||
#else
|
||||
if (i2sBusCount > 1) {
|
||||
useParallelI2S = true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize());
|
||||
// create buses/outputs
|
||||
unsigned mem = 0;
|
||||
for (const auto &bus : busConfigs) {
|
||||
mem += bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // includes global buffer
|
||||
if (mem <= MAX_LED_MEMORY) {
|
||||
if (BusManager::add(bus) == -1) break;
|
||||
} else {
|
||||
errorFlag = ERR_NORAM_PX; // alert UI
|
||||
DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)bus.type, (int)bus.count, digitalCount);
|
||||
unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers
|
||||
unsigned I2SdmaMem = 0;
|
||||
for (auto &bus : busConfigs) {
|
||||
// assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels
|
||||
// store the result in iType for later use during bus creation (getI() must only be called once per BusConfig)
|
||||
// note: this needs to be determined for all buses prior to creating them as it also determines parallel I2S usage
|
||||
bus.iType = BusManager::getI(bus.type, bus.pins, bus.driverType);
|
||||
}
|
||||
for (auto &bus : busConfigs) {
|
||||
bool use_placeholder = false;
|
||||
unsigned busMemUsage = bus.memUsage(); // does not include DMA/RMT buffer but includes pixel buffers (segment buffer + global buffer)
|
||||
mem += busMemUsage;
|
||||
// estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled)
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
|
||||
bool usesI2S = (bus.iType & 0x01) == 0; // I2S bus types are even numbered, can't use bus.driverType == 1 as getI() may have defaulted to RMT
|
||||
if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && usesI2S) {
|
||||
#ifdef NPB_CONF_4STEP_CADENCE
|
||||
constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit)
|
||||
#else
|
||||
constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit)
|
||||
#endif
|
||||
unsigned i2sCommonMem = (stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1));
|
||||
if (useParallelI2S) i2sCommonMem *= 8; // parallel I2S uses 8 channels, requiring 8x the DMA buffer size (common buffer shared between all parallel busses)
|
||||
if (i2sCommonMem > I2SdmaMem) I2SdmaMem = i2sCommonMem;
|
||||
}
|
||||
#endif
|
||||
if (mem + I2SdmaMem > MAX_LED_MEMORY + 1024) { // +1k to allow some margin to not drop buses that are allowed in UI (calculation here includes bus overhead)
|
||||
DEBUG_PRINTF_P(PSTR("Bus %d with %d LEDS memory usage exceeds limit\n"), (int)bus.type, bus.count);
|
||||
errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: not enough memory for bus
|
||||
use_placeholder = true;
|
||||
}
|
||||
if (BusManager::add(bus, use_placeholder) != -1) {
|
||||
mem += BusManager::busses.back()->getBusSize();
|
||||
if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count
|
||||
}
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("Estimated buses + pixel-buffers size: %uB\n"), mem + I2SdmaMem);
|
||||
busConfigs.clear();
|
||||
busConfigs.shrink_to_fit();
|
||||
|
||||
@@ -1228,62 +1266,56 @@ void WS2812FX::finalizeInit() {
|
||||
|
||||
void WS2812FX::service() {
|
||||
unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days
|
||||
now = nowUp + timebase;
|
||||
unsigned long elapsed = nowUp - _lastServiceShow;
|
||||
if (_suspend || elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited
|
||||
if (!_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime
|
||||
if (elapsed < _frametime) return; // too early for service
|
||||
}
|
||||
bool timeToShow = (elapsed >= _frametime); // all segments are running at the same speed
|
||||
if (_triggered || _targetFps == FPS_UNLIMITED) timeToShow = true; // unlimited mode = no frametime; strip.trigger() can overrule timing
|
||||
|
||||
bool doShow = false;
|
||||
now = nowUp + timebase; // common time base for all effects
|
||||
if (!timeToShow) return; // too early for service
|
||||
if (_suspend || elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited
|
||||
|
||||
_isServicing = true;
|
||||
_segment_index = 0;
|
||||
|
||||
for (Segment &seg : _segments) {
|
||||
if (_suspend) break; // immediately stop processing segments if suspend requested during service()
|
||||
bool doShow = _triggered; // true if ≥1 active segment was processed (and strip was not suspended mid-loop), or trigger received → triggers show()
|
||||
for (size_t i = 0; i < _segments.size(); i++) {
|
||||
Segment &seg = _segments[i];
|
||||
_segment_index = i;
|
||||
if (_suspend) break; // abort processing segments if suspend requested during service()
|
||||
|
||||
// process transition (also pre-calculates progress value)
|
||||
seg.handleTransition();
|
||||
// reset the segment runtime data if needed
|
||||
seg.resetIfRequired();
|
||||
|
||||
if (!seg.isActive()) continue;
|
||||
|
||||
// last condition ensures all solid segments are updated at the same time
|
||||
if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC))
|
||||
{
|
||||
if (seg.isActive()) {
|
||||
// current segment is active -> re-run effect, and remember that show() call is necessary
|
||||
// if we arrive here, its always showtime (timeToShow == true)
|
||||
doShow = true;
|
||||
unsigned frameDelay = FRAMETIME;
|
||||
|
||||
if (!seg.freeze) { //only run effect function if not frozen
|
||||
// Effect blending
|
||||
uint16_t prog = seg.progress();
|
||||
seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE)
|
||||
_currentSegment = &seg; // set current segment for effect functions (SEGMENT & SEGENV)
|
||||
// workaround for on/off transition to respect blending style
|
||||
frameDelay = (*_mode[seg.mode])(); // run new/current mode (needed for bri workaround)
|
||||
_mode[seg.mode](); // run new/current mode (needed for bri workaround)
|
||||
seg.call++;
|
||||
// if segment is in transition and no old segment exists we don't need to run the old mode
|
||||
// (blendSegments() takes care of On/Off transitions and clipping)
|
||||
Segment *segO = seg.getOldSegment();
|
||||
if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE ||
|
||||
if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != TRANSITION_FADE ||
|
||||
(segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) {
|
||||
Segment::modeBlend(true); // set semaphore for beginDraw() to blend colors and palette
|
||||
Segment::modeBlend(true); // set flag for beginDraw() to blend colors and palette
|
||||
segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress
|
||||
_currentSegment = segO; // set current segment
|
||||
// workaround for on/off transition to respect blending style
|
||||
frameDelay = min(frameDelay, (unsigned)(*_mode[segO->mode])()); // run old mode (needed for bri workaround; semaphore!!)
|
||||
_mode[segO->mode](); // run old mode (needed for bri workaround; semaphore!!)
|
||||
segO->call++; // increment old mode run counter
|
||||
Segment::modeBlend(false); // unset semaphore
|
||||
Segment::modeBlend(false); // unset flag
|
||||
}
|
||||
if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition
|
||||
}
|
||||
|
||||
seg.next_time = nowUp + frameDelay;
|
||||
}
|
||||
_segment_index++;
|
||||
}
|
||||
_segment_index = 0; // segment index is only valid while effects are serviced
|
||||
_currentSegment = &_segments[0]; // safe fallback to prevent stale pointer - SEGMENT/SEGENV should not be used outside of the service loop
|
||||
|
||||
#ifdef WLED_DEBUG
|
||||
if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime);
|
||||
@@ -1298,14 +1330,14 @@ void WS2812FX::service() {
|
||||
if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime);
|
||||
#endif
|
||||
|
||||
_triggered = false;
|
||||
if (!_suspend) _triggered = false; // avoid losing "trigger" events if suspend requested during effect service()
|
||||
_isServicing = false;
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Blend_modes but using a for top layer & b for bottom layer
|
||||
static uint8_t _top (uint8_t a, uint8_t b) { return a; }
|
||||
static uint8_t _bottom (uint8_t a, uint8_t b) { return b; }
|
||||
static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; }
|
||||
static uint8_t _top (uint8_t a, uint8_t b) { return a; } // function unused
|
||||
static uint8_t _bottom (uint8_t a, uint8_t b) { return b; } // function unused
|
||||
static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; } // function unused
|
||||
static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; }
|
||||
static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); }
|
||||
static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; }
|
||||
@@ -1327,24 +1359,41 @@ static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a)
|
||||
#endif
|
||||
static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); }
|
||||
static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); }
|
||||
static uint8_t _stencil (uint8_t a, uint8_t b) { return a ? a : b; } // function unused
|
||||
static uint8_t _dummy (uint8_t a, uint8_t b) { return a; } // dummy (same as _top) to fill the function list and make it safe from OOB access
|
||||
|
||||
#define BLENDMODES 17 // number of blend modes must match "bm" in index.js, all cases must be handled in segblend() @ blendSegment()
|
||||
|
||||
void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
|
||||
typedef uint8_t(*FuncType)(uint8_t, uint8_t);
|
||||
// function pointer array: fill with _dummy if using special case: avoid OOB access and always provide a valid path
|
||||
FuncType funcs[] = {
|
||||
_top, _bottom,
|
||||
_add, _subtract, _difference, _average,
|
||||
_multiply, _divide, _lighten, _darken, _screen, _overlay,
|
||||
_hardlight, _softlight, _dodge, _burn
|
||||
_dummy, _dummy, _dummy, _subtract,
|
||||
_difference, _average, _dummy, _divide,
|
||||
_lighten, _darken, _screen, _overlay,
|
||||
_hardlight, _softlight, _dodge, _burn,
|
||||
_dummy
|
||||
};
|
||||
|
||||
const size_t blendMode = topSegment.blendMode < (sizeof(funcs) / sizeof(FuncType)) ? topSegment.blendMode : 0;
|
||||
const auto func = funcs[blendMode]; // blendMode % (sizeof(funcs) / sizeof(FuncType))
|
||||
const auto blend = [&](uint32_t top, uint32_t bottom){ return RGBW32(func(R(top),R(bottom)), func(G(top),G(bottom)), func(B(top),B(bottom)), func(W(top),W(bottom))); };
|
||||
const size_t blendMode = topSegment.blendMode < BLENDMODES ? topSegment.blendMode : 0; // default to top if unsupported mode
|
||||
const auto segblend = [&](uint32_t t, uint32_t b){
|
||||
// use direct calculations/returns for simple/frequent modes (faster)
|
||||
switch (blendMode) {
|
||||
case 0 : return t; // top
|
||||
case 1 : return b; // bottom
|
||||
case 2 : return color_add(t,b,true); // add with preserve color ratio to avoid color clipping
|
||||
case 6 : return RGBW32(_multiply(R(t),R(b)), _multiply(G(t),G(b)), _multiply(B(t),B(b)), _multiply(W(t),W(b))); // multiply (7% faster than lambda at 100bytes flash cost)
|
||||
case 16: return t ? t : b; // stencil (use top layer if not black, else bottom)
|
||||
}
|
||||
// default: use function pointer from array
|
||||
const auto func = funcs[blendMode];
|
||||
return RGBW32(func(R(t),R(b)), func(G(t),G(b)), func(B(t),B(b)), func(W(t),W(b)));
|
||||
};
|
||||
|
||||
const int length = topSegment.length(); // physical segment length (counts all pixels in 2D segment)
|
||||
const int width = topSegment.width();
|
||||
const int height = topSegment.height();
|
||||
//const uint32_t bgColor = topSegment.colors[1]; // background color (unused, could add it to stencil mode if requested)
|
||||
const auto XY = [](int x, int y){ return x + y*Segment::maxWidth; };
|
||||
const size_t matrixSize = Segment::maxWidth * Segment::maxHeight;
|
||||
const size_t startIndx = XY(topSegment.start, topSegment.startY);
|
||||
@@ -1353,61 +1402,62 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
const unsigned progInv = 0xFFFFU - progress;
|
||||
uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE
|
||||
uint8_t cct = topSegment.currentCCT();
|
||||
if (gammaCorrectCol) opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details)
|
||||
|
||||
Segment::setClippingRect(0, 0); // disable clipping by default
|
||||
|
||||
const unsigned dw = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1;
|
||||
const unsigned dh = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1;
|
||||
const unsigned dw = (blendingStyle==TRANSITION_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1;
|
||||
const unsigned dh = (blendingStyle==TRANSITION_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1;
|
||||
const unsigned orgBS = blendingStyle;
|
||||
if (width*height == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead)
|
||||
if (width*height == 1) blendingStyle = TRANSITION_FADE; // disable style for single pixel segments (use fade instead)
|
||||
switch (blendingStyle) {
|
||||
case BLEND_STYLE_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped())
|
||||
case BLEND_STYLE_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped())
|
||||
case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped())
|
||||
case TRANSITION_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped())
|
||||
case TRANSITION_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped())
|
||||
case TRANSITION_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped())
|
||||
Segment::setClippingRect(0, width, 0, height);
|
||||
break;
|
||||
case BLEND_STYLE_SWIPE_RIGHT: // left-to-right
|
||||
case BLEND_STYLE_PUSH_RIGHT: // left-to-right
|
||||
case TRANSITION_SWIPE_RIGHT: // left-to-right
|
||||
case TRANSITION_PUSH_RIGHT: // left-to-right
|
||||
Segment::setClippingRect(0, dw, 0, height);
|
||||
break;
|
||||
case BLEND_STYLE_SWIPE_LEFT: // right-to-left
|
||||
case BLEND_STYLE_PUSH_LEFT: // right-to-left
|
||||
case TRANSITION_SWIPE_LEFT: // right-to-left
|
||||
case TRANSITION_PUSH_LEFT: // right-to-left
|
||||
Segment::setClippingRect(width - dw, width, 0, height);
|
||||
break;
|
||||
case BLEND_STYLE_OUTSIDE_IN: // corners
|
||||
case TRANSITION_OUTSIDE_IN: // corners
|
||||
Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted!!
|
||||
break;
|
||||
case BLEND_STYLE_INSIDE_OUT: // outward
|
||||
case TRANSITION_INSIDE_OUT: // outward
|
||||
Segment::setClippingRect((width - dw)/2, (width + dw)/2, (height - dh)/2, (height + dh)/2);
|
||||
break;
|
||||
case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D)
|
||||
case BLEND_STYLE_PUSH_DOWN: // top-to-bottom (2D)
|
||||
case TRANSITION_SWIPE_DOWN: // top-to-bottom (2D)
|
||||
case TRANSITION_PUSH_DOWN: // top-to-bottom (2D)
|
||||
Segment::setClippingRect(0, width, 0, dh);
|
||||
break;
|
||||
case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D)
|
||||
case BLEND_STYLE_PUSH_UP: // bottom-to-top (2D)
|
||||
case TRANSITION_SWIPE_UP: // bottom-to-top (2D)
|
||||
case TRANSITION_PUSH_UP: // bottom-to-top (2D)
|
||||
Segment::setClippingRect(0, width, height - dh, height);
|
||||
break;
|
||||
case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D
|
||||
case TRANSITION_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D
|
||||
Segment::setClippingRect((width - dw)/2, (width + dw)/2, 0, height);
|
||||
break;
|
||||
case BLEND_STYLE_OPEN_V: // vertical-outward (2D)
|
||||
case TRANSITION_OPEN_V: // vertical-outward (2D)
|
||||
Segment::setClippingRect(0, width, (height - dh)/2, (height + dh)/2);
|
||||
break;
|
||||
case BLEND_STYLE_SWIPE_TL: // TL-to-BR (2D)
|
||||
case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D)
|
||||
case TRANSITION_SWIPE_TL: // TL-to-BR (2D)
|
||||
case TRANSITION_PUSH_TL: // TL-to-BR (2D)
|
||||
Segment::setClippingRect(0, dw, 0, dh);
|
||||
break;
|
||||
case BLEND_STYLE_SWIPE_TR: // TR-to-BL (2D)
|
||||
case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D)
|
||||
case TRANSITION_SWIPE_TR: // TR-to-BL (2D)
|
||||
case TRANSITION_PUSH_TR: // TR-to-BL (2D)
|
||||
Segment::setClippingRect(width - dw, width, 0, dh);
|
||||
break;
|
||||
case BLEND_STYLE_SWIPE_BR: // BR-to-TL (2D)
|
||||
case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D)
|
||||
case TRANSITION_SWIPE_BR: // BR-to-TL (2D)
|
||||
case TRANSITION_PUSH_BR: // BR-to-TL (2D)
|
||||
Segment::setClippingRect(width - dw, width, height - dh, height);
|
||||
break;
|
||||
case BLEND_STYLE_SWIPE_BL: // BL-to-TR (2D)
|
||||
case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D)
|
||||
case TRANSITION_SWIPE_BL: // BL-to-TR (2D)
|
||||
case TRANSITION_PUSH_BL: // BL-to-TR (2D)
|
||||
Segment::setClippingRect(0, dw, height - dh, height);
|
||||
break;
|
||||
}
|
||||
@@ -1424,18 +1474,18 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
const int baseX = topSegment.start + x;
|
||||
const int baseY = topSegment.startY + y;
|
||||
size_t indx = XY(baseX, baseY); // absolute address on strip
|
||||
_pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o);
|
||||
_pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx]), o);
|
||||
if (_pixelCCT) _pixelCCT[indx] = cct;
|
||||
// Apply mirroring
|
||||
// Apply mirroring if enabled
|
||||
if (topSegment.mirror || topSegment.mirror_y) {
|
||||
const int mirrorX = topSegment.start + width - x - 1;
|
||||
const int mirrorY = topSegment.startY + height - y - 1;
|
||||
const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY);
|
||||
const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY);
|
||||
const size_t idxMM = XY(mirrorX, mirrorY);
|
||||
if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], blend(c, _pixels[idxMX]), o);
|
||||
if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], blend(c, _pixels[idxMY]), o);
|
||||
if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], blend(c, _pixels[idxMM]), o);
|
||||
if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], segblend(c, _pixels[idxMX]), o);
|
||||
if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], segblend(c, _pixels[idxMY]), o);
|
||||
if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], segblend(c, _pixels[idxMM]), o);
|
||||
if (_pixelCCT) {
|
||||
if (topSegment.mirror) _pixelCCT[idxMX] = cct;
|
||||
if (topSegment.mirror_y) _pixelCCT[idxMY] = cct;
|
||||
@@ -1445,9 +1495,22 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
};
|
||||
|
||||
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
|
||||
unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU;
|
||||
unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU;
|
||||
|
||||
unsigned offsetX = (blendingStyle == TRANSITION_PUSH_UP || blendingStyle == TRANSITION_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU;
|
||||
unsigned offsetY = (blendingStyle == TRANSITION_PUSH_LEFT || blendingStyle == TRANSITION_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU;
|
||||
const unsigned groupLen = topSegment.groupLength();
|
||||
bool applyReverse = topSegment.reverse || topSegment.reverse_y || topSegment.transpose;
|
||||
int pushOffsetX = 0, pushOffsetY = 0;
|
||||
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
|
||||
switch (blendingStyle) {
|
||||
case TRANSITION_PUSH_RIGHT: pushOffsetX = offsetX; break;
|
||||
case TRANSITION_PUSH_LEFT: pushOffsetX = -offsetX + nCols; break;
|
||||
case TRANSITION_PUSH_DOWN: pushOffsetY = offsetY; break;
|
||||
case TRANSITION_PUSH_UP: pushOffsetY = -offsetY + nRows; break;
|
||||
case TRANSITION_PUSH_TL: pushOffsetX = offsetX; pushOffsetY = offsetY; break; // unused
|
||||
case TRANSITION_PUSH_TR: pushOffsetX = -offsetX + nCols; pushOffsetY = offsetY; break; // unused
|
||||
case TRANSITION_PUSH_BR: pushOffsetX = -offsetX + nCols; pushOffsetY = -offsetY + nRows; break; // unused
|
||||
case TRANSITION_PUSH_BL: pushOffsetX = offsetX; pushOffsetY = -offsetY + nRows; break; // unused
|
||||
}
|
||||
// we only traverse new segment, not old one
|
||||
for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) {
|
||||
const bool clipped = topSegment.isPixelXYClipped(c, r);
|
||||
@@ -1457,34 +1520,31 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions
|
||||
int x = c;
|
||||
int y = r;
|
||||
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
|
||||
switch (blendingStyle) {
|
||||
case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break;
|
||||
case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break;
|
||||
case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break;
|
||||
case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break;
|
||||
}
|
||||
if (pushOffsetX != 0) x = (x + pushOffsetX) % nCols;
|
||||
if (pushOffsetY != 0) y = (y + pushOffsetY) % nRows;
|
||||
uint32_t c_a = BLACK;
|
||||
if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment
|
||||
if (segO && blendingStyle == BLEND_STYLE_FADE
|
||||
if (segO && blendingStyle == TRANSITION_FADE
|
||||
&& (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0))
|
||||
&& x < oCols && y < oRows) {
|
||||
// we need to blend old segment using fade as pixels are not clipped
|
||||
c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv);
|
||||
} else if (blendingStyle != BLEND_STYLE_FADE) {
|
||||
} else if (blendingStyle != TRANSITION_FADE) {
|
||||
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
|
||||
// workaround for On/Off transition
|
||||
// (bri != briT) && !bri => from On to Off
|
||||
// (bri != briT) && bri => from Off to On
|
||||
if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK;
|
||||
if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
|
||||
}
|
||||
// map it into frame buffer
|
||||
x = c; // restore coordiates if we were PUSHing
|
||||
y = r;
|
||||
if (topSegment.reverse ) x = nCols - x - 1;
|
||||
if (topSegment.reverse_y) y = nRows - y - 1;
|
||||
if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed
|
||||
if (applyReverse) {
|
||||
if (topSegment.reverse ) x = nCols - x - 1;
|
||||
if (topSegment.reverse_y) y = nRows - y - 1;
|
||||
if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed
|
||||
}
|
||||
// expand pixel
|
||||
const unsigned groupLen = topSegment.groupLength();
|
||||
if (groupLen == 1) {
|
||||
setMirroredPixel(x, y, c_a, opacity);
|
||||
} else {
|
||||
@@ -1513,12 +1573,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
unsigned indxM = topSegment.stop - i - 1;
|
||||
indxM += topSegment.offset; // offset/phase
|
||||
if (indxM >= topSegment.stop) indxM -= length; // wrap
|
||||
_pixels[indxM] = color_blend(_pixels[indxM], blend(c, _pixels[indxM]), o);
|
||||
_pixels[indxM] = color_blend(_pixels[indxM], segblend(c, _pixels[indxM]), o);
|
||||
if (_pixelCCT) _pixelCCT[indxM] = cct;
|
||||
}
|
||||
indx += topSegment.offset; // offset/phase
|
||||
if (indx >= topSegment.stop) indx -= length; // wrap
|
||||
_pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o);
|
||||
_pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx]), o);
|
||||
if (_pixelCCT) _pixelCCT[indx] = cct;
|
||||
};
|
||||
|
||||
@@ -1533,19 +1593,20 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
|
||||
int i = k;
|
||||
// if we blend using "push" style we need to "shift" canvas to left or right
|
||||
switch (blendingStyle) {
|
||||
case BLEND_STYLE_PUSH_RIGHT: i = (i + offsetI) % nLen; break;
|
||||
case BLEND_STYLE_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break;
|
||||
case TRANSITION_PUSH_RIGHT: i = (i + offsetI) % nLen; break;
|
||||
case TRANSITION_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break;
|
||||
}
|
||||
uint32_t c_a = BLACK;
|
||||
if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment
|
||||
if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && i < oLen) {
|
||||
if (segO && blendingStyle == TRANSITION_FADE && topSegment.mode != segO->mode && i < oLen) {
|
||||
// we need to blend old segment using fade as pixels are not clipped
|
||||
c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv);
|
||||
} else if (blendingStyle != BLEND_STYLE_FADE) {
|
||||
} else if (blendingStyle != TRANSITION_FADE) {
|
||||
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
|
||||
// workaround for On/Off transition
|
||||
// (bri != briT) && !bri => from On to Off
|
||||
// (bri != briT) && bri => from Off to On
|
||||
if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK;
|
||||
if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;
|
||||
}
|
||||
// map into frame buffer
|
||||
i = k; // restore index if we were PUSHing
|
||||
@@ -1605,7 +1666,7 @@ void WS2812FX::show() {
|
||||
}
|
||||
|
||||
uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32)
|
||||
if(c > 0 && !(realtimeMode && arlsDisableGammaCorrection))
|
||||
if (c > 0 && !(realtimeMode && arlsDisableGammaCorrection))
|
||||
c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss
|
||||
BusManager::setPixelColor(getMappedPixelIndex(i), c);
|
||||
}
|
||||
@@ -1651,12 +1712,17 @@ void WS2812FX::setTransitionMode(bool t) {
|
||||
resume();
|
||||
}
|
||||
|
||||
// wait until frame is over (service() has finished or time for 1 frame has passed; yield() crashes on 8266)
|
||||
// wait until frame is over (service() has finished or time for 2 frames have passed; yield() crashes on 8266)
|
||||
// the latter may, in rare circumstances, lead to incorrectly assuming strip is done servicing but will not block
|
||||
// other processing "indefinitely"
|
||||
// rare circumstances are: setting FPS to high number (i.e. 120) and have very slow effect that will need more
|
||||
// time than 2 * _frametime (1000/FPS) to draw content
|
||||
void WS2812FX::waitForIt() {
|
||||
unsigned long maxWait = millis() + getFrameTime() + 100; // TODO: this needs a proper fix for timeout!
|
||||
while (isServicing() && maxWait > millis()) delay(1);
|
||||
unsigned long waitStart = millis();
|
||||
unsigned long maxWait = 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779
|
||||
while (isServicing() && (millis() - waitStart < maxWait)) delay(1); // safe even when millis() rolls over
|
||||
#ifdef WLED_DEBUG
|
||||
if (millis() >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
|
||||
if (millis()-waitStart >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -1686,7 +1752,7 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) {
|
||||
BusManager::setBrightness(scaledBri(b));
|
||||
if (!direct) {
|
||||
unsigned long t = millis();
|
||||
if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon
|
||||
if (t - _lastShow > min(_frametime, uint16_t(FRAMETIME_FIXED))) trigger(); //apply brightness change immediately if no refresh soon, but don't speed up above 42fps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1780,14 +1846,23 @@ Segment& WS2812FX::getSegment(unsigned id) {
|
||||
return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors
|
||||
}
|
||||
|
||||
// WARNING: resetSegments(), makeAutoSegments() and fixInvalidSegments() must not be called while
|
||||
// strip is being serviced (strip.service()), you must call suspend prior if changing segments outside
|
||||
// loop() context
|
||||
void WS2812FX::resetSegments() {
|
||||
if (isServicing()) return;
|
||||
_segments.clear(); // destructs all Segment as part of clearing
|
||||
_segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1);
|
||||
if(_segments.size() == 0) {
|
||||
_segments.emplace_back(); // if out of heap, create a default segment
|
||||
errorFlag = ERR_NORAM_PX;
|
||||
}
|
||||
_segments.shrink_to_fit(); // just in case ...
|
||||
_mainSegment = 0;
|
||||
}
|
||||
|
||||
void WS2812FX::makeAutoSegments(bool forceReset) {
|
||||
if (isServicing()) return;
|
||||
if (autoSegments) { //make one segment per bus
|
||||
unsigned segStarts[MAX_NUM_SEGMENTS] = {0};
|
||||
unsigned segStops [MAX_NUM_SEGMENTS] = {0};
|
||||
@@ -1804,7 +1879,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
|
||||
|
||||
for (size_t i = s; i < BusManager::getNumBusses(); i++) {
|
||||
const Bus *bus = BusManager::getBus(i);
|
||||
if (!bus || !bus->isOk()) break;
|
||||
if (!bus) break;
|
||||
|
||||
segStarts[s] = bus->getStart();
|
||||
segStops[s] = segStarts[s] + bus->getLength();
|
||||
@@ -1859,6 +1934,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
|
||||
}
|
||||
|
||||
void WS2812FX::fixInvalidSegments() {
|
||||
if (isServicing()) return;
|
||||
//make sure no segment is longer than total (sanity check)
|
||||
for (size_t i = getSegmentsNum()-1; i > 0; i--) {
|
||||
if (isMatrix) {
|
||||
@@ -1921,6 +1997,7 @@ void WS2812FX::printSize() {
|
||||
|
||||
// load custom mapping table from JSON file (called from finalizeInit() or deserializeState())
|
||||
// if this is a matrix set-up and default ledmap.json file does not exist, create mapping table using setUpMatrix() from panel information
|
||||
// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context
|
||||
bool WS2812FX::deserializeMap(unsigned n) {
|
||||
char fileName[32];
|
||||
strcpy_P(fileName, PSTR("/ledmap"));
|
||||
@@ -1938,7 +2015,7 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isFile || !requestJSONBufferLock(7)) return false;
|
||||
if (!isFile || !requestJSONBufferLock(JSON_LOCK_LEDMAP)) return false;
|
||||
|
||||
StaticJsonDocument<64> filter;
|
||||
filter[F("width")] = true;
|
||||
@@ -1950,15 +2027,13 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
} else
|
||||
DEBUG_PRINTF_P(PSTR("Reading LED map from %s\n"), fileName);
|
||||
|
||||
suspend();
|
||||
waitForIt();
|
||||
|
||||
JsonObject root = pDoc->as<JsonObject>();
|
||||
// if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps)
|
||||
if (n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) {
|
||||
Segment::maxWidth = min(max(root[F("width")].as<int>(), 1), 255);
|
||||
Segment::maxHeight = min(max(root[F("height")].as<int>(), 1), 255);
|
||||
isMatrix = true;
|
||||
DEBUG_PRINTF_P(PSTR("LED map width=%d, height=%d\n"), Segment::maxWidth, Segment::maxHeight);
|
||||
}
|
||||
|
||||
d_free(customMappingTable);
|
||||
@@ -1982,9 +2057,9 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
} while (i < 32);
|
||||
if (!foundDigit) break;
|
||||
int index = atoi(number);
|
||||
if (index < 0 || index > 16384) index = 0xFFFF;
|
||||
if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around
|
||||
customMappingTable[customMappingSize++] = index;
|
||||
if (customMappingSize > getLengthTotal()) break;
|
||||
if (customMappingSize >= getLengthTotal()) break;
|
||||
} else break; // there was nothing to read, stop
|
||||
}
|
||||
currentLedmap = n;
|
||||
@@ -1994,7 +2069,7 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
DEBUG_PRINT(F("Loaded ledmap:"));
|
||||
for (unsigned i=0; i<customMappingSize; i++) {
|
||||
if (!(i%Segment::maxWidth)) DEBUG_PRINTLN();
|
||||
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i]);
|
||||
DEBUG_PRINTF_P(PSTR("%4d,"), customMappingTable[i] < 0xFFFFU ? customMappingTable[i] : -1);
|
||||
}
|
||||
DEBUG_PRINTLN();
|
||||
#endif
|
||||
@@ -2010,8 +2085,6 @@ bool WS2812FX::deserializeMap(unsigned n) {
|
||||
DEBUG_PRINTLN(F("ERROR LED map allocation error."));
|
||||
}
|
||||
|
||||
resume();
|
||||
|
||||
releaseJSONBufferLock();
|
||||
return (customMappingSize > 0);
|
||||
}
|
||||
|
||||
+508
-486
File diff suppressed because it is too large
Load Diff
+30
-11
@@ -17,7 +17,7 @@
|
||||
#include <stdint.h>
|
||||
#include "wled.h"
|
||||
|
||||
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8)
|
||||
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8), limiting below 127 to avoid overflows in collisions due to rounding errors
|
||||
#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!)
|
||||
|
||||
//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash
|
||||
@@ -103,7 +103,7 @@ typedef union {
|
||||
|
||||
// struct for additional particle settings (option)
|
||||
typedef struct { // 2 bytes
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, 0 means use global size (including single pixel rendering)
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable
|
||||
uint8_t forcecounter; // counter for applying forces to individual particles
|
||||
} PSadvancedParticle;
|
||||
|
||||
@@ -143,7 +143,7 @@ public:
|
||||
ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor
|
||||
// note: memory is allcated in the FX function, no deconstructor needed
|
||||
void update(void); //update the particles according to set options and render to the matrix
|
||||
void updateFire(const uint8_t intensity, const bool renderonly); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips)
|
||||
void updateFire(const uint8_t intensity); // update function for fire
|
||||
void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions
|
||||
void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function
|
||||
// particle emitters
|
||||
@@ -190,16 +190,18 @@ public:
|
||||
int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1
|
||||
uint32_t numSources; // number of sources
|
||||
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
|
||||
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
|
||||
//note: some variables are 32bit for speed and code size at the cost of ram
|
||||
|
||||
private:
|
||||
//rendering functions
|
||||
void render();
|
||||
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
|
||||
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
|
||||
//paricle physics applied by system if flags are set
|
||||
void applyGravity(); // applies gravity to all particles
|
||||
void handleCollisions();
|
||||
[[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const uint32_t collDistSq);
|
||||
void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2);
|
||||
void fireParticleupdate();
|
||||
//utility functions
|
||||
void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space
|
||||
@@ -226,12 +228,26 @@ private:
|
||||
uint8_t smearBlur; // 2D smeared blurring of full frame
|
||||
};
|
||||
|
||||
void blur2D(uint32_t *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false);
|
||||
// initialization functions (not part of class)
|
||||
bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false);
|
||||
uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol);
|
||||
uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources);
|
||||
bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes);
|
||||
|
||||
// distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center
|
||||
inline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) {
|
||||
// square the distances
|
||||
uint32_t dx_sq = dx * dx;
|
||||
uint32_t dy_sq = dy * dy;
|
||||
|
||||
uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256
|
||||
|
||||
if (dist_sq >= 256) return 0; // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0
|
||||
//if (dist_sq <= 96) return maxBrightness; // core at full brightness
|
||||
int32_t falloff = 256 - dist_sq;
|
||||
return (maxBrightness * falloff) >> 8; // linear falloff
|
||||
//return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges
|
||||
}
|
||||
#endif // WLED_DISABLE_PARTICLESYSTEM2D
|
||||
|
||||
////////////////////////
|
||||
@@ -301,9 +317,9 @@ typedef union {
|
||||
|
||||
// struct for additional particle settings (optional)
|
||||
typedef struct {
|
||||
uint8_t sat; //color saturation
|
||||
uint8_t sat; // color saturation
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting
|
||||
uint8_t forcecounter;
|
||||
uint8_t forcecounter; // counter for applying forces to individual particles
|
||||
} PSadvancedParticle1D;
|
||||
|
||||
//struct for a particle source (20 bytes)
|
||||
@@ -346,10 +362,11 @@ public:
|
||||
void setColorByPosition(const bool enable);
|
||||
void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero
|
||||
void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame
|
||||
void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled if advanced particle is used
|
||||
void setParticleSize(const uint8_t size); // particle diameter: size 0 = 1 pixel, size 1 = 2 pixels, size = 255 = 10 pixels, disables per particle size control if called
|
||||
void setGravity(int8_t force = 8);
|
||||
void enableParticleCollisions(bool enable, const uint8_t hardness = 255);
|
||||
|
||||
|
||||
PSparticle1D *particles; // pointer to particle array
|
||||
PSparticleFlags1D *particleFlags; // pointer to particle flags array
|
||||
PSsource1D *sources; // pointer to sources
|
||||
@@ -360,16 +377,18 @@ public:
|
||||
int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1
|
||||
uint32_t numSources; // number of sources
|
||||
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
|
||||
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
|
||||
|
||||
private:
|
||||
//rendering functions
|
||||
void render(void);
|
||||
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
|
||||
void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
|
||||
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap);
|
||||
|
||||
//paricle physics applied by system if flags are set
|
||||
void applyGravity(); // applies gravity to all particles
|
||||
void handleCollisions();
|
||||
[[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance);
|
||||
void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance);
|
||||
|
||||
//utility functions
|
||||
void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space
|
||||
@@ -397,5 +416,5 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso
|
||||
uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced);
|
||||
uint32_t calculateNumberOfSources1D(const uint32_t requestedsources);
|
||||
bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes);
|
||||
void blur1D(uint32_t *colorbuffer, uint32_t size, uint32_t blur, uint32_t start);
|
||||
|
||||
#endif // WLED_DISABLE_PARTICLESYSTEM1D
|
||||
|
||||
@@ -15,6 +15,22 @@
|
||||
#define NODE_TYPE_ID_ESP32S3 34
|
||||
#define NODE_TYPE_ID_ESP32C3 35
|
||||
|
||||
// updated node types from the ESP Easy project
|
||||
// https://github.com/letscontrolit/ESPEasy/blob/mega/src/src/DataTypes/NodeTypeID.h
|
||||
//#define NODE_TYPE_ID_ESP32 33
|
||||
//#define NODE_TYPE_ID_ESP32S2 34
|
||||
//#define NODE_TYPE_ID_ESP32C3 35
|
||||
//#define NODE_TYPE_ID_ESP32S3 36
|
||||
#define NODE_TYPE_ID_ESP32C2 37
|
||||
#define NODE_TYPE_ID_ESP32H2 38
|
||||
#define NODE_TYPE_ID_ESP32C6 39
|
||||
#define NODE_TYPE_ID_ESP32C61 40
|
||||
#define NODE_TYPE_ID_ESP32C5 41
|
||||
#define NODE_TYPE_ID_ESP32P4 42
|
||||
#define NODE_TYPE_ID_ESP32P4r3 45
|
||||
#define NODE_TYPE_ID_ESP32H21 43
|
||||
#define NODE_TYPE_ID_ESP32H4 44
|
||||
|
||||
/*********************************************************************************************\
|
||||
* NodeStruct
|
||||
\*********************************************************************************************/
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
/*
|
||||
asyncDNS.h - wrapper class for asynchronous DNS lookups using lwIP
|
||||
by @dedehai, C++ improvements & hardening by @willmmiles
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <lwip/dns.h>
|
||||
#include <lwip/err.h>
|
||||
|
||||
class AsyncDNS {
|
||||
|
||||
// C++14 shim
|
||||
#if __cplusplus < 201402L
|
||||
// Really simple C++11 shim for non-array case; implementation from cppreference.com
|
||||
template<class T, class... Args>
|
||||
static std::unique_ptr<T>
|
||||
make_unique(Args&&... args)
|
||||
{
|
||||
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
|
||||
}
|
||||
#endif
|
||||
|
||||
public:
|
||||
// note: passing the IP as a pointer to query() is not implemented because it is not thread-safe without mutexes
|
||||
// with the IDF V4 bug external error handling is required anyway or dns can just stay stuck
|
||||
enum class result { Idle, Busy, Success, Error };
|
||||
|
||||
// non-blocking query function to start DNS lookup
|
||||
static std::shared_ptr<AsyncDNS> query(const char* hostname, std::shared_ptr<AsyncDNS> current = {}) {
|
||||
if (!current || (current->_status == result::Busy)) {
|
||||
current.reset(new AsyncDNS());
|
||||
}
|
||||
|
||||
#if __cplusplus >= 201402L
|
||||
using std::make_unique;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<std::shared_ptr<AsyncDNS>> callback_state = make_unique<std::shared_ptr<AsyncDNS> >(current);
|
||||
if (!callback_state) return {};
|
||||
|
||||
current->_status = result::Busy;
|
||||
err_t err = dns_gethostbyname(hostname, ¤t->_raw_addr, _dns_callback, callback_state.get());
|
||||
if (err == ERR_OK) {
|
||||
current->_status = result::Success; // result already in cache
|
||||
} else if (err == ERR_INPROGRESS) {
|
||||
callback_state.release(); // belongs to the callback now
|
||||
} else {
|
||||
current->_status = result::Error;
|
||||
current->_errorcount++;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
// get the IP once Success is returned
|
||||
const IPAddress getIP() {
|
||||
if (_status != result::Success) return IPAddress(0,0,0,0);
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
return IPAddress(_raw_addr.u_addr.ip4.addr);
|
||||
#else
|
||||
return IPAddress(_raw_addr.addr);
|
||||
#endif
|
||||
}
|
||||
|
||||
void reset() { _errorcount = 0; } // reset status and error count
|
||||
const result status() { return _status; }
|
||||
const uint16_t getErrorCount() { return _errorcount; }
|
||||
|
||||
private:
|
||||
ip_addr_t _raw_addr;
|
||||
std::atomic<result> _status { result::Idle };
|
||||
uint16_t _errorcount = 0;
|
||||
|
||||
AsyncDNS(){}; // may not be explicitly instantiated - use query()
|
||||
|
||||
// callback for dns_gethostbyname(), called when lookup is complete or timed out
|
||||
static void _dns_callback(const char *name, const ip_addr_t *ipaddr, void *arg) {
|
||||
std::shared_ptr<AsyncDNS>* instance_ptr = reinterpret_cast<std::shared_ptr<AsyncDNS>*>(arg);
|
||||
AsyncDNS& instance = **instance_ptr;
|
||||
if (ipaddr) {
|
||||
instance._raw_addr = *ipaddr;
|
||||
instance._status = result::Success;
|
||||
} else {
|
||||
instance._status = result::Error; // note: if query timed out (~5s), DNS lookup is broken until WiFi connection is reset (IDF V4 bug)
|
||||
instance._errorcount++;
|
||||
}
|
||||
delete instance_ptr;
|
||||
}
|
||||
};
|
||||
+569
-173
@@ -9,67 +9,44 @@
|
||||
#include "src/dependencies/network/Network.h" // for isConnected() (& WiFi)
|
||||
#include "driver/ledc.h"
|
||||
#include "soc/ledc_struct.h"
|
||||
#if !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3))
|
||||
#define LEDC_MUTEX_LOCK() do {} while (xSemaphoreTake(_ledc_sys_lock, portMAX_DELAY) != pdPASS)
|
||||
#define LEDC_MUTEX_UNLOCK() xSemaphoreGive(_ledc_sys_lock)
|
||||
extern xSemaphoreHandle _ledc_sys_lock;
|
||||
#else
|
||||
#define LEDC_MUTEX_LOCK()
|
||||
#define LEDC_MUTEX_UNLOCK()
|
||||
#endif
|
||||
#endif
|
||||
#ifdef ESP8266
|
||||
#include "core_esp8266_waveform.h"
|
||||
#endif
|
||||
#include "const.h"
|
||||
#include "colors.h"
|
||||
#include "pin_manager.h"
|
||||
#include "bus_manager.h"
|
||||
#include "bus_wrapper.h"
|
||||
#include <bits/unique_ptr.h>
|
||||
#include "wled.h"
|
||||
|
||||
extern char cmDNS[];
|
||||
extern bool cctICused;
|
||||
extern bool useParallelI2S;
|
||||
|
||||
//colors.cpp
|
||||
uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb);
|
||||
|
||||
//udp.cpp
|
||||
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const byte *buffer, uint8_t bri=255, bool isRGBW=false);
|
||||
|
||||
//util.cpp
|
||||
// memory allocation wrappers
|
||||
extern "C" {
|
||||
// prefer DRAM over PSRAM (if available) in d_ alloc functions
|
||||
void *d_malloc(size_t);
|
||||
void *d_calloc(size_t, size_t);
|
||||
void *d_realloc_malloc(void *ptr, size_t size);
|
||||
#ifndef ESP8266
|
||||
inline void d_free(void *ptr) { heap_caps_free(ptr); }
|
||||
#else
|
||||
inline void d_free(void *ptr) { free(ptr); }
|
||||
#endif
|
||||
#if defined(BOARD_HAS_PSRAM)
|
||||
// prefer PSRAM over DRAM in p_ alloc functions
|
||||
void *p_malloc(size_t);
|
||||
void *p_calloc(size_t, size_t);
|
||||
void *p_realloc_malloc(void *ptr, size_t size);
|
||||
inline void p_free(void *ptr) { heap_caps_free(ptr); }
|
||||
#else
|
||||
#define p_malloc d_malloc
|
||||
#define p_calloc d_calloc
|
||||
#define p_free d_free
|
||||
#endif
|
||||
// functions to get/set bits in an array - based on functions created by Brandon for GOL
|
||||
// toDo : make this a class that's completely defined in a header file
|
||||
// note: these functions are automatically inline by the compiler
|
||||
static inline bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value
|
||||
size_t byteIndex = position >> 3; // divide by 8
|
||||
unsigned bitIndex = position & 0x07; // modulo 8
|
||||
uint8_t byteValue = byteArray[byteIndex];
|
||||
return (byteValue >> bitIndex) & 1;
|
||||
}
|
||||
|
||||
//color mangling macros
|
||||
#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))
|
||||
#define R(c) (byte((c) >> 16))
|
||||
#define G(c) (byte((c) >> 8))
|
||||
#define B(c) (byte(c))
|
||||
#define W(c) (byte((c) >> 24))
|
||||
static inline void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr
|
||||
//if (byteArray == nullptr) return;
|
||||
size_t byteIndex = position >> 3; // divide by 8
|
||||
unsigned bitIndex = position & 0x07; // modulo 8
|
||||
if (value)
|
||||
byteArray[byteIndex] |= (1 << bitIndex);
|
||||
else
|
||||
byteArray[byteIndex] &= ~(1 << bitIndex);
|
||||
}
|
||||
|
||||
static inline size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits
|
||||
return (num_bits + 7) >> 3;
|
||||
}
|
||||
|
||||
static inline void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value
|
||||
if (byteArray == nullptr) return;
|
||||
size_t len = getBitArrayBytes(numBits);
|
||||
if (value) memset(byteArray, 0xFF, len);
|
||||
else memset(byteArray, 0x00, len);
|
||||
}
|
||||
|
||||
static ColorOrderMap _colorOrderMap = {};
|
||||
|
||||
@@ -101,42 +78,67 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) {
|
||||
cct = (approximateKelvinFromRGB(c) - 1900) >> 5; // convert K (from RGB value) to relative format
|
||||
}
|
||||
|
||||
//0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold)
|
||||
if (cct < _cctBlend) ww = 255;
|
||||
else ww = ((255-cct) * 255) / (255 - _cctBlend);
|
||||
if ((255-cct) < _cctBlend) cw = 255;
|
||||
else cw = (cct * 255) / (255 - _cctBlend);
|
||||
// CCT blending modes (_cctBlend):
|
||||
// blend<0: ww: ▓▓▒░__ | blend=0: ww: ▓▒▒░░ | blend>0 ww: ▓▓▓▒░
|
||||
// cw: __░▒▓▓ | cw: ░░▒▒▓ | cw: ░▒▓▓▓
|
||||
int32_t ww_val, cw_val;
|
||||
if (_cctBlend < 0) {
|
||||
uint16_t range = 255 - 2 * (uint16_t)(-_cctBlend);
|
||||
if (range > 255) range = 255; // prevent overflow
|
||||
ww_val = range ? ((int32_t)(255 + _cctBlend - cct) * 255) / range : (cct < 128 ? 255 : 0); // exclusive blending
|
||||
cw_val = 255 - ww_val;
|
||||
} else {
|
||||
ww_val = _cctBlend ? ((int32_t)(255 - cct) * 255) / (255 - _cctBlend) : 255 - cct; // additive blending
|
||||
cw_val = _cctBlend ? ((int32_t) cct * 255) / (255 - _cctBlend) : cct;
|
||||
}
|
||||
ww = (uint8_t)(ww_val < 0 ? 0 : ww_val > 255 ? 255 : ww_val);
|
||||
cw = (uint8_t)(cw_val < 0 ? 0 : cw_val > 255 ? 255 : cw_val);
|
||||
|
||||
ww = (w * ww) / 255; //brightness scaling
|
||||
cw = (w * cw) / 255;
|
||||
}
|
||||
|
||||
uint32_t Bus::autoWhiteCalc(uint32_t c) const {
|
||||
// calculates white channel and CCT values based on given settings
|
||||
uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const {
|
||||
unsigned aWM = _autoWhiteMode;
|
||||
if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM;
|
||||
if (aWM == RGBW_MODE_MANUAL_ONLY) return c;
|
||||
CRGBW cIn = c; // save original color for CCT calculation
|
||||
unsigned w = W(c);
|
||||
//ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0)
|
||||
if (w > 0 && aWM == RGBW_MODE_DUAL) return c;
|
||||
unsigned r = R(c);
|
||||
unsigned g = G(c);
|
||||
unsigned b = B(c);
|
||||
if (aWM == RGBW_MODE_MAX) return RGBW32(r, g, b, r > g ? (r > b ? r : b) : (g > b ? g : b)); // brightest RGB channel
|
||||
w = r < g ? (r < b ? r : b) : (g < b ? g : b);
|
||||
if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode
|
||||
return RGBW32(r, g, b, w);
|
||||
if (aWM != RGBW_MODE_MANUAL_ONLY) {
|
||||
unsigned r = R(c); // note: using uint8_t generates larger code
|
||||
unsigned g = G(c);
|
||||
unsigned b = B(c);
|
||||
if (aWM == RGBW_MODE_DUAL && w > 0) {
|
||||
//ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0)
|
||||
} else if (aWM == RGBW_MODE_MAX) {
|
||||
w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel
|
||||
} else {
|
||||
w = r < g ? (r < b ? r : b) : (g < b ? g : b); // darkest RGB channel
|
||||
if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode
|
||||
}
|
||||
c = RGBW32(r, g, b, w);
|
||||
}
|
||||
if (_hasCCT) {
|
||||
cIn.w = w; // need original rgb values in case CCT is derived from RGB
|
||||
calculateCCT(cIn, ww, cw);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
BusDigital::BusDigital(const BusConfig &bc, uint8_t nr)
|
||||
BusDigital::BusDigital(const BusConfig &bc)
|
||||
: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814))
|
||||
, _skip(bc.skipAmount) //sacrificial pixels
|
||||
, _colorOrder(bc.colorOrder)
|
||||
, _milliAmpsPerLed(bc.milliAmpsPerLed)
|
||||
, _milliAmpsMax(bc.milliAmpsMax)
|
||||
, _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S)
|
||||
{
|
||||
DEBUGBUS_PRINTLN(F("Bus: Creating digital bus."));
|
||||
if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; }
|
||||
_iType = bc.iType; // reuse the iType that was determined by polyBus in getI() in finalizeInit()
|
||||
if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; }
|
||||
|
||||
if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; }
|
||||
_frequencykHz = 0U;
|
||||
_colorSum = 0;
|
||||
@@ -150,28 +152,30 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr)
|
||||
_pins[1] = bc.pins[1];
|
||||
_frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined
|
||||
}
|
||||
_iType = PolyBus::getI(bc.type, _pins, nr);
|
||||
if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; }
|
||||
|
||||
_hasRgb = hasRGB(bc.type);
|
||||
_hasWhite = hasWhite(bc.type);
|
||||
_hasCCT = hasCCT(bc.type);
|
||||
uint16_t lenToCreate = bc.count;
|
||||
if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus
|
||||
_busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, nr);
|
||||
_busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip);
|
||||
_valid = (_busPtr != nullptr) && bc.count > 0;
|
||||
// fix for wled#4759
|
||||
if (_valid) for (unsigned i = 0; i < _skip; i++) {
|
||||
PolyBus::setPixelColor(_busPtr, _iType, i, 0, COL_ORDER_GRB); // set sacrificial pixels to black (CO does not matter here)
|
||||
}
|
||||
DEBUGBUS_PRINTF_P(PSTR("Bus: %successfully inited #%u (len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u] mA=%d/%d)\n"),
|
||||
_valid?"S":"Uns",
|
||||
(int)nr,
|
||||
else {
|
||||
cleanup();
|
||||
}
|
||||
DEBUGBUS_PRINTF_P(PSTR("Bus len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u, driver:%s] mA=%d/%d %s\n"),
|
||||
(int)bc.count,
|
||||
(int)bc.type,
|
||||
(int)_hasRgb, (int)_hasWhite, (int)_hasCCT,
|
||||
(unsigned)_pins[0], is2Pin(bc.type)?(unsigned)_pins[1]:255U,
|
||||
(unsigned)_iType,
|
||||
(int)_milliAmpsPerLed, (int)_milliAmpsMax
|
||||
isI2S() ? "I2S" : "RMT",
|
||||
(int)_milliAmpsPerLed, (int)_milliAmpsMax,
|
||||
_valid ? " " : "FAILED"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,6 +207,7 @@ void BusDigital::estimateCurrent() {
|
||||
|
||||
void BusDigital::applyBriLimit(uint8_t newBri) {
|
||||
// a newBri of 0 means calculate per-bus brightness limit
|
||||
_NPBbri = 255; // reset, intermediate value is set below, final value is calculated in bus::show()
|
||||
if (newBri == 0) {
|
||||
if (_milliAmpsLimit == 0 || _milliAmpsTotal == 0) return; // ABL not used for this bus
|
||||
newBri = 255;
|
||||
@@ -220,15 +225,21 @@ void BusDigital::applyBriLimit(uint8_t newBri) {
|
||||
}
|
||||
|
||||
if (newBri < 255) {
|
||||
uint8_t cctWW = 0, cctCW = 0;
|
||||
_NPBbri = newBri; // store value so it can be updated in show() (must be updated even if ABL is not used)
|
||||
uint16_t wwcw = 0;
|
||||
unsigned hwLen = _len;
|
||||
if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus
|
||||
for (unsigned i = 0; i < hwLen; i++) {
|
||||
uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); // need to revert color order for correct color scaling and CCT calc in case white is swapped
|
||||
uint32_t c = PolyBus::getPixelColor(_busPtr, _iType, i, co);
|
||||
uint32_t c = PolyBus::getPixelColor(_busPtr, _iType, i, co); // Note: if ABL would be calculated as a seperate loop (as it was before) it is slower but could use original color, making it more color-accurate
|
||||
if (hasCCT()) {
|
||||
uint8_t cctWW, cctCW;
|
||||
Bus::calculateCCT(c, cctWW, cctCW); // calculate CCT before fade (more accurate) | Note: if using "accurate" white calculation mode, approximateKelvinFromRGB can be very inaccurate (white is subtracted)
|
||||
wwcw = ((cctCW + 1) * newBri) & 0xFF00; // apply brightness to CCT (leave it in upper byte for 16bit NeoPixelBus value)
|
||||
wwcw |= ((cctWW + 1) * newBri) >> 8;
|
||||
}
|
||||
c = color_fade(c, newBri, true); // apply additional dimming note: using inline version is a bit faster but overhead of getPixelColor() dominates the speed impact by far
|
||||
if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW);
|
||||
PolyBus::setPixelColor(_busPtr, _iType, i, c, co, (cctCW<<8) | cctWW); // repaint all pixels with new brightness
|
||||
PolyBus::setPixelColor(_busPtr, _iType, i, c, co, wwcw); // repaint all pixels with new brightness
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +248,7 @@ void BusDigital::applyBriLimit(uint8_t newBri) {
|
||||
|
||||
void BusDigital::show() {
|
||||
if (!_valid) return;
|
||||
_NPBbri = (_NPBbri * _bri) / 255; // total applied brightness for use in restoreColorLossy (see applyBriLimit())
|
||||
PolyBus::show(_busPtr, _iType, _skip); // faster if buffer consistency is not important (no skipped LEDs)
|
||||
}
|
||||
|
||||
@@ -254,12 +266,21 @@ void BusDigital::setStatusPixel(uint32_t c) {
|
||||
}
|
||||
}
|
||||
|
||||
// note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash
|
||||
void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) {
|
||||
if (!_valid) return;
|
||||
if (hasWhite()) c = autoWhiteCalc(c);
|
||||
if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT
|
||||
uint8_t cctWW = 0, cctCW = 0;
|
||||
uint16_t wwcw = 0;
|
||||
if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW);
|
||||
c = color_fade(c, _bri, true); // apply brightness
|
||||
|
||||
if (hasCCT()) {
|
||||
wwcw = ((cctCW + 1) * _bri) & 0xFF00; // apply brightness to CCT (store CW in upper byte)
|
||||
wwcw |= ((cctWW + 1) * _bri) >> 8;
|
||||
if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w
|
||||
}
|
||||
|
||||
if (BusManager::_useABL) {
|
||||
// if using ABL, sum all color channels to estimate current and limit brightness in show()
|
||||
uint8_t r = R(c), g = G(c), b = B(c);
|
||||
@@ -283,13 +304,7 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) {
|
||||
case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break;
|
||||
}
|
||||
}
|
||||
uint16_t wwcw = 0;
|
||||
if (hasCCT()) {
|
||||
uint8_t cctWW = 0, cctCW = 0;
|
||||
Bus::calculateCCT(c, cctWW, cctCW);
|
||||
wwcw = (cctCW<<8) | cctWW;
|
||||
if (_type == TYPE_WS2812_WWA) c = RGBW32(cctWW, cctCW, 0, W(c));
|
||||
}
|
||||
|
||||
PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw);
|
||||
}
|
||||
|
||||
@@ -299,7 +314,7 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const {
|
||||
if (_reversed) pix = _len - pix -1;
|
||||
pix += _skip;
|
||||
const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder);
|
||||
uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri);
|
||||
uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_NPBbri);
|
||||
if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs
|
||||
uint8_t r = R(c);
|
||||
uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed?
|
||||
@@ -324,7 +339,7 @@ size_t BusDigital::getPins(uint8_t* pinArray) const {
|
||||
}
|
||||
|
||||
size_t BusDigital::getBusSize() const {
|
||||
return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0);
|
||||
return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0); // does not include common I2S DMA buffer
|
||||
}
|
||||
|
||||
void BusDigital::setColorOrder(uint8_t colorOrder) {
|
||||
@@ -359,6 +374,10 @@ std::vector<LEDType> BusDigital::getLEDTypes() {
|
||||
};
|
||||
}
|
||||
|
||||
bool BusDigital::isI2S() {
|
||||
return (_iType & 0x01) == 0; // I2S types have even iType values
|
||||
}
|
||||
|
||||
void BusDigital::begin() {
|
||||
if (!_valid) return;
|
||||
PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz);
|
||||
@@ -451,11 +470,13 @@ BusPwm::BusPwm(const BusConfig &bc)
|
||||
|
||||
void BusPwm::setPixelColor(unsigned pix, uint32_t c) {
|
||||
if (pix != 0 || !_valid) return; //only react to first pixel
|
||||
if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c);
|
||||
if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) {
|
||||
c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT
|
||||
}
|
||||
uint8_t cctWW, cctCW;
|
||||
if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c, cctWW, cctCW);
|
||||
uint8_t r = R(c), g = G(c), b = B(c), w = W(c);
|
||||
// note: no color scaling, brightness is applied in show()
|
||||
|
||||
switch (_type) {
|
||||
case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation
|
||||
@@ -466,14 +487,18 @@ void BusPwm::setPixelColor(unsigned pix, uint32_t c) {
|
||||
_data[0] = w;
|
||||
_data[1] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct;
|
||||
} else {
|
||||
Bus::calculateCCT(c, _data[0], _data[1]);
|
||||
_data[0] = cctWW;
|
||||
_data[1] = cctCW;
|
||||
}
|
||||
break;
|
||||
case TYPE_ANALOG_5CH: //RGB + warm white + cold white
|
||||
if (cctICused)
|
||||
_data[4] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct;
|
||||
else
|
||||
Bus::calculateCCT(c, w, _data[4]);
|
||||
else {
|
||||
w = cctWW;
|
||||
_data[4] = cctCW;
|
||||
}
|
||||
// fall through to set RGBW channels
|
||||
case TYPE_ANALOG_4CH: //RGBW
|
||||
_data[3] = w;
|
||||
case TYPE_ANALOG_3CH: //standard dumb RGB
|
||||
@@ -540,7 +565,7 @@ void BusPwm::show() {
|
||||
unsigned duty = (_data[i] * pwmBri) / 255;
|
||||
unsigned deadTime = 0;
|
||||
|
||||
if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend == 0) {
|
||||
if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend <= 0) {
|
||||
// add dead time between signals (when using dithering, two full 8bit pulses are required)
|
||||
deadTime = (1+dithering) << bitShift;
|
||||
// we only need to take care of shortening the signal at (almost) full brightness otherwise pulses may overlap
|
||||
@@ -629,9 +654,7 @@ BusOnOff::BusOnOff(const BusConfig &bc)
|
||||
|
||||
void BusOnOff::setPixelColor(unsigned pix, uint32_t c) {
|
||||
if (pix != 0 || !_valid) return; //only react to first pixel
|
||||
c = autoWhiteCalc(c);
|
||||
uint8_t r = R(c), g = G(c), b = B(c), w = W(c);
|
||||
_data = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0;
|
||||
_data = (c > 0) && bool(_bri) ? 0xFF : 0; // if any color channel is on and brightness is not zero, set to on
|
||||
}
|
||||
|
||||
uint32_t BusOnOff::getPixelColor(unsigned pix) const {
|
||||
@@ -691,7 +714,8 @@ BusNetwork::BusNetwork(const BusConfig &bc)
|
||||
|
||||
void BusNetwork::setPixelColor(unsigned pix, uint32_t c) {
|
||||
if (!_valid || pix >= _len) return;
|
||||
if (_hasWhite) c = autoWhiteCalc(c);
|
||||
uint8_t ww, cw; // dummy, unused
|
||||
if (_hasWhite) c = autoWhiteCalc(c, ww, cw);
|
||||
if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT
|
||||
unsigned offset = pix * _UDPchannels;
|
||||
_data[offset] = R(c);
|
||||
@@ -720,13 +744,23 @@ size_t BusNetwork::getPins(uint8_t* pinArray) const {
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
void BusNetwork::resolveHostname() {
|
||||
static unsigned long nextResolve = 0;
|
||||
if (Network.isConnected() && millis() > nextResolve && _hostname.length() > 0) {
|
||||
nextResolve = millis() + 600000; // resolve only every 10 minutes
|
||||
static std::shared_ptr<AsyncDNS> DNSlookup; // TODO: make this dynamic? requires to handle the callback properly
|
||||
if (Network.isConnected()) {
|
||||
IPAddress clnt;
|
||||
if (strlen(cmDNS) > 0) clnt = MDNS.queryHost(_hostname);
|
||||
else WiFi.hostByName(_hostname.c_str(), clnt);
|
||||
if (clnt != IPAddress()) _client = clnt;
|
||||
if (strlen(cmDNS) > 0) {
|
||||
clnt = MDNS.queryHost(_hostname);
|
||||
if (clnt != IPAddress()) _client = clnt; // update client IP if not null
|
||||
}
|
||||
else {
|
||||
int timeout = 5000; // 5 seconds timeout
|
||||
DNSlookup = AsyncDNS::query(_hostname.c_str(), DNSlookup); // start async DNS query
|
||||
while (DNSlookup->status() == AsyncDNS::result::Busy && timeout-- > 0) {
|
||||
delay(1);
|
||||
}
|
||||
if (DNSlookup->status() == AsyncDNS::result::Success) {
|
||||
_client = DNSlookup->getIP(); // update client IP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -754,50 +788,403 @@ void BusNetwork::cleanup() {
|
||||
_valid = false;
|
||||
}
|
||||
|
||||
// ***************************************************************************
|
||||
|
||||
//utility to get the approx. memory usage of a given BusConfig
|
||||
size_t BusConfig::memUsage(unsigned nr) const {
|
||||
if (Bus::isVirtual(type)) {
|
||||
return sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type));
|
||||
} else if (Bus::isDigital(type)) {
|
||||
return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, PolyBus::getI(type, pins, nr)) /*+ doubleBuffer * (count + skipAmount) * Bus::getNumberOfChannels(type)*/;
|
||||
} else if (Bus::isOnOff(type)) {
|
||||
return sizeof(BusOnOff);
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
#warning "HUB75 driver enabled (experimental)"
|
||||
#ifdef ESP8266
|
||||
#error ESP8266 does not support HUB75
|
||||
#endif
|
||||
|
||||
BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {
|
||||
size_t lastHeap = ESP.getFreeHeap();
|
||||
_valid = false;
|
||||
_hasRgb = true;
|
||||
_hasWhite = false;
|
||||
virtualDisp = nullptr; // todo: this should be solved properly, can cause memory leak (if omitted here, nothing seems to work)
|
||||
// aliases for easier reading
|
||||
uint8_t panelWidth = bc.pins[0];
|
||||
uint8_t panelHeight = bc.pins[1];
|
||||
uint8_t chainLength = bc.pins[2];
|
||||
_rows = bc.pins[3];
|
||||
_cols = bc.pins[4];
|
||||
|
||||
mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer
|
||||
|
||||
// mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver
|
||||
// mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel
|
||||
|
||||
// mxconfig.latch_blanking = 3;
|
||||
// mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz
|
||||
// mxconfig.min_refresh_rate = 90;
|
||||
// mxconfig.min_refresh_rate = 120;
|
||||
|
||||
mxconfig.clkphase = bc.reversed;
|
||||
// allow chain length up to 4, limit to prevent bad data from preventing boot due to low memory
|
||||
mxconfig.chain_length = max((uint8_t) 1, min(chainLength, (uint8_t) 4));
|
||||
|
||||
if (mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {
|
||||
DEBUGBUS_PRINTLN(F("WARNING, only single panel can be used of 64 pixel boards due to memory"));
|
||||
mxconfig.chain_length = 1;
|
||||
}
|
||||
|
||||
if (bc.type == TYPE_HUB75MATRIX_HS) {
|
||||
mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64
|
||||
mxconfig.mx_height = min((uint8_t) 64, panelHeight);
|
||||
} else if (bc.type == TYPE_HUB75MATRIX_QS) {
|
||||
_isVirtual = true;
|
||||
mxconfig.mx_width = min((uint8_t) 64, panelWidth) * 2;
|
||||
mxconfig.mx_height = min((uint8_t) 64, panelHeight) / 2;
|
||||
} else {
|
||||
return sizeof(BusPwm);
|
||||
DEBUGBUS_PRINTLN("Unknown type");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)// classic esp32, or esp32-s2: reduce bitdepth for large panels
|
||||
if (mxconfig.mx_height >= 64) {
|
||||
if (mxconfig.chain_length * mxconfig.mx_width > 192) mxconfig.setPixelColorDepthBits(3);
|
||||
else if (mxconfig.chain_length * mxconfig.mx_width > 64) mxconfig.setPixelColorDepthBits(4);
|
||||
else mxconfig.setPixelColorDepthBits(8);
|
||||
} else mxconfig.setPixelColorDepthBits(8);
|
||||
#endif
|
||||
|
||||
|
||||
size_t BusManager::memUsage() {
|
||||
// when ESP32, S2 & S3 use parallel I2S only the largest bus determines the total memory requirements for back buffers
|
||||
// front buffers are always allocated per bus
|
||||
unsigned size = 0;
|
||||
unsigned maxI2S = 0;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
|
||||
unsigned digitalCount = 0;
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
#define MAX_RMT 4
|
||||
#else
|
||||
#define MAX_RMT 8
|
||||
#endif
|
||||
|
||||
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
|
||||
|
||||
#if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3
|
||||
|
||||
// https://www.adafruit.com/product/5778
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config");
|
||||
mxconfig.gpio = { 42, 41, 40, 38, 39, 37, 45, 36, 48, 35, 21, 47, 14, 2 };
|
||||
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM
|
||||
|
||||
#if defined(MOONHUB_S3_PINOUT)
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - T7 S3 with PSRAM, MOONHUB pinout");
|
||||
|
||||
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
|
||||
mxconfig.gpio = { 1, 5, 6, 7, 13, 9, 16, 48, 47, 21, 38, 8, 4, 18 };
|
||||
|
||||
#else
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 with PSRAM");
|
||||
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
|
||||
mxconfig.gpio = {1, 2, 42, 41, 40, 39, 45, 48, 47, 21, 38, 8, 3, 18};
|
||||
#endif
|
||||
#elif defined(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix
|
||||
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - ESP32_FORUM_PINOUT");
|
||||
/*
|
||||
ESP32 with SmartMatrix's default pinout - ESP32_FORUM_PINOUT
|
||||
https://github.com/pixelmatix/SmartMatrix/blob/teensylc/src/MatrixHardware_ESP32_V0.h
|
||||
Can use a board like https://github.com/rorosaurus/esp32-hub75-driver
|
||||
*/
|
||||
|
||||
mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 };
|
||||
|
||||
#else
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Default pins");
|
||||
/*
|
||||
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA?tab=readme-ov-file
|
||||
|
||||
Boards
|
||||
|
||||
https://esp32trinity.com/
|
||||
https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/
|
||||
|
||||
*/
|
||||
mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 19, 5, 17, 18, 4, 15, 16 };
|
||||
|
||||
#endif
|
||||
|
||||
int8_t pins[PIN_COUNT];
|
||||
memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));
|
||||
if (!PinManager::allocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75, true)) {
|
||||
DEBUGBUS_PRINTLN("Failed to allocate pins for HUB75");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bc.colorOrder == COL_ORDER_RGB) {
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = Default color order (RGB)");
|
||||
} else if (bc.colorOrder == COL_ORDER_BGR) {
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = color order BGR");
|
||||
int8_t tmpPin;
|
||||
tmpPin = mxconfig.gpio.r1;
|
||||
mxconfig.gpio.r1 = mxconfig.gpio.b1;
|
||||
mxconfig.gpio.b1 = tmpPin;
|
||||
tmpPin = mxconfig.gpio.r2;
|
||||
mxconfig.gpio.r2 = mxconfig.gpio.b2;
|
||||
mxconfig.gpio.b2 = tmpPin;
|
||||
}
|
||||
else {
|
||||
DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA = unsupported color order %u\n", bc.colorOrder);
|
||||
}
|
||||
|
||||
DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA config - %ux%u length: %u\n", mxconfig.mx_width, mxconfig.mx_height, mxconfig.chain_length);
|
||||
DEBUGBUS_PRINTF("R1_PIN=%u, G1_PIN=%u, B1_PIN=%u, R2_PIN=%u, G2_PIN=%u, B2_PIN=%u, A_PIN=%u, B_PIN=%u, C_PIN=%u, D_PIN=%u, E_PIN=%u, LAT_PIN=%u, OE_PIN=%u, CLK_PIN=%u\n",
|
||||
mxconfig.gpio.r1, mxconfig.gpio.g1, mxconfig.gpio.b1, mxconfig.gpio.r2, mxconfig.gpio.g2, mxconfig.gpio.b2,
|
||||
mxconfig.gpio.a, mxconfig.gpio.b, mxconfig.gpio.c, mxconfig.gpio.d, mxconfig.gpio.e, mxconfig.gpio.lat, mxconfig.gpio.oe, mxconfig.gpio.clk);
|
||||
|
||||
// OK, now we can create our matrix object
|
||||
display = new MatrixPanel_I2S_DMA(mxconfig);
|
||||
if (display == nullptr) {
|
||||
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********");
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
return;
|
||||
}
|
||||
|
||||
this->_len = (display->width() * display->height()); // note: this returns correct number of pixels but incorrect dimensions if using virtual display (updated below)
|
||||
|
||||
DEBUGBUS_PRINTF("Length: %u\n", _len);
|
||||
if (this->_len >= MAX_LEDS) {
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA Too many LEDS - playing safe");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA created");
|
||||
|
||||
// as noted in HUB75_I2S_DMA library, some panels can show ghosting if set higher than 239, so let users override at compile time
|
||||
#ifndef WLED_HUB75_MAX_BRIGHTNESS
|
||||
#define WLED_HUB75_MAX_BRIGHTNESS 255
|
||||
#endif
|
||||
for (const auto &bus : busses) {
|
||||
unsigned busSize = bus->getBusSize();
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)
|
||||
if (bus->isDigital() && !bus->is2Pin()) digitalCount++;
|
||||
if (PolyBus::isParallelI2S1Output() && digitalCount > MAX_RMT) {
|
||||
unsigned i2sCommonSize = 3 * bus->getLength() * bus->getNumberOfChannels() * (bus->is16bit()+1);
|
||||
if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize;
|
||||
busSize -= i2sCommonSize;
|
||||
}
|
||||
#endif
|
||||
size += busSize;
|
||||
// let's adjust default brightness (128), brightness scaling is handled by WLED
|
||||
//display->setBrightness8(WLED_HUB75_MAX_BRIGHTNESS); // range is 0-255, 0 - 0%, 255 - 100%
|
||||
|
||||
delay(24); // experimental
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
// Allocate memory and start DMA display
|
||||
if (!display->begin() ) {
|
||||
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********");
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
return;
|
||||
}
|
||||
else {
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA begin ok");
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
delay(18); // experiment - give the driver a moment (~ one full frame @ 60hz) to settle
|
||||
_valid = true;
|
||||
display->clearScreen(); // initially clear the screen buffer
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA clear ok");
|
||||
|
||||
if (_ledBuffer) d_free(_ledBuffer); // should not happen
|
||||
if (_ledsDirty) d_free(_ledsDirty); // should not happen
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory");
|
||||
_ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len)); // create LEDs dirty bits
|
||||
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok");
|
||||
|
||||
if (_ledsDirty == nullptr) {
|
||||
display->stopDMAoutput();
|
||||
delete display; display = nullptr;
|
||||
_valid = false;
|
||||
DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA not started - not enough memory for dirty bits!"));
|
||||
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
|
||||
return; // fail is we cannot get memory for the buffer
|
||||
}
|
||||
setBitArray(_ledsDirty, _len, false); // reset dirty bits
|
||||
|
||||
if (mxconfig.double_buff == false) {
|
||||
// create LEDs buffer (initialized to BLACK), prefer DRAM if enough heap is available (faster in case global _pixels buffer is in PSRAM as not both will fit the cache)
|
||||
_ledBuffer = static_cast<CRGB*>(allocate_buffer(_len * sizeof(CRGB), BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR));
|
||||
}
|
||||
}
|
||||
|
||||
PANEL_CHAIN_TYPE chainType = CHAIN_NONE; // default for quarter-scan panels that do not use chaining
|
||||
// chained panels with cols and rows define need the virtual display driver, so do quarter-scan panels
|
||||
if (chainLength > 1 && (_rows > 1 || _cols > 1) || bc.type == TYPE_HUB75MATRIX_QS) {
|
||||
_isVirtual = true;
|
||||
chainType = CHAIN_BOTTOM_LEFT_UP; // TODO: is there any need to support other chaining types?
|
||||
DEBUGBUS_PRINTF_P(PSTR("Using virtual matrix: %ux%u panels of %ux%u pixels\n"), _cols, _rows, mxconfig.mx_width, mxconfig.mx_height);
|
||||
}
|
||||
else {
|
||||
_isVirtual = false;
|
||||
}
|
||||
|
||||
if (_isVirtual) {
|
||||
virtualDisp = new VirtualMatrixPanel((*display), _rows, _cols, mxconfig.mx_width, mxconfig.mx_height, chainType);
|
||||
virtualDisp->setRotation(0);
|
||||
if (bc.type == TYPE_HUB75MATRIX_QS) {
|
||||
switch(panelHeight) {
|
||||
case 16:
|
||||
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
|
||||
break;
|
||||
case 32:
|
||||
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
|
||||
break;
|
||||
case 64:
|
||||
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
|
||||
break;
|
||||
default:
|
||||
DEBUGBUS_PRINTLN("Unsupported height");
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_valid) {
|
||||
_panelWidth = virtualDisp ? virtualDisp->width() : display->width(); // cache width - it will never change
|
||||
}
|
||||
|
||||
DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA "));
|
||||
DEBUGBUS_PRINTF_P(PSTR("%sstarted, width=%u, %u pixels.\n"), _valid? "":"not ", _panelWidth, _len);
|
||||
|
||||
if (_ledBuffer != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled."));
|
||||
if (_ledsDirty != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled."));
|
||||
if ((_ledBuffer != nullptr) || (_ledsDirty != nullptr)) {
|
||||
DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA LEDS buffer uses "));
|
||||
DEBUGBUS_PRINT((_ledBuffer? _len*sizeof(CRGB) :0) + (_ledsDirty? getBitArrayBytes(_len) :0));
|
||||
DEBUGBUS_PRINTLN(F(" bytes."));
|
||||
}
|
||||
return size + maxI2S;
|
||||
}
|
||||
|
||||
int BusManager::add(const BusConfig &bc) {
|
||||
void IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {
|
||||
if (!_valid) return; // note: no need to check pix >= _len as that is checked in containsPixel()
|
||||
// if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT
|
||||
|
||||
if (_ledBuffer) {
|
||||
CRGB fastled_col = CRGB(c);
|
||||
if (_ledBuffer[pix] != fastled_col) {
|
||||
_ledBuffer[pix] = fastled_col;
|
||||
setBitInArray(_ledsDirty, pix, true); // flag pixel as "dirty"
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ((c == IS_BLACK) && (getBitFromArray(_ledsDirty, pix) == false)) return; // ignore black if pixel is already black
|
||||
setBitInArray(_ledsDirty, pix, c != IS_BLACK); // dirty = true means "color is not BLACK"
|
||||
|
||||
uint8_t r = R(c);
|
||||
uint8_t g = G(c);
|
||||
uint8_t b = B(c);
|
||||
|
||||
if (virtualDisp != nullptr) {
|
||||
int x = pix % _panelWidth; // TODO: check if using & and shift would be faster here, it limits to power-of-2 widths though
|
||||
int y = pix / _panelWidth;
|
||||
virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
|
||||
} else {
|
||||
int x = pix % _panelWidth;
|
||||
int y = pix / _panelWidth;
|
||||
display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const {
|
||||
if (!_valid) return IS_BLACK; // note: no need to check pix >= _len as that is checked in containsPixel()
|
||||
if (_ledBuffer)
|
||||
return uint32_t(_ledBuffer[pix]);
|
||||
else
|
||||
return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK; // just a hack - we only know if the pixel is black or not
|
||||
}
|
||||
|
||||
void BusHub75Matrix::setBrightness(uint8_t b) {
|
||||
_bri = b;
|
||||
display->setBrightness(_bri);
|
||||
}
|
||||
|
||||
void BusHub75Matrix::show(void) {
|
||||
if (!_valid) return;
|
||||
if (_ledBuffer) {
|
||||
// write out buffered LEDs
|
||||
unsigned height = _isVirtual ? virtualDisp->height() : display->height();
|
||||
unsigned width = _panelWidth;
|
||||
|
||||
//while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker.
|
||||
size_t pix = 0; // running pixel index
|
||||
for (int y=0; y<height; y++) for (int x=0; x<width; x++) {
|
||||
if (getBitFromArray(_ledsDirty, pix) == true) { // only repaint the "dirty" pixels
|
||||
CRGB c = _ledBuffer[pix];
|
||||
//c.nscale8_video(_bri); // apply brightness
|
||||
if (_isVirtual) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
|
||||
else display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
|
||||
}
|
||||
pix++;
|
||||
}
|
||||
setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits
|
||||
}
|
||||
}
|
||||
|
||||
void BusHub75Matrix::cleanup() {
|
||||
if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black)
|
||||
_valid = false;
|
||||
delay(30); // give some time to finish DMA
|
||||
_panelWidth = 0;
|
||||
deallocatePins();
|
||||
DEBUGBUS_PRINTLN(F("HUB75 output ended."));
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32S3 // on ESP32-S3 deleting display/virtualDisp does not work and leads to crash (DMA issues), request reboot from user instead
|
||||
if (virtualDisp != nullptr) delete virtualDisp; // note: in MM there is a warning to not do this but if using "NO_GFX" this is safe
|
||||
if (display != nullptr) delete display;
|
||||
display = nullptr;
|
||||
virtualDisp = nullptr; // note: when not using "NO_GFX" this causes a memory leak
|
||||
#endif
|
||||
if (_ledBuffer != nullptr) d_free(_ledBuffer); _ledBuffer = nullptr;
|
||||
if (_ledsDirty != nullptr) d_free(_ledsDirty); _ledsDirty = nullptr;
|
||||
}
|
||||
|
||||
void BusHub75Matrix::deallocatePins() {
|
||||
uint8_t pins[PIN_COUNT];
|
||||
memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));
|
||||
PinManager::deallocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75);
|
||||
}
|
||||
|
||||
std::vector<LEDType> BusHub75Matrix::getLEDTypes() {
|
||||
return {
|
||||
{TYPE_HUB75MATRIX_HS, "H", PSTR("HUB75 (Half Scan)")},
|
||||
{TYPE_HUB75MATRIX_QS, "H", PSTR("HUB75 (Quarter Scan)")},
|
||||
};
|
||||
}
|
||||
|
||||
size_t BusHub75Matrix::getPins(uint8_t* pinArray) const {
|
||||
if (pinArray) {
|
||||
pinArray[0] = mxconfig.mx_width;
|
||||
pinArray[1] = mxconfig.mx_height;
|
||||
pinArray[2] = mxconfig.chain_length;
|
||||
pinArray[3] = _rows;
|
||||
pinArray[4] = _cols;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
#endif
|
||||
// ***************************************************************************
|
||||
|
||||
BusPlaceholder::BusPlaceholder(const BusConfig &bc)
|
||||
: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, bc.refreshReq)
|
||||
, _colorOrder(bc.colorOrder)
|
||||
, _skipAmount(bc.skipAmount)
|
||||
, _driverType(bc.driverType)
|
||||
, _frequency(bc.frequency)
|
||||
, _milliAmpsPerLed(bc.milliAmpsPerLed)
|
||||
, _milliAmpsMax(bc.milliAmpsMax)
|
||||
, _text(bc.text)
|
||||
{
|
||||
memcpy(_pins, bc.pins, sizeof(_pins));
|
||||
}
|
||||
|
||||
size_t BusPlaceholder::getPins(uint8_t* pinArray) const {
|
||||
size_t nPins = Bus::getNumberOfPins(_type);
|
||||
if (pinArray) {
|
||||
for (size_t i = 0; i < nPins; i++) pinArray[i] = _pins[i];
|
||||
}
|
||||
return nPins;
|
||||
}
|
||||
|
||||
//utility to get the approx. memory usage of a given BusConfig inclduding segmentbuffer and global buffer (4 bytes per pixel)
|
||||
size_t BusConfig::memUsage() const {
|
||||
size_t mem = (count + skipAmount) * 8; // 8 bytes per pixel for segment + global buffer
|
||||
if (Bus::isVirtual(type)) {
|
||||
mem += sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); // note: getNumberOfChannels() includes CCT channel if applicable but virtual buses do not use CCT channel buffer
|
||||
} else if (Bus::isDigital(type)) {
|
||||
// if any of digital buses uses I2S, there is additional common I2S DMA buffer not accounted for here
|
||||
mem += sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, iType);
|
||||
} else if (Bus::isOnOff(type)) {
|
||||
mem += sizeof(BusOnOff);
|
||||
} else {
|
||||
mem += sizeof(BusPwm);
|
||||
}
|
||||
return mem;
|
||||
}
|
||||
|
||||
int BusManager::add(const BusConfig &bc, bool placeholder) {
|
||||
DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses());
|
||||
unsigned digital = 0;
|
||||
unsigned analog = 0;
|
||||
@@ -807,11 +1194,19 @@ int BusManager::add(const BusConfig &bc) {
|
||||
if (bus->isDigital() && !bus->is2Pin()) digital++;
|
||||
if (bus->is2Pin()) twoPin++;
|
||||
}
|
||||
if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) return -1;
|
||||
if (Bus::isVirtual(bc.type)) {
|
||||
digital += (Bus::isDigital(bc.type) && !Bus::is2Pin(bc.type));
|
||||
analog += (Bus::isPWM(bc.type) ? Bus::numPWMPins(bc.type) : 0);
|
||||
if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; // TODO: add errorFlag here
|
||||
if (placeholder) {
|
||||
busses.push_back(make_unique<BusPlaceholder>(bc));
|
||||
} else if (Bus::isVirtual(bc.type)) {
|
||||
busses.push_back(make_unique<BusNetwork>(bc));
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
} else if (Bus::isHub75(bc.type)) {
|
||||
busses.push_back(make_unique<BusHub75Matrix>(bc));
|
||||
#endif
|
||||
} else if (Bus::isDigital(bc.type)) {
|
||||
busses.push_back(make_unique<BusDigital>(bc, Bus::is2Pin(bc.type) ? twoPin : digital));
|
||||
busses.push_back(make_unique<BusDigital>(bc));
|
||||
} else if (Bus::isOnOff(bc.type)) {
|
||||
busses.push_back(make_unique<BusOnOff>(bc));
|
||||
} else {
|
||||
@@ -841,53 +1236,43 @@ String BusManager::getLEDTypesJSONString() {
|
||||
json += LEDTypesToJson(BusPwm::getLEDTypes());
|
||||
json += LEDTypesToJson(BusNetwork::getLEDTypes());
|
||||
//json += LEDTypesToJson(BusVirtual::getLEDTypes());
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
json += LEDTypesToJson(BusHub75Matrix::getLEDTypes());
|
||||
#endif
|
||||
|
||||
json.setCharAt(json.length()-1, ']'); // replace last comma with bracket
|
||||
return json;
|
||||
}
|
||||
|
||||
void BusManager::useParallelOutput() {
|
||||
DEBUGBUS_PRINTLN(F("Bus: Enabling parallel I2S."));
|
||||
PolyBus::setParallelI2S1Output();
|
||||
uint8_t BusManager::getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) {
|
||||
return PolyBus::getI(busType, pins, driverPreference);
|
||||
}
|
||||
|
||||
bool BusManager::hasParallelOutput() {
|
||||
return PolyBus::isParallelI2S1Output();
|
||||
}
|
||||
|
||||
//do not call this method from system context (network callback)
|
||||
void BusManager::removeAll() {
|
||||
DEBUGBUS_PRINTLN(F("Removing all."));
|
||||
//prevents crashes due to deleting busses while in use.
|
||||
while (!canAllShow()) yield();
|
||||
busses.clear();
|
||||
PolyBus::setParallelI2S1Output(false);
|
||||
#ifndef ESP8266
|
||||
// Reset channel tracking for fresh allocation
|
||||
PolyBus::resetChannelTracking();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ESP32_DATA_IDLE_HIGH
|
||||
// #2478
|
||||
// If enabled, RMT idle level is set to HIGH when off
|
||||
// to prevent leakage current when using an N-channel MOSFET to toggle LED power
|
||||
// since I2S outputs are known only during config of buses, lets just assume RMT is used for digital buses
|
||||
// unused RMT channels should have no effect
|
||||
void BusManager::esp32RMTInvertIdle() {
|
||||
bool idle_out;
|
||||
unsigned rmt = 0;
|
||||
unsigned u = 0;
|
||||
for (auto &bus : busses) {
|
||||
if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue;
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, only has 1 I2S but NPB does not support it ATM
|
||||
if (u > 1) return;
|
||||
rmt = u;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, only has 1 I2S bus, supported in NPB
|
||||
if (u > 3) return;
|
||||
rmt = u;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, has 2 I2S but NPB does not support them ATM
|
||||
if (u > 3) return;
|
||||
rmt = u;
|
||||
#else
|
||||
unsigned numI2S = !PolyBus::isParallelI2S1Output(); // if using parallel I2S, RMT is used 1st
|
||||
if (numI2S > u) continue;
|
||||
if (u > 7 + numI2S) return;
|
||||
rmt = u - numI2S;
|
||||
#endif
|
||||
if (static_cast<BusDigital*>(bus.get())->isI2S()) continue;
|
||||
if (u >= WLED_MAX_RMT_CHANNELS) return;
|
||||
//assumes that bus number to rmt channel mapping stays 1:1
|
||||
rmt_channel_t ch = static_cast<rmt_channel_t>(rmt);
|
||||
rmt_idle_level_t lvl;
|
||||
@@ -907,7 +1292,7 @@ void BusManager::on() {
|
||||
if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) {
|
||||
for (auto &bus : busses) {
|
||||
uint8_t pins[2] = {255,255};
|
||||
if (bus->isDigital() && bus->getPins(pins)) {
|
||||
if (bus->isDigital() && bus->getPins(pins) && bus->isOk()) {
|
||||
if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) {
|
||||
BusDigital &b = static_cast<BusDigital&>(*bus);
|
||||
b.begin();
|
||||
@@ -917,12 +1302,17 @@ void BusManager::on() {
|
||||
}
|
||||
}
|
||||
#else
|
||||
static uint32_t nextResolve = 0; // initial resolve is done on bus creation
|
||||
bool resolveNow = (millis() - nextResolve >= 600000); // wait at least 10 minutes between hostname resolutions (blocking call)
|
||||
for (auto &bus : busses) if (bus->isVirtual()) {
|
||||
// virtual/network bus should check for IP change if hostname is specified
|
||||
// otherwise there are no endpoints to force DNS resolution
|
||||
BusNetwork &b = static_cast<BusNetwork&>(*bus);
|
||||
b.resolveHostname();
|
||||
if (resolveNow)
|
||||
b.resolveHostname();
|
||||
}
|
||||
if (resolveNow)
|
||||
nextResolve = millis();
|
||||
#endif
|
||||
#ifdef ESP32_DATA_IDLE_HIGH
|
||||
esp32RMTInvertIdle();
|
||||
@@ -1002,7 +1392,7 @@ void BusManager::initializeABL() {
|
||||
_useABL = true; // at least one bus has ABL set
|
||||
uint32_t ESPshare = MA_FOR_ESP / numABLbuses; // share of ESP current per ABL bus
|
||||
for (auto &bus : busses) {
|
||||
if (bus->isDigital()) {
|
||||
if (bus->isDigital() && bus->isOk()) {
|
||||
BusDigital &busd = static_cast<BusDigital&>(*bus);
|
||||
uint32_t busLength = busd.getLength();
|
||||
uint32_t busDemand = busLength * busd.getLEDCurrent();
|
||||
@@ -1062,12 +1452,18 @@ void BusManager::applyABL() {
|
||||
|
||||
ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; }
|
||||
|
||||
|
||||
#ifndef ESP8266
|
||||
// PolyBus channel tracking for dynamic allocation
|
||||
bool PolyBus::_useParallelI2S = false;
|
||||
|
||||
uint8_t PolyBus::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check
|
||||
uint8_t PolyBus::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create()
|
||||
uint8_t PolyBus::_i2sChannelsAssigned = 0; // number of I2S channels assigned durig getI() check
|
||||
uint8_t PolyBus::_parallelBusItype = 0; // type I_NONE
|
||||
uint8_t PolyBus::_2PchannelsAssigned = 0;
|
||||
#endif
|
||||
// Bus static member definition
|
||||
int16_t Bus::_cct = -1;
|
||||
uint8_t Bus::_cctBlend = 0; // 0 - 127
|
||||
int16_t Bus::_cct = -1; // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin()
|
||||
int8_t Bus::_cctBlend = 0; // -128 to +127
|
||||
uint8_t Bus::_gAWM = 255;
|
||||
|
||||
uint16_t BusDigital::_milliAmpsTotal = 0;
|
||||
|
||||
+113
-23
@@ -2,6 +2,13 @@
|
||||
#ifndef BusManager_h
|
||||
#define BusManager_h
|
||||
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
|
||||
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
|
||||
#include <ESP32-VirtualMatrixPanel-I2S-DMA.h>
|
||||
#include "src/dependencies/fastled_slim/fastled_slim.h"
|
||||
|
||||
#endif
|
||||
/*
|
||||
* Class for addressing various light types
|
||||
*/
|
||||
@@ -10,6 +17,9 @@
|
||||
#include "pin_manager.h"
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "asyncDNS.h"
|
||||
#endif
|
||||
|
||||
#if __cplusplus >= 201402L
|
||||
using std::make_unique;
|
||||
@@ -105,6 +115,7 @@ class Bus {
|
||||
Bus(uint8_t type, uint16_t start, uint8_t aw, uint16_t len = 1, bool reversed = false, bool refresh = false)
|
||||
: _type(type)
|
||||
, _bri(255)
|
||||
, _NPBbri(255)
|
||||
, _start(start)
|
||||
, _len(std::max(len,(uint16_t)1))
|
||||
, _reversed(reversed)
|
||||
@@ -125,14 +136,15 @@ class Bus {
|
||||
virtual void setColorOrder(uint8_t co) {}
|
||||
virtual uint32_t getPixelColor(unsigned pix) const { return 0; }
|
||||
virtual size_t getPins(uint8_t* pinArray = nullptr) const { return 0; }
|
||||
virtual uint16_t getLength() const { return isOk() ? _len : 0; }
|
||||
virtual uint16_t getLength() const { return _len; }
|
||||
virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; }
|
||||
virtual unsigned skippedLeds() const { return 0; }
|
||||
virtual uint16_t getFrequency() const { return 0U; }
|
||||
virtual uint16_t getLEDCurrent() const { return 0; }
|
||||
virtual uint16_t getUsedCurrent() const { return 0; }
|
||||
virtual uint16_t getMaxCurrent() const { return 0; }
|
||||
virtual size_t getBusSize() const { return sizeof(Bus); }
|
||||
virtual uint8_t getDriverType() const { return 0; } // Default to RMT (0) for non-digital buses
|
||||
virtual size_t getBusSize() const { return sizeof(Bus); } // currently unused
|
||||
virtual const String getCustomText() const { return String(); }
|
||||
|
||||
inline bool hasRGB() const { return _hasRgb; }
|
||||
@@ -144,6 +156,7 @@ class Bus {
|
||||
inline bool isPWM() const { return isPWM(_type); }
|
||||
inline bool isVirtual() const { return isVirtual(_type); }
|
||||
inline bool is16bit() const { return is16bit(_type); }
|
||||
virtual bool isPlaceholder() const { return false; }
|
||||
inline bool mustRefresh() const { return mustRefresh(_type); }
|
||||
inline void setReversed(bool reversed) { _reversed = reversed; }
|
||||
inline void setStart(uint16_t start) { _start = start; }
|
||||
@@ -158,7 +171,7 @@ class Bus {
|
||||
inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; }
|
||||
|
||||
static inline std::vector<LEDType> getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes
|
||||
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK
|
||||
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK
|
||||
static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); }
|
||||
static constexpr bool hasRGB(uint8_t type) {
|
||||
return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF);
|
||||
@@ -182,6 +195,7 @@ class Bus {
|
||||
static constexpr bool isOnOff(uint8_t type) { return (type == TYPE_ONOFF); }
|
||||
static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); }
|
||||
static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); }
|
||||
static constexpr bool isHub75(uint8_t type) { return (type >= TYPE_HUB75MATRIX_MIN && type <= TYPE_HUB75MATRIX_MAX); }
|
||||
static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; }
|
||||
static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; }
|
||||
static constexpr int numPWMPins(uint8_t type) { return (type - 40); }
|
||||
@@ -190,9 +204,9 @@ class Bus {
|
||||
static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; }
|
||||
static inline uint8_t getGlobalAWMode() { return _gAWM; }
|
||||
static inline void setCCT(int16_t cct) { _cct = cct; }
|
||||
static inline uint8_t getCCTBlend() { return (_cctBlend * 100 + 64) / 127; } // returns 0-100, 100% = 127. +64 for rounding
|
||||
static inline void setCCTBlend(uint8_t b) { // input is 0-100
|
||||
_cctBlend = (std::min((int)b,100) * 127 + 50) / 100; // +50 for rounding, b=100% -> 127
|
||||
static inline int8_t getCCTBlend() { return (_cctBlend * 100 + (_cctBlend >= 0 ? 64 : -64)) / 127; } // returns -100 to +100, +/-100% = +/-127. +/-64 for rounding
|
||||
static inline void setCCTBlend(int8_t b) { // input is -100 to +100
|
||||
_cctBlend = (std::max(-100, std::min(100, (int)b)) * 127 + (b >= 0 ? 50 : -50)) / 100; // +/-50 for rounding, b=+/-100% -> +/-127
|
||||
//compile-time limiter for hardware that can't power both white channels at max
|
||||
#ifdef WLED_MAX_CCT_BLEND
|
||||
if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND;
|
||||
@@ -202,7 +216,9 @@ class Bus {
|
||||
|
||||
protected:
|
||||
uint8_t _type;
|
||||
uint8_t _bri;
|
||||
uint8_t _bri; // bus brightness
|
||||
uint8_t _NPBbri; // total brightness applied to colors in NPB buffer (_bri + ABL)
|
||||
uint8_t _autoWhiteMode; // global Auto White Calculation override
|
||||
uint16_t _start;
|
||||
uint16_t _len;
|
||||
//struct { //using bitfield struct adds abour 250 bytes to binary size
|
||||
@@ -213,27 +229,26 @@ class Bus {
|
||||
bool _hasWhite;// : 1;
|
||||
bool _hasCCT;// : 1;
|
||||
//} __attribute__ ((packed));
|
||||
uint8_t _autoWhiteMode;
|
||||
// global Auto White Calculation override
|
||||
static uint8_t _gAWM;
|
||||
// _cct has the following meanings (see calculateCCT() & BusManager::setSegmentCCT()):
|
||||
// -1 means to extract approximate CCT value in K from RGB (in calcualteCCT())
|
||||
// [0,255] is the exact CCT value where 0 means warm and 255 cold
|
||||
// [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin())
|
||||
static int16_t _cct;
|
||||
// _cctBlend determines WW/CW blending:
|
||||
// _cctBlend determines WW/CW blending, see calculateCCT()
|
||||
// < 0 - linear blending in center, single white at both ends, single white zone extends with decreased value (-127 min)
|
||||
// 0 - linear (CCT 127 => 50% warm, 50% cold)
|
||||
// 63 - semi additive/nonlinear (CCT 127 => 66% warm, 66% cold)
|
||||
// 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold)
|
||||
static uint8_t _cctBlend;
|
||||
static int8_t _cctBlend;
|
||||
|
||||
uint32_t autoWhiteCalc(uint32_t c) const;
|
||||
uint32_t autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const;
|
||||
};
|
||||
|
||||
|
||||
class BusDigital : public Bus {
|
||||
public:
|
||||
BusDigital(const BusConfig &bc, uint8_t nr);
|
||||
BusDigital(const BusConfig &bc);
|
||||
~BusDigital() { cleanup(); }
|
||||
|
||||
void show() override;
|
||||
@@ -249,10 +264,12 @@ class BusDigital : public Bus {
|
||||
uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; }
|
||||
uint16_t getUsedCurrent() const override { return _milliAmpsTotal; }
|
||||
uint16_t getMaxCurrent() const override { return _milliAmpsMax; }
|
||||
uint8_t getDriverType() const override { return _driverType; }
|
||||
void setCurrentLimit(uint16_t milliAmps) { _milliAmpsLimit = milliAmps; }
|
||||
void estimateCurrent(); // estimate used current from summed colors
|
||||
void applyBriLimit(uint8_t newBri);
|
||||
size_t getBusSize() const override;
|
||||
bool isI2S(); // true if this bus uses I2S driver
|
||||
void begin() override;
|
||||
void cleanup();
|
||||
|
||||
@@ -263,6 +280,7 @@ class BusDigital : public Bus {
|
||||
uint8_t _colorOrder;
|
||||
uint8_t _pins[2];
|
||||
uint8_t _iType;
|
||||
uint8_t _driverType; // 0=RMT (default), 1=I2S
|
||||
uint16_t _frequencykHz;
|
||||
uint16_t _milliAmpsMax;
|
||||
uint8_t _milliAmpsPerLed;
|
||||
@@ -363,6 +381,75 @@ class BusNetwork : public Bus {
|
||||
#endif
|
||||
};
|
||||
|
||||
// Placeholder for buses that we can't construct due to resource limitations
|
||||
// This preserves the configuration so it can be read back to the settings pages
|
||||
// Function calls "mimic" the replaced bus, isPlaceholder() can be used to identify a placeholder
|
||||
class BusPlaceholder : public Bus {
|
||||
public:
|
||||
BusPlaceholder(const BusConfig &bc);
|
||||
|
||||
// Actual calls are stubbed out
|
||||
void setPixelColor(unsigned pix, uint32_t c) override {};
|
||||
void show() override {};
|
||||
|
||||
// Accessors
|
||||
uint8_t getColorOrder() const override { return _colorOrder; }
|
||||
size_t getPins(uint8_t* pinArray) const override;
|
||||
unsigned skippedLeds() const override { return _skipAmount; }
|
||||
uint16_t getFrequency() const override { return _frequency; }
|
||||
uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; }
|
||||
uint16_t getMaxCurrent() const override { return _milliAmpsMax; }
|
||||
uint8_t getDriverType() const override { return _driverType; }
|
||||
const String getCustomText() const override { return _text; }
|
||||
bool isPlaceholder() const override { return true; }
|
||||
|
||||
size_t getBusSize() const override { return sizeof(BusPlaceholder); }
|
||||
|
||||
private:
|
||||
uint8_t _colorOrder;
|
||||
uint8_t _skipAmount;
|
||||
uint8_t _pins[OUTPUT_MAX_PINS];
|
||||
uint8_t _driverType;
|
||||
uint16_t _frequency;
|
||||
uint8_t _milliAmpsPerLed;
|
||||
uint16_t _milliAmpsMax;
|
||||
String _text;
|
||||
};
|
||||
|
||||
#ifdef WLED_ENABLE_HUB75MATRIX
|
||||
class BusHub75Matrix : public Bus {
|
||||
public:
|
||||
BusHub75Matrix(const BusConfig &bc);
|
||||
[[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override;
|
||||
[[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override;
|
||||
void show() override;
|
||||
void setBrightness(uint8_t b) override;
|
||||
size_t getPins(uint8_t* pinArray = nullptr) const override;
|
||||
void deallocatePins();
|
||||
void cleanup();
|
||||
|
||||
~BusHub75Matrix() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
static std::vector<LEDType> getLEDTypes(void);
|
||||
|
||||
private:
|
||||
MatrixPanel_I2S_DMA *display = nullptr;
|
||||
VirtualMatrixPanel *virtualDisp = nullptr;
|
||||
HUB75_I2S_CFG mxconfig;
|
||||
unsigned _panelWidth = 0;
|
||||
uint8_t _rows = 1; // panels per row
|
||||
uint8_t _cols = 1; // panels per column
|
||||
bool _isVirtual = false; // note: this is not strictly needed but there are padding bytes here anyway
|
||||
CRGB *_ledBuffer = nullptr; // note: using uint32_t buffer is only 2% faster and not worth the extra RAM
|
||||
byte *_ledsDirty = nullptr;
|
||||
// workaround for missing constants on include path for non-MM
|
||||
static constexpr uint32_t IS_BLACK = 0x000000u;
|
||||
static constexpr uint32_t IS_DARKGREY = 0x333333u;
|
||||
static constexpr int PIN_COUNT = 14;
|
||||
};
|
||||
#endif
|
||||
|
||||
//temporary struct for passing bus configuration to bus
|
||||
struct BusConfig {
|
||||
@@ -374,13 +461,15 @@ struct BusConfig {
|
||||
uint8_t skipAmount;
|
||||
bool refreshReq;
|
||||
uint8_t autoWhite;
|
||||
uint8_t pins[5] = {255, 255, 255, 255, 255};
|
||||
uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};
|
||||
uint16_t frequency;
|
||||
uint8_t milliAmpsPerLed;
|
||||
uint16_t milliAmpsMax;
|
||||
uint8_t driverType; // 0=RMT (default), 1=I2S
|
||||
uint8_t iType; // internal bus type (I_*) determined during memory estimation, used for bus creation
|
||||
String text;
|
||||
|
||||
BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, String sometext = "")
|
||||
BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "")
|
||||
: count(std::max(len,(uint16_t)1))
|
||||
, start(pstart)
|
||||
, colorOrder(pcolorOrder)
|
||||
@@ -390,13 +479,15 @@ struct BusConfig {
|
||||
, frequency(clock_kHz)
|
||||
, milliAmpsPerLed(maPerLed)
|
||||
, milliAmpsMax(maMax)
|
||||
, driverType(driver)
|
||||
, iType(0) // default to I_NONE
|
||||
, text(sometext)
|
||||
{
|
||||
refreshReq = (bool) GET_BIT(busType,7);
|
||||
type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh)
|
||||
size_t nPins = Bus::getNumberOfPins(type);
|
||||
for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i];
|
||||
DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"),
|
||||
DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d, driver:%s)\n"),
|
||||
(int)start, (int)(start+len),
|
||||
(int)type,
|
||||
(int)colorOrder,
|
||||
@@ -404,14 +495,15 @@ struct BusConfig {
|
||||
(int)skipAmount,
|
||||
(int)autoWhite,
|
||||
(int)frequency,
|
||||
(int)milliAmpsPerLed, (int)milliAmpsMax
|
||||
(int)milliAmpsPerLed, (int)milliAmpsMax,
|
||||
driverType == 0 ? "RMT" : "I2S"
|
||||
);
|
||||
}
|
||||
|
||||
//validates start and length and extends total if needed
|
||||
bool adjustBounds(uint16_t& total) {
|
||||
if (!count) count = 1;
|
||||
if (count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS;
|
||||
if (!Bus::isVirtual(type) && count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS;
|
||||
if (start >= MAX_LEDS) return false;
|
||||
//limit length of strip if it would exceed total permissible LEDs
|
||||
if (start + count > MAX_LEDS) count = MAX_LEDS - start;
|
||||
@@ -420,7 +512,7 @@ struct BusConfig {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t memUsage(unsigned nr = 0) const;
|
||||
size_t memUsage() const;
|
||||
};
|
||||
|
||||
|
||||
@@ -451,7 +543,6 @@ namespace BusManager {
|
||||
return j;
|
||||
}
|
||||
|
||||
size_t memUsage();
|
||||
inline uint16_t currentMilliamps() { return _gMilliAmpsUsed + MA_FOR_ESP; }
|
||||
//inline uint16_t ablMilliampsMax() { unsigned sum = 0; for (auto &bus : busses) sum += bus->getMaxCurrent(); return sum; }
|
||||
inline uint16_t ablMilliampsMax() { return _gMilliAmpsMax; } // used for compatibility reasons (and enabling virtual global ABL)
|
||||
@@ -459,12 +550,11 @@ namespace BusManager {
|
||||
void initializeABL(); // setup automatic brightness limiter parameters, call once after buses are initialized
|
||||
void applyABL(); // apply automatic brightness limiter, global or per bus
|
||||
|
||||
void useParallelOutput(); // workaround for inaccessible PolyBus
|
||||
bool hasParallelOutput(); // workaround for inaccessible PolyBus
|
||||
uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference); // workaround for access to PolyBus function from FX_fcn.cpp
|
||||
|
||||
//do not call this method from system context (network callback)
|
||||
void removeAll();
|
||||
int add(const BusConfig &bc);
|
||||
int add(const BusConfig &bc, bool placeholder);
|
||||
|
||||
void on();
|
||||
void off();
|
||||
|
||||
+206
-215
@@ -339,11 +339,17 @@
|
||||
//handles pointer type conversion for all possible bus types
|
||||
class PolyBus {
|
||||
private:
|
||||
static bool _useParallelI2S;
|
||||
|
||||
#ifndef ESP8266
|
||||
static bool _useParallelI2S; // use parallel I2S/LCD (8 channels)
|
||||
static uint8_t _rmtChannelsAssigned; // RMT channel tracking for dynamic allocation
|
||||
static uint8_t _rmtChannel; // physical RMT channel to use during bus creation
|
||||
static uint8_t _i2sChannelsAssigned; // I2S channel tracking for dynamic allocation
|
||||
static uint8_t _parallelBusItype; // parallel output does not allow mixed LED types, track I_Type
|
||||
static uint8_t _2PchannelsAssigned; // 2-Pin (SPI) channel assigned: first one gets the hardware SPI, others use bit-banged SPI
|
||||
// note on 2-Pin Types: all supported types except WS2801 use start/stop or latch frames, speed is not critical. WS2801 uses a 500us timeout and is prone to flickering if bit-banged too slow.
|
||||
// TODO: according to #4863 using more than one bit-banged output can cause glitches even in APA102. This needs investigation as from a hardware perspective all but WS2801 should be immune to timing issues.
|
||||
#endif
|
||||
public:
|
||||
static inline void setParallelI2S1Output(bool b = true) { _useParallelI2S = b; }
|
||||
static inline bool isParallelI2S1Output(void) { return _useParallelI2S; }
|
||||
|
||||
// initialize SPI bus speed for DotStar methods
|
||||
template <class T>
|
||||
@@ -476,21 +482,7 @@ class PolyBus {
|
||||
}
|
||||
}
|
||||
|
||||
static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel) {
|
||||
// NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
if (_useParallelI2S && (channel >= 8)) {
|
||||
// Parallel I2S channels are to be used first, so subtract 8 to get the RMT channel number
|
||||
channel -= 8;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3))
|
||||
// since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation
|
||||
if (!_useParallelI2S && channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32
|
||||
#endif
|
||||
|
||||
static void* create(uint8_t busType, uint8_t* pins, uint16_t len) {
|
||||
void* busPtr = nullptr;
|
||||
switch (busType) {
|
||||
case I_NONE: break;
|
||||
@@ -546,18 +538,18 @@ class PolyBus {
|
||||
#endif
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// RMT buses
|
||||
case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)channel); break;
|
||||
case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;
|
||||
// I2S1 bus or paralell buses
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
case I_32_I2_NEO_3: if (_useParallelI2S) busPtr = new B_32_IP_NEO_3(len, pins[0]); else busPtr = new B_32_I2_NEO_3(len, pins[0]); break;
|
||||
@@ -1118,86 +1110,89 @@ class PolyBus {
|
||||
|
||||
static unsigned getDataSize(void* busPtr, uint8_t busType) {
|
||||
unsigned size = 0;
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
size = 100; // ~100bytes for NPB internal structures (measured for both I2S and RMT, much smaller and more variable on ESP8266)
|
||||
#endif
|
||||
switch (busType) {
|
||||
case I_NONE: break;
|
||||
#ifdef ESP8266
|
||||
case I_8266_U0_NEO_3: size = (static_cast<B_8266_U0_NEO_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_NEO_3: size = (static_cast<B_8266_U1_NEO_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_NEO_3: size = (static_cast<B_8266_U0_NEO_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_NEO_3: size = (static_cast<B_8266_U1_NEO_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_NEO_3: size = (static_cast<B_8266_DM_NEO_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_NEO_3: size = (static_cast<B_8266_BB_NEO_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_NEO_4: size = (static_cast<B_8266_U0_NEO_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_NEO_4: size = (static_cast<B_8266_U1_NEO_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_NEO_3: size = (static_cast<B_8266_BB_NEO_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_NEO_4: size = (static_cast<B_8266_U0_NEO_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_NEO_4: size = (static_cast<B_8266_U1_NEO_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_NEO_4: size = (static_cast<B_8266_DM_NEO_4*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_NEO_4: size = (static_cast<B_8266_BB_NEO_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_400_3: size = (static_cast<B_8266_U0_400_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_400_3: size = (static_cast<B_8266_U1_400_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_NEO_4: size = (static_cast<B_8266_BB_NEO_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_400_3: size = (static_cast<B_8266_U0_400_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_400_3: size = (static_cast<B_8266_U1_400_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_400_3: size = (static_cast<B_8266_DM_400_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_400_3: size = (static_cast<B_8266_BB_400_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_TM1_4: size = (static_cast<B_8266_U0_TM1_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_TM1_4: size = (static_cast<B_8266_U1_TM1_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_400_3: size = (static_cast<B_8266_BB_400_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_TM1_4: size = (static_cast<B_8266_U0_TM1_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_TM1_4: size = (static_cast<B_8266_U1_TM1_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_TM1_4: size = (static_cast<B_8266_DM_TM1_4*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_TM1_4: size = (static_cast<B_8266_BB_TM1_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_TM2_3: size = (static_cast<B_8266_U0_TM2_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_TM2_3: size = (static_cast<B_8266_U1_TM2_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_TM1_4: size = (static_cast<B_8266_BB_TM1_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_TM2_3: size = (static_cast<B_8266_U0_TM2_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_TM2_3: size = (static_cast<B_8266_U1_TM2_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_TM2_3: size = (static_cast<B_8266_DM_TM2_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_TM2_3: size = (static_cast<B_8266_BB_TM2_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_UCS_3: size = (static_cast<B_8266_U0_UCS_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_UCS_3: size = (static_cast<B_8266_U1_UCS_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_TM2_3: size = (static_cast<B_8266_BB_TM2_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_UCS_3: size = (static_cast<B_8266_U0_UCS_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_UCS_3: size = (static_cast<B_8266_U1_UCS_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_UCS_3: size = (static_cast<B_8266_DM_UCS_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_UCS_3: size = (static_cast<B_8266_BB_UCS_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_UCS_4: size = (static_cast<B_8266_U0_UCS_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_UCS_4: size = (static_cast<B_8266_U1_UCS_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_UCS_3: size = (static_cast<B_8266_BB_UCS_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_UCS_4: size = (static_cast<B_8266_U0_UCS_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_UCS_4: size = (static_cast<B_8266_U1_UCS_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_UCS_4: size = (static_cast<B_8266_DM_UCS_4*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_UCS_4: size = (static_cast<B_8266_BB_UCS_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_APA106_3: size = (static_cast<B_8266_U0_APA106_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_APA106_3: size = (static_cast<B_8266_U1_APA106_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_UCS_4: size = (static_cast<B_8266_BB_UCS_4*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_APA106_3: size = (static_cast<B_8266_U0_APA106_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_APA106_3: size = (static_cast<B_8266_U1_APA106_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_APA106_3: size = (static_cast<B_8266_DM_APA106_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_APA106_3: size = (static_cast<B_8266_BB_APA106_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_FW6_5: size = (static_cast<B_8266_U0_FW6_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_FW6_5: size = (static_cast<B_8266_U1_FW6_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_APA106_3: size = (static_cast<B_8266_BB_APA106_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_FW6_5: size = (static_cast<B_8266_U0_FW6_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_FW6_5: size = (static_cast<B_8266_U1_FW6_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_FW6_5: size = (static_cast<B_8266_DM_FW6_5*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_FW6_5: size = (static_cast<B_8266_BB_FW6_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_2805_5: size = (static_cast<B_8266_U0_2805_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_2805_5: size = (static_cast<B_8266_U1_2805_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_FW6_5: size = (static_cast<B_8266_BB_FW6_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_2805_5: size = (static_cast<B_8266_U0_2805_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_2805_5: size = (static_cast<B_8266_U1_2805_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_2805_5: size = (static_cast<B_8266_DM_2805_5*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_2805_5: size = (static_cast<B_8266_BB_2805_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_TM1914_3: size = (static_cast<B_8266_U0_TM1914_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_TM1914_3: size = (static_cast<B_8266_U1_TM1914_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_2805_5: size = (static_cast<B_8266_BB_2805_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_TM1914_3: size = (static_cast<B_8266_U0_TM1914_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_TM1914_3: size = (static_cast<B_8266_U1_TM1914_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_TM1914_3: size = (static_cast<B_8266_DM_TM1914_3*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_TM1914_3: size = (static_cast<B_8266_BB_TM1914_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U0_SM16825_5: size = (static_cast<B_8266_U0_SM16825_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_U1_SM16825_5: size = (static_cast<B_8266_U1_SM16825_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_TM1914_3: size = (static_cast<B_8266_BB_TM1914_3*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U0_SM16825_5: size = (static_cast<B_8266_U0_SM16825_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_U1_SM16825_5: size = (static_cast<B_8266_U1_SM16825_5*>(busPtr))->PixelsSize(); break;
|
||||
case I_8266_DM_SM16825_5: size = (static_cast<B_8266_DM_SM16825_5*>(busPtr))->PixelsSize()*5; break;
|
||||
case I_8266_BB_SM16825_5: size = (static_cast<B_8266_BB_SM16825_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_8266_BB_SM16825_5: size = (static_cast<B_8266_BB_SM16825_5*>(busPtr))->PixelsSize(); break;
|
||||
#endif
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// RMT buses (front + back + small system managed RMT)
|
||||
case I_32_RN_NEO_3: size = (static_cast<B_32_RN_NEO_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_NEO_4: size = (static_cast<B_32_RN_NEO_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_400_3: size = (static_cast<B_32_RN_400_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_TM1_4: size = (static_cast<B_32_RN_TM1_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_TM2_3: size = (static_cast<B_32_RN_TM2_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_UCS_3: size = (static_cast<B_32_RN_UCS_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_UCS_4: size = (static_cast<B_32_RN_UCS_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_APA106_3: size = (static_cast<B_32_RN_APA106_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_FW6_5: size = (static_cast<B_32_RN_FW6_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_2805_5: size = (static_cast<B_32_RN_2805_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_TM1914_3: size = (static_cast<B_32_RN_TM1914_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_SM16825_5: size = (static_cast<B_32_RN_SM16825_5*>(busPtr))->PixelsSize()*2; break;
|
||||
// I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes)
|
||||
case I_32_RN_NEO_3: size += (static_cast<B_32_RN_NEO_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_NEO_4: size += (static_cast<B_32_RN_NEO_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_400_3: size += (static_cast<B_32_RN_400_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_TM1_4: size += (static_cast<B_32_RN_TM1_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_TM2_3: size += (static_cast<B_32_RN_TM2_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_UCS_3: size += (static_cast<B_32_RN_UCS_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_UCS_4: size += (static_cast<B_32_RN_UCS_4*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_APA106_3: size += (static_cast<B_32_RN_APA106_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_FW6_5: size += (static_cast<B_32_RN_FW6_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_2805_5: size += (static_cast<B_32_RN_2805_5*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_TM1914_3: size += (static_cast<B_32_RN_TM1914_3*>(busPtr))->PixelsSize()*2; break;
|
||||
case I_32_RN_SM16825_5: size += (static_cast<B_32_RN_SM16825_5*>(busPtr))->PixelsSize()*2; break;
|
||||
// I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes) not: for parallel I2S only the largest bus counts for DMA memory, this is not done correctly here, also assumes 3-step cadence
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
case I_32_I2_NEO_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_NEO_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_NEO_4: size = (_useParallelI2S) ? (static_cast<B_32_IP_NEO_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_4*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_400_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_400_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_400_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_TM1_4: size = (_useParallelI2S) ? (static_cast<B_32_IP_TM1_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1_4*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_TM2_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_TM2_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM2_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_UCS_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_UCS_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_UCS_4: size = (_useParallelI2S) ? (static_cast<B_32_IP_UCS_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_4*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_APA106_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_APA106_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_APA106_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_FW6_5: size = (_useParallelI2S) ? (static_cast<B_32_IP_FW6_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_FW6_5*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_2805_5: size = (_useParallelI2S) ? (static_cast<B_32_IP_2805_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_2805_5*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_TM1914_3: size = (_useParallelI2S) ? (static_cast<B_32_IP_TM1914_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1914_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_SM16825_5: size = (_useParallelI2S) ? (static_cast<B_32_IP_SM16825_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_SM16825_5*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_NEO_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_NEO_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_NEO_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_NEO_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_4*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_400_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_400_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_400_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_TM1_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM1_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1_4*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_TM2_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM2_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM2_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_UCS_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_UCS_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_UCS_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_UCS_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_4*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_APA106_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_APA106_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_APA106_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_FW6_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_FW6_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_FW6_5*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_2805_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_2805_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_2805_5*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_TM1914_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM1914_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1914_3*>(busPtr))->PixelsSize()*4; break;
|
||||
case I_32_I2_SM16825_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_SM16825_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_SM16825_5*>(busPtr))->PixelsSize()*4; break;
|
||||
#endif
|
||||
#endif
|
||||
case I_HS_DOT_3: size = (static_cast<B_HS_DOT_3*>(busPtr))->PixelsSize()*2; break;
|
||||
@@ -1220,95 +1215,100 @@ class PolyBus {
|
||||
case I_NONE: size = 0; break;
|
||||
#ifdef ESP8266
|
||||
// UART methods have front + back buffers + small UART
|
||||
case I_8266_U0_NEO_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_U1_NEO_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_BB_NEO_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_U0_TM1_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_U1_TM1_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_BB_TM1_4: size = (size + count)*2; break; // 4 channels
|
||||
case I_8266_U0_UCS_3: size *= 4; break; // 16 bit
|
||||
case I_8266_U1_UCS_3: size *= 4; break; // 16 bit
|
||||
case I_8266_BB_UCS_3: size *= 4; break; // 16 bit
|
||||
case I_8266_U0_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels
|
||||
case I_8266_U1_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels
|
||||
case I_8266_BB_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels
|
||||
case I_8266_U0_FW6_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_U1_FW6_5: size = (size + 2*count)*2; break; // 5channels
|
||||
case I_8266_BB_FW6_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_U0_2805_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_U1_2805_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_BB_2805_5: size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_8266_U0_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels
|
||||
case I_8266_U1_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels
|
||||
case I_8266_BB_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels
|
||||
// DMA methods have front + DMA buffer = ((1+(3+1)) * channels)
|
||||
case I_8266_DM_NEO_3: size *= 5; break;
|
||||
case I_8266_DM_NEO_4: size = (size + count)*5; break;
|
||||
case I_8266_DM_400_3: size *= 5; break;
|
||||
case I_8266_DM_TM1_4: size = (size + count)*5; break;
|
||||
case I_8266_DM_TM2_3: size *= 5; break;
|
||||
case I_8266_DM_UCS_3: size *= 2*5; break;
|
||||
case I_8266_DM_UCS_4: size = (size + count)*2*5; break;
|
||||
case I_8266_DM_APA106_3: size *= 5; break;
|
||||
case I_8266_DM_FW6_5: size = (size + 2*count)*5; break;
|
||||
case I_8266_DM_2805_5: size = (size + 2*count)*5; break;
|
||||
case I_8266_DM_TM1914_3: size *= 5; break;
|
||||
case I_8266_U0_NEO_4 : // fallthrough
|
||||
case I_8266_U1_NEO_4 : // fallthrough
|
||||
case I_8266_BB_NEO_4 : // fallthrough
|
||||
case I_8266_U0_TM1_4 : // fallthrough
|
||||
case I_8266_U1_TM1_4 : // fallthrough
|
||||
case I_8266_BB_TM1_4 : size = (size + count); break; // 4 channels
|
||||
case I_8266_U0_UCS_3 : // fallthrough
|
||||
case I_8266_U1_UCS_3 : // fallthrough
|
||||
case I_8266_BB_UCS_3 : size *= 2; break; // 16 bit
|
||||
case I_8266_U0_UCS_4 : // fallthrough
|
||||
case I_8266_U1_UCS_4 : // fallthrough
|
||||
case I_8266_BB_UCS_4 : size = (size + count)*2; break; // 16 bit 4 channels
|
||||
case I_8266_U0_FW6_5 : // fallthrough
|
||||
case I_8266_U1_FW6_5 : // fallthrough
|
||||
case I_8266_BB_FW6_5 : // fallthrough
|
||||
case I_8266_U0_2805_5 : // fallthrough
|
||||
case I_8266_U1_2805_5 : // fallthrough
|
||||
case I_8266_BB_2805_5 : size = (size + 2*count); break; // 5 channels
|
||||
case I_8266_U0_SM16825_5: // fallthrough
|
||||
case I_8266_U1_SM16825_5: // fallthrough
|
||||
case I_8266_BB_SM16825_5: size = (size + 2*count)*2; break; // 16 bit 5 channels
|
||||
// DMA methods have front + DMA buffer = ((1+(3+1)) * channels; exact value is a bit of mistery - needs a dig into NPB)
|
||||
case I_8266_DM_NEO_3 : // fallthrough
|
||||
case I_8266_DM_400_3 : // fallthrough
|
||||
case I_8266_DM_TM2_3 : // fallthrough
|
||||
case I_8266_DM_APA106_3 : // fallthrough
|
||||
case I_8266_DM_TM1914_3 : size *= 5; break;
|
||||
case I_8266_DM_NEO_4 : // fallthrough
|
||||
case I_8266_DM_TM1_4 : size = (size + count)*5; break;
|
||||
case I_8266_DM_UCS_3 : size *= 2*5; break;
|
||||
case I_8266_DM_UCS_4 : size = (size + count)*2*5; break;
|
||||
case I_8266_DM_FW6_5 : // fallthrough
|
||||
case I_8266_DM_2805_5 : size = (size + 2*count)*5; break;
|
||||
case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break;
|
||||
#endif
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// RMT buses (1x front and 1x back buffer)
|
||||
case I_32_RN_NEO_4: size = (size + count)*2; break;
|
||||
case I_32_RN_TM1_4: size = (size + count)*2; break;
|
||||
case I_32_RN_UCS_3: size *= 2*2; break;
|
||||
case I_32_RN_UCS_4: size = (size + count)*2*2; break;
|
||||
case I_32_RN_FW6_5: size = (size + 2*count)*2; break;
|
||||
case I_32_RN_2805_5: size = (size + 2*count)*2; break;
|
||||
case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break;
|
||||
// I2S1 bus or paralell buses (individual 1x front and 1 DMA (3x or 4x pixel count) or common back DMA buffers)
|
||||
#else
|
||||
// note: RMT and I2S buses use ~100 bytes of internal NPB memory each, not included here for simplicity
|
||||
// RMT buses (1x front and 1x back buffer, does not include small RMT buffer)
|
||||
case I_32_RN_NEO_4 : // fallthrough
|
||||
case I_32_RN_TM1_4 : size = (size + count)*2; break; // 4 channels
|
||||
case I_32_RN_UCS_3 : size *= 2*2; break; // 16bit
|
||||
case I_32_RN_UCS_4 : size = (size + count)*2*2; break; // 16bit, 4 channels
|
||||
case I_32_RN_FW6_5 : // fallthrough
|
||||
case I_32_RN_2805_5 : size = (size + 2*count)*2; break; // 5 channels
|
||||
case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels
|
||||
// I2S bus or paralell I2S buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD)
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
||||
case I_32_I2_NEO_3: size *= 4; break;
|
||||
case I_32_I2_NEO_4: size = (size + count)*4; break;
|
||||
case I_32_I2_400_3: size *= 4; break;
|
||||
case I_32_I2_TM1_4: size = (size + count)*4; break;
|
||||
case I_32_I2_TM2_3: size *= 4; break;
|
||||
case I_32_I2_UCS_3: size *= 2*4; break;
|
||||
case I_32_I2_UCS_4: size = (size + count)*2*4; break;
|
||||
case I_32_I2_APA106_3: size *= 4; break;
|
||||
case I_32_I2_FW6_5: size = (size + 2*count)*4; break;
|
||||
case I_32_I2_2805_5: size = (size + 2*count)*4; break;
|
||||
case I_32_I2_TM1914_3: size *= 4; break;
|
||||
case I_32_I2_SM16825_5: size = (size + 2*count)*2*4; break;
|
||||
case I_32_I2_NEO_3 : // fallthrough
|
||||
case I_32_I2_400_3 : // fallthrough
|
||||
case I_32_I2_TM2_3 : // fallthrough
|
||||
case I_32_I2_APA106_3 : break; // do nothing, I2S uses single buffer + DMA buffer
|
||||
case I_32_I2_NEO_4 : // fallthrough
|
||||
case I_32_I2_TM1_4 : size = (size + count); break; // 4 channels
|
||||
case I_32_I2_UCS_3 : size *= 2; break; // 16 bit
|
||||
case I_32_I2_UCS_4 : size = (size + count)*2; break; // 16 bit, 4 channels
|
||||
case I_32_I2_FW6_5 : // fallthrough
|
||||
case I_32_I2_2805_5 : size = (size + 2*count); break; // 5 channels
|
||||
case I_32_I2_SM16825_5: size = (size + 2*count)*2; break; // 16 bit, 5 channels
|
||||
#endif
|
||||
default : size *= 2; break; // everything else uses 2 buffers
|
||||
#endif
|
||||
// everything else uses 2 buffers
|
||||
default: size *= 2; break;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
//gives back the internal type index (I_XX_XXX_X above) for the input
|
||||
static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t num = 0) {
|
||||
#ifndef ESP8266
|
||||
// Reset channel tracking (call before adding buses)
|
||||
static void resetChannelTracking() {
|
||||
_useParallelI2S = false;
|
||||
_rmtChannelsAssigned = 0;
|
||||
_rmtChannel = 0;
|
||||
_i2sChannelsAssigned = 0;
|
||||
_parallelBusItype = I_NONE;
|
||||
_2PchannelsAssigned = 0;
|
||||
}
|
||||
#endif
|
||||
// reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins
|
||||
static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) {
|
||||
if (!Bus::isDigital(busType)) return I_NONE;
|
||||
uint8_t t = I_NONE;
|
||||
if (Bus::is2Pin(busType)) { //SPI LED chips
|
||||
bool isHSPI = false;
|
||||
#ifdef ESP8266
|
||||
if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true;
|
||||
#else
|
||||
// temporary hack to limit use of hardware SPI to a single SPI peripheral (HSPI): only allow ESP32 hardware serial on segment 0
|
||||
// SPI global variable is normally linked to VSPI on ESP32 (or FSPI C3, S3)
|
||||
if (!num) isHSPI = true;
|
||||
if (_2PchannelsAssigned == 0) isHSPI = true; // first 2-pin channel uses hardware SPI
|
||||
_2PchannelsAssigned++;
|
||||
#endif
|
||||
uint8_t t = I_NONE;
|
||||
switch (busType) {
|
||||
case TYPE_APA102: t = I_SS_DOT_3; break;
|
||||
case TYPE_LPD8806: t = I_SS_LPD_3; break;
|
||||
case TYPE_LPD6803: t = I_SS_LPO_3; break;
|
||||
case TYPE_WS2801: t = I_SS_WS1_3; break;
|
||||
case TYPE_P9813: t = I_SS_P98_3; break;
|
||||
default: t=I_NONE;
|
||||
}
|
||||
if (t > I_NONE && isHSPI) t--; //hardware SPI has one smaller ID than software
|
||||
return t;
|
||||
} else {
|
||||
#ifdef ESP8266
|
||||
uint8_t offset = pins[0] -1; //for driver: 0 = uart0, 1 = uart1, 2 = dma, 3 = bitbang
|
||||
@@ -1318,96 +1318,87 @@ class PolyBus {
|
||||
case TYPE_WS2812_2CH_X3:
|
||||
case TYPE_WS2812_RGB:
|
||||
case TYPE_WS2812_WWA:
|
||||
return I_8266_U0_NEO_3 + offset;
|
||||
t = I_8266_U0_NEO_3 + offset; break;
|
||||
case TYPE_SK6812_RGBW:
|
||||
return I_8266_U0_NEO_4 + offset;
|
||||
t = I_8266_U0_NEO_4 + offset; break;
|
||||
case TYPE_WS2811_400KHZ:
|
||||
return I_8266_U0_400_3 + offset;
|
||||
t = I_8266_U0_400_3 + offset; break;
|
||||
case TYPE_TM1814:
|
||||
return I_8266_U0_TM1_4 + offset;
|
||||
t = I_8266_U0_TM1_4 + offset; break;
|
||||
case TYPE_TM1829:
|
||||
return I_8266_U0_TM2_3 + offset;
|
||||
t = I_8266_U0_TM2_3 + offset; break;
|
||||
case TYPE_UCS8903:
|
||||
return I_8266_U0_UCS_3 + offset;
|
||||
t = I_8266_U0_UCS_3 + offset; break;
|
||||
case TYPE_UCS8904:
|
||||
return I_8266_U0_UCS_4 + offset;
|
||||
t = I_8266_U0_UCS_4 + offset; break;
|
||||
case TYPE_APA106:
|
||||
return I_8266_U0_APA106_3 + offset;
|
||||
t = I_8266_U0_APA106_3 + offset; break;
|
||||
case TYPE_FW1906:
|
||||
return I_8266_U0_FW6_5 + offset;
|
||||
t = I_8266_U0_FW6_5 + offset; break;
|
||||
case TYPE_WS2805:
|
||||
return I_8266_U0_2805_5 + offset;
|
||||
t = I_8266_U0_2805_5 + offset; break;
|
||||
case TYPE_TM1914:
|
||||
return I_8266_U0_TM1914_3 + offset;
|
||||
t = I_8266_U0_TM1914_3 + offset; break;
|
||||
case TYPE_SM16825:
|
||||
return I_8266_U0_SM16825_5 + offset;
|
||||
t = I_8266_U0_SM16825_5 + offset; break;
|
||||
}
|
||||
#else //ESP32
|
||||
uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S1 [I2S0 is used by Audioreactive]
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
// ESP32-S2 only has 4 RMT channels
|
||||
if (_useParallelI2S) {
|
||||
if (num > 11) return I_NONE;
|
||||
if (num < 8) offset = 1; // use x8 parallel I2S0 channels followed by RMT
|
||||
// Note: conflicts with AudioReactive if enabled
|
||||
// dynamic channel allocation based on driver preference
|
||||
// determine which driver to use based on preference and availability. First I2S bus locks the I2S type, all subsequent I2S buses are assigned the same type (hardware restriction)
|
||||
uint8_t offset = 0; // 0 = RMT, 1 = I2S/LCD
|
||||
if (driverPreference == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) {
|
||||
_rmtChannelsAssigned++;
|
||||
} else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) {
|
||||
offset = 1; // I2S requested or RMT full
|
||||
_i2sChannelsAssigned++;
|
||||
} else {
|
||||
if (num > 4) return I_NONE;
|
||||
if (num > 3) offset = 1; // only one I2S0 (use last to allow Audioreactive)
|
||||
return I_NONE; // No channels available
|
||||
}
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
// On ESP32-C3 only the first 2 RMT channels are usable for transmitting
|
||||
if (num > 1) return I_NONE;
|
||||
//if (num > 1) offset = 1; // I2S not supported yet (only 1 I2S)
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// On ESP32-S3 only the first 4 RMT channels are usable for transmitting
|
||||
if (_useParallelI2S) {
|
||||
if (num > 11) return I_NONE;
|
||||
if (num < 8) offset = 1; // use x8 parallel I2S LCD channels, followed by RMT
|
||||
} else {
|
||||
if (num > 3) return I_NONE; // do not use single I2S (as it is not supported)
|
||||
}
|
||||
#else
|
||||
// standard ESP32 has 8 RMT and x1/x8 I2S1 channels
|
||||
if (_useParallelI2S) {
|
||||
if (num > 15) return I_NONE;
|
||||
if (num < 8) offset = 1; // 8 I2S followed by 8 RMT
|
||||
} else {
|
||||
if (num > 9) return I_NONE;
|
||||
if (num == 0) offset = 1; // prefer I2S1 for 1st bus (less flickering but more RAM needed)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Now determine actual bus type with the chosen offset
|
||||
switch (busType) {
|
||||
case TYPE_WS2812_1CH_X3:
|
||||
case TYPE_WS2812_2CH_X3:
|
||||
case TYPE_WS2812_RGB:
|
||||
case TYPE_WS2812_WWA:
|
||||
return I_32_RN_NEO_3 + offset;
|
||||
t = I_32_RN_NEO_3 + offset; break;
|
||||
case TYPE_SK6812_RGBW:
|
||||
return I_32_RN_NEO_4 + offset;
|
||||
t = I_32_RN_NEO_4 + offset; break;
|
||||
case TYPE_WS2811_400KHZ:
|
||||
return I_32_RN_400_3 + offset;
|
||||
t = I_32_RN_400_3 + offset; break;
|
||||
case TYPE_TM1814:
|
||||
return I_32_RN_TM1_4 + offset;
|
||||
t = I_32_RN_TM1_4 + offset; break;
|
||||
case TYPE_TM1829:
|
||||
return I_32_RN_TM2_3 + offset;
|
||||
t = I_32_RN_TM2_3 + offset; break;
|
||||
case TYPE_UCS8903:
|
||||
return I_32_RN_UCS_3 + offset;
|
||||
t = I_32_RN_UCS_3 + offset; break;
|
||||
case TYPE_UCS8904:
|
||||
return I_32_RN_UCS_4 + offset;
|
||||
t = I_32_RN_UCS_4 + offset; break;
|
||||
case TYPE_APA106:
|
||||
return I_32_RN_APA106_3 + offset;
|
||||
t = I_32_RN_APA106_3 + offset; break;
|
||||
case TYPE_FW1906:
|
||||
return I_32_RN_FW6_5 + offset;
|
||||
t = I_32_RN_FW6_5 + offset; break;
|
||||
case TYPE_WS2805:
|
||||
return I_32_RN_2805_5 + offset;
|
||||
t = I_32_RN_2805_5 + offset; break;
|
||||
case TYPE_TM1914:
|
||||
return I_32_RN_TM1914_3 + offset;
|
||||
t = I_32_RN_TM1914_3 + offset; break;
|
||||
case TYPE_SM16825:
|
||||
return I_32_RN_SM16825_5 + offset;
|
||||
t = I_32_RN_SM16825_5 + offset; break;
|
||||
}
|
||||
// If using parallel I2S, set the type accordingly
|
||||
if (_i2sChannelsAssigned == 1 && offset == 1) { // first I2S channel request, lock the type
|
||||
_parallelBusItype = t;
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
_useParallelI2S = true; // ESP32-S3 always uses parallel I2S (LCD method)
|
||||
#endif
|
||||
}
|
||||
else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag
|
||||
_useParallelI2S = true;
|
||||
t = _parallelBusItype;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return I_NONE;
|
||||
return t;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
+82
-78
@@ -17,13 +17,13 @@ static bool buttonBriDirection = false; // true: increase brightness, false: dec
|
||||
|
||||
void shortPressAction(uint8_t b)
|
||||
{
|
||||
if (!macroButton[b]) {
|
||||
if (!buttons[b].macroButton) {
|
||||
switch (b) {
|
||||
case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
|
||||
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -38,7 +38,7 @@ void shortPressAction(uint8_t b)
|
||||
|
||||
void longPressAction(uint8_t b)
|
||||
{
|
||||
if (!macroLongPress[b]) {
|
||||
if (!buttons[b].macroLongPress) {
|
||||
switch (b) {
|
||||
case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break;
|
||||
case 1:
|
||||
@@ -52,11 +52,11 @@ void longPressAction(uint8_t b)
|
||||
else bri -= WLED_LONG_BRI_STEPS;
|
||||
}
|
||||
stateUpdated(CALL_MODE_BUTTON);
|
||||
buttonPressedTime[b] = millis();
|
||||
buttons[b].pressedTime = millis();
|
||||
break; // repeatable action
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -71,13 +71,13 @@ void longPressAction(uint8_t b)
|
||||
|
||||
void doublePressAction(uint8_t b)
|
||||
{
|
||||
if (!macroDoublePress[b]) {
|
||||
if (!buttons[b].macroDoublePress) {
|
||||
switch (b) {
|
||||
//case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set
|
||||
case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -92,10 +92,10 @@ void doublePressAction(uint8_t b)
|
||||
|
||||
bool isButtonPressed(uint8_t b)
|
||||
{
|
||||
if (btnPin[b]<0) return false;
|
||||
unsigned pin = btnPin[b];
|
||||
if (buttons[b].pin < 0) return false;
|
||||
unsigned pin = buttons[b].pin;
|
||||
|
||||
switch (buttonType[b]) {
|
||||
switch (buttons[b].type) {
|
||||
case BTN_TYPE_NONE:
|
||||
case BTN_TYPE_RESERVED:
|
||||
break;
|
||||
@@ -113,7 +113,7 @@ bool isButtonPressed(uint8_t b)
|
||||
#ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt)
|
||||
if (touchInterruptGetLastStatus(pin)) return true;
|
||||
#else
|
||||
if (digitalPinToTouchChannel(btnPin[b]) >= 0 && touchRead(pin) <= touchThreshold) return true;
|
||||
if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true;
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
@@ -124,25 +124,25 @@ bool isButtonPressed(uint8_t b)
|
||||
void handleSwitch(uint8_t b)
|
||||
{
|
||||
// isButtonPressed() handles inverted/noninverted logic
|
||||
if (buttonPressedBefore[b] != isButtonPressed(b)) {
|
||||
if (buttons[b].pressedBefore != isButtonPressed(b)) {
|
||||
DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b);
|
||||
buttonPressedTime[b] = millis();
|
||||
buttonPressedBefore[b] = !buttonPressedBefore[b];
|
||||
buttons[b].pressedTime = millis();
|
||||
buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state
|
||||
}
|
||||
|
||||
if (buttonLongPressed[b] == buttonPressedBefore[b]) return;
|
||||
if (buttons[b].longPressed == buttons[b].pressedBefore) return;
|
||||
|
||||
if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b);
|
||||
if (!buttonPressedBefore[b]) { // on -> off
|
||||
if (!buttons[b].pressedBefore) { // on -> off
|
||||
DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b);
|
||||
if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
|
||||
if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
|
||||
else { //turn on
|
||||
if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
|
||||
}
|
||||
} else { // off -> on
|
||||
DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b);
|
||||
if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
|
||||
if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
|
||||
else { //turn off
|
||||
if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
|
||||
}
|
||||
@@ -152,13 +152,13 @@ void handleSwitch(uint8_t b)
|
||||
// publish MQTT message
|
||||
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
|
||||
char subuf[MQTT_MAX_TOPIC_LEN + 32];
|
||||
if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
|
||||
if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
|
||||
else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
|
||||
mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on");
|
||||
mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on");
|
||||
}
|
||||
#endif
|
||||
|
||||
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
|
||||
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,17 +178,17 @@ void handleAnalog(uint8_t b)
|
||||
#ifdef ESP8266
|
||||
rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit
|
||||
#else
|
||||
if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
|
||||
rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution
|
||||
if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
|
||||
rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution
|
||||
#endif
|
||||
yield(); // keep WiFi task running - analog read may take several millis on ESP8266
|
||||
|
||||
filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]
|
||||
unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit
|
||||
if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
|
||||
if(aRead >= 255-POT_SENSITIVITY) aRead = 255;
|
||||
if (aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
|
||||
if (aRead >= 255-POT_SENSITIVITY) aRead = 255;
|
||||
|
||||
if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
|
||||
if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
|
||||
|
||||
// remove noise & reduce frequency of UI updates
|
||||
if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading
|
||||
@@ -206,10 +206,10 @@ void handleAnalog(uint8_t b)
|
||||
oldRead[b] = aRead;
|
||||
|
||||
// if no macro for "short press" and "long press" is defined use brightness control
|
||||
if (!macroButton[b] && !macroLongPress[b]) {
|
||||
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), macroDoublePress[b]);
|
||||
if (!buttons[b].macroButton && !buttons[b].macroLongPress) {
|
||||
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress);
|
||||
// if "double press" macro defines which option to change
|
||||
if (macroDoublePress[b] >= 250) {
|
||||
if (buttons[b].macroDoublePress >= 250) {
|
||||
// global brightness
|
||||
if (aRead == 0) {
|
||||
briLast = bri;
|
||||
@@ -218,27 +218,30 @@ void handleAnalog(uint8_t b)
|
||||
if (bri == 0) strip.restartRuntime();
|
||||
bri = aRead;
|
||||
}
|
||||
} else if (macroDoublePress[b] == 249) {
|
||||
} else if (buttons[b].macroDoublePress == 249) {
|
||||
// effect speed
|
||||
effectSpeed = aRead;
|
||||
} else if (macroDoublePress[b] == 248) {
|
||||
} else if (buttons[b].macroDoublePress == 248) {
|
||||
// effect intensity
|
||||
effectIntensity = aRead;
|
||||
} else if (macroDoublePress[b] == 247) {
|
||||
} else if (buttons[b].macroDoublePress == 247) {
|
||||
// selected palette
|
||||
effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1);
|
||||
effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result
|
||||
} else if (macroDoublePress[b] == 200) {
|
||||
} else if (buttons[b].macroDoublePress == 200) {
|
||||
// primary color, hue, full saturation
|
||||
colorHStoRGB(aRead*256,255,colPri);
|
||||
colorHStoRGB(aRead*256, 255, colPri);
|
||||
} else {
|
||||
// otherwise use "double press" for segment selection
|
||||
Segment& seg = strip.getSegment(macroDoublePress[b]);
|
||||
Segment& seg = strip.getSegment(buttons[b].macroDoublePress);
|
||||
if (aRead == 0) {
|
||||
seg.setOption(SEG_OPTION_ON, false); // off (use transition)
|
||||
seg.on = false; // do not use transition
|
||||
//seg.setOption(SEG_OPTION_ON, false); // off (use transition)
|
||||
} else {
|
||||
seg.setOpacity(aRead);
|
||||
seg.setOption(SEG_OPTION_ON, true); // on (use transition)
|
||||
seg.opacity = aRead; // set brightness (opacity) of segment
|
||||
seg.on = true;
|
||||
//seg.setOpacity(aRead);
|
||||
//seg.setOption(SEG_OPTION_ON, true); // on (use transition)
|
||||
}
|
||||
// this will notify clients of update (websockets,mqtt,etc)
|
||||
updateInterfaces(CALL_MODE_BUTTON);
|
||||
@@ -261,16 +264,16 @@ void handleButton()
|
||||
if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips)
|
||||
lastRun = now;
|
||||
|
||||
for (unsigned b=0; b<WLED_MAX_BUTTONS; b++) {
|
||||
for (unsigned b = 0; b < buttons.size(); b++) {
|
||||
#ifdef ESP8266
|
||||
if ((btnPin[b]<0 && !(buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)) || buttonType[b] == BTN_TYPE_NONE) continue;
|
||||
if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue;
|
||||
#else
|
||||
if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue;
|
||||
if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue;
|
||||
#endif
|
||||
|
||||
if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons
|
||||
|
||||
if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
|
||||
if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
|
||||
if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {
|
||||
handleAnalog(b);
|
||||
}
|
||||
@@ -278,7 +281,7 @@ void handleButton()
|
||||
}
|
||||
|
||||
// button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
|
||||
if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_TOUCH_SWITCH || buttonType[b] == BTN_TYPE_PIR_SENSOR) {
|
||||
if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) {
|
||||
handleSwitch(b);
|
||||
continue;
|
||||
}
|
||||
@@ -287,70 +290,66 @@ void handleButton()
|
||||
if (isButtonPressed(b)) { // pressed
|
||||
|
||||
// if all macros are the same, fire action immediately on rising edge
|
||||
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
|
||||
if (!buttonPressedBefore[b])
|
||||
shortPressAction(b);
|
||||
buttonPressedBefore[b] = true;
|
||||
buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler)
|
||||
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
|
||||
if (!buttons[b].pressedBefore) shortPressAction(b);
|
||||
buttons[b].pressedBefore = true;
|
||||
buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = true;
|
||||
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
|
||||
if (!buttonLongPressed[b]) {
|
||||
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
|
||||
if (!buttons[b].longPressed) {
|
||||
buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press
|
||||
longPressAction(b);
|
||||
} else if (b) { //repeatable action (~5 times per s) on button > 0
|
||||
longPressAction(b);
|
||||
buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms
|
||||
buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms
|
||||
}
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
}
|
||||
|
||||
} else if (buttonPressedBefore[b]) { //released
|
||||
long dur = now - buttonPressedTime[b];
|
||||
} else if (buttons[b].pressedBefore) { //released
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
|
||||
// released after rising-edge short press action
|
||||
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
|
||||
if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released
|
||||
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
|
||||
if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce
|
||||
bool doublePress = buttonWaitTime[b]; //did we have a short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce
|
||||
bool doublePress = buttons[b].waitTime; //did we have a short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
|
||||
if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
|
||||
WLED_FS.format();
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
clearEEPROM();
|
||||
#endif
|
||||
doReboot = true;
|
||||
} else {
|
||||
WLED::instance().initAP(true);
|
||||
}
|
||||
} else if (!buttonLongPressed[b]) { //short press
|
||||
} else if (!buttons[b].longPressed) { //short press
|
||||
//NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling
|
||||
if (b != 1 && !macroDoublePress[b]) { //don't wait for double press on buttons without a default action if no double press macro set
|
||||
if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set
|
||||
shortPressAction(b);
|
||||
} else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
doublePressAction(b);
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
|
||||
//if 350ms elapsed since last short press release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
shortPressAction(b);
|
||||
}
|
||||
}
|
||||
@@ -368,24 +367,29 @@ void handleIO()
|
||||
|
||||
// if we want to control on-board LED (ESP8266) or relay we have to do it here as the final show() may not happen until
|
||||
// next loop() cycle
|
||||
if (strip.getBrightness()) {
|
||||
handleOnOff();
|
||||
}
|
||||
|
||||
void handleOnOff(bool forceOff)
|
||||
{
|
||||
if (strip.getBrightness() && !forceOff) {
|
||||
lastOnTime = millis();
|
||||
if (offMode) {
|
||||
BusManager::on();
|
||||
if (rlyPin>=0) {
|
||||
pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT);
|
||||
digitalWrite(rlyPin, rlyMde);
|
||||
delay(50); // wait for relay to switch and power to stabilize
|
||||
// note: pinMode is set in first call to handleOnOff(true) in beginStrip()
|
||||
digitalWrite(rlyPin, rlyMde); // set to on state
|
||||
delay(RELAY_DELAY); // let power stabilize before sending LED data (#346 #812 #3581 #3955)
|
||||
}
|
||||
offMode = false;
|
||||
}
|
||||
} else if (millis() - lastOnTime > 600 && !strip.needsUpdate()) {
|
||||
} else if ((millis() - lastOnTime > 600 && !strip.needsUpdate()) || forceOff) {
|
||||
// for turning LED or relay off we need to wait until strip no longer needs updates (strip.trigger())
|
||||
if (!offMode) {
|
||||
BusManager::off();
|
||||
if (rlyPin>=0) {
|
||||
digitalWrite(rlyPin, !rlyMde); // set output before disabling high-z state to avoid output glitches
|
||||
pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT);
|
||||
digitalWrite(rlyPin, !rlyMde);
|
||||
}
|
||||
offMode = true;
|
||||
}
|
||||
|
||||
+155
-164
@@ -38,7 +38,7 @@ static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTy
|
||||
//simple macro for ArduinoJSON's or syntax
|
||||
#define CJSON(a,b) a = b | a
|
||||
|
||||
void getStringFromJson(char* dest, const char* src, size_t len) {
|
||||
static inline void getStringFromJson(char* dest, const char* src, size_t len) {
|
||||
if (src != nullptr) strlcpy(dest, src, len);
|
||||
}
|
||||
|
||||
@@ -49,13 +49,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
|
||||
//long vid = doc[F("vid")]; // 2010020
|
||||
|
||||
#ifdef WLED_USE_ETHERNET
|
||||
JsonObject ethernet = doc[F("eth")];
|
||||
CJSON(ethernetType, ethernet["type"]);
|
||||
// NOTE: Ethernet configuration takes priority over other use of pins
|
||||
initEthernet();
|
||||
#endif
|
||||
|
||||
JsonObject id = doc["id"];
|
||||
getStringFromJson(cmDNS, id[F("mdns")], 33);
|
||||
getStringFromJson(serverDescription, id[F("name")], 33);
|
||||
@@ -91,7 +84,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
JsonArray nw_ins = nw["ins"];
|
||||
if (!nw_ins.isNull()) {
|
||||
// as password are stored separately in wsec.json when reading configuration vector resize happens there, but for dynamic config we need to resize if necessary
|
||||
if (nw_ins.size() > 1 && nw_ins.size() > multiWiFi.size()) multiWiFi.resize(nw_ins.size()); // resize constructs objects while resizing
|
||||
size_t newSize = min((size_t)WLED_MAX_WIFI_COUNT, nw_ins.size()); // cap at WLED_MAX_WIFI_COUNT (prevent oversizing when too many Wi-Fi entries)
|
||||
if (nw_ins.size() > 1 && newSize > multiWiFi.size()) multiWiFi.resize(newSize); // resize constructs objects while resizing
|
||||
for (JsonObject wifi : nw_ins) {
|
||||
JsonArray ip = wifi["ip"];
|
||||
JsonArray gw = wifi["gw"];
|
||||
@@ -114,6 +108,17 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
multiWiFi[n].staticIP = nIP;
|
||||
multiWiFi[n].staticGW = nGW;
|
||||
multiWiFi[n].staticSN = nSN;
|
||||
#ifdef WLED_ENABLE_WPA_ENTERPRISE
|
||||
byte encType = WIFI_ENCRYPTION_TYPE_PSK;
|
||||
char anonIdent[65] = "";
|
||||
char ident[65] = "";
|
||||
CJSON(encType, wifi[F("enc_type")]);
|
||||
getStringFromJson(anonIdent, wifi["e_anon_ident"], 65);
|
||||
getStringFromJson(ident, wifi["e_ident"], 65);
|
||||
multiWiFi[n].encryptionType = encType;
|
||||
strlcpy(multiWiFi[n].enterpriseAnonIdentity, anonIdent, 65);
|
||||
strlcpy(multiWiFi[n].enterpriseIdentity, ident, 65);
|
||||
#endif
|
||||
if (++n >= WLED_MAX_WIFI_COUNT) break;
|
||||
}
|
||||
}
|
||||
@@ -125,12 +130,20 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/wled/WLED/issues/5247
|
||||
#ifdef WLED_USE_ETHERNET
|
||||
JsonObject ethernet = doc[F("eth")];
|
||||
CJSON(ethernetType, ethernet["type"]);
|
||||
// NOTE: Ethernet configuration takes priority over other use of pins
|
||||
initEthernet();
|
||||
#endif
|
||||
|
||||
JsonObject ap = doc["ap"];
|
||||
getStringFromJson(apSSID, ap[F("ssid")], 33);
|
||||
getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security
|
||||
//int ap_pskl = ap[F("pskl")];
|
||||
CJSON(apChannel, ap[F("chan")]);
|
||||
if (apChannel > 13 || apChannel < 1) apChannel = 1;
|
||||
if (apChannel > 13 || apChannel < 1) apChannel = 6; // reset to default if invalid
|
||||
CJSON(apHide, ap[F("hide")]);
|
||||
if (apHide > 1) apHide = 1;
|
||||
CJSON(apBehavior, ap[F("behav")]);
|
||||
@@ -164,10 +177,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
CJSON(cctICused, hw_led[F("ic")]);
|
||||
uint8_t cctBlending = hw_led[F("cb")] | Bus::getCCTBlend();
|
||||
Bus::setCCTBlend(cctBlending);
|
||||
strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
CJSON(useParallelI2S, hw_led[F("prl")]);
|
||||
#endif
|
||||
unsigned targetFPS = hw_led["fps"] | WLED_FPS;
|
||||
strip.setTargetFps(targetFPS); //unlimited if 0, default 42 FPS
|
||||
|
||||
#ifndef WLED_DISABLE_2D
|
||||
// 2D Matrix Settings
|
||||
@@ -207,7 +218,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
int s = 0; // bus iterator
|
||||
for (JsonObject elm : ins) {
|
||||
if (s >= WLED_MAX_BUSSES) break; // only counts physical buses
|
||||
uint8_t pins[5] = {255, 255, 255, 255, 255};
|
||||
uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};
|
||||
JsonArray pinArr = elm["pin"];
|
||||
if (pinArr.size() == 0) continue;
|
||||
//pins[0] = pinArr[0];
|
||||
@@ -234,9 +245,10 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
maMax = 0;
|
||||
}
|
||||
ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh
|
||||
uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available
|
||||
|
||||
String host = elm[F("text")] | String();
|
||||
busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, host);
|
||||
busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host);
|
||||
doInitBusses = true; // finalization done in beginStrip()
|
||||
if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want
|
||||
}
|
||||
@@ -256,9 +268,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins),
|
||||
"The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES");
|
||||
|
||||
unsigned mem = 0;
|
||||
unsigned pinsIndex = 0;
|
||||
unsigned digitalCount = 0;
|
||||
for (unsigned i = 0; i < WLED_MAX_BUSSES; i++) {
|
||||
uint8_t defPin[OUTPUT_MAX_PINS];
|
||||
// if we have less types than requested outputs and they do not align, use last known type to set current type
|
||||
@@ -321,16 +331,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
unsigned start = 0;
|
||||
// analog always has length 1
|
||||
if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1;
|
||||
BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0);
|
||||
mem += defCfg.memUsage(Bus::isDigital(dataType) && !Bus::is2Pin(dataType) ? digitalCount++ : 0);
|
||||
if (mem > MAX_LED_MEMORY) {
|
||||
DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)dataType, (int)count, digitalCount);
|
||||
break;
|
||||
}
|
||||
busConfigs.push_back(defCfg); // use push_back for simplification as we needed defCfg to calculate memory usage
|
||||
busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, LED_MILLIAMPS_DEFAULT, ABL_MILLIAMPS_DEFAULT, 0); // driver=0 (RMT default)
|
||||
doInitBusses = true; // finalization done in beginStrip()
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem, BusManager::memUsage());
|
||||
}
|
||||
if (hw_led["rev"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus
|
||||
|
||||
@@ -354,97 +357,91 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
JsonArray hw_btn_ins = btn_obj["ins"];
|
||||
if (!hw_btn_ins.isNull()) {
|
||||
// deallocate existing button pins
|
||||
for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
|
||||
for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
|
||||
buttons.clear(); // clear existing buttons
|
||||
unsigned s = 0;
|
||||
for (JsonObject btn : hw_btn_ins) {
|
||||
CJSON(buttonType[s], btn["type"]);
|
||||
int8_t pin = btn["pin"][0] | -1;
|
||||
uint8_t type = btn["type"] | BTN_TYPE_NONE;
|
||||
int8_t pin = btn["pin"][0] | -1;
|
||||
if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) {
|
||||
btnPin[s] = pin;
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// ESP32 only: check that analog button pin is a valid ADC gpio
|
||||
if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) {
|
||||
if (digitalPinToAnalogChannel(btnPin[s]) < 0) {
|
||||
if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) {
|
||||
if (digitalPinToAnalogChannel(pin) < 0) {
|
||||
// not an ADC analog pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s);
|
||||
btnPin[s] = -1;
|
||||
PinManager::deallocatePin(pin,PinOwner::Button);
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s);
|
||||
PinManager::deallocatePin(pin, PinOwner::Button);
|
||||
pin = -1;
|
||||
continue;
|
||||
} else {
|
||||
analogReadResolution(12); // see #4040
|
||||
}
|
||||
}
|
||||
else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH))
|
||||
{
|
||||
if (digitalPinToTouchChannel(btnPin[s]) < 0) {
|
||||
} else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) {
|
||||
if (digitalPinToTouchChannel(pin) < 0) {
|
||||
// not a touch pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s);
|
||||
btnPin[s] = -1;
|
||||
PinManager::deallocatePin(pin,PinOwner::Button);
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s);
|
||||
PinManager::deallocatePin(pin, PinOwner::Button);
|
||||
pin = -1;
|
||||
continue;
|
||||
}
|
||||
//if touch pin, enable the touch interrupt on ESP32 S2 & S3
|
||||
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so
|
||||
else
|
||||
{
|
||||
touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
}
|
||||
else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
#endif
|
||||
}
|
||||
else
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// regular buttons and switches
|
||||
if (disablePullUp) {
|
||||
pinMode(btnPin[s], INPUT);
|
||||
pinMode(pin, INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(btnPin[s], INPUT_PULLUP);
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else {
|
||||
btnPin[s] = -1;
|
||||
JsonArray hw_btn_ins_0_macros = btn["macros"];
|
||||
uint8_t press = hw_btn_ins_0_macros[0] | 0;
|
||||
uint8_t longPress = hw_btn_ins_0_macros[1] | 0;
|
||||
uint8_t doublePress = hw_btn_ins_0_macros[2] | 0;
|
||||
buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector
|
||||
}
|
||||
JsonArray hw_btn_ins_0_macros = btn["macros"];
|
||||
CJSON(macroButton[s], hw_btn_ins_0_macros[0]);
|
||||
CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]);
|
||||
CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]);
|
||||
if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached
|
||||
}
|
||||
// clear remaining buttons
|
||||
for (; s<WLED_MAX_BUTTONS; s++) {
|
||||
btnPin[s] = -1;
|
||||
buttonType[s] = BTN_TYPE_NONE;
|
||||
macroButton[s] = 0;
|
||||
macroLongPress[s] = 0;
|
||||
macroDoublePress[s] = 0;
|
||||
}
|
||||
} else if (fromFS) {
|
||||
// new install/missing configuration (button 0 has defaults)
|
||||
// relies upon only being called once with fromFS == true, which is currently true.
|
||||
for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) {
|
||||
if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) {
|
||||
btnPin[s] = -1;
|
||||
buttonType[s] = BTN_TYPE_NONE;
|
||||
constexpr uint8_t defTypes[] = {BTNTYPE};
|
||||
constexpr int8_t defPins[] = {BTNPIN};
|
||||
constexpr unsigned numTypes = (sizeof(defTypes) / sizeof(defTypes[0]));
|
||||
constexpr unsigned numPins = (sizeof(defPins) / sizeof(defPins[0]));
|
||||
// check if the number of pins and types are valid; count of pins must be greater than or equal to types
|
||||
static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE");
|
||||
|
||||
uint8_t type = BTN_TYPE_NONE;
|
||||
buttons.clear(); // clear existing buttons (just in case)
|
||||
for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) {
|
||||
type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins
|
||||
if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) {
|
||||
if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined)
|
||||
continue; // pin not available or invalid, skip configuring this GPIO
|
||||
}
|
||||
if (btnPin[s] >= 0) {
|
||||
if (disablePullUp) {
|
||||
pinMode(btnPin[s], INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(btnPin[s], INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
if (disablePullUp) {
|
||||
pinMode(defPins[s], INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(defPins[s], INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
macroButton[s] = 0;
|
||||
macroLongPress[s] = 0;
|
||||
macroDoublePress[s] = 0;
|
||||
buttons.emplace_back(defPins[s], type); // add button to vector
|
||||
}
|
||||
}
|
||||
|
||||
CJSON(buttonPublishMqtt,btn_obj["mqtt"]);
|
||||
CJSON(buttonPublishMqtt, btn_obj["mqtt"]);
|
||||
|
||||
#ifndef WLED_DISABLE_INFRARED
|
||||
int hw_ir_pin = hw["ir"]["pin"] | -2; // 4
|
||||
@@ -521,13 +518,13 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
CJSON(strip.autoSegments, light[F("aseg")]);
|
||||
|
||||
CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.2
|
||||
float light_gc_bri = light["gc"]["bri"];
|
||||
float light_gc_col = light["gc"]["col"];
|
||||
if (light_gc_bri > 1.0f) gammaCorrectBri = true;
|
||||
else gammaCorrectBri = false;
|
||||
if (light_gc_col > 1.0f) gammaCorrectCol = true;
|
||||
else gammaCorrectCol = false;
|
||||
if (gammaCorrectVal <= 1.0f || gammaCorrectVal > 3) {
|
||||
float light_gc_bri = light["gc"]["bri"] | 1.0f; // default to 1.0 (false)
|
||||
float light_gc_col = light["gc"]["col"] | gammaCorrectVal; // default to gammaCorrectVal (true)
|
||||
if (light_gc_bri != 1.0f) gammaCorrectBri = true;
|
||||
else gammaCorrectBri = false;
|
||||
if (light_gc_col != 1.0f) gammaCorrectCol = true;
|
||||
else gammaCorrectCol = false;
|
||||
if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) {
|
||||
gammaCorrectVal = 1.0f; // no gamma correction
|
||||
gammaCorrectBri = false;
|
||||
gammaCorrectCol = false;
|
||||
@@ -694,37 +691,28 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
CJSON(macroCountdown, cntdwn["macro"]);
|
||||
setCountdown();
|
||||
|
||||
JsonArray timers = tm["ins"];
|
||||
uint8_t it = 0;
|
||||
for (JsonObject timer : timers) {
|
||||
if (it > 9) break;
|
||||
if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset
|
||||
CJSON(timerHours[it], timer[F("hour")]);
|
||||
CJSON(timerMinutes[it], timer["min"]);
|
||||
CJSON(timerMacro[it], timer["macro"]);
|
||||
|
||||
byte dowPrev = timerWeekday[it];
|
||||
//note: act is currently only 0 or 1.
|
||||
//the reason we are not using bool is that the on-disk type in 0.11.0 was already int
|
||||
int actPrev = timerWeekday[it] & 0x01;
|
||||
CJSON(timerWeekday[it], timer[F("dow")]);
|
||||
if (timerWeekday[it] != dowPrev) { //present in JSON
|
||||
timerWeekday[it] <<= 1; //add active bit
|
||||
int act = timer["en"] | actPrev;
|
||||
if (act) timerWeekday[it]++;
|
||||
JsonArray timersArray = tm["ins"];
|
||||
if (!timersArray.isNull()) {
|
||||
clearTimers();
|
||||
for (JsonObject timer : timersArray) {
|
||||
uint8_t h = timer[F("hour")] | 0;
|
||||
int8_t m = timer[F("min")] | 0;
|
||||
uint8_t p = timer[F("macro")] | 0;
|
||||
uint8_t dow = timer[F("dow")] | 127;
|
||||
uint8_t wd = (dow << 1) | ((timer[F("en")] | 0) ? 1 : 0);
|
||||
uint8_t ms = 1, me = 12, ds = 1, de = 31;
|
||||
JsonObject start = timer[F("start")];
|
||||
if (!start.isNull()) {
|
||||
ms = start[F("mon")] | 1;
|
||||
ds = start[F("day")] | 1;
|
||||
}
|
||||
JsonObject end = timer[F("end")];
|
||||
if (!end.isNull()) {
|
||||
me = end[F("mon")] | 12;
|
||||
de = end[F("day")] | 31;
|
||||
}
|
||||
addTimer(p, h, m, wd, ms, me, ds, de);
|
||||
}
|
||||
if (it<8) {
|
||||
JsonObject start = timer["start"];
|
||||
byte startm = start["mon"];
|
||||
if (startm) timerMonth[it] = (startm << 4);
|
||||
CJSON(timerDay[it], start["day"]);
|
||||
JsonObject end = timer["end"];
|
||||
CJSON(timerDayEnd[it], end["day"]);
|
||||
byte endm = end["mon"];
|
||||
if (startm) timerMonth[it] += endm & 0x0F;
|
||||
if (!(timerMonth[it] & 0x0F)) timerMonth[it] += 12; //default end month to 12
|
||||
}
|
||||
it++;
|
||||
}
|
||||
|
||||
JsonObject ota = doc["ota"];
|
||||
@@ -786,6 +774,10 @@ bool verifyConfig() {
|
||||
return validateJsonFile(s_cfg_json);
|
||||
}
|
||||
|
||||
bool configBackupExists() {
|
||||
return checkBackupExists(s_cfg_json);
|
||||
}
|
||||
|
||||
// rename config file and reboot
|
||||
// if the cfg file doesn't exist, such as after a reset, do nothing
|
||||
void resetConfig() {
|
||||
@@ -799,19 +791,14 @@ void resetConfig() {
|
||||
}
|
||||
|
||||
bool deserializeConfigFromFS() {
|
||||
[[maybe_unused]] bool success = deserializeConfigSec();
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
if (!success) { //if file does not exist, try reading from EEPROM
|
||||
deEEPSettings();
|
||||
}
|
||||
#endif
|
||||
(void) deserializeConfigSec(); // success/failure is ignored intentionally. We need to read cfg.json even when wsec.json has errors.
|
||||
|
||||
if (!requestJSONBufferLock(1)) return false;
|
||||
if (!requestJSONBufferLock(JSON_LOCK_CFG_DES)) return false;
|
||||
|
||||
DEBUG_PRINTLN(F("Reading settings from /cfg.json..."));
|
||||
|
||||
success = readObjectFromFile(s_cfg_json, nullptr, pDoc);
|
||||
|
||||
bool success = readObjectFromFile(s_cfg_json, nullptr, pDoc);
|
||||
if (!success || (pDoc->overflowed())) pDoc->clear(); // corrupted/too-large → same as missing: seed defaults
|
||||
// NOTE: This routine deserializes *and* applies the configuration
|
||||
// Therefore, must also initialize ethernet from this function
|
||||
JsonObject root = pDoc->as<JsonObject>();
|
||||
@@ -827,7 +814,7 @@ void serializeConfigToFS() {
|
||||
|
||||
DEBUG_PRINTLN(F("Writing settings to /cfg.json..."));
|
||||
|
||||
if (!requestJSONBufferLock(2)) return;
|
||||
if (!requestJSONBufferLock(JSON_LOCK_CFG_SER)) return;
|
||||
|
||||
JsonObject root = pDoc->to<JsonObject>();
|
||||
|
||||
@@ -881,6 +868,13 @@ void serializeConfig(JsonObject root) {
|
||||
wifi_gw.add(multiWiFi[n].staticGW[i]);
|
||||
wifi_sn.add(multiWiFi[n].staticSN[i]);
|
||||
}
|
||||
#ifdef WLED_ENABLE_WPA_ENTERPRISE
|
||||
wifi[F("enc_type")] = multiWiFi[n].encryptionType;
|
||||
if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_ENTERPRISE) {
|
||||
wifi[F("e_anon_ident")] = multiWiFi[n].enterpriseAnonIdentity;
|
||||
wifi[F("e_ident")] = multiWiFi[n].enterpriseIdentity;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JsonArray dns = nw.createNestedArray(F("dns"));
|
||||
@@ -944,9 +938,6 @@ void serializeConfig(JsonObject root) {
|
||||
hw_led[F("cb")] = Bus::getCCTBlend();
|
||||
hw_led["fps"] = strip.getTargetFps();
|
||||
hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override
|
||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
hw_led[F("prl")] = BusManager::hasParallelOutput();
|
||||
#endif
|
||||
|
||||
#ifndef WLED_DISABLE_2D
|
||||
// 2D Matrix Settings
|
||||
@@ -973,7 +964,7 @@ void serializeConfig(JsonObject root) {
|
||||
for (size_t s = 0; s < BusManager::getNumBusses(); s++) {
|
||||
DEBUG_PRINTF_P(PSTR("Cfg: Saving bus #%u\n"), s);
|
||||
const Bus *bus = BusManager::getBus(s);
|
||||
if (!bus || !bus->isOk()) break;
|
||||
if (!bus) break; // Memory corruption, iterator invalid
|
||||
DEBUG_PRINTF_P(PSTR(" (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"),
|
||||
(int)bus->getStart(), (int)(bus->getStart()+bus->getLength()),
|
||||
(int)(bus->getType() & 0x7F),
|
||||
@@ -1000,6 +991,7 @@ void serializeConfig(JsonObject root) {
|
||||
ins[F("freq")] = bus->getFrequency();
|
||||
ins[F("maxpwr")] = bus->getMaxCurrent();
|
||||
ins[F("ledma")] = bus->getLEDCurrent();
|
||||
ins[F("drv")] = bus->getDriverType();
|
||||
ins[F("text")] = bus->getCustomText();
|
||||
}
|
||||
|
||||
@@ -1021,15 +1013,15 @@ void serializeConfig(JsonObject root) {
|
||||
JsonArray hw_btn_ins = hw_btn.createNestedArray("ins");
|
||||
|
||||
// configuration for all buttons
|
||||
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
|
||||
for (const auto &button : buttons) {
|
||||
JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject();
|
||||
hw_btn_ins_0["type"] = buttonType[i];
|
||||
hw_btn_ins_0["type"] = button.type;
|
||||
JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin");
|
||||
hw_btn_ins_0_pin.add(btnPin[i]);
|
||||
hw_btn_ins_0_pin.add(button.pin);
|
||||
JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros");
|
||||
hw_btn_ins_0_macros.add(macroButton[i]);
|
||||
hw_btn_ins_0_macros.add(macroLongPress[i]);
|
||||
hw_btn_ins_0_macros.add(macroDoublePress[i]);
|
||||
hw_btn_ins_0_macros.add(button.macroButton);
|
||||
hw_btn_ins_0_macros.add(button.macroLongPress);
|
||||
hw_btn_ins_0_macros.add(button.macroDoublePress);
|
||||
}
|
||||
|
||||
hw_btn[F("tt")] = touchThreshold;
|
||||
@@ -1217,23 +1209,21 @@ void serializeConfig(JsonObject root) {
|
||||
cntdwn["macro"] = macroCountdown;
|
||||
|
||||
JsonArray timers_ins = timers.createNestedArray("ins");
|
||||
|
||||
for (unsigned i = 0; i < 10; i++) {
|
||||
if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; // sunrise/sunset get saved always (timerHours=255)
|
||||
JsonObject timers_ins0 = timers_ins.createNestedObject();
|
||||
timers_ins0["en"] = (timerWeekday[i] & 0x01);
|
||||
timers_ins0[F("hour")] = timerHours[i];
|
||||
timers_ins0["min"] = timerMinutes[i];
|
||||
timers_ins0["macro"] = timerMacro[i];
|
||||
timers_ins0[F("dow")] = timerWeekday[i] >> 1;
|
||||
if (i<8) {
|
||||
JsonObject start = timers_ins0.createNestedObject("start");
|
||||
start["mon"] = (timerMonth[i] >> 4) & 0xF;
|
||||
start["day"] = timerDay[i];
|
||||
JsonObject end = timers_ins0.createNestedObject("end");
|
||||
end["mon"] = timerMonth[i] & 0xF;
|
||||
end["day"] = timerDayEnd[i];
|
||||
}
|
||||
for (size_t i = 0; i < ::timers.size(); i++) {
|
||||
const Timer& t = ::timers[i];
|
||||
if (t.preset == 0 && t.hour == 0 && t.minute == 0) continue;
|
||||
JsonObject ti = timers_ins.createNestedObject();
|
||||
ti[F("en")] = t.isEnabled() ? 1 : 0;
|
||||
ti[F("hour")] = t.hour;
|
||||
ti[F("min")] = t.minute;
|
||||
ti[F("macro")] = t.preset;
|
||||
ti[F("dow")] = t.weekdays >> 1;
|
||||
JsonObject start = ti.createNestedObject(F("start"));
|
||||
start[F("mon")] = t.monthStart;
|
||||
start[F("day")] = t.dayStart;
|
||||
JsonObject end = ti.createNestedObject(F("end"));
|
||||
end[F("mon")] = t.monthEnd;
|
||||
end[F("day")] = t.dayEnd;
|
||||
}
|
||||
|
||||
JsonObject ota = root.createNestedObject("ota");
|
||||
@@ -1271,10 +1261,10 @@ static const char s_wsec_json[] PROGMEM = "/wsec.json";
|
||||
bool deserializeConfigSec() {
|
||||
DEBUG_PRINTLN(F("Reading settings from /wsec.json..."));
|
||||
|
||||
if (!requestJSONBufferLock(3)) return false;
|
||||
if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_DES)) return false;
|
||||
|
||||
bool success = readObjectFromFile(s_wsec_json, nullptr, pDoc);
|
||||
if (!success) {
|
||||
if (!success || pDoc->overflowed() || pDoc->size() < 1) { // reject if overflowed (noMemory) or empty
|
||||
releaseJSONBufferLock();
|
||||
return false;
|
||||
}
|
||||
@@ -1284,7 +1274,8 @@ bool deserializeConfigSec() {
|
||||
size_t n = 0;
|
||||
JsonArray nw_ins = root["nw"]["ins"];
|
||||
if (!nw_ins.isNull()) {
|
||||
if (nw_ins.size() > 1 && nw_ins.size() > multiWiFi.size()) multiWiFi.resize(nw_ins.size()); // resize constructs objects while resizing
|
||||
size_t newSize = min((size_t)WLED_MAX_WIFI_COUNT, nw_ins.size()); // cap at WLED_MAX_WIFI_COUNT (prevent oversizing when too many Wi-Fi entries)
|
||||
if (nw_ins.size() > 1 && newSize > multiWiFi.size()) multiWiFi.resize(newSize); // resize constructs objects while resizing
|
||||
for (JsonObject wifi : nw_ins) {
|
||||
char pw[65] = "";
|
||||
getStringFromJson(pw, wifi["psk"], 65);
|
||||
@@ -1325,7 +1316,7 @@ bool deserializeConfigSec() {
|
||||
void serializeConfigSec() {
|
||||
DEBUG_PRINTLN(F("Writing settings to /wsec.json..."));
|
||||
|
||||
if (!requestJSONBufferLock(4)) return;
|
||||
if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_SER)) return;
|
||||
|
||||
JsonObject root = pDoc->to<JsonObject>();
|
||||
|
||||
|
||||
+157
-105
@@ -1,14 +1,23 @@
|
||||
#include "wled.h"
|
||||
#include "fcn_declare.h"
|
||||
#include "colors.h"
|
||||
|
||||
/*
|
||||
* Color conversion & utility methods
|
||||
*/
|
||||
|
||||
/*
|
||||
* color blend function, based on FastLED blend function
|
||||
* the calculation for each color is: result = (A*(amountOfA) + A + B*(amountOfB) + B) / 256 with amountOfA = 255 - amountOfB
|
||||
* FastLED Reference
|
||||
* -----------------
|
||||
* functions in this file derived from FastLED @ 3.6.0 (https://github.com/FastLED/FastLED) are marked with a comment containing "derived from FastLED"
|
||||
* those functions are therefore licensed under the MIT license See /src/dependencies/fastled_slim/LICENSE.txt for details.
|
||||
*/
|
||||
uint32_t IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) {
|
||||
|
||||
/*
|
||||
* color blend function
|
||||
* the calculation for each color is: result = (C1*(256-blend)+C2+C2*blend) / 256
|
||||
*/
|
||||
uint32_t WLED_O2_ATTR IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) {
|
||||
// min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance
|
||||
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated (poorman's SIMD; https://github.com/wled/WLED/pull/4568#discussion_r1986587221)
|
||||
uint32_t rb1 = color1 & TWO_CHANNEL_MASK; // extract R & B channels from color1
|
||||
@@ -25,86 +34,88 @@ uint32_t IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend)
|
||||
* original idea: https://github.com/wled-dev/WLED/pull/2465 by https://github.com/Proto-molecule
|
||||
* speed optimisations by @dedehai
|
||||
*/
|
||||
uint32_t color_add(uint32_t c1, uint32_t c2, bool preserveCR)
|
||||
uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR)
|
||||
{
|
||||
if (c1 == BLACK) return c2;
|
||||
if (c2 == BLACK) return c1;
|
||||
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated
|
||||
uint32_t rb = ( c1 & TWO_CHANNEL_MASK) + ( c2 & TWO_CHANNEL_MASK); // mask and add two colors at once
|
||||
uint32_t wg = ((c1>>8) & TWO_CHANNEL_MASK) + ((c2>>8) & TWO_CHANNEL_MASK);
|
||||
uint32_t r = rb >> 16; // extract single color values
|
||||
uint32_t b = rb & 0xFFFF;
|
||||
uint32_t w = wg >> 16;
|
||||
uint32_t g = wg & 0xFFFF;
|
||||
|
||||
if (preserveCR) { // preserve color ratios
|
||||
uint32_t max = std::max(r,g); // check for overflow note
|
||||
max = std::max(max,b);
|
||||
max = std::max(max,w);
|
||||
//unsigned max = r; // check for overflow note
|
||||
//max = g > max ? g : max;
|
||||
//max = b > max ? b : max;
|
||||
//max = w > max ? w : max;
|
||||
if (max > 255) {
|
||||
const uint32_t scale = (uint32_t(255)<<8) / max; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead
|
||||
uint32_t overflow = (rb | wg) & 0x01000100; // detect overflow by checking 9th bit
|
||||
if (overflow) {
|
||||
uint32_t r = rb >> 16; // extract single color values
|
||||
uint32_t b = rb & 0xFFFF;
|
||||
uint32_t w = wg >> 16;
|
||||
uint32_t g = wg & 0xFFFF;
|
||||
uint32_t maxval = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // find max value. note: faster than using max() function or on par
|
||||
maxval = (w > maxval) ? w : maxval; // check white channel as well to avoid division by zero in pure white input
|
||||
const uint32_t scale = (uint32_t(255)<<8) / maxval; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead
|
||||
rb = ((rb * scale) >> 8) & TWO_CHANNEL_MASK;
|
||||
wg = (wg * scale) & ~TWO_CHANNEL_MASK;
|
||||
} else wg <<= 8; //shift white and green back to correct position
|
||||
return rb | wg;
|
||||
} else {
|
||||
r = r > 255 ? 255 : r;
|
||||
g = g > 255 ? 255 : g;
|
||||
b = b > 255 ? 255 : b;
|
||||
w = w > 255 ? 255 : w;
|
||||
return RGBW32(r,g,b,w);
|
||||
// branchless per-channel saturation to 255 (extract 9th bit, subtract 1 if it is set, mask with 0xFF, input is 0xFF+0xFF=0x1EF max)
|
||||
// example with overflow: input: 0x01EF01EF -> (0x0100100 - 0x00010001) = 0x00FF00FF -> input|0x00FF00FF = 0x00FF00FF (saturate)
|
||||
// example without overflow: input: 0x007F007F -> (0x00000000 - 0x00000000) = 0x00000000 -> input|0x00000000 = input (no change)
|
||||
rb |= ((rb & 0x01000100) - ((rb >> 8) & 0x00010001)) & 0x00FF00FF;
|
||||
wg |= ((wg & 0x01000100) - ((wg >> 8) & 0x00010001)) & 0x00FF00FF;
|
||||
wg <<= 8; // restore WG position
|
||||
}
|
||||
return rb | wg;
|
||||
}
|
||||
|
||||
/*
|
||||
* fades color toward black
|
||||
* if using "video" method the resulting color will never become black unless it is already black
|
||||
* if using "video" method the resulting color will not become black unless it is already black or distorts the hue
|
||||
*/
|
||||
uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) {
|
||||
if (c1 == 0 || amount == 0) return 0; // black or no change
|
||||
if (amount == 255) return c1;
|
||||
uint32_t addRemains = 0;
|
||||
|
||||
if (!video) amount++; // add one for correct scaling using bitshifts
|
||||
else {
|
||||
// video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue
|
||||
uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels
|
||||
uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation
|
||||
uint8_t quarterMax = maxc >> 2; // note: using half of max results in color artefacts
|
||||
addRemains = r && r > quarterMax ? 0x00010000 : 0;
|
||||
addRemains |= g && g > quarterMax ? 0x00000100 : 0;
|
||||
addRemains |= b && b > quarterMax ? 0x00000001 : 0;
|
||||
addRemains |= w ? 0x01000000 : 0;
|
||||
}
|
||||
if (c1 == BLACK || amount == 0) return 0; // black or full fade
|
||||
if (amount == 255) return c1; // no change
|
||||
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
|
||||
uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue
|
||||
uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; // scale white and green
|
||||
return (rb | wg) + addRemains;
|
||||
uint32_t rb = c1 & TWO_CHANNEL_MASK; // extract R and B channels
|
||||
uint32_t wg = (c1 >> 8) & TWO_CHANNEL_MASK; // extract W and G channels (shifted for multiplication)
|
||||
uint32_t rb_scaled;
|
||||
uint32_t wg_scaled;
|
||||
|
||||
// video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue
|
||||
if (video) {
|
||||
rb_scaled = ((rb * amount + 0x007F007F) >> 8) & TWO_CHANNEL_MASK; // scale red and blue, add 0.5 for rounding
|
||||
wg_scaled = (wg * amount + 0x007F007F) & ~TWO_CHANNEL_MASK; // scale white and green, add 0.5 for rounding
|
||||
uint8_t r = byte(rb>>16), g = byte(wg), b = byte(rb), w = byte(wg>>16); // extract r, g, b, w channels from original color (wg is shifted)
|
||||
uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation
|
||||
maxc = (maxc>>2) + 1; // divide by 4 to get ~25% threshold for hue preservation, add 1 to prevent "washout" of very dark colors (prevents them becoming gray)
|
||||
rb_scaled |= r > maxc ? 0x00010000 : 0;
|
||||
wg_scaled |= g > maxc ? 0x00000100 : 0;
|
||||
rb_scaled |= b > maxc ? 0x00000001 : 0;
|
||||
wg_scaled |= w ? 0x01000000 : 0; // preserve white if it is present
|
||||
} else {
|
||||
rb_scaled = ((rb * (amount + 1)) >> 8) & TWO_CHANNEL_MASK; // scale red and blue
|
||||
wg_scaled = ((wg * (amount + 1)) & ~TWO_CHANNEL_MASK); // scale white and green
|
||||
}
|
||||
|
||||
return (rb_scaled | wg_scaled);
|
||||
}
|
||||
|
||||
/*
|
||||
* color adjustment in HSV color space (converts RGB to HSV and back), color conversions are not 100% accurate!
|
||||
shifts hue, increase brightness, decreases saturation (if not black)
|
||||
note: inputs are 32bit to speed up the function, useful input value ranges are 0-255
|
||||
* shifts hue, increase brightness, decreases saturation (if not black)
|
||||
* note: inputs are 32bit to speed up the function, useful input value ranges are -255 to +255
|
||||
* note2: if only one hue change is needed, use CRGBW.adjust_hue() instead (much faster)
|
||||
*/
|
||||
uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten) {
|
||||
if (rgb == 0 | hueShift + lighten + brighten == 0) return rgb; // black or no change
|
||||
CHSV32 hsv;
|
||||
rgb2hsv(rgb, hsv); //convert to HSV
|
||||
hsv.h += (hueShift << 8); // shift hue (hue is 16 bits)
|
||||
hsv.s = max((int32_t)0, (int32_t)hsv.s - (int32_t)lighten); // desaturate
|
||||
hsv.v = min((uint32_t)255, (uint32_t)hsv.v + brighten); // increase brightness
|
||||
uint32_t rgb_adjusted;
|
||||
hsv2rgb(hsv, rgb_adjusted); // convert back to RGB TODO: make this into 16 bit conversion
|
||||
return rgb_adjusted;
|
||||
WLED_O3_ATTR void adjust_color(CRGBW& rgb, int32_t hueShift, int32_t satChange, int32_t valueChange) {
|
||||
if(rgb.color32 == 0 && valueChange <= 0) return; // black and no value change -> return black
|
||||
CHSV32 hsv;
|
||||
rgb2hsv(rgb, hsv); //convert to HSV
|
||||
hsv.h += (hueShift << 8); // shift hue (hue is 16 bits)
|
||||
hsv.s = (int)hsv.s + satChange < 0 ? 0 : ((int)hsv.s + satChange > 255 ? 255 : (int)hsv.s + satChange);
|
||||
hsv.v = (int)hsv.v + valueChange < 0 ? 0 : ((int)hsv.v + valueChange > 255 ? 255 : (int)hsv.v + valueChange);
|
||||
hsv2rgb_spectrum(hsv, rgb); // convert back to RGB
|
||||
}
|
||||
|
||||
// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes)
|
||||
uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) {
|
||||
// derived from FastLED: replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes)
|
||||
uint32_t ColorFromPalette(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) {
|
||||
if (blendType == LINEARBLEND_NOWRAP) {
|
||||
index = (index * 0xF0) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping
|
||||
}
|
||||
@@ -252,12 +263,13 @@ void loadCustomPalettes() {
|
||||
byte tcp[72]; //support gradient palettes with up to 18 entries
|
||||
CRGBPalette16 targetPalette;
|
||||
customPalettes.clear(); // start fresh
|
||||
for (int index = 0; index<10; index++) {
|
||||
StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers -> TODO: current format uses 214 bytes max per palette, why is this buffer so large?
|
||||
unsigned emptyPaletteGap = 0; // count gaps in palette files to stop looking for more (each exists() call takes ~5ms)
|
||||
for (int index = 0; index < WLED_MAX_CUSTOM_PALETTES; index++) {
|
||||
char fileName[32];
|
||||
sprintf_P(fileName, PSTR("/palette%d.json"), index);
|
||||
|
||||
StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers
|
||||
if (WLED_FS.exists(fileName)) {
|
||||
emptyPaletteGap = 0; // reset gap counter if file exists
|
||||
DEBUGFX_PRINTF_P(PSTR("Reading palette from %s\n"), fileName);
|
||||
if (readObjectFromFile(fileName, nullptr, &pDoc)) {
|
||||
JsonArray pal = pDoc[F("palette")];
|
||||
@@ -291,69 +303,109 @@ void loadCustomPalettes() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
emptyPaletteGap++;
|
||||
if (emptyPaletteGap > WLED_MAX_CUSTOM_PALETTE_GAP) break; // stop looking for more palettes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0)
|
||||
{
|
||||
unsigned int remainder, region, p, q, t;
|
||||
unsigned int h = hsv.h;
|
||||
unsigned int s = hsv.s;
|
||||
unsigned int v = hsv.v;
|
||||
if (s == 0) {
|
||||
rgb = v << 16 | v << 8 | v;
|
||||
return;
|
||||
// convert HSV (16bit hue) to RGB (32bit with white = 0), optimized for speed
|
||||
WLED_O2_ATTR void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb) {
|
||||
unsigned p, q, t;
|
||||
unsigned region = ((unsigned)hsv.h * 6) >> 16; // h / (65536 / 6)
|
||||
unsigned remainder = (hsv.h - (region * 10923)) * 6; // 10923 = (65536 / 6)
|
||||
|
||||
// check for zero saturation
|
||||
if (hsv.s == 0) {
|
||||
rgb.r = rgb.g = rgb.b = hsv.v;
|
||||
return;
|
||||
}
|
||||
region = h / 10923; // 65536 / 6 = 10923
|
||||
remainder = (h - (region * 10923)) * 6;
|
||||
p = (v * (255 - s)) >> 8;
|
||||
q = (v * (255 - ((s * remainder) >> 16))) >> 8;
|
||||
t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8;
|
||||
|
||||
p = (hsv.v * (255 - hsv.s)) >> 8;
|
||||
q = (hsv.v * (255 - ((hsv.s * remainder) >> 16))) >> 8;
|
||||
t = (hsv.v * (255 - ((hsv.s * (65535 - remainder)) >> 16))) >> 8;
|
||||
switch (region) {
|
||||
case 0:
|
||||
rgb = v << 16 | t << 8 | p; break;
|
||||
rgb.r = hsv.v;
|
||||
rgb.g = t;
|
||||
rgb.b = p;
|
||||
break;
|
||||
case 1:
|
||||
rgb = q << 16 | v << 8 | p; break;
|
||||
rgb.r = q;
|
||||
rgb.g = hsv.v;
|
||||
rgb.b = p;
|
||||
break;
|
||||
case 2:
|
||||
rgb = p << 16 | v << 8 | t; break;
|
||||
rgb.r = p;
|
||||
rgb.g = hsv.v;
|
||||
rgb.b = t;
|
||||
break;
|
||||
case 3:
|
||||
rgb = p << 16 | q << 8 | v; break;
|
||||
rgb.r = p;
|
||||
rgb.g = q;
|
||||
rgb.b = hsv.v;
|
||||
break;
|
||||
case 4:
|
||||
rgb = t << 16 | p << 8 | v; break;
|
||||
rgb.r = t;
|
||||
rgb.g = p;
|
||||
rgb.b = hsv.v;
|
||||
break;
|
||||
default:
|
||||
rgb = v << 16 | p << 8 | q; break;
|
||||
rgb.r = hsv.v;
|
||||
rgb.g = p;
|
||||
rgb.b = q;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version
|
||||
{
|
||||
hsv.raw = 0;
|
||||
int32_t r = (rgb>>16)&0xFF;
|
||||
int32_t g = (rgb>>8)&0xFF;
|
||||
int32_t b = rgb&0xFF;
|
||||
int32_t minval, maxval, delta;
|
||||
minval = min(r, g);
|
||||
minval = min(minval, b);
|
||||
maxval = max(r, g);
|
||||
maxval = max(maxval, b);
|
||||
if (maxval == 0) return; // black
|
||||
hsv.v = maxval;
|
||||
delta = maxval - minval;
|
||||
// CHSV to CRGB wrapper conversion: slower so this should not be used in time critical code, use rainbow version instead
|
||||
void hsv2rgb_spectrum(const CHSV& hsv, CRGB& rgb) {
|
||||
CHSV32 hsv32(hsv);
|
||||
CRGBW rgb32;
|
||||
hsv2rgb_spectrum(hsv32, rgb32);
|
||||
rgb = CRGB(rgb32);
|
||||
}
|
||||
|
||||
// convert RGB to HSV (16bit hue), not 100% color accurate. note: using "O3" makes it ~5% faster at minimal flash cost (~20 bytes)
|
||||
WLED_O3_ATTR void rgb2hsv(const CRGBW& rgb, CHSV32& hsv) {
|
||||
int32_t r = rgb.r; // note: using 32bit variables tested faster than 8bit
|
||||
int32_t g = rgb.g;
|
||||
int32_t b = rgb.b;
|
||||
uint32_t minval, maxval;
|
||||
int32_t delta;
|
||||
// find min/max value. note: faster than using min/max functions (lets compiler optimize more when using "O3"), other variants (nested ifs, xor) tested slower
|
||||
maxval = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b);
|
||||
if (maxval == 0) {
|
||||
hsv.hsv32 = 0;
|
||||
return; // black, avoids division by zero
|
||||
}
|
||||
minval = (r < g) ? ((r < b) ? r : b) : ((g < b) ? g : b);
|
||||
hsv.v = maxval;
|
||||
delta = maxval - minval;
|
||||
if (delta != 0) {
|
||||
hsv.s = (255 * delta) / maxval;
|
||||
if (hsv.s == 0) return; // gray value
|
||||
if (maxval == r) hsv.h = (10923 * (g - b)) / delta;
|
||||
else if (maxval == g) hsv.h = 21845 + (10923 * (b - r)) / delta;
|
||||
else hsv.h = 43690 + (10923 * (r - g)) / delta;
|
||||
// note: early return if s==0 is omitted here to increase speed as gray values are rarely used
|
||||
if (maxval == r) hsv.h = (uint16_t)((10923 * (g - b)) / delta);
|
||||
else if (maxval == g) hsv.h = (uint16_t)(21845 + (10923 * (b - r)) / delta);
|
||||
else hsv.h = (uint16_t)(43690 + (10923 * (r - g)) / delta);
|
||||
} else {
|
||||
hsv.s = 0;
|
||||
hsv.h = 0; // gray, hue is undefined but set to 0 for consistency
|
||||
}
|
||||
}
|
||||
|
||||
CHSV rgb2hsv(const CRGB c) { // CRGB to CHSV
|
||||
CHSV32 hsv;
|
||||
rgb2hsv(CRGBW(c), hsv);
|
||||
return CHSV(hsv);
|
||||
}
|
||||
|
||||
void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb
|
||||
uint32_t crgb;
|
||||
hsv2rgb(CHSV32(hue, sat, 255), crgb);
|
||||
rgb[0] = byte((crgb) >> 16);
|
||||
rgb[1] = byte((crgb) >> 8);
|
||||
rgb[2] = byte(crgb);
|
||||
CRGBW crgb;
|
||||
hsv2rgb_spectrum(CHSV32(hue, sat, 255), crgb);
|
||||
rgb[0] = crgb.r;
|
||||
rgb[1] = crgb.g;
|
||||
rgb[2] = crgb.b;
|
||||
}
|
||||
|
||||
//get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html)
|
||||
@@ -597,13 +649,13 @@ void NeoGammaWLEDMethod::calcGammaTable(float gamma)
|
||||
gammaT_inv[0] = 0;
|
||||
}
|
||||
|
||||
uint8_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct(uint8_t value)
|
||||
uint8_t NeoGammaWLEDMethod::Correct(uint8_t value)
|
||||
{
|
||||
if (!gammaCorrectCol) return value;
|
||||
return gammaT[value];
|
||||
}
|
||||
|
||||
uint32_t IRAM_ATTR_YN NeoGammaWLEDMethod::inverseGamma32(uint32_t color)
|
||||
uint32_t NeoGammaWLEDMethod::inverseGamma32(uint32_t color)
|
||||
{
|
||||
if (!gammaCorrectCol) return color;
|
||||
uint8_t w = W(color);
|
||||
|
||||
+154
-92
@@ -1,96 +1,34 @@
|
||||
#pragma once
|
||||
#ifndef WLED_COLORS_H
|
||||
#define WLED_COLORS_H
|
||||
|
||||
#include <vector>
|
||||
#include "src/dependencies/fastled_slim/fastled_slim.h"
|
||||
/*
|
||||
* Color structs and color utility functions
|
||||
*/
|
||||
#include <vector>
|
||||
#include "FastLED.h"
|
||||
/*
|
||||
Note on color types and conversions:
|
||||
- WLED uses 32bit colors (RGBW), if possible, use CRGBW instead of CRGB for better performance (no conversion in setPixelColor)
|
||||
- use CRGB if RAM usage is of concern (i.e. for larger color arrays)
|
||||
- direct conversion (assignment or construction) from CHSV/CHSV32 to CRGB/CRGBW use the "rainbow" method (nicer colors, see fastled documentation)
|
||||
- converting CRGB(W) to HSV32 color is quite accurate but still not 100% (but much more accurate than fastled's "hsv2rgb_approximate" function)
|
||||
- when converting CRGB(W) to HSV32 and back, "hsv2rgb_spectrum" preserves the colors better than the _rainbow version
|
||||
- to manipulate an RGB color in HSV space, use the adjust_color function or the CRGBW.adjust_hue method
|
||||
|
||||
#define ColorFromPalette ColorFromPaletteWLED // override fastled version
|
||||
Some functions in this file are derived from FastLED (https://github.com/FastLED/FastLED) licensed under the MIT license.
|
||||
See /src/dependencies/fastled_slim/LICENSE.txt for details.
|
||||
*/
|
||||
|
||||
// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color
|
||||
// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts
|
||||
// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB
|
||||
struct CRGBW {
|
||||
union {
|
||||
uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB)
|
||||
struct {
|
||||
uint8_t b;
|
||||
uint8_t g;
|
||||
uint8_t r;
|
||||
uint8_t w;
|
||||
};
|
||||
uint8_t raw[4]; // Access as an array in the order B, G, R, W
|
||||
};
|
||||
// 32bit color mangling macros
|
||||
#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))
|
||||
#define R(c) (byte((c) >> 16))
|
||||
#define G(c) (byte((c) >> 8))
|
||||
#define B(c) (byte(c))
|
||||
#define W(c) (byte((c) >> 24))
|
||||
|
||||
// Default constructor
|
||||
inline CRGBW() __attribute__((always_inline)) = default;
|
||||
struct CRGBW; // forward declations
|
||||
struct CHSV32;
|
||||
|
||||
// Constructor from a 32-bit color (0xWWRRGGBB)
|
||||
constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {}
|
||||
|
||||
// Constructor with r, g, b, w values
|
||||
constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {}
|
||||
|
||||
// Constructor from CRGB
|
||||
constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {}
|
||||
|
||||
// Access as an array
|
||||
inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; }
|
||||
|
||||
// Assignment from 32-bit color
|
||||
inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; }
|
||||
|
||||
// Assignment from r, g, b, w
|
||||
inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; }
|
||||
|
||||
// Conversion operator to uint32_t
|
||||
inline operator uint32_t() const __attribute__((always_inline)) {
|
||||
return color32;
|
||||
}
|
||||
/*
|
||||
// Conversion operator to CRGB
|
||||
inline operator CRGB() const __attribute__((always_inline)) {
|
||||
return CRGB(r, g, b);
|
||||
}
|
||||
|
||||
CRGBW& scale32 (uint8_t scaledown) // 32bit math
|
||||
{
|
||||
if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit
|
||||
uint32_t scale = scaledown + 1;
|
||||
uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue
|
||||
uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green
|
||||
color32 = rb | wg;
|
||||
return *this;
|
||||
}*/
|
||||
|
||||
};
|
||||
|
||||
struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions
|
||||
union {
|
||||
struct {
|
||||
uint16_t h; // hue
|
||||
uint8_t s; // saturation
|
||||
uint8_t v; // value
|
||||
};
|
||||
uint32_t raw; // 32bit access
|
||||
};
|
||||
inline CHSV32() __attribute__((always_inline)) = default; // default constructor
|
||||
|
||||
/// Allow construction from hue, saturation, and value
|
||||
/// @param ih input hue
|
||||
/// @param is input saturation
|
||||
/// @param iv input value
|
||||
inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v
|
||||
: h(ih), s(is), v(iv) {}
|
||||
inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v
|
||||
: h((uint16_t)ih << 8), s(is), v(iv) {}
|
||||
inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV
|
||||
: h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {}
|
||||
inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV
|
||||
};
|
||||
extern bool gammaCorrectCol;
|
||||
// similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod)
|
||||
class NeoGammaWLEDMethod {
|
||||
@@ -117,18 +55,21 @@ class NeoGammaWLEDMethod {
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend);
|
||||
inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); };
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false);
|
||||
[[gnu::hot, gnu::pure]] uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten);
|
||||
[[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND);
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false);
|
||||
void adjust_color(CRGBW& rgb, int32_t hueShift, int32_t satChange,int32_t valueChange);
|
||||
|
||||
[[gnu::hot, gnu::pure]] uint32_t ColorFromPalette(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND);
|
||||
CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette);
|
||||
CRGBPalette16 generateRandomPalette();
|
||||
void loadCustomPalettes();
|
||||
extern std::vector<CRGBPalette16> customPalettes;
|
||||
inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); }
|
||||
inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); }
|
||||
void hsv2rgb(const CHSV32& hsv, uint32_t& rgb);
|
||||
inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); }
|
||||
|
||||
void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb);
|
||||
void hsv2rgb_spectrum(const CHSV& hsv, CRGB& rgb);
|
||||
void rgb2hsv(const CRGBW& rgb, CHSV32& hsv);
|
||||
CHSV rgb2hsv(const CRGB c);
|
||||
void colorHStoRGB(uint16_t hue, byte sat, byte* rgb);
|
||||
void rgb2hsv(const uint32_t rgb, CHSV32& hsv);
|
||||
inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv
|
||||
void colorKtoRGB(uint16_t kelvin, byte* rgb);
|
||||
void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb
|
||||
void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO
|
||||
@@ -139,6 +80,127 @@ uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb);
|
||||
uint16_t approximateKelvinFromRGB(uint32_t rgb);
|
||||
void setRandomColor(byte* rgb);
|
||||
|
||||
[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false);
|
||||
// fast scaling function for colors, performs color*scale/256 for all four channels, speed over accuracy
|
||||
// note: inlining uses less code than actual function calls
|
||||
static inline uint32_t fast_color_scale(const uint32_t c, const uint8_t scale) {
|
||||
uint32_t rb = (((c & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF;
|
||||
uint32_t wg = (((c>>8) & 0x00FF00FF) * scale) & ~0x00FF00FF;
|
||||
return rb | wg;
|
||||
}
|
||||
|
||||
#endif
|
||||
// palettes
|
||||
extern const TProgmemRGBPalette16 PartyColors_gc22 PROGMEM;
|
||||
extern const TProgmemRGBPalette16* const fastledPalettes[];
|
||||
extern const uint8_t* const gGradientPalettes[];
|
||||
|
||||
|
||||
struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions
|
||||
union {
|
||||
struct {
|
||||
uint16_t h; // hue
|
||||
uint8_t s; // saturation
|
||||
uint8_t v; // value
|
||||
};
|
||||
uint32_t hsv32; // 32bit access
|
||||
};
|
||||
inline CHSV32() __attribute__((always_inline)) = default; // default constructor
|
||||
|
||||
// allow construction from hue (ih), saturation (is), and value (iv)
|
||||
inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v
|
||||
: h(ih), s(is), v(iv) {}
|
||||
|
||||
inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v
|
||||
: h((uint16_t)ih << 8), s(is), v(iv) {}
|
||||
|
||||
inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV
|
||||
: h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {}
|
||||
|
||||
inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV
|
||||
|
||||
// construction from a 32bit rgb color (white channel is ignored)
|
||||
inline CHSV32(const CRGBW& rgb) __attribute__((always_inline));
|
||||
|
||||
inline CHSV32& operator= (const CRGBW& rgb) __attribute__((always_inline)); // assignment from 32bit rgb color (white channel is ignored)
|
||||
};
|
||||
|
||||
// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color
|
||||
// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts
|
||||
// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB
|
||||
struct CRGBW {
|
||||
union {
|
||||
uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB)
|
||||
struct {
|
||||
uint8_t b;
|
||||
uint8_t g;
|
||||
uint8_t r;
|
||||
uint8_t w;
|
||||
};
|
||||
uint8_t raw[4]; // Access as an array in the order B, G, R, W (matches 32 bit colors)
|
||||
};
|
||||
|
||||
// Default constructor
|
||||
inline CRGBW() __attribute__((always_inline)) = default;
|
||||
|
||||
// Constructor from a 32-bit color (0xWWRRGGBB)
|
||||
constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {}
|
||||
|
||||
// Constructor with r, g, b, w values
|
||||
constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {}
|
||||
|
||||
// Constructor from CRGB
|
||||
constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {}
|
||||
|
||||
// Constructor from CHSV32
|
||||
inline CRGBW(CHSV32 hsv) __attribute__((always_inline)) { hsv2rgb_rainbow(hsv.h, hsv.s, hsv.v, raw, true); }
|
||||
|
||||
// Constructor from CHSV
|
||||
inline CRGBW(CHSV hsv) __attribute__((always_inline)) { hsv2rgb_rainbow(hsv.h<<8, hsv.s, hsv.v, raw, true); }
|
||||
|
||||
// Access as an array
|
||||
inline const uint8_t& operator[](uint8_t x) const __attribute__((always_inline)) { return raw[x]; }
|
||||
|
||||
// Assignment from 32-bit color
|
||||
inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; }
|
||||
|
||||
// Assignment from CHSV32
|
||||
inline CRGBW& operator=(CHSV32 hsv) __attribute__((always_inline)) { hsv2rgb_rainbow(hsv.h, hsv.s, hsv.v, raw, true); return *this; }
|
||||
|
||||
// Assignment from CHSV
|
||||
inline CRGBW& operator=(CHSV hsv) __attribute__((always_inline)) { hsv2rgb_rainbow(hsv.h<<8, hsv.s, hsv.v, raw, true); return *this; }
|
||||
|
||||
// Assignment from r, g, b, w
|
||||
inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; }
|
||||
|
||||
// Conversion operator to uint32_t
|
||||
inline operator uint32_t() const __attribute__((always_inline)) {
|
||||
return color32;
|
||||
}
|
||||
|
||||
// adjust hue: input range is 256 for full color cycle, input can be negative
|
||||
inline void adjust_hue(int hueshift) __attribute__((always_inline)) {
|
||||
CHSV32 hsv = *this;
|
||||
hsv.h += hueshift << 8;
|
||||
hsv2rgb_spectrum(hsv, *this);
|
||||
}
|
||||
|
||||
// get the average of the R, G, B and W values
|
||||
uint8_t getAverageLight() const {
|
||||
return (r + g + b + w) >> 2;
|
||||
}
|
||||
};
|
||||
|
||||
inline CHSV32::CHSV32(const CRGBW& rgb) {
|
||||
rgb2hsv(rgb, *this);
|
||||
}
|
||||
|
||||
inline CHSV32& CHSV32::operator= (const CRGBW& rgb) { // assignment from 32bit rgb color (white channel is ignored)
|
||||
rgb2hsv(rgb, *this);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// explicit hsv2rgb conversions for compatibility
|
||||
inline CRGBW hsv2rgb(const CHSV32& hsv) { return CRGBW(hsv); }
|
||||
inline void hsv2rgb(const CHSV32& hsv, CRGBW& rgb) { rgb = CRGBW(hsv); }
|
||||
inline void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) { rgb = CRGBW(hsv).color32; }
|
||||
|
||||
#endif
|
||||
+98
-26
@@ -6,7 +6,16 @@
|
||||
* Readability defines and their associated numerical values + compile-time constants
|
||||
*/
|
||||
|
||||
#define GRADIENT_PALETTE_COUNT 59
|
||||
constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]);
|
||||
constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);
|
||||
constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black)
|
||||
constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes
|
||||
#ifndef ESP8266
|
||||
#define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10
|
||||
#else
|
||||
#define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10
|
||||
#endif
|
||||
#define WLED_MAX_CUSTOM_PALETTE_GAP 20 // max number of empty palette files in a row before stopping to look for more (20 takes 100ms)
|
||||
|
||||
// You can define custom product info from build flags.
|
||||
// This is useful to allow API consumer to identify what type of WLED version
|
||||
@@ -47,32 +56,39 @@
|
||||
|
||||
#ifdef ESP8266
|
||||
#define WLED_MAX_DIGITAL_CHANNELS 3
|
||||
#define WLED_MAX_RMT_CHANNELS 0 // ESP8266 does not have RMT nor I2S
|
||||
#define WLED_MAX_I2S_CHANNELS 0
|
||||
#define WLED_MAX_ANALOG_CHANNELS 5
|
||||
#define WLED_MIN_VIRTUAL_BUSSES 3 // no longer used for bus creation but used to distinguish S2/S3 in UI
|
||||
#define WLED_MAX_TIMERS 16 // reduced limit for ESP8266 due to memory constraints
|
||||
#define WLED_PLATFORM_ID 0 // used in UI to distinguish ESP types, needs a proper fix!
|
||||
#else
|
||||
#if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX)
|
||||
#include "driver/ledc.h" // needed for analog/LEDC channel counts
|
||||
#endif
|
||||
#define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX)
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM
|
||||
#define WLED_MAX_DIGITAL_CHANNELS 2
|
||||
#define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels
|
||||
#define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB
|
||||
//#define WLED_MAX_ANALOG_CHANNELS 6
|
||||
#define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI
|
||||
#define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix!
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB
|
||||
// the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though)
|
||||
#define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x1/x8 I2S0
|
||||
#define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels
|
||||
#define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB
|
||||
//#define WLED_MAX_ANALOG_CHANNELS 8
|
||||
#define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI
|
||||
#define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1
|
||||
#define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x8 I2S-LCD
|
||||
#define WLED_MAX_RMT_CHANNELS 4 // ESP32-S3 has 4 RMT output channels
|
||||
#define WLED_MAX_I2S_CHANNELS 8 // uses LCD parallel output not I2S
|
||||
//#define WLED_MAX_ANALOG_CHANNELS 8
|
||||
#define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI
|
||||
#define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix!
|
||||
#else
|
||||
// the last digital bus (I2S0) will prevent Audioreactive usermod from functioning
|
||||
#define WLED_MAX_DIGITAL_CHANNELS 16 // x1/x8 I2S1 + x8 RMT
|
||||
#define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels
|
||||
#define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB
|
||||
//#define WLED_MAX_ANALOG_CHANNELS 16
|
||||
#define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI
|
||||
#define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix!
|
||||
#endif
|
||||
#define WLED_MAX_TIMERS 64 // maximum number of timers
|
||||
#define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS)
|
||||
#endif
|
||||
// WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed
|
||||
// instead it will help determine max number of buses that can be defined at compile time
|
||||
@@ -94,9 +110,9 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
|
||||
#ifndef WLED_MAX_BUTTONS
|
||||
#ifdef ESP8266
|
||||
#define WLED_MAX_BUTTONS 2
|
||||
#define WLED_MAX_BUTTONS 10
|
||||
#else
|
||||
#define WLED_MAX_BUTTONS 4
|
||||
#define WLED_MAX_BUTTONS 32
|
||||
#endif
|
||||
#else
|
||||
#if WLED_MAX_BUTTONS < 2
|
||||
@@ -105,6 +121,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define RELAY_DELAY 50 // delay in ms between switching on relay and sending data to LEDs
|
||||
|
||||
#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
#define WLED_MAX_COLOR_ORDER_MAPPINGS 5
|
||||
#else
|
||||
@@ -200,6 +218,12 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h"
|
||||
#define USERMOD_ID_USER_FX 58 //Usermod "user_fx"
|
||||
|
||||
//Wifi encryption type
|
||||
#ifdef WLED_ENABLE_WPA_ENTERPRISE
|
||||
#define WIFI_ENCRYPTION_TYPE_PSK 0 //None/WPA/WPA2
|
||||
#define WIFI_ENCRYPTION_TYPE_ENTERPRISE 1 //WPA/WPA2-Enterprise
|
||||
#endif
|
||||
|
||||
//Access point behavior
|
||||
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
|
||||
#define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost)
|
||||
@@ -291,7 +315,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define TYPE_UCS8903 26
|
||||
#define TYPE_APA106 27
|
||||
#define TYPE_FW1906 28 //RGB + CW + WW + unused channel (6 channels per IC)
|
||||
#define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp, memUsage())
|
||||
#define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp)
|
||||
#define TYPE_SK6812_RGBW 30
|
||||
#define TYPE_TM1814 31
|
||||
#define TYPE_WS2805 32 //RGB + WW + CW
|
||||
@@ -316,6 +340,12 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define TYPE_P9813 53
|
||||
#define TYPE_LPD6803 54
|
||||
#define TYPE_2PIN_MAX 63
|
||||
|
||||
#define TYPE_HUB75MATRIX_MIN 64
|
||||
#define TYPE_HUB75MATRIX_HS 65
|
||||
#define TYPE_HUB75MATRIX_QS 66
|
||||
#define TYPE_HUB75MATRIX_MAX 71
|
||||
|
||||
//Network types (master broadcast) (80-95)
|
||||
#define TYPE_VIRTUAL_MIN 80
|
||||
#define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus)
|
||||
@@ -352,7 +382,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define BTN_TYPE_TOUCH_SWITCH 9
|
||||
|
||||
//Ethernet board types
|
||||
#define WLED_NUM_ETH_TYPES 13
|
||||
#define WLED_NUM_ETH_TYPES 14
|
||||
|
||||
|
||||
#define WLED_ETH_NONE 0
|
||||
#define WLED_ETH_WT32_ETH01 1
|
||||
@@ -367,6 +398,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define WLED_ETH_SERG74 10
|
||||
#define WLED_ETH_ESP32_POE_WROVER 11
|
||||
#define WLED_ETH_LILYGO_T_POE_PRO 12
|
||||
#define WLED_ETH_GLEDOPTO 13
|
||||
|
||||
//Hue error codes
|
||||
#define HUE_ERROR_INACTIVE 0
|
||||
@@ -425,6 +457,31 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented)
|
||||
#define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented)
|
||||
|
||||
// JSON buffer lock owners
|
||||
#define JSON_LOCK_UNKNOWN 255
|
||||
#define JSON_LOCK_CFG_DES 1
|
||||
#define JSON_LOCK_CFG_SER 2
|
||||
#define JSON_LOCK_CFG_SEC_DES 3
|
||||
#define JSON_LOCK_CFG_SEC_SER 4
|
||||
#define JSON_LOCK_SETTINGS 5
|
||||
#define JSON_LOCK_XML 6
|
||||
#define JSON_LOCK_LEDMAP 7
|
||||
// unused 8
|
||||
#define JSON_LOCK_PRESET_LOAD 9
|
||||
#define JSON_LOCK_PRESET_SAVE 10
|
||||
#define JSON_LOCK_WS_RECEIVE 11
|
||||
#define JSON_LOCK_WS_SEND 12
|
||||
#define JSON_LOCK_IR 13
|
||||
#define JSON_LOCK_SERVER 14
|
||||
#define JSON_LOCK_MQTT 15
|
||||
#define JSON_LOCK_SERIAL 16
|
||||
#define JSON_LOCK_SERVEJSON 17
|
||||
#define JSON_LOCK_NOTIFY 18
|
||||
#define JSON_LOCK_PRESET_NAME 19
|
||||
#define JSON_LOCK_LEDGAP 20
|
||||
#define JSON_LOCK_LEDMAP_ENUM 21
|
||||
#define JSON_LOCK_REMOTE 22
|
||||
|
||||
// Timer mode types
|
||||
#define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness
|
||||
#define NL_MODE_FADE 1 //Fade to target brightness gradually
|
||||
@@ -443,6 +500,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define SUBPAGE_UM 8
|
||||
#define SUBPAGE_UPDATE 9
|
||||
#define SUBPAGE_2D 10
|
||||
#define SUBPAGE_PINS 11
|
||||
#define SUBPAGE_LAST SUBPAGE_PINS
|
||||
#define SUBPAGE_LOCK 251
|
||||
#define SUBPAGE_PINREQ 252
|
||||
#define SUBPAGE_CSS 253
|
||||
@@ -458,23 +517,28 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define MAX_LEDS 1536 //can't rely on memory limit to limit this to 1536 LEDs
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
#define MAX_LEDS 2048 //due to memory constraints S2
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#define MAX_LEDS 4096
|
||||
#else
|
||||
#define MAX_LEDS 16384
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// maximum total memory that can be used for bus-buffers and pixel buffers
|
||||
#ifndef MAX_LED_MEMORY
|
||||
#ifdef ESP8266
|
||||
#define MAX_LED_MEMORY 4096
|
||||
#define MAX_LED_MEMORY (8*1024)
|
||||
#else
|
||||
#if defined(ARDUINO_ARCH_ESP32S2)
|
||||
#define MAX_LED_MEMORY 16384
|
||||
#elif defined(ARDUINO_ARCH_ESP32C3)
|
||||
#define MAX_LED_MEMORY 32768
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
#ifndef BOARD_HAS_PSRAM
|
||||
#define MAX_LED_MEMORY (28*1024) // S2 has ~170k of free heap after boot, using 28k is the absolute limit to keep WLED functional
|
||||
#else
|
||||
#define MAX_LED_MEMORY (48*1024) // with PSRAM there is more wiggle room as buffers get moved to PSRAM when needed (prioritize functionality over speed)
|
||||
#endif
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
#define MAX_LED_MEMORY (192*1024) // S3 has ~330k of free heap after boot
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#define MAX_LED_MEMORY (100*1024) // C3 has ~240k of free heap after boot, even with 8000 LEDs configured (2D) there is 30k of contiguous heap left
|
||||
#else
|
||||
#define MAX_LED_MEMORY 65536
|
||||
#define MAX_LED_MEMORY (85*1024) // ESP32 has ~160k of free heap after boot and an additional 64k of 32bit access memory that is used for pixel buffers
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
@@ -539,7 +603,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#ifdef ESP8266
|
||||
#define JSON_BUFFER_SIZE 10240
|
||||
#else
|
||||
#if defined(ARDUINO_ARCH_ESP32S2)
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
#define JSON_BUFFER_SIZE 24576
|
||||
#else
|
||||
#define JSON_BUFFER_SIZE 32767
|
||||
@@ -595,7 +659,12 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define DEFAULT_LED_PIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board
|
||||
#endif
|
||||
#else
|
||||
#define DEFAULT_LED_PIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit())
|
||||
#if defined(WLED_USE_ETHERNET)
|
||||
#define DEFAULT_LED_PIN 4 // GPIO4 seems to be a "safe bet" for all known ethernet boards (issue #5155)
|
||||
//#warning "Compiling with Ethernet support. The default LED pin has been changed to pin 4."
|
||||
#else
|
||||
#define DEFAULT_LED_PIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit())
|
||||
#endif
|
||||
#endif
|
||||
#define DEFAULT_LED_TYPE TYPE_WS2812_RGB
|
||||
#define DEFAULT_LED_COUNT 30
|
||||
@@ -668,4 +737,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define IRAM_ATTR_YN IRAM_ATTR
|
||||
#endif
|
||||
|
||||
#define WLED_O2_ATTR __attribute__((optimize("O2")))
|
||||
#define WLED_O3_ATTR __attribute__((optimize("O3")))
|
||||
|
||||
#endif
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content='width=device-width' name='viewport'>
|
||||
|
||||
+112
-6
@@ -51,6 +51,38 @@ function tooltip(cont=null) {
|
||||
});
|
||||
});
|
||||
};
|
||||
// sequential loading of external resources (JS or CSS) with retry, calls init() when done
|
||||
function loadResources(files, init) {
|
||||
let i = 0;
|
||||
const loadNext = () => {
|
||||
if (i >= files.length) {
|
||||
if (init) {
|
||||
d.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display)
|
||||
d.readyState === 'complete' ? init() : window.addEventListener('load', init);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const file = files[i++];
|
||||
const isCSS = file.endsWith('.css');
|
||||
const el = d.createElement(isCSS ? 'link' : 'script');
|
||||
if (isCSS) {
|
||||
el.rel = 'stylesheet';
|
||||
el.href = file;
|
||||
const st = d.head.querySelector('style');
|
||||
if (st) d.head.insertBefore(el, st); // insert before any <style> to allow overrides
|
||||
else d.head.appendChild(el);
|
||||
} else {
|
||||
el.src = file;
|
||||
d.head.appendChild(el);
|
||||
}
|
||||
el.onload = () => { loadNext(); };
|
||||
el.onerror = () => {
|
||||
i--; // load this file again
|
||||
setTimeout(loadNext, 100);
|
||||
};
|
||||
};
|
||||
loadNext();
|
||||
}
|
||||
// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript
|
||||
function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) {
|
||||
let scE = d.createElement("script");
|
||||
@@ -58,7 +90,7 @@ function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefine
|
||||
scE.setAttribute("type", "text/javascript");
|
||||
scE.setAttribute("async", async);
|
||||
d.body.appendChild(scE);
|
||||
// success event
|
||||
// success event
|
||||
scE.addEventListener("load", () => {
|
||||
//console.log("File loaded");
|
||||
if (preGetV) preGetV();
|
||||
@@ -94,6 +126,10 @@ function getLoc() {
|
||||
}
|
||||
}
|
||||
function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; }
|
||||
// HTML entity escaper – use on any remote/user-supplied text inserted into innerHTML
|
||||
function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
// URL sanitizer – blocks javascript: and data: URIs, use for externally supplied URLs for some basic safety
|
||||
function safeUrl(u) { return /^https?:\/\//.test(u) ? u : '#'; }
|
||||
function B() { window.open(getURL("/settings"),"_self"); }
|
||||
var timeout;
|
||||
function showToast(text, error = false) {
|
||||
@@ -105,14 +141,84 @@ function showToast(text, error = false) {
|
||||
x.style.animation = 'none';
|
||||
timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900);
|
||||
}
|
||||
function uploadFile(fileObj, name) {
|
||||
async function uploadFile(fileObj, name, callback) {
|
||||
let file = fileObj.files?.[0]; // get first file, "?"" = optional chaining in case no file is selected
|
||||
if (!file) { callback?.(false); return; }
|
||||
if (/\.json$/i.test(name)) { // same as name.toLowerCase().endsWith('.json')
|
||||
try {
|
||||
const minified = JSON.stringify(JSON.parse(await file.text())); // validate and minify JSON
|
||||
file = new Blob([minified], { type: file.type || "application/json" });
|
||||
} catch (err) {
|
||||
if (!confirm("JSON invalid. Continue?")) { callback?.(false); return; }
|
||||
// proceed with original file if invalid but user confirms
|
||||
}
|
||||
}
|
||||
var req = new XMLHttpRequest();
|
||||
req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)});
|
||||
req.addEventListener('error', function(e){showToast(e.stack,true);});
|
||||
req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400); if(callback) callback(this.status < 400);});
|
||||
req.addEventListener('error', function(e){showToast("Upload failed",true); if(callback) callback(false);});
|
||||
req.open("POST", "/upload");
|
||||
var formData = new FormData();
|
||||
formData.append("data", fileObj.files[0], name);
|
||||
formData.append("data", file, name);
|
||||
req.send(formData);
|
||||
fileObj.value = '';
|
||||
return false;
|
||||
}
|
||||
// connect to WebSocket, use parent WS or open new, callback function gets passed the new WS object
|
||||
function connectWs(onOpen) {
|
||||
let ws;
|
||||
try { ws = top.window.ws;} catch (e) {}
|
||||
// reuse if open
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
if (onOpen) onOpen(ws);
|
||||
} else {
|
||||
// create new ws connection
|
||||
getLoc(); // ensure globals are up to date
|
||||
let url = loc ? getURL('/ws').replace("http", "ws")
|
||||
: "ws://" + window.location.hostname + "/ws";
|
||||
ws = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
if (onOpen) ws.onopen = () => onOpen(ws);
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
// send LED colors to ESP using WebSocket and DDP protocol (RGB)
|
||||
// ws: WebSocket object
|
||||
// start: start pixel index
|
||||
// len: number of pixels to send
|
||||
// colors: Uint8Array with RGB values (3*len bytes)
|
||||
function sendDDP(ws, start, len, colors) {
|
||||
if (!colors || colors.length < len * 3) return false; // not enough color data
|
||||
let maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
|
||||
//let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
||||
// send in chunks of maxDDPpx
|
||||
for (let i = 0; i < len; i += maxDDPpx) {
|
||||
let cnt = Math.min(maxDDPpx, len - i);
|
||||
let off = (start + i) * 3; // DDP pixel offset in bytes
|
||||
let dLen = cnt * 3;
|
||||
let cOff = i * 3; // offset in color buffer
|
||||
let pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator
|
||||
pkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1
|
||||
pkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0
|
||||
pkt[2] = 0x00; // reserved
|
||||
pkt[3] = 0x0B; // RGB, 8bit per channel
|
||||
pkt[4] = 0x01; // destination id (not used but 0x01 is default output)
|
||||
pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset
|
||||
pkt[6] = (off >> 16) & 255;
|
||||
pkt[7] = (off >> 8) & 255;
|
||||
pkt[8] = off & 255;
|
||||
pkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length
|
||||
pkt[10] = dLen & 255;
|
||||
pkt.set(colors.subarray(cOff, cOff + dLen), 11);
|
||||
if(i + cnt >= len) {
|
||||
pkt[1] = 0x41; //if this is last packet, set the "push" flag to render the frame
|
||||
}
|
||||
try {
|
||||
ws.send(pkt.buffer);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+889
-740
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta content='width=device-width' name='viewport'>
|
||||
<html lang="en"><head><meta content='width=device-width' name='viewport'>
|
||||
<title>DMX Map</title>
|
||||
<script>function B(){window.history.back()};function RS(){window.location = "/settings";}function RP(){top.location.href="/";}function FM() {
|
||||
var dmxlabels = ["SET 0","RED","GREEN","BLUE","WHITE","SHUTTER","SET 255", "DISABLED"];
|
||||
|
||||
+551
-529
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
To edit the current font, this is the workflow:
|
||||
|
||||
go to https://icomoon.io/
|
||||
In the menu, go to manage projects and import the json file from this folder and load it
|
||||
Add new icons or exchange existing ones: if changing existing one, make sure the unicode stays the same (can be edited before exporting)
|
||||
Go to "Generate SVG & More" and check the size of new icons (clicking on icons brings up the editor) -> scale new icons to match the size of existing ones
|
||||
Go to "Generate font" tab, check unicodes are correct (can use any unicode, range > e900 is "custom range" and now preferred)
|
||||
Download the font package and replace the files in this folder with new files
|
||||
Using an online converter, convert the *.woff font into woff2 format (about half the file size)
|
||||
Using another online converter, convert the woff2 font to base64 encoding for CSS
|
||||
in index.css, replace the font string at the top, keep the "data:font/woff2;charset=utf-8;" and dont use octet-stream (browser compatibility).
|
||||
|
||||
enjoy your new icons in the UI :)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.
|
||||
|
||||
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/#docs/local-fonts
|
||||
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs#install
|
||||
|
||||
You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.
|
||||
|
||||
|
||||
@@ -147,6 +147,12 @@ p {
|
||||
font-size: 16px;
|
||||
}
|
||||
.fs1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
.fs2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
.fs3 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,45 @@
|
||||
<link rel="stylesheet" href="style.css"></head>
|
||||
<body>
|
||||
<div class="bgc1 clearfix">
|
||||
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> wled122 <small class="fgc1">(Glyphs: 23)</small></h1>
|
||||
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> wled122 <small class="fgc1">(Glyphs: 25)</small></h1>
|
||||
</div>
|
||||
<div class="clearfix mhl ptl">
|
||||
<h1 class="mvm mtn fgc1">Grid Size: 16</h1>
|
||||
<div class="glyph fs1">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-pixelforge"></span>
|
||||
<span class="mls"> i-pixelforge</span>
|
||||
</div>
|
||||
<fieldset class="fs0 size1of1 clearfix hidden-false">
|
||||
<input type="text" readonly value="e900" class="unit size1of2" />
|
||||
<input type="text" maxlength="1" readonly value="" class="unitRight size1of2 talign-right" />
|
||||
</fieldset>
|
||||
<div class="fs0 bshadow0 clearfix hidden-true">
|
||||
<span class="unit pvs fgc1">liga: </span>
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix mhl ptl">
|
||||
<h1 class="mvm mtn fgc1">Grid Size: 14</h1>
|
||||
<div class="glyph fs2">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-editor"></span>
|
||||
<span class="mls"> i-editor</span>
|
||||
</div>
|
||||
<fieldset class="fs0 size1of1 clearfix hidden-false">
|
||||
<input type="text" readonly value="e901" class="unit size1of2" />
|
||||
<input type="text" maxlength="1" readonly value="" class="unitRight size1of2 talign-right" />
|
||||
</fieldset>
|
||||
<div class="fs0 bshadow0 clearfix hidden-true">
|
||||
<span class="unit pvs fgc1">liga: </span>
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix mhl ptl">
|
||||
<h1 class="mvm mtn fgc1">Grid Size: Unknown</h1>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-pattern"></span>
|
||||
<span class="mls"> i-pattern</span>
|
||||
@@ -27,7 +61,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-segments"></span>
|
||||
<span class="mls"> i-segments</span>
|
||||
@@ -41,7 +75,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-sun"></span>
|
||||
<span class="mls"> i-sun</span>
|
||||
@@ -55,7 +89,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-palette"></span>
|
||||
<span class="mls"> i-palette</span>
|
||||
@@ -69,7 +103,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-eye"></span>
|
||||
<span class="mls"> i-eye</span>
|
||||
@@ -83,7 +117,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-speed"></span>
|
||||
<span class="mls"> i-speed</span>
|
||||
@@ -97,7 +131,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-expand"></span>
|
||||
<span class="mls"> i-expand</span>
|
||||
@@ -111,7 +145,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-power"></span>
|
||||
<span class="mls"> i-power</span>
|
||||
@@ -125,7 +159,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-settings"></span>
|
||||
<span class="mls"> i-settings</span>
|
||||
@@ -139,7 +173,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-playlist"></span>
|
||||
<span class="mls"> i-playlist</span>
|
||||
@@ -153,7 +187,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-night"></span>
|
||||
<span class="mls"> i-night</span>
|
||||
@@ -167,7 +201,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-cancel"></span>
|
||||
<span class="mls"> i-cancel</span>
|
||||
@@ -181,7 +215,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-sync"></span>
|
||||
<span class="mls"> i-sync</span>
|
||||
@@ -195,7 +229,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-confirm"></span>
|
||||
<span class="mls"> i-confirm</span>
|
||||
@@ -209,7 +243,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-brightness"></span>
|
||||
<span class="mls"> i-brightness</span>
|
||||
@@ -223,7 +257,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-nodes"></span>
|
||||
<span class="mls"> i-nodes</span>
|
||||
@@ -237,7 +271,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-add"></span>
|
||||
<span class="mls"> i-add</span>
|
||||
@@ -251,7 +285,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-edit"></span>
|
||||
<span class="mls"> i-edit</span>
|
||||
@@ -265,7 +299,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-intensity"></span>
|
||||
<span class="mls"> i-intensity</span>
|
||||
@@ -279,7 +313,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-star"></span>
|
||||
<span class="mls"> i-star</span>
|
||||
@@ -293,7 +327,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-info"></span>
|
||||
<span class="mls"> i-info</span>
|
||||
@@ -307,7 +341,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-del"></span>
|
||||
<span class="mls"> i-del</span>
|
||||
@@ -321,7 +355,7 @@
|
||||
<input type="text" readonly value="" class="liga unitRight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glyph fs1">
|
||||
<div class="glyph fs3">
|
||||
<div class="clearfix bshadow0 pbs">
|
||||
<span class="i-presets"></span>
|
||||
<span class="mls"> i-presets</span>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="wled122" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="819.2" descent="-204.8" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="0" d="" />
|
||||
<glyph unicode="" glyph-name="del" d="M256 21.333v512h512v-512c0-46.933-38.4-85.333-85.333-85.333h-341.334c-46.933 0-85.333 38.4-85.333 85.333zM810.667 661.333v-85.333h-597.334v85.333h149.334l42.666 42.667h213.334l42.666-42.667h149.334z" />
|
||||
<glyph unicode="" glyph-name="presets" d="M704 704c131.413 0 234.667-103.253 234.667-234.667 0-161.28-145.067-292.693-364.8-491.946l-61.867-56.32-61.867 55.893c-219.733 199.68-364.8 331.093-364.8 492.373 0 131.414 103.254 234.667 234.667 234.667 74.24 0 145.493-34.56 192-89.173 46.507 54.613 117.76 89.173 192 89.173zM516.267 40.533c203.093 183.894 337.066 305.494 337.066 428.8 0 85.334-64 149.334-149.333 149.334-65.707 0-129.707-42.24-151.893-100.694h-79.787c-22.613 58.454-86.613 100.694-152.32 100.694-85.333 0-149.333-64-149.333-149.334 0-123.306 133.973-244.906 337.066-428.8l4.267-4.266z" />
|
||||
<glyph unicode="" glyph-name="info" d="M512 746.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667-426.667 191.147-426.667 426.667 191.147 426.667 426.667 426.667zM554.667 106.667v256h-85.334v-256h85.334zM554.667 448v85.333h-85.334v-85.333h85.334z" />
|
||||
<glyph unicode="" glyph-name="power" d="M554.667 704v-426.667h-85.334v426.667h85.334zM760.747 611.413c82.773-70.4 135.253-174.506 135.253-291.413 0-212.053-171.947-384-384-384s-384 171.947-384 384c0 116.907 52.48 221.013 135.253 291.413l60.16-60.16c-66.986-54.613-110.080-137.813-110.080-231.253 0-165.12 133.547-298.667 298.667-298.667s298.667 133.547 298.667 298.667c0 93.44-43.094 176.64-110.507 230.827z" />
|
||||
<glyph unicode="" glyph-name="settings" d="M816.64 280.064l85.504-67.584c8.192-6.144 10.24-16.896 5.12-26.112l-81.92-141.824c-5.12-9.216-15.872-12.8-25.088-9.216l-101.888 40.96c-20.992-15.872-44.032-29.696-69.12-39.936l-15.36-108.544c-1.024-10.24-9.728-17.408-19.968-17.408h-163.84c-10.24 0-18.432 7.168-20.48 17.408l-15.36 108.544c-25.088 10.24-47.616 23.552-69.12 39.936l-101.888-40.96c-9.216-3.072-19.968 0-25.088 9.216l-81.92 141.824c-4.608 8.704-2.56 19.968 5.12 26.112l86.528 67.584c-2.048 12.8-3.072 26.624-3.072 39.936s1.536 27.136 3.584 39.936l-86.528 67.584c-8.192 6.144-10.24 16.896-5.12 26.112l81.92 141.824c5.12 9.216 15.872 12.8 25.088 9.216l101.888-40.96c20.992 15.872 44.032 29.696 69.12 39.936l15.36 108.544c1.536 10.24 9.728 17.408 19.968 17.408h163.84c10.24 0 18.944-7.168 20.48-17.408l15.36-108.544c25.088-10.24 47.616-23.552 69.12-39.936l101.888 40.96c9.216 3.072 19.968 0 25.088-9.216l81.92-141.824c4.608-8.704 2.56-19.968-5.12-26.112l-86.528-67.584c2.048-12.8 3.072-26.112 3.072-39.936s-1.024-27.136-2.56-39.936zM512 166.4c84.48 0 153.6 69.12 153.6 153.6s-69.12 153.6-153.6 153.6-153.6-69.12-153.6-153.6 69.12-153.6 153.6-153.6z" />
|
||||
<glyph unicode="" glyph-name="eye" d="M512 640c213.333 0 395.52-132.693 469.333-320-73.813-187.307-256-320-469.333-320s-395.52 132.693-469.333 320c73.813 187.307 256 320 469.333 320zM512 106.667c117.76 0 213.333 95.573 213.333 213.333s-95.573 213.333-213.333 213.333-213.333-95.573-213.333-213.333 95.573-213.333 213.333-213.333zM512 448c70.827 0 128-57.173 128-128s-57.173-128-128-128-128 57.173-128 128 57.173 128 128 128z" />
|
||||
<glyph unicode="" glyph-name="sync" d="M512 661.333c188.587 0 341.333-152.746 341.333-341.333 0-66.987-19.626-129.28-52.906-181.76l-62.294 62.293c19.2 35.414 29.867 76.374 29.867 119.467 0 141.227-114.773 256-256 256v-128l-170.667 170.667 170.667 170.666v-128zM512 64v128l170.667-170.667-170.667-170.666v128c-188.587 0-341.333 152.746-341.333 341.333 0 66.987 19.626 129.28 52.906 181.76l62.294-62.293c-19.2-35.414-29.867-76.374-29.867-119.467 0-141.227 114.773-256 256-256z" />
|
||||
<glyph unicode="" glyph-name="playlist" d="M556.8 414.293l125.867-94.293-256-192v384zM556.8 414.293l125.867-94.293-256-192v384zM556.8 414.293l-130.133 97.707v-384l256 192zM469.333 658.347c-62.293-7.68-119.040-32.427-166.4-69.12l-60.586 61.013c63.146 51.627 141.226 85.76 226.986 94.293v-86.186zM242.773 529.067c-36.693-47.36-61.44-104.107-69.12-166.4h-86.186c8.533 85.76 42.666 163.84 94.293 226.986zM173.653 277.333c7.68-62.293 32.427-119.040 69.12-165.973l-61.013-61.013c-51.627 63.146-85.76 141.226-94.293 226.986h86.186zM242.347-10.24l60.586 61.013c47.36-36.693 104.107-61.44 166.4-69.12v-86.186c-85.333 8.533-163.84 42.666-226.986 94.293zM938.667 320c0-220.16-167.254-401.92-381.867-424.533v86.186c167.253 22.187 296.533 165.547 296.533 338.347s-129.28 316.16-296.533 338.347v86.186c214.613-22.613 381.867-204.373 381.867-424.533z" />
|
||||
<glyph unicode="" glyph-name="add" d="M810.667 277.333h-256v-256h-85.334v256h-256v85.334h256v256h85.334v-256h256v-85.334z" />
|
||||
<glyph unicode="" glyph-name="nodes" d="M85.333 106.667v42.666h128v-170.666h-128v42.666h85.334v21.334h-42.667v42.666h42.667v21.334h-85.334zM128 490.667v128h-42.667v42.666h85.334v-170.666h-42.667zM85.333 362.667v42.666h128v-38.4l-76.8-89.6h76.8v-42.666h-128v38.4l76.8 89.6h-76.8zM298.667 618.667h597.333v-85.334h-597.333v85.334zM298.667 21.333v85.334h597.333v-85.334h-597.333zM298.667 277.333v85.334h597.333v-85.334h-597.333z" />
|
||||
<glyph unicode="" glyph-name="pattern" d="M511.573 746.667c235.947 0 427.094-191.147 427.094-426.667s-191.147-426.667-427.094-426.667c-235.52 0-426.24 191.147-426.24 426.667s190.72 426.667 426.24 426.667zM512-21.333c188.587 0 341.333 152.746 341.333 341.333s-152.746 341.333-341.333 341.333-341.333-152.746-341.333-341.333 152.746-341.333 341.333-341.333zM661.333 362.667c-35.413 0-64 28.586-64 64s28.587 64 64 64c35.414 0 64-28.587 64-64s-28.586-64-64-64zM362.667 362.667c-35.414 0-64 28.586-64 64s28.586 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64zM512 85.333c-99.413 0-183.893 62.294-218.027 149.334h436.054c-34.134-87.040-118.614-149.334-218.027-149.334z" />
|
||||
<glyph unicode="" glyph-name="night" d="M386.4 738.667c231.104 0 418.667-187.563 418.667-418.667s-187.563-418.667-418.667-418.667c-43.96 0-85.827 6.699-125.6 19.259 169.979 53.171 293.067 211.845 293.067 399.408s-123.088 346.237-293.067 399.408c39.773 12.56 81.64 19.259 125.6 19.259z" />
|
||||
<glyph unicode="" glyph-name="brightness" d="M853.333 461.227l141.227-141.227-141.227-141.227v-200.106h-200.106l-141.227-141.227-141.227 141.227h-200.106v200.106l-141.227 141.227 141.227 141.227v200.106h200.106l141.227 141.227 141.227-141.227h200.106v-200.106zM512 64c141.227 0 256 114.773 256 256s-114.773 256-256 256-256-114.773-256-256 114.773-256 256-256zM512 490.667c94.293 0 170.667-76.374 170.667-170.667s-76.374-170.667-170.667-170.667-170.667 76.374-170.667 170.667 76.374 170.667 170.667 170.667z" />
|
||||
<glyph unicode="" glyph-name="palette" d="M512 704c212.053 0 384-152.747 384-341.333 0-117.76-95.573-213.334-213.333-213.334h-75.52c-35.414 0-64-28.586-64-64 0-16.213 6.4-31.146 16.213-42.24 10.24-11.52 16.64-26.453 16.64-43.093 0-35.413-28.587-64-64-64-212.053 0-384 171.947-384 384s171.947 384 384 384zM277.333 320c35.414 0 64 28.587 64 64s-28.586 64-64 64c-35.413 0-64-28.587-64-64s28.587-64 64-64zM405.333 490.667c35.414 0 64 28.586 64 64s-28.586 64-64 64c-35.413 0-64-28.587-64-64s28.587-64 64-64zM618.667 490.667c35.413 0 64 28.586 64 64s-28.587 64-64 64c-35.414 0-64-28.587-64-64s28.586-64 64-64zM746.667 320c35.413 0 64 28.587 64 64s-28.587 64-64 64c-35.414 0-64-28.587-64-64s28.586-64 64-64z" />
|
||||
<glyph unicode="" glyph-name="edit" d="M128 96l471.893 471.893 160-160-471.893-471.893h-160v160zM883.627 531.627l-78.080-78.080-160 160 78.080 78.080c16.64 16.64 43.52 16.64 60.16 0l99.84-99.84c16.64-16.64 16.64-43.52 0-60.16z" />
|
||||
<glyph unicode="" glyph-name="speed" d="M640 789.333v-85.333h-256v85.333h256zM469.333 234.667v256h85.334v-256h-85.334zM811.947 516.693c52.48-65.706 84.053-148.906 84.053-239.36 0-212.053-171.52-384-384-384s-384 171.947-384 384c0 212.054 171.947 384 384 384 90.453 0 173.653-31.573 239.787-84.48l60.586 60.587c21.76-17.92 41.814-38.4 60.16-60.16zM512-21.333c165.12 0 298.667 133.546 298.667 298.666s-133.547 298.667-298.667 298.667-298.667-133.547-298.667-298.667 133.547-298.666 298.667-298.666z" />
|
||||
<glyph unicode="" glyph-name="sun" d="M288.427 625.493l-60.587-60.16-76.373 76.374 60.16 60.16zM170.667 384v-85.333h-128v85.333h128zM554.667 808.533v-125.866h-85.334v125.866h85.334zM872.533 641.707l-76.373-76.374-60.16 60.16 76.373 76.374zM735.573 57.173l59.734 59.734 76.8-76.374-60.16-60.16zM853.333 384h128v-85.333h-128v85.333zM512 597.333c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.774-256 256c0 141.227 114.773 256 256 256zM469.333-125.867v125.867h85.334v-125.867h-85.334zM151.467 40.96l76.373 76.8 60.16-60.16-76.373-76.8z" />
|
||||
<glyph unicode="" glyph-name="segments" d="M511.573 40.96l314.88 244.907 69.547-54.187-384-298.667-384 298.667 69.12 53.76zM512 149.333l-384 298.667 384 298.667 384-298.667-69.973-54.187z" />
|
||||
<glyph unicode="" glyph-name="cancel" d="M512 746.667c235.947 0 426.667-190.72 426.667-426.667s-190.72-426.667-426.667-426.667-426.667 190.72-426.667 426.667 190.72 426.667 426.667 426.667zM725.333 166.827l-153.173 153.173 153.173 153.173-60.16 60.16-153.173-153.173-153.173 153.173-60.16-60.16 153.173-153.173-153.173-153.173 60.16-60.16 153.173 153.173 153.173-153.173z" />
|
||||
<glyph unicode="" glyph-name="confirm" d="M384 142.080l451.84 451.413 60.16-60.16-512-512-238.507 238.507 60.587 60.16z" />
|
||||
<glyph unicode="" glyph-name="expand" d="M707.84 465.493l60.16-60.16-256-256-256 256 60.16 60.16 195.84-195.413z" />
|
||||
<glyph unicode="" glyph-name="intensity" d="M576 803.413c166.827-133.546 277.333-338.773 277.333-568.746 0-188.587-152.746-341.334-341.333-341.334s-341.333 152.747-341.333 341.334c0 144.213 51.626 276.906 137.813 379.306l-1.28-15.36c0-87.893 66.56-159.146 154.88-159.146 87.893 0 145.493 71.253 145.493 159.146 0 91.734-31.573 204.8-31.573 204.8zM499.627 21.333c113.066 0 204.8 91.734 204.8 204.8 0 59.307-8.534 117.334-25.174 172.374-43.52-58.454-121.6-94.72-197.12-110.080-75.093-15.36-119.893-64-119.893-133.12 0-74.24 61.44-133.974 137.387-133.974z" />
|
||||
<glyph unicode="" glyph-name="star" d="M938.667 437.76l-232.534-201.813 69.547-299.947-263.68 159.147-263.68-159.147 69.973 299.947-232.96 201.813 306.774 26.027 119.893 282.88 119.893-282.454zM512 174.933l160.853-97.28-42.666 182.614 141.653 122.88-186.88 16.213-72.96 172.373-72.533-171.946-186.88-16.214 141.653-122.88-42.667-182.613z" />
|
||||
<glyph unicode="" glyph-name="pixelforge" d="M910.398 66.419l-241.236 241.236c-14.934 14.934-39.371 14.934-54.306 0l-18.102-18.102-147.2 147.2 241.646 241.648h-256.001l-113.645-113.645-11.249 11.247h-54.306v-54.306l11.247-11.247-164.848-164.849 127.999-127.999 164.848 164.848 147.2-147.2-18.102-18.102c-14.934-14.934-14.934-39.371 0-54.306l241.236-241.236c14.934-14.934 39.371-14.934 54.306 0l90.509 90.509c14.935 14.934 14.935 39.371 0.002 54.306z" />
|
||||
<glyph unicode="" glyph-name="editor" horiz-adv-x="1074" d="M976.272 266.38c0-11.223-7.016-22.448-14.5-30.867l-157.14-185.202c-27.126-31.802-82.311-57.055-123.469-57.055h-508.837c-16.837 0-40.688 5.146-40.688 26.191 0 11.223 7.016 22.448 14.5 30.867l157.14 185.202c27.126 31.802 82.311 57.055 123.469 57.055h508.837c16.837 0 40.688-5.146 40.688-26.191zM815.856 427.264v-74.828h-389.112c-58.461 0-130.952-33.208-168.835-78.104l-159.949-188.009c0 3.74-0.467 7.951-0.467 11.691v448.977c0 57.523 47.233 104.761 104.761 104.761h149.66c57.523 0 104.761-47.233 104.761-104.761v-14.968h254.418c57.523 0 104.761-47.233 104.761-104.761z" />
|
||||
</font></defs></svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user