7 Commits

24 changed files with 3465 additions and 592 deletions

View File

@@ -8,6 +8,7 @@ mpremote = "*"
pyserial = "*" pyserial = "*"
esptool = "*" esptool = "*"
watchfiles = "*" watchfiles = "*"
uvicorn = "*"
[dev-packages] [dev-packages]
@@ -15,5 +16,4 @@ watchfiles = "*"
python_version = "3.12" python_version = "3.12"
[scripts] [scripts]
dev = 'watchfiles "./dev.py /dev/ttyACM0 src reset follow"' dev = 'watchfiles "./dev.py /dev/ttyACM0 src reset follow"'

546
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "7b8033c15743e27f2589635c75bd0bb86ffc3a725b179d7db9ef200119aa9164" "sha256": "53809b70ded7a2b3e577a8a4263fbadbb722d1e8d92eb016e134b0776fd40f6b"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -26,142 +26,142 @@
}, },
"bitarray": { "bitarray": {
"hashes": [ "hashes": [
"sha256:00628196dd3592972a5183194ab1475dadf9ef2a4cf3fd8c7c184a94934012e8", "sha256:002b73bf4a9f7b3ecb02260bd4dd332a6ee4d7f74ee9779a1ef342a36244d0cf",
"sha256:01d6dc548e7fe5c66913c2274f44855b0f8474935acff7811e84fe1f4024c94f", "sha256:01e3ba46c2dee6d47a4ab22561a01d8ee6772f681defc9fcb357097a055e48cf",
"sha256:056fe779f01a867d572e071c0944ac2f3bf58d8bced326040f0bd060af33a209", "sha256:03dc877ec286b7f2813185ea6bc5f1f5527fd859e61038d38768883b134e06b3",
"sha256:080a7bf55c432abdae74f25dc3dbff407418346aeae1d43e31f65e8ef114f785", "sha256:03eeab48f376c3cd988add2b75c20d2d084b6fcc9a164adb0dc390ef152255b4",
"sha256:0956322bf4d5e2293e57600aa929c241edf1e209e94e12483bf58c5c691432db", "sha256:05ee46a734b5110c5ac483815da4379f7622f4316362872ec7c0ed16db4b0148",
"sha256:0a6f9e897907757e9c2d722ae6c203d48a04826a14e1495e33935c8583c163a9", "sha256:0751596f60f33df66245b2dafa3f7fbe13cb7ac91dd14ead87d8c2eec57cb3ed",
"sha256:0ac446f557eb28e3f7c65372608810ff073840627e9037e22ed10bd081793a34", "sha256:08c114cf02a63e13ce6d70bc5b9e7bdcfa8d5db17cece207cfa085c4bc4a7a0c",
"sha256:0b47843f2f288fa746dead4394591a3432a358aaad48240283fa230d6e74b0e7", "sha256:0ed4a87eda16e2f95d536152c5acccae07841fbdda3b9a752f3dbf43e39f4d6b",
"sha256:11fc8bc65f964c7278deb1b7a69379dab3ecc90095f252deb17365637ebb274d", "sha256:101230b8074919970433ef79866570989157ade3421246d4c3afb7a994fdc614",
"sha256:129165b68a3e0c2a633ed0d8557cf5ade24a0b37ca97d7805fa6fc5fb73c19d5", "sha256:11fcfdf272549a3d876f10d8422bcd5f675750aa746ce04ff04937ec3bb2329e",
"sha256:139963494fc3dd5caee5e38c0a03783ef50be118565e94b1dbb0210770f0b32d", "sha256:160f449bb91686f8fc9984200e78b8d793b79e382decf7eb1dc9948d7c21b36f",
"sha256:157313a124287cbc8a11b55a75def0dd59e68badbc82c2dc2d204dc852742874", "sha256:16426a843b1bc9c552a7c97d6d7555e69730c2de1e2f560503d3fc0e7f6d8005",
"sha256:16d0edab54bb9d214319418f65bd15cfc4210ec41a16c3dd0b71e626c803212d", "sha256:1f1575cc0f66aa70a0bb5cb57c8d9d1b7d541d920455169c6266919bf804dc20",
"sha256:1971050b447023288a2b694a03b400bd5163829cd67b10f19e757fe87cd1161e", "sha256:1f7a8fc5085450635a539c47c9fce6d441b4a973686f88fc220aa20e3921fe55",
"sha256:1c4e75bbf9ade3d2cdf1b607a8b353b17d9b3cf54e88b2a5a773f50ae6f1bfbc", "sha256:1fb0a46ae4b8d244a3fb80c3055717baa3dec6be17938e6871042a8d5b4ce670",
"sha256:1c9f36055a89b9517db66eb8e80137126bf629c767ceeade4d004e40bc8bcd99", "sha256:2965fd8ba31b04c42e4b696fad509dc5ab50663efca6eb06bb3b6d08587f3a09",
"sha256:2020102a40edd094c0aa80e09203af71c533c41f76ce3237c99fd194a473ea33", "sha256:2b524306104c1296f1e91d74ee4ccbeeea621f6a13e44addf0bb630a1839fd72",
"sha256:20febc849a1f858e6a57a7d47b323fe9e727c579ddd526d317ad8831748a66a8", "sha256:2db04b165a57499fbcfe0eaa2f7752f118552bbcfab2163a43fef8d95f4ae745",
"sha256:220d4b8649ef54ac98e5e0e3dd92230247f67270d1524a8b31aa9859007affb0", "sha256:3092f6bbf4a75b1e6f14a5b1030e27c435f341afeb23987115e45a25cc68ba91",
"sha256:22188943a29072b684cd7c99e0b2cfc0af317cea3366c583d820507e6d1f2ed4", "sha256:30a2fc37698820cbf9b51d5f801219ef4bed828a04f3307072b8f983dc422a0e",
"sha256:222cb27ff05bc0aec72498d075dba1facec49a76a7da45740690cebbe3e81e43", "sha256:3110b98c5dfb31dc1cf82d8b0c32e3fa6d6d0b268ff9f2a1599165770c1af80f",
"sha256:243825f56b58bef28bfc602992a8c6d09bbc625628c195498d6020120d632a09", "sha256:33f604bffd06b170637f8a48ddcf42074ed1e1980366ac46058e065ce04bfe2a",
"sha256:25060e7162e44242a449ed1a14a4e94b5aef340812754c443459f19c7954be91", "sha256:340c524c7c934b61d1985d805bffe7609180fb5d16ece6ce89b51aa535b936f2",
"sha256:26691454a6770628882b68fe74e9f84ca2a51512edd49cbb025b14df5a9dd85a", "sha256:37a6a8382864a1defb5b370b66a635e04358c7334054457bbbb8645610cd95b2",
"sha256:27d13c7b886afc5d2fc49d6e92f9c96b1f0a14dc7b5502520c29f3da7550d401", "sha256:3875578748b484638f6ea776f534e9088cfb15eee131aac051036cba40fd5d05",
"sha256:27eeee915258b105a21a4b0f8aebc5f77bb4dc4fb4063a09dd329fa1fdcbd234", "sha256:38b0261483c59bb39ae9300ad46bf0bbf431ab604266382d986a349c96171b36",
"sha256:29ed022189a7997de46cb9bd4e2e49d6163d4f8d78dea72ac5a0e0293b856810", "sha256:3b9a2eb7d2e0e9c2f25256d2663c0a2a4798fe3110e3ddbbb1a7b71740b4de08",
"sha256:2a324e3007afb5c667026f5235b35efe3c4a95f1b83cd93aa9fce67b42f08e7c", "sha256:3bb3cf22c3c03ae698647e6766314149c9cf04aa2018d9f48d5efddc3ced2764",
"sha256:2c533c828d0007fac27cf45e5c1a711e5914dd469db5fe6be5f4e606bf2d7f63", "sha256:3db0648536f3e08afa7ceb928153c39913f98fd50a5c3adf92a4d0d4268f213e",
"sha256:30ba4fba3de1dca653de41c879349ec6ca521d85cff6a7ca5d2fdd8f76c93781", "sha256:3dc654da62b3a3027b7c922f7e9f4b27feaabd5d38b2a98ea98de5e8107c72f2",
"sha256:357e07c827bad01f98d0bd0dfdc722f483febeed39140fd75ffd016a451b60b9", "sha256:4079857566077f290d35e23ff0e8ba593069c139ae85b0d152b9fa476494f50a",
"sha256:3800f3c8c9780f281cf590543fd4b3278fea6988202273a260ecc58136895efb", "sha256:44f468fb4857fff86c65bec5e2fb67067789e40dad69258e9bb78fc6a6df49e7",
"sha256:3d6f3a94abd8b44b2bf346ca81ab2ff41ab9146c53905eedf5178b19d9fe53bf", "sha256:45660e2fabcdc1bab9699a468b312f47956300d41d6a2ea91c8f067572aaf38a",
"sha256:3eb1390a8b062fe9125e5cc4c5eba990b5d383eec54f2b996e7ce73ac43150f9", "sha256:477b9456eb7d70f385dc8f097a1d66ee40771b62e47b3b3e33406dcfbc1c6a3b",
"sha256:407920e9318d94cc6c9611aaa5b5e5963a09f1cbfa17b16b66edea453b3754f4", "sha256:481239cd0966f965c2b8fa78b88614be5f12a64e7773bb5feecc567d39bb2dd5",
"sha256:42622c42c159ea4535bba7e1e3c97f1fec79505bc6873ae657dc0a8f861c60de", "sha256:4a83d247420b147d4b3cba0335e484365e117dc1cfe5ab35acd6a0817ad9244f",
"sha256:4695fcd37478988b1d0a16d5bc0df56dcb677fd5db37f1893d993fd3ebef914b", "sha256:53d2abeabb91a822e9d76420c9b44980edd2d6b21767c7bb9cb2b1b4cf091049",
"sha256:4798f6744fa2633666e17b4ea8ff70250781b52a25afdbf5ffb5e176c58848f1", "sha256:55c31bc3d2c9e48741c812ee5ce4607c6f33e33f339831c214d923ffc7777d21",
"sha256:4a5b0d277087a5bf261a607fc6ff4aaffcf80b300cd19b7a1e9754a4649f5fd4", "sha256:567d6891cb1ddbfd0051fcff3cb1bb86efc82ec818d9c5f98c37d59c1d23cc96",
"sha256:4e297fd2e58afe17e33dd80c231c3a9d850279a2a8625aed1d39f9be9534809e", "sha256:57b9df5d38ab49c13eaa9e0152fdfa8501fc23987f6dcf421b73484bfe573918",
"sha256:507e567aee4806576e20752f22533e8b7ec61e7e75062a7ce9222a0675aa0da6", "sha256:59ddb8a9f47ec807009c69e582d0de1c86c005f9f614557f4cebc7b8ac9b7d28",
"sha256:50d702149747852923be60cae125285eca8d189d4c7d8832c0c958d4071a0f78", "sha256:61b9f3cf3a55322baed8f0532b73bce77d688a01446c179392c4056ab74eb551",
"sha256:51947a00ae9924584fb14c0c1b1f4c1fd916d9abd6f47582f318ab9c9cb9f3d0", "sha256:639389b023315596e0293f85999645f47ec3dc28c892e51242dde6176c91486b",
"sha256:52328192d454ca2ddad09fbc088872b014c74b22ecdd5164717dc7e6442014fa", "sha256:64d1143e90299ba8c967324840912a63a903494b1870a52f6675bda53dc332f7",
"sha256:531e6dfec8058fcf5d69e863b61e6b28e3749b615a4dcc0ab8ad36307c4017fc", "sha256:6542e1cfe060badd160cd383ad93a84871595c14bb05fb8129f963248affd946",
"sha256:54bd71f14a5fa9bae73ef92f2e2be894dc36c7a6d1c4962e5969bd8a9aa39325", "sha256:69687ef16d501c9217675af36fa3c68c009c03e184b07d22ba245e5c01d47e6b",
"sha256:552a93be286ca485914777461b384761519db313e0a7f3012dca424c9610a4d5", "sha256:6f7e1cdf0abb11718e655bb258920453b1e89c2315e9019f60f0775704b12a8c",
"sha256:583b46b3ba44121de5e87e95ae379932dc5fd2e37ebdf2c11a6d7975891425c1", "sha256:7378055c9f456c5bb034ac313d9a9028fc6597619a0b16584099adba5a589fdb",
"sha256:5b58a672ec448fb36839a5fc7bf2b2f60df9a97b872d8bd6ca1a28da6126f5c7", "sha256:78103afbd0a94ac4c1f0b4014545fd149b968d5ea423aaa3b1f6e2c3fc19423e",
"sha256:5cfbdccddaa0ff07789e9e180db127906c676e479e05c04830cd458945de3511", "sha256:79038bf1a7b13d243e51f4b6909c6997c2ba2bffc45bcae264704308a2d17198",
"sha256:5da4939e241301f5e1d18118695e8d2c300be90431b66bd43a00376acec45e1e", "sha256:795b1760418ab750826420ae24f06f392c08e21dc234f0a369a69cc00444f8ec",
"sha256:5dd9edcab8979a50c2c4dec6d5b66789fb6f630bb52ab90a4548111075a75e48", "sha256:7998dfb1e9e0255fb8553abb019c3e7f558925de4edc8604243775ff9dd3898d",
"sha256:5e304f94c0353f6ae5711533b5793b3a45b17aa2c5b07e656649b0af4e0939b5", "sha256:7afc740ad45ee0e0cef055765faf64789c2c183eb4aa3ecb8cecdb4b607396b3",
"sha256:60408ec9c0bd76f1fa00d28034429a0316246d31069b982a86aec8d5c99e910a", "sha256:7b4a41dc183d7d16750634f65566205990f94144755a39f33da44c0350c3e1a8",
"sha256:62f71b268f14ee6cc3045b95441bfe0518cef1d0b2ffbc6f3e9758f786ff5a03", "sha256:7f825ebedcad87a2825ddb6cf62f6d7d5b7a56ddaf7c93eef4b974e7ddc16408",
"sha256:664d462a4c0783fd755fe3440f07b7e46d149859c96caacadf3f28890f19a8de", "sha256:7f9f9bb2c5cc1f679605ebbeb72f46fc395d850b93fa7de7addd502a1dc66e99",
"sha256:66d8b7a89fac6042f7df9ea97d97ed0f5e404281110a882e3babd909161f85b6", "sha256:7fdf059d4e3acec44f512ebe247718ae511fde632e2b06992022df8e637385a6",
"sha256:6755cfcfa7d8966e704d580c831e39818f85e7b2b7852ad22708973176f0009e", "sha256:81e4648c09103bc18f488957c1e0863d2397bab6625c0e6771891f151ee0bd96",
"sha256:679856547f0b27b98811b73756bdf53769c23b045a6f95177cae634daabf1ddf", "sha256:8489bff00a1f81ac0754355772e76775878c32a42f16f01d427c3645546761c4",
"sha256:6841c08b51417f8ffe398b2828fc0593440c99525c868f640e0302476745320b", "sha256:851398428f5604c53371b72c5e0a28163274264ada4a08cd1eafe65fde1f68d0",
"sha256:68f6e64d4867ee79e25c49d7f35b2b1f04a6d6f778176dcf5b759f3b17a02b2b", "sha256:87a29b8a4cc72af6118954592dcd4e49223420470ccc3f8091c255f6c7330bb1",
"sha256:69d2d507c1174330c71c834b5d65e66181ad7b42b0d88b5b31804ee9b4f5dae7", "sha256:8b8e07374d60040b24d1a158895d9758424db13be63d4b2fe1870e37f9dec009",
"sha256:6f0be27d06732e2833b672a8fcc32fa195bdb22161eb88f8890de15e30264a01", "sha256:8d4aa56782368269eb9402caf7378b2a5ada6f05eb9c7edc2362be258973fd7e",
"sha256:6f7d2dbe628f3db935622a5b80a5c4d95665cdefc4904372aa3c4d786289477f", "sha256:97c448a20aded59727261468873d9b11dfdcce5a6338a359135667d5e3f1d070",
"sha256:72760411d60d8d76979a20ed3f15586d824db04668b581b86e61158c2b616db0", "sha256:98373c273e01a5a7c17103ecb617de7c9980b7608351d58c72198e3525f0002e",
"sha256:727f7a969416f02ef5c1256541e06f0836fb615022699fa8e2591e85296c5570", "sha256:98e4a17f55f3cbf6fe06cc79234269572f234467c8355b6758eb252073f78e6b",
"sha256:77d2368a06a86a18919c05a9b4b0ee9869f770e6a5f414b0fecc911870fe3974", "sha256:99124e39658b2f72d296819ec03418609dd4f1b275b00289c2f278a19da6f9c0",
"sha256:79ab1c5f26f23e51d4a44c4397c8a3bf56c306c125dfab6b3eebdfa13d1dca6f", "sha256:9ad0df7886cb9d6d2ff75e87d323108a0e32bdca5c9918071681864129ce8ea8",
"sha256:79db23eda81627132327ed292bd813a9af64399b98aaac3d42ad8deeed24cd5e", "sha256:9bfdfe2e2af434d3f4e47250f693657334e34a7ec557cd703b129a814422b4b8",
"sha256:7c20d6e6cafce5027e7092beb2ac6eec0d71045d6318b34f36e1387a8c8859a3", "sha256:9faa4c6fcb19a31240ad389426699a99df481b6576f7286471e24efbf1b44dfc",
"sha256:7e0851a985a7b10f634188117c825ef99d63402555cca5bc32c7bfc5adaf0d6f", "sha256:a048e41e1cb0c1a37021269d02698e30d2a7cc9a0205dd3390e0807745b76dae",
"sha256:7e2e1ff784c2cdfd863bad31985851427f2d2796e445cec85080c7510cba4315", "sha256:a05982bb49c73463cb0f0f4bed2d8da82631708a2c2d1926107ba99651b419ec",
"sha256:7f8b12424f8fdf29d1c0749c628bd1530cecfc77374935d096cccc0e4eada232", "sha256:a23b5f13f9b292004e94b0b13fead4dae79c7512db04dc817ff2c2478298e04a",
"sha256:81e84054b22babcd6c5cc1eac0de2bfc1054ecdf742720cbfb36efbe89ec6c30", "sha256:a393b0f881eff94440f72846a6f0f95b983594a0a50af81c41ed18107420d6a7",
"sha256:84bb57010a1ab76cf880424a2e0bce8dd26989849d2122ff073aa11bfc271c27", "sha256:a3b6bd81c77d9925809b714980cd30b1831a86bd090316d37cab124d92af1daf",
"sha256:870ed23361e2918ab1ffc23fe0ab293abf3c372a68ee7387456d13da3e213008", "sha256:a43f4631ecb87bedc510568fef67db53f2a20c4a5953a9d1e07457e7b1d14911",
"sha256:8cf44b012e7493127ce7ca6e469138ac96b3295a117877d5469aabe7c8728d87", "sha256:a569c993942ac26c6c590639ed6712c6c9c3f0c8d287a067bf2a60eb615f3c6b",
"sha256:8d6c9bc14bacdfbfd51fed85f0576973eaaa7b30d81ef93264f8e22b86a9c9f7", "sha256:a5b89349f05431270d1ccc7321aaab91c42ff33f463868779e502438b7f0e668",
"sha256:8d759cecfa8aab4a1eb4e23b6420126b15c7743e85b33f389916bb98c4ecbb84", "sha256:ac39319e6322c2c093a660c02cea6bb3b1ae53d049b573d4781df8896e443e04",
"sha256:8ef3f0977c21190f949d5cfd71ded09de87d330c6d98bd5ecb5bb1135d666d0d", "sha256:acc56700963f63307ac096689d4547e8061028a66bb78b90e42c5da2898898fb",
"sha256:8f95daf0ce2b24815ddf62667229ba5dfc0cfee43eb43b2549766170d0f24ae9", "sha256:b723f9d10f7d8259f010b87fa66e924bb4d67927d9dcff4526a755e9ee84fef4",
"sha256:911b4a16dce370657e5b8d8b6ba0fbb50dd5e2b24c4416f4b9e664503d3f0502", "sha256:b99a0347bc6131046c19e056a113daa34d7df99f1f45510161bc78bc8461a470",
"sha256:96117212229905da864794df9ea7bd54987c30a5dcbab3432edc3f344231adae", "sha256:bc0880011b86f81c5353ce4abaeb2472d942ba2320985166a2a3dd4f783563a9",
"sha256:963cbcf296943f7017470d0b705e63e908f32b4f7dbe43f72c22f6fe1bd9ef66", "sha256:be2f40045432e8aa33d9fd5cb43c91b0c61d77d3d8810f88e84e2e46411c27a7",
"sha256:975a118aa019d745f1398613b27fd8789f60a8cea057a00cdc1abedee123ffe6", "sha256:bebb17125373c499beea009cc5bced757bde52bcb3fa1d6335650e6c2d8111d7",
"sha256:9930853d451086c4c084d83a87294bdb0c5bc0fa4105a26c487ac09ea62e565b", "sha256:befac6644c6f304a1b6a7948a04095682849c426cebcc44cb2459aa92d3e1735",
"sha256:99d16862e802e7c50c3b6cdd1bf041b6142335c9c2b426631f731257adfe5a15", "sha256:c1f4880bcb6fb7a8e2ab89128032b3dcf59e1e877ff4493b11c8bf7c3a5b3df2",
"sha256:9ed4a2852b3de7a64884afcc6936db771707943249a060aec8e551c16361d478", "sha256:c3e014f7295b9327fa6f0b3e55a3fd485abac98be145b9597e0cdbb05c44ad07",
"sha256:9f7796959c9c036a115d34696563f75d4a2912d3b97c15c15f2a36bdd5496ce9", "sha256:c427dfcce13a8c814556dfe7c110b8ef61b8fab5fca0d856d4890856807321dc",
"sha256:a04b7a9017b8d0341ebbe77f61b74df1cf1b714f42b671a06f4912dc93d82597", "sha256:c44cf0059633470c6bb415091def546adbeb5dcfa91cc3fcb1ac16593f14e52a",
"sha256:a1b3c4ca3bec8e0ad9d32ce62444c5f3913588124a922629aa7d39357b2adf3f", "sha256:c4e04c12f507942f1ddf215cb3a08c244d24051cdd2ba571060166ce8a92be16",
"sha256:a290a417608f50137bec731d1f22ff3efebac72845530807a8433b2db9358c95", "sha256:c65257899bb8faf6a111297b4ff0066324a6b901318582c0453a01422c3bcd5a",
"sha256:a33f7c5acf44961f29018b13f0b5f5e1589ac0cfdf75a97c9774cf7ec84d09e0", "sha256:c6c48cf5a92244ef3df4161c8625ee1890bb3d931db9a9f3b699e61a037cd58a",
"sha256:a39be79a7c36e9a2e20376261c30deb3cdca86b50f7462ae9ff10a755c6720b9", "sha256:c9bf2bf29854f165a47917b8782b6cf3a7d602971bf454806208d0cbb96f797a",
"sha256:a50a66fa34dd7f9dcdbc7602a1b7bf6f9ab030b4f43e892324193423d9ede180", "sha256:ca4b6298c89b92d6b0a67dfc5f98d68ae92b08101d227263ef2033b9c9a03a72",
"sha256:a5ce1bdee102f7e60c075274df10b892d9ff5183ad6f5f515973eda8903dfe4c", "sha256:cc76ad7453816318d794248fba4032967eaffd992d76e5d1af10ef9d46589770",
"sha256:a763dd33d6e27c9b4db3f8089a5fa39179a8a3cf48ce702b24a857d7c621333c", "sha256:cd7f6bfa2a36fb91b7dec9ddf905716f2ed0c3675d2b63c69b7530c9d211e715",
"sha256:a773199dc42b5d02fcd46c8add552da2c4725ce2caa069527c7e27b5b6089e85", "sha256:d12c45da97b2f31d0233e15f8d68731cfa86264c9f04b2669b9fdf46aaf68e1f",
"sha256:aa3c925502bd0b957a96a5619134bcdc0382ef73cffd40bad218ced3586bcf8d", "sha256:d160173efdad8a57c22e422a034196df3d84753672c497aee2f94bd5b128f8dd",
"sha256:aeb6db2f4ab54ac21a3851d05130a2aa78a6f6a5f14003f9ae3114fb8b210850", "sha256:d2b1ed363a4ef5622dccbf7822f01b51195062c4f382b28c9bd125d046d0324c",
"sha256:af670708e145b048ead87375b899229443f2d0b4af2d1450d7701c74cd932b03", "sha256:d30e7daaf228e3d69cdd8b02c0dd4199cec034c4b93c80109f56f4675a6db957",
"sha256:afa24e5750c9b89ad5a7efef037efe49f4e339f20a94bf678c422c0c71e1207a", "sha256:d3f38373d9b2629dedc559e647010541cc4ec4ad9bea560e2eb1017e6a00d9ef",
"sha256:b02cc1cac9099c0ec72da09593e7fadb1b6cf88a101acc8153592c700d732d80", "sha256:d7e274ac1975e55ebfb8166cce27e13dc99120c1d6ce9e490d7a716b9be9abb5",
"sha256:b37c9ea942395de029be270f0eca8c141eb14e8455941495cd3b6f95bbe465f4", "sha256:d877759842ff9eb16d9c2b8b497953a7d994d4b231c171515f0bf3a2ae185c0c",
"sha256:b3b521e117ab991d6b3b830656f464b354a42dbea2ca16a0e7d93d573f7ab7ff", "sha256:da3dfd2776226e15d3288a3a24c7975f9ee160ba198f2efa66bc28c5ba76d792",
"sha256:b5ad8261f47c2a72d0f676bc40f752db8cfdcab911e970753343836e41d5a9a7", "sha256:db0441e80773d747a1ed9edfb9f75e7acb68ce8627583bbb6f770b7ec49f0064",
"sha256:b9616ea14917d06736339cf36bb9eaf4eb52110a74136b0dc5eff94e92417d22", "sha256:dbbaa147cf28b3e87738c624d390a3a9e2a5dfef4316f4c38b4ecaf3155a3eab",
"sha256:b9a03767c937b621ee267507bc394df97fb2f8f61130f39f2033f3c6c191f124", "sha256:ddc646cec4899a137c134b13818469e4178a251d77f9f4b23229267e3da78cfb",
"sha256:b9ae0008cff25e154ef1e3975a1705d344e844ffdeb34c25b007fd48c876e95d", "sha256:df7cc9584614f495f474a5ded365cf72decbcee4efcdc888d2943f8a794c789e",
"sha256:bdd6412c1f38da7565126b174f4e644f362e317ef0560fac1fb9d0c70900ff4d", "sha256:dfde50ae55e075dcd5801e2c3ea0e749c849ed2cbbee991af0f97f1bdbadb2a6",
"sha256:bfc417e58f277e949ed662d9cd050ddbb00c0dea8a828abaccc93dc357b7a6d1", "sha256:e15e70a3cf5bb519e2448524d689c02ff6bcd4750587a517e2bffee06065bf27",
"sha256:c15b9e37bbca59657e4dcc63ad068c821a4676def15f04742c406748a0a11b9c", "sha256:e3572889fcb87e5ca94add412d8b365dbb7b59773a4362e52caa556e5fd98643",
"sha256:c677849947d523a082be6e0b5c9137f443a54e951a1711ef003ec52910c41ece", "sha256:e39f5e85e1e3d7d84ac2217cd095b3678306c979e991532df47012880e02215d",
"sha256:c9d247fcc33c90f2758f4162693250341e3f38cd094f64390076ef33ad0887f9", "sha256:e501bd27c795105aaba02b5212ecd1bb552ca2ee2ede53e5a8cb74deee0e2052",
"sha256:ca643295bf5441dd38dadf7571ca4b63961820eedbffbe46ceba0893bf226203", "sha256:e62892645f6a214eefb58a42c3ed2501af2e40a797844e0e09ec1e400ce75f3d",
"sha256:ca87f639094c72268e17bc7f57c1225cc38f9e191a489a0134762e3fec402c1a", "sha256:e75eb1734046291c554d9addecca9a8785bdf5d53a64f525569f8549da863dde",
"sha256:cc060bc17b9de27874997d612e37d52f72092f9b59d1e04284a90ed8113cadca", "sha256:e84cff8e8fe71903a6cf873fb3c8731df8bd7c1dac878e7a0fe19d8e2ef39aa9",
"sha256:ccf4a73e07bfbd790443d6b3c1f1447ffda23cc9391e40c035d9b7d3514b57b8", "sha256:ea60cf85b4e5a78b5a41eed3a65abc3839a50d915c6e0f6966cbcf81b85991bd",
"sha256:cf36cadeb9c989f760a13058dbc455e5406ec3d2d247c705c8d4bc6dd1b0fcc6", "sha256:ec3fd30622180cbe2326d48c14a4ab7f98a504b104bdca7dda88b134adad6e31",
"sha256:d47e2bdeba4fb1986af2ba395ce51223f4d460e6e77119439e78f2b592cafade", "sha256:eccc6829035c8b7b391a0aa124fade54932bb937dd1079f2740b9f1bde829226",
"sha256:db78cc5c03b446a43413165aa873e2f408e9fd5ddb45533e7bd3b638bace867c", "sha256:eda67136343db96752e58ef36ac37116f36cba40961e79fd0e9bd858f5a09b38",
"sha256:dbc5029c61f9ebb2d4c247f13584a0ef0e8e49abb13e56460310821aca3ffcaf", "sha256:ef5a99a8d1a5c47b4cf85925d1420fc4ee584c98be8efc548651447b3047242f",
"sha256:ddb319f869d497ef2d3d56319360b61284a9a1d8b3de3bc936748698acfda6be", "sha256:f0795e2be2aa8afd013635f30ffe599cc00f1bbaca2d1d19b6187b4d1c58fb44",
"sha256:e0e4fdeae6c0a9d886749780ec5dcf469e98f27b312efa93008d03eaa2426fd5", "sha256:f31d8c2168bf2a52e4539232392352832c2296e07e0e14b6e06a44da574099ba",
"sha256:e4c5e7edf1e7bcbde3b52058f171a411e2a24a081b3e951d685dfea4c3c383d5", "sha256:f41a4b57cbc128a699e9d716a56c90c7fc76554e680fe2962f49cc4d8688b051",
"sha256:e71c9dba78671d38a549e3b2d52514f50e199f9d7e18ed9b0180adeef0d04130", "sha256:f583a1fb180a123c00064fab1a3bfb9d43e574b6474be1be3f6469e0331e3e2e",
"sha256:e997d22e0d1e08c8752f61675a75d93659f7aa4dbeaee54207f8d877817b4a0c", "sha256:f7c531722e8c3901f6bb303db464cac98ab44ed422c0fd0c762baa4a8d49ffa1",
"sha256:efa5834ba5e6c70b22afdca3894097e5a592d8d483c976359654ba990477799a", "sha256:f8ab90410b2ba5b8276657c66941bcaae556a38be8dd81630a7647e8735f0a20",
"sha256:f2d951002b11962b26afb31f758c18ad39771f287b100fa5adb1d09a47eaaf5b", "sha256:fa05460dc4f57358680b977b4a254d331b24c8beb501319b998625fd6a22654b",
"sha256:f3f96f57cea35ba19fd23a20b38fa0dfa3d87d582507129b8c8e314aa298f59b", "sha256:fbe1ef622748d2edb3dd4fef933b934e90e479f9831dfe31bda3fdc16bf5287f",
"sha256:f738051052abc95dc17f9a4c92044294a263fb7f762efdb13e528d419005c0e4", "sha256:fdb7af369df317527d697c5bb37ab944bb9a17ea1a5e82e47d5c7c638f3ccdd6",
"sha256:f76784355060999c36fa807b59faecb38f5769ae58283d00270835773f95e35b", "sha256:fe1f1f4010244cb07f6a079854a12e1627e4fb9ea99d672f2ceccaf6653ca514",
"sha256:f92462ea3888c99439f58f7561ecd5dd4cf8b8b1b259ccf5376667b8c46ee747", "sha256:fe2493d3f49e314e573022ead4d8c845c9748979b7eb95e815429fe947c4bde2",
"sha256:fefd18b29f3b84a0cdea1d86340219d9871c3b0673a38e722a73a2c39591eaa7" "sha256:ffd112646486a31ea5a45aa1eca0e2cd90b6a12f67e848e50349e324c24cc2e7"
], ],
"version": "==3.6.0" "version": "==3.7.1"
}, },
"bitstring": { "bitstring": {
"hashes": [ "hashes": [
@@ -173,76 +173,93 @@
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
"sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
"sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
"sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
"sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
"sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
"sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
"sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
"sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
"sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
"sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
"sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
"sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
"sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
"sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
"sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
"sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
"sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
"sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
"sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
"sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
"sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
"sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
"sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
"sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
"sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
"sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
"sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
"sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
"sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
"sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
"sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
"sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
"sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
"sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
"sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
"sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
"sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
"sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
"sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
"sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
"sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
"sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
"sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
"sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
"sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
"sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
"sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
"sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
"sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
"sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
"sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
"sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
"sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
"sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
"sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
"sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
"sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
"sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
"sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
"sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
"sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
"sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
"sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
"sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
"sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
"sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
"sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
], ],
"markers": "platform_python_implementation != 'PyPy'", "markers": "platform_python_implementation != 'PyPy'",
"version": "==1.17.1" "version": "==2.0.0"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@@ -254,46 +271,46 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34",
"sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513",
"sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5",
"sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c",
"sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63",
"sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130",
"sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae",
"sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18", "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443",
"sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59",
"sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee",
"sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf",
"sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27",
"sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db", "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde",
"sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971",
"sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8",
"sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339",
"sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6",
"sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90",
"sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691",
"sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3",
"sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083",
"sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012", "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6",
"sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1",
"sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3",
"sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d", "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8",
"sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2",
"sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d", "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7",
"sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141",
"sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3",
"sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9",
"sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4",
"sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4",
"sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b",
"sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252",
"sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17",
"sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da", "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b",
"sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983" "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd"
], ],
"markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'",
"version": "==45.0.6" "version": "==45.0.7"
}, },
"esptool": { "esptool": {
"hashes": [ "hashes": [
@@ -303,6 +320,14 @@
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==5.0.2" "version": "==5.0.2"
}, },
"h11": {
"hashes": [
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
],
"markers": "python_version >= '3.8'",
"version": "==0.16.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
@@ -320,11 +345,11 @@
}, },
"markdown-it-py": { "markdown-it-py": {
"hashes": [ "hashes": [
"sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147",
"sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.10'",
"version": "==3.0.0" "version": "==4.0.0"
}, },
"mdurl": { "mdurl": {
"hashes": [ "hashes": [
@@ -345,19 +370,19 @@
}, },
"platformdirs": { "platformdirs": {
"hashes": [ "hashes": [
"sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85",
"sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4" "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==4.3.8" "version": "==4.4.0"
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
"sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
"sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
], ],
"markers": "python_version >= '3.8'", "markers": "implementation_name != 'PyPy'",
"version": "==2.22" "version": "==2.23"
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
@@ -467,11 +492,20 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
"sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76" "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
], ],
"markers": "python_version < '3.13'",
"version": "==4.15.0"
},
"uvicorn": {
"hashes": [
"sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a",
"sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"
],
"index": "pypi",
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==4.14.1" "version": "==0.35.0"
}, },
"watchfiles": { "watchfiles": {
"hashes": [ "hashes": [

2
lib/microdot/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from microdot.microdot import Microdot, Request, Response, abort, redirect, \
send_file # noqa: F401

8
lib/microdot/helpers.py Normal file
View File

@@ -0,0 +1,8 @@
try:
from functools import wraps
except ImportError: # pragma: no cover
# MicroPython does not currently implement functools.wraps
def wraps(wrapped):
def _(wrapper):
return wrapper
return _

1450
lib/microdot/microdot.py Normal file

File diff suppressed because it is too large Load Diff

70
lib/microdot/utemplate.py Normal file
View File

@@ -0,0 +1,70 @@
from utemplate import recompile
_loader = None
class Template:
"""A template object.
:param template: The filename of the template to render, relative to the
configured template directory.
"""
@classmethod
def initialize(cls, template_dir='templates',
loader_class=recompile.Loader):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load
templates from a *templates* subdirectory.
:param loader_class: the ``utemplate.Loader`` class to use when loading
templates. This argument is optional. The default
is the ``recompile.Loader`` class, which
automatically recompiles templates when they
change.
"""
global _loader
_loader = loader_class(None, template_dir)
def __init__(self, template):
if _loader is None: # pragma: no cover
self.initialize()
#: The name of the template
self.name = template
self.template = _loader.load(template)
def generate(self, *args, **kwargs):
"""Return a generator that renders the template in chunks, with the
given arguments."""
return self.template(*args, **kwargs)
def render(self, *args, **kwargs):
"""Render the template with the given arguments and return it as a
string."""
return ''.join(self.generate(*args, **kwargs))
def generate_async(self, *args, **kwargs):
"""Return an asynchronous generator that renders the template in
chunks, using the given arguments."""
class sync_to_async_iter():
def __init__(self, iter):
self.iter = iter
def __aiter__(self):
return self
async def __anext__(self):
try:
return next(self.iter)
except StopIteration:
raise StopAsyncIteration
return sync_to_async_iter(self.generate(*args, **kwargs))
async def render_async(self, *args, **kwargs):
"""Render the template with the given arguments asynchronously and
return it as a string."""
response = ''
async for chunk in self.generate_async(*args, **kwargs):
response += chunk
return response

231
lib/microdot/websocket.py Normal file
View File

@@ -0,0 +1,231 @@
import binascii
import hashlib
from microdot import Request, Response
from microdot.microdot import MUTED_SOCKET_ERRORS, print_exception
from microdot.helpers import wraps
class WebSocketError(Exception):
"""Exception raised when an error occurs in a WebSocket connection."""
pass
class WebSocket:
"""A WebSocket connection object.
An instance of this class is sent to handler functions to manage the
WebSocket connection.
"""
CONT = 0
TEXT = 1
BINARY = 2
CLOSE = 8
PING = 9
PONG = 10
#: Specify the maximum message size that can be received when calling the
#: ``receive()`` method. Messages with payloads that are larger than this
#: size will be rejected and the connection closed. Set to 0 to disable
#: the size check (be aware of potential security issues if you do this),
#: or to -1 to use the value set in
#: ``Request.max_body_length``. The default is -1.
#:
#: Example::
#:
#: WebSocket.max_message_length = 4 * 1024 # up to 4KB messages
max_message_length = -1
def __init__(self, request):
self.request = request
self.closed = False
async def handshake(self):
response = self._handshake_response()
await self.request.sock[1].awrite(
b'HTTP/1.1 101 Switching Protocols\r\n')
await self.request.sock[1].awrite(b'Upgrade: websocket\r\n')
await self.request.sock[1].awrite(b'Connection: Upgrade\r\n')
await self.request.sock[1].awrite(
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
async def receive(self):
"""Receive a message from the client."""
while True:
opcode, payload = await self._read_frame()
send_opcode, data = self._process_websocket_frame(opcode, payload)
if send_opcode: # pragma: no cover
await self.send(data, send_opcode)
elif data: # pragma: no branch
return data
async def send(self, data, opcode=None):
"""Send a message to the client.
:param data: the data to send, given as a string or bytes.
:param opcode: a custom frame opcode to use. If not given, the opcode
is ``TEXT`` or ``BINARY`` depending on the type of the
data.
"""
frame = self._encode_websocket_frame(
opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
data)
await self.request.sock[1].awrite(frame)
async def close(self):
"""Close the websocket connection."""
if not self.closed: # pragma: no cover
self.closed = True
await self.send(b'', self.CLOSE)
def _handshake_response(self):
connection = False
upgrade = False
websocket_key = None
for header, value in self.request.headers.items():
h = header.lower()
if h == 'connection':
connection = True
if 'upgrade' not in value.lower():
return self.request.app.abort(400)
elif h == 'upgrade':
upgrade = True
if not value.lower() == 'websocket':
return self.request.app.abort(400)
elif h == 'sec-websocket-key':
websocket_key = value
if not connection or not upgrade or not websocket_key:
return self.request.app.abort(400)
d = hashlib.sha1(websocket_key.encode())
d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
return binascii.b2a_base64(d.digest())[:-1]
@classmethod
def _parse_frame_header(cls, header):
fin = header[0] & 0x80
opcode = header[0] & 0x0f
if fin == 0 or opcode == cls.CONT: # pragma: no cover
raise WebSocketError('Continuation frames not supported')
has_mask = header[1] & 0x80
length = header[1] & 0x7f
if length == 126:
length = -2
elif length == 127:
length = -8
return fin, opcode, has_mask, length
def _process_websocket_frame(self, opcode, payload):
if opcode == self.TEXT:
payload = payload.decode()
elif opcode == self.BINARY:
pass
elif opcode == self.CLOSE:
raise WebSocketError('Websocket connection closed')
elif opcode == self.PING:
return self.PONG, payload
elif opcode == self.PONG: # pragma: no branch
return None, None
return None, payload
@classmethod
def _encode_websocket_frame(cls, opcode, payload):
frame = bytearray()
frame.append(0x80 | opcode)
if opcode == cls.TEXT:
payload = payload.encode()
if len(payload) < 126:
frame.append(len(payload))
elif len(payload) < (1 << 16):
frame.append(126)
frame.extend(len(payload).to_bytes(2, 'big'))
else:
frame.append(127)
frame.extend(len(payload).to_bytes(8, 'big'))
frame.extend(payload)
return frame
async def _read_frame(self):
header = await self.request.sock[0].read(2)
if len(header) != 2: # pragma: no cover
raise WebSocketError('Websocket connection closed')
fin, opcode, has_mask, length = self._parse_frame_header(header)
if length == -2:
length = await self.request.sock[0].read(2)
length = int.from_bytes(length, 'big')
elif length == -8:
length = await self.request.sock[0].read(8)
length = int.from_bytes(length, 'big')
max_allowed_length = Request.max_body_length \
if self.max_message_length == -1 else self.max_message_length
if length > max_allowed_length:
raise WebSocketError('Message too large')
if has_mask: # pragma: no cover
mask = await self.request.sock[0].read(4)
payload = await self.request.sock[0].read(length)
if has_mask: # pragma: no cover
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
return opcode, payload
async def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
async def echo(request):
if not authenticate_user(request):
abort(401)
ws = await websocket_upgrade(request)
while True:
message = await ws.receive()
await ws.send(message)
"""
ws = WebSocket(request)
await ws.handshake()
@request.after_request
async def after_request(request, response):
return Response.already_handled
return ws
def websocket_wrapper(f, upgrade_function):
@wraps(f)
async def wrapper(request, *args, **kwargs):
ws = await upgrade_function(request)
try:
await f(request, ws, *args, **kwargs)
except OSError as exc:
if exc.errno not in MUTED_SOCKET_ERRORS: # pragma: no cover
raise
except WebSocketError:
pass
except Exception as exc:
print_exception(exc)
finally: # pragma: no cover
try:
await ws.close()
except Exception:
pass
return Response.already_handled
return wrapper
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
message = await ws.receive()
await ws.send(message)
"""
return websocket_wrapper(f, websocket_upgrade)

View File

14
lib/utemplate/compiled.py Normal file
View File

@@ -0,0 +1,14 @@
class Loader:
def __init__(self, pkg, dir):
if dir == ".":
dir = ""
else:
dir = dir.replace("/", ".") + "."
if pkg and pkg != "__main__":
dir = pkg + "." + dir
self.p = dir
def load(self, name):
name = name.replace(".", "_")
return __import__(self.p + name, None, None, (name,)).render

View File

@@ -0,0 +1,21 @@
# (c) 2014-2020 Paul Sokolovsky. MIT license.
try:
from uos import stat, remove
except:
from os import stat, remove
from . import source
class Loader(source.Loader):
def load(self, name):
o_path = self.pkg_path + self.compiled_path(name)
i_path = self.pkg_path + self.dir + "/" + name
try:
o_stat = stat(o_path)
i_stat = stat(i_path)
if i_stat[8] > o_stat[8]:
# input file is newer, remove output to force recompile
remove(o_path)
finally:
return super().load(name)

188
lib/utemplate/source.py Normal file
View File

@@ -0,0 +1,188 @@
# (c) 2014-2019 Paul Sokolovsky. MIT license.
from . import compiled
class Compiler:
START_CHAR = "{"
STMNT = "%"
STMNT_END = "%}"
EXPR = "{"
EXPR_END = "}}"
def __init__(self, file_in, file_out, indent=0, seq=0, loader=None):
self.file_in = file_in
self.file_out = file_out
self.loader = loader
self.seq = seq
self._indent = indent
self.stack = []
self.in_literal = False
self.flushed_header = False
self.args = "*a, **d"
def indent(self, adjust=0):
if not self.flushed_header:
self.flushed_header = True
self.indent()
self.file_out.write("def render%s(%s):\n" % (str(self.seq) if self.seq else "", self.args))
self.stack.append("def")
self.file_out.write(" " * (len(self.stack) + self._indent + adjust))
def literal(self, s):
if not s:
return
if not self.in_literal:
self.indent()
self.file_out.write('yield """')
self.in_literal = True
self.file_out.write(s.replace('"', '\\"'))
def close_literal(self):
if self.in_literal:
self.file_out.write('"""\n')
self.in_literal = False
def render_expr(self, e):
self.indent()
self.file_out.write('yield str(' + e + ')\n')
def parse_statement(self, stmt):
tokens = stmt.split(None, 1)
if tokens[0] == "args":
if len(tokens) > 1:
self.args = tokens[1]
else:
self.args = ""
elif tokens[0] == "set":
self.indent()
self.file_out.write(stmt[3:].strip() + "\n")
elif tokens[0] == "include":
if not self.flushed_header:
# If there was no other output, we still need a header now
self.indent()
tokens = tokens[1].split(None, 1)
args = ""
if len(tokens) > 1:
args = tokens[1]
if tokens[0][0] == "{":
self.indent()
# "1" as fromlist param is uPy hack
self.file_out.write('_ = __import__(%s.replace(".", "_"), None, None, 1)\n' % tokens[0][2:-2])
self.indent()
self.file_out.write("yield from _.render(%s)\n" % args)
return
with self.loader.input_open(tokens[0][1:-1]) as inc:
self.seq += 1
c = Compiler(inc, self.file_out, len(self.stack) + self._indent, self.seq)
inc_id = self.seq
self.seq = c.compile()
self.indent()
self.file_out.write("yield from render%d(%s)\n" % (inc_id, args))
elif len(tokens) > 1:
if tokens[0] == "elif":
assert self.stack[-1] == "if"
self.indent(-1)
self.file_out.write(stmt + ":\n")
else:
self.indent()
self.file_out.write(stmt + ":\n")
self.stack.append(tokens[0])
else:
if stmt.startswith("end"):
assert self.stack[-1] == stmt[3:]
self.stack.pop(-1)
elif stmt == "else":
assert self.stack[-1] == "if"
self.indent(-1)
self.file_out.write("else:\n")
else:
assert False
def parse_line(self, l):
while l:
start = l.find(self.START_CHAR)
if start == -1:
self.literal(l)
return
self.literal(l[:start])
self.close_literal()
sel = l[start + 1]
#print("*%s=%s=" % (sel, EXPR))
if sel == self.STMNT:
end = l.find(self.STMNT_END)
assert end > 0
stmt = l[start + len(self.START_CHAR + self.STMNT):end].strip()
self.parse_statement(stmt)
end += len(self.STMNT_END)
l = l[end:]
if not self.in_literal and l == "\n":
break
elif sel == self.EXPR:
# print("EXPR")
end = l.find(self.EXPR_END)
assert end > 0
expr = l[start + len(self.START_CHAR + self.EXPR):end].strip()
self.render_expr(expr)
end += len(self.EXPR_END)
l = l[end:]
else:
self.literal(l[start])
l = l[start + 1:]
def header(self):
self.file_out.write("# Autogenerated file\n")
def compile(self):
self.header()
for l in self.file_in:
self.parse_line(l)
self.close_literal()
return self.seq
class Loader(compiled.Loader):
def __init__(self, pkg, dir):
super().__init__(pkg, dir)
self.dir = dir
if pkg == "__main__":
# if pkg isn't really a package, don't bother to use it
# it means we're running from "filesystem directory", not
# from a package.
pkg = None
self.pkg_path = ""
if pkg:
p = __import__(pkg)
if isinstance(p.__path__, str):
# uPy
self.pkg_path = p.__path__
else:
# CPy
self.pkg_path = p.__path__[0]
self.pkg_path += "/"
def input_open(self, template):
path = self.pkg_path + self.dir + "/" + template
return open(path)
def compiled_path(self, template):
return self.dir + "/" + template.replace(".", "_") + ".py"
def load(self, name):
try:
return super().load(name)
except (OSError, ImportError):
pass
compiled_path = self.pkg_path + self.compiled_path(name)
f_in = self.input_open(name)
f_out = open(compiled_path, "w")
c = Compiler(f_in, f_out, loader=self)
c.compile()
f_in.close()
f_out.close()
return super().load(name)

BIN
screenshots/controls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
screenshots/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

9
src/boot.py Normal file
View File

@@ -0,0 +1,9 @@
import settings
import wifi
from settings import Settings
s = Settings()
name = s.get('name', 'led')
password = s.get("ap_password", "")
# wifi.ap(name, password)

View File

@@ -1,79 +1,68 @@
import asyncio
import aioespnow
import patterns
from settings import Settings from settings import Settings
from web import web from web import web
from patterns import Patterns from patterns import Patterns
import gc import gc
import utime
import machine
import time
import wifi
import json import json
from p2p import p2p
import espnow import espnow
import network import network
import asyncio
import json
import machine
def main(): def main():
settings = Settings() settings = Settings()
print(settings) print(settings)
if settings.get("color_order", "rgb") == "rbg": patterns = Patterns(settings["led_pin"], settings["num_leds"], selected=settings["pattern"])
color_order = (1, 5, 3) if settings["color_order"] == "rbg": color_order = (1, 5, 3)
else: else: color_order = (1, 3, 5)
color_order = (1, 3, 5) patterns.set_color1(tuple(int(settings["color1"][i:i+2], 16) for i in color_order))
patterns = Patterns(settings["led_pin"], settings["num_leds"], brightness=255) patterns.set_color2(tuple(int(settings["color2"][i:i+2], 16) for i in color_order))
patterns.set_brightness(int(settings["brightness"]))
patterns.set_delay(int(settings["delay"]))
sta_if = network.WLAN(network.STA_IF) sta_if = network.WLAN(network.STA_IF)
sta_if.active(True) sta_if.active(True)
e = espnow.ESPNow() e = espnow.ESPNow()
e.config(rxbuf=1024)
e.active(True) e.active(True)
# Increase buffer size for 8-bar payloads (default 526 bytes might be too small) # Set to 1KB to handle larger multi-bar payloads
wdt = machine.WDT(timeout=10000) wdt = machine.WDT(timeout=10000)
wdt.feed() wdt.feed()
#print mac in hex
print("Mac address", sta_if.config("mac").hex())
print("Patterns", patterns.colors)
print("Patterns", patterns.selected)
patterns.select(patterns.selected)
while True: while True:
# advance pattern based on its own returned schedule
# due = patterns.tick(due)
wdt.feed()
patterns.tick() patterns.tick()
# Drain all pending packets and only process the latest wdt.feed()
last_msg = None host, msg = e.recv(0)
while True: if msg:
host, msg = e.recv(0)
if not msg:
break
last_msg = msg
if last_msg:
try: try:
data = json.loads(last_msg) data = json.loads(msg)
print(data) print(data)
# Always update parameters from message defaults = data.get("d", {})
patterns.brightness = data.get("brightness", patterns.brightness) bar = data.get(settings.get("name"), {})
patterns.delay = data.get("delay", patterns.delay)
patterns.colors = data.get("colors", patterns.colors)
patterns.selected = data.get("pattern", patterns.selected)
patterns.n1 = data.get("n1", patterns.n1)
patterns.n2 = data.get("n2", patterns.n2)
patterns.n3 = data.get("n3", patterns.n3)
patterns.n4 = data.get("n4", patterns.n4)
patterns.step = data.get("step", patterns.step)
patterns.auto = data.get("auto", patterns.auto)
patterns.select(patterns.selected) patterns.set_brightness(bar.get("brightness", defaults.get("brightness", 100)))
print("Selected pattern", patterns.selected) patterns.set_delay(bar.get("delay", defaults.get("delay", 100)))
colors = bar.get("colors", defaults.get("colors", ["#000000", "#000000"]))
patterns.colors = [tuple(int(color[i:i+2], 16) for i in settings.color_order) for color in colors]
patterns.select(bar.get("pattern", defaults.get("pattern", "off")))
patterns.n1 = bar.get("n1", defaults.get("n1", 0))
patterns.n2 = bar.get("n2", defaults.get("n2", 58))
patterns.on_width = bar.get("on_width", defaults.get("on_width", 1))
patterns.off_width = bar.get("off_width", defaults.get("off_width", 2))
patterns.oneshot = bar.get("oneshot", defaults.get("oneshot", False))
patterns.beat = bar.get("beat", defaults.get("beat", False))
patterns.beat_mode = bar.get("beat_mode", defaults.get("beat_mode", False))
patterns.auto = bar.get("auto", defaults.get("auto", True))
except Exception as ex: except:
print(f"Failed to load espnow data {last_msg}: {ex}") print(f"Failed to load espnow data {msg}")
continue continue
finally:
gc.collect()
main() main()

20
src/p2p.py Normal file
View File

@@ -0,0 +1,20 @@
import asyncio
import aioespnow
import json
async def p2p(settings, patterns):
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
e.active(True)
async for mac, msg in e:
try:
data = json.loads(msg)
except:
print(f"Failed to load espnow data {msg}")
continue
print(data)
if "names" not in data or settings.get("name") in data.get("names", []):
if "step" in settings and isinstance(settings["step"], int):
patterns.set_pattern_step(settings["step"])
else:
settings.set_settings(data.get("settings", {}), patterns, data.get("save", False))
print("should not print")

View File

@@ -1,177 +1,79 @@
from machine import Pin
from neopixel import NeoPixel
import utime import utime
import random import random
from patterns_base import PatternBase # Import PatternBase
class Patterns: class Patterns(PatternBase): # Inherit from PatternBase
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100): def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) super().__init__(pin, num_leds, color1, color2, brightness, selected, delay) # Call parent constructor
self.num_leds = num_leds
self.pattern_step = 0 # Pattern-specific initializations
self.last_update = utime.ticks_ms() self.on_width = 1 # Default on width
self.delay = delay self.off_width = 2 # Default off width (so total segment is 3, matching original behavior)
self.brightness = brightness self.n1 = 0 # Default start of fill range
self.n2 = self.num_leds - 1 # Default end of fill range
self.oneshot = False # New: One-shot flag for patterns like fill_range
self.patterns = { self.patterns = {
"off": self.off, "off": self.off,
"on" : self.on, "on" : self.on,
"color_wipe": self.color_wipe_step, "color_wipe": self.color_wipe,
"rainbow_cycle": self.rainbow_cycle_step, "rainbow_cycle": self.rainbow_cycle,
"theater_chase": self.theater_chase_step, "theater_chase": self.theater_chase,
"blink": self.blink_step, "blink": self.blink,
"color_transition": self.color_transition_step, # Added new pattern "color_transition": self.color_transition, # Added new pattern
"flicker": self.flicker_step, "flicker": self.flicker,
"scanner": self.scanner_step, # New: Single direction scanner "scanner": self.scanner, # New: Single direction scanner
"bidirectional_scanner": self.bidirectional_scanner_step, # New: Bidirectional scanner "bidirectional_scanner": self.bidirectional_scanner, # New: Bidirectional scanner
"external": None "fill_range": self.fill_range, # New: Fill from n1 to n2
"n_chase": self.n_chase, # New: N1 on, N2 off repeating chase
"alternating": self.alternating, # New: N1 on/off, N2 off/on alternating chase
"external": None,
"pulse": self.pulse
} }
self.selected = selected # Beat-related functionality removed
# self.selected is already initialized in PatternBase, but we need to ensure it uses our patterns dict
# self.selected = selected # Handled by PatternBase
# Ensure colors list always starts with at least two for robust transition handling # Ensure colors list always starts with at least two for robust transition handling
self.colors = [color1, color2] if color1 != color2 else [color1, (255, 255, 255)] # Fallback if initial colors are same # self.colors handled by PatternBase
if not self.colors: # Ensure at least one color exists
self.colors = [(0, 0, 0)]
self.transition_duration = delay * 50 # Default transition duration # Transition attributes handled by PatternBase
self.hold_duration = delay * 10 # Default hold duration at each color
self.transition_step = 0 # Current step in the transition
self.current_color_idx = 0 # Index of the color currently being held/transitioned from
self.current_color = self.colors[self.current_color_idx] # The actual blended color
self.hold_start_time = utime.ticks_ms() # Time when the current color hold started # Scanner attributes handled by PatternBase
# self.run handled by PatternBase
# New attributes for scanner patterns
self.scanner_direction = 1 # 1 for forward, -1 for backward
self.scanner_tail_length = 3 # Number of trailing pixels
def sync(self): def sync(self):
self.pattern_step=0 super().sync() # Call parent sync
self.last_update = utime.ticks_ms() - self.delay # Reset pattern_step for theater_chase when chase_width changes
if self.selected == "color_transition": if self.selected == "theater_chase" or self.selected == "fill_range" or self.selected == "n_chase" or self.selected == "alternating":
self.transition_step = 0 self.pattern_step = 0
self.current_color_idx = 0
self.current_color = self.colors[self.current_color_idx]
self.hold_start_time = utime.ticks_ms() # Reset hold time
# Reset scanner specific variables
self.scanner_direction = 1
self.tick() self.tick()
def set_pattern_step(self, step): def set_on_width(self, on_width):
self.pattern_step = step self.on_width = on_width
def tick(self): def set_off_width(self, off_width):
if self.patterns[self.selected]: self.off_width = off_width
self.patterns[self.selected]()
def update_num_leds(self, pin, num_leds): def set_on_off_width(self, on_width, off_width):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) self.on_width = on_width
self.num_leds = num_leds self.off_width = off_width
self.pattern_step = 0 self.sync()
def set_delay(self, delay): def set_fill_range(self, n1, n2):
self.delay = delay self.n1 = n1
# Update transition duration and hold duration when delay changes self.n2 = n2
self.transition_duration = self.delay * 50 self.sync()
self.hold_duration = self.delay * 10
def set_oneshot(self, oneshot_value):
def set_brightness(self, brightness): self.oneshot = oneshot_value
self.brightness = brightness if self.oneshot: # Reset pattern step if enabling one-shot
self.pattern_step = 0
def set_color1(self, color): self.sync()
if len(self.colors) > 0:
self.colors[0] = color
if self.selected == "color_transition":
# If the first color is changed, potentially reset transition
# to start from this new color if we were about to transition from it
if self.current_color_idx == 0:
self.transition_step = 0
self.current_color = self.colors[0]
self.hold_start_time = utime.ticks_ms()
else:
self.colors.append(color)
def set_color2(self, color):
if len(self.colors) > 1:
self.colors[1] = color
elif len(self.colors) == 1:
self.colors.append(color)
else: # List is empty
self.colors.append((0,0,0)) # Dummy color
self.colors.append(color)
def set_colors(self, colors):
if colors and len(colors) >= 2:
self.colors = colors
if self.selected == "color_transition":
self.sync() # Reset transition if new color list is provided
elif colors and len(colors) == 1:
self.colors = [colors[0], (255,255,255)] # Add a default second color
if self.selected == "color_transition":
print("Warning: 'color_transition' requires at least two colors. Adding a default second color.")
self.sync()
else:
print("Error: set_colors requires a list of at least one color.")
self.colors = [(0,0,0), (255,255,255)] # Fallback
if self.selected == "color_transition":
self.sync()
def set_color(self, num, color):
# Changed: More robust index check
if 0 <= num < len(self.colors):
self.colors[num] = color
# If the changed color is part of the current or next transition,
# restart the transition for smoother updates
if self.selected == "color_transition":
current_from_idx = self.current_color_idx
current_to_idx = (self.current_color_idx + 1) % len(self.colors)
if num == current_from_idx or num == current_to_idx:
# If we change a color involved in the current transition,
# it's best to restart the transition state for smoothness.
self.transition_step = 0
self.current_color_idx = current_from_idx # Stay at the current starting color
self.current_color = self.colors[self.current_color_idx]
self.hold_start_time = utime.ticks_ms() # Reset hold
return True
elif num == len(self.colors): # Allow setting a new color at the end
self.colors.append(color)
return True
return False
def add_color(self, color):
self.colors.append(color)
if self.selected == "color_transition" and len(self.colors) == 2:
# If we just added the second color needed for transition
self.sync()
def del_color(self, num):
# Changed: More robust index check and using del for lists
if 0 <= num < len(self.colors):
del self.colors[num]
# If the color being deleted was part of the current transition,
# re-evaluate the current_color_idx
if self.selected == "color_transition":
if len(self.colors) < 2: # Need at least two colors for transition
print("Warning: Not enough colors for 'color_transition'. Switching to 'on'.")
self.select("on") # Or some other default
else:
# Adjust index if it's out of bounds after deletion or was the one transitioning from
self.current_color_idx %= len(self.colors)
self.transition_step = 0
self.current_color = self.colors[self.current_color_idx]
self.hold_start_time = utime.ticks_ms()
return True
return False
def apply_brightness(self, color, brightness_override=None):
effective_brightness = brightness_override if brightness_override is not None else self.brightness
return tuple(int(c * effective_brightness / 255) for c in color)
def select(self, pattern): def select(self, pattern):
if pattern in self.patterns: if pattern in self.patterns:
self.selected = pattern super().select(pattern) # Use parent select to set self.selected and self.transition_step
self.sync() # Reset pattern state when selecting a new pattern self.run = True # Set run flag
if pattern == "color_transition": if pattern == "color_transition":
if len(self.colors) < 2: if len(self.colors) < 2:
print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.") print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.")
@@ -187,81 +89,64 @@ class Patterns:
return True return True
return False return False
def set(self, i, color):
self.n[i] = color
def write(self):
self.n.write()
def fill(self, color=None):
fill_color = color if color is not None else self.colors[0]
for i in range(self.num_leds):
self.n[i] = fill_color
self.n.write()
def off(self): def off(self):
self.fill((0, 0, 0)) self.fill((0, 0, 0))
def on(self): def on(self):
self.fill(self.apply_brightness(self.colors[0])) self.fill(self.apply_brightness(self.colors[0]))
def color_wipe_step(self): def color_wipe(self):
color = self.apply_brightness(self.colors[0]) color = self.apply_brightness(self.colors[0])
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay: if self.pattern_step < self.num_leds:
if self.pattern_step < self.num_leds:
for i in range(self.num_leds):
self.n[i] = (0, 0, 0)
self.n[self.pattern_step] = self.apply_brightness(color)
self.n.write()
self.pattern_step += 1
else:
self.pattern_step = 0
self.last_update = current_time
def rainbow_cycle_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay/5:
def wheel(pos):
if pos < 85:
return (pos * 3, 255 - pos * 3, 0)
elif pos < 170:
pos -= 85
return (255 - pos * 3, 0, pos * 3)
else:
pos -= 170
return (0, pos * 3, 255 - pos * 3)
for i in range(self.num_leds): for i in range(self.num_leds):
rc_index = (i * 256 // self.num_leds) + self.pattern_step self.n[i] = (0, 0, 0)
self.n[i] = self.apply_brightness(wheel(rc_index & 255)) self.n[self.pattern_step] = self.apply_brightness(color)
self.n.write() self.n.write()
self.pattern_step = (self.pattern_step + 1) % 256 self.pattern_step += 1
self.last_update = current_time else:
self.pattern_step = 0
self.last_update = current_time
def theater_chase_step(self): def rainbow_cycle(self):
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay: def wheel(pos):
for i in range(self.num_leds): if pos < 85:
if (i + self.pattern_step) % 3 == 0: return (pos * 3, 255 - pos * 3, 0)
self.n[i] = self.apply_brightness(self.colors[0]) elif pos < 170:
else: pos -= 85
self.n[i] = (0, 0, 0) return (255 - pos * 3, 0, pos * 3)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 3
self.last_update = current_time
def blink_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
if self.pattern_step % 2 == 0:
self.fill(self.apply_brightness(self.colors[0]))
else: else:
self.fill((0, 0, 0)) pos -= 170
self.pattern_step = (self.pattern_step + 1) % 2 return (0, pos * 3, 255 - pos * 3)
self.last_update = current_time
def color_transition_step(self): for i in range(self.num_leds):
rc_index = (i * 256 // self.num_leds) + self.pattern_step
self.n[i] = self.apply_brightness(wheel(rc_index & 255))
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 256
self.last_update = current_time
def theater_chase(self):
segment_length = self.on_width + self.off_width
for i in range(self.num_leds):
if (i + self.pattern_step) % segment_length < self.on_width:
self.n[i] = self.apply_brightness(self.colors[0])
else:
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % segment_length
def blink(self):
current_time = utime.ticks_ms()
if self.pattern_step % 2 == 0:
self.fill(self.apply_brightness(self.colors[0]))
else:
self.fill((0, 0, 0))
self.pattern_step = (self.pattern_step + 1) % 2
self.last_update = current_time
def color_transition(self):
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
# Check for hold duration first # Check for hold duration first
@@ -309,88 +194,241 @@ class Patterns:
self.last_update = current_time self.last_update = current_time
def flicker_step(self): def flicker(self):
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay/5: base_color = self.colors[0]
base_color = self.colors[0] # Increase the range for flicker_brightness_offset
# Increase the range for flicker_brightness_offset # Changed from self.brightness // 4 to self.brightness // 2 (or even self.brightness for max intensity)
# Changed from self.brightness // 4 to self.brightness // 2 (or even self.brightness for max intensity) flicker_brightness_offset = random.randint(-int(self.brightness // 1.5), int(self.brightness // 1.5))
flicker_brightness_offset = random.randint(-int(self.brightness // 1.5), int(self.brightness // 1.5)) flicker_brightness = max(0, min(255, self.brightness + flicker_brightness_offset))
flicker_brightness = max(0, min(255, self.brightness + flicker_brightness_offset))
flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness) flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness)
self.fill(flicker_color) self.fill(flicker_color)
self.last_update = current_time self.last_update = current_time
def scanner_step(self): def scanner(self):
""" """
Mimics a 'Knight Rider' style scanner, moving in one direction. Mimics a 'Knight Rider' style scanner, moving in one direction.
""" """
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay: self.fill((0, 0, 0)) # Clear all LEDs
self.fill((0, 0, 0)) # Clear all LEDs
# Calculate the head and tail position # Calculate the head and tail position
head_pos = self.pattern_step head_pos = self.pattern_step
color = self.apply_brightness(self.colors[0]) color = self.apply_brightness(self.colors[0])
# Draw the head # Draw the head
if 0 <= head_pos < self.num_leds: if 0 <= head_pos < self.num_leds:
self.n[head_pos] = color self.n[head_pos] = color
# Draw the trailing pixels with decreasing brightness # Draw the trailing pixels with decreasing brightness
for i in range(1, self.scanner_tail_length + 1): for i in range(1, self.scanner_tail_length + 1):
tail_pos = head_pos - i tail_pos = head_pos - i
if 0 <= tail_pos < self.num_leds: if 0 <= tail_pos < self.num_leds:
# Calculate fading color for tail # Calculate fading color for tail
# Example: linear fade from full brightness to off # Example: linear fade from full brightness to off
fade_factor = 1.0 - (i / (self.scanner_tail_length + 1)) fade_factor = 1.0 - (i / (self.scanner_tail_length + 1))
faded_color = tuple(int(c * fade_factor) for c in color) faded_color = tuple(int(c * fade_factor) for c in color)
self.n[tail_pos] = faded_color self.n[tail_pos] = faded_color
self.n.write() self.n.write()
self.pattern_step += 1 self.pattern_step += 1
if self.pattern_step >= self.num_leds + self.scanner_tail_length: if self.pattern_step >= self.num_leds + self.scanner_tail_length:
self.pattern_step = 0 # Reset to start self.pattern_step = 0 # Reset to start
self.last_update = current_time self.last_update = current_time
def bidirectional_scanner_step(self): def bidirectional_scanner(self):
""" """
Mimics a 'Knight Rider' style scanner, moving back and forth. Mimics a 'Knight Rider' style scanner, moving back and forth.
""" """
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay/100: self.fill((0, 0, 0)) # Clear all LEDs
self.fill((0, 0, 0)) # Clear all LEDs
color = self.apply_brightness(self.colors[0])
# Calculate the head position based on direction
head_pos = self.pattern_step
# Draw the head
if 0 <= head_pos < self.num_leds:
self.n[head_pos] = color
# Draw the trailing pixels with decreasing brightness
for i in range(1, self.scanner_tail_length + 1):
tail_pos = head_pos - (i * self.scanner_direction)
if 0 <= tail_pos < self.num_leds:
fade_factor = 1.0 - (i / (self.scanner_tail_length + 1))
faded_color = tuple(int(c * fade_factor) for c in color)
self.n[tail_pos] = faded_color
self.n.write()
self.pattern_step += self.scanner_direction
# Change direction if boundaries are reached
if self.scanner_direction == 1 and self.pattern_step >= self.num_leds:
self.scanner_direction = -1
self.pattern_step = self.num_leds - 1 # Start moving back from the last LED
elif self.scanner_direction == -1 and self.pattern_step < 0:
self.scanner_direction = 1
self.pattern_step = 0 # Start moving forward from the first LED
self.last_update = current_time
def fill_range(self):
"""
Fills a range of LEDs from n1 to n2 with a solid color.
If self.oneshot is True, it fills once and then turns off the LEDs.
"""
current_time = utime.ticks_ms()
if self.oneshot and self.pattern_step >= 1:
self.fill((0, 0, 0)) # Turn off LEDs if one-shot already happened
else:
color = self.apply_brightness(self.colors[0]) color = self.apply_brightness(self.colors[0])
for i in range(self.n1, self.n2 + 1):
# Calculate the head position based on direction self.n[i] = color
head_pos = self.pattern_step
# Draw the head
if 0 <= head_pos < self.num_leds:
self.n[head_pos] = color
# Draw the trailing pixels with decreasing brightness
for i in range(1, self.scanner_tail_length + 1):
tail_pos = head_pos - (i * self.scanner_direction)
if 0 <= tail_pos < self.num_leds:
fade_factor = 1.0 - (i / (self.scanner_tail_length + 1))
faded_color = tuple(int(c * fade_factor) for c in color)
self.n[tail_pos] = faded_color
self.n.write() self.n.write()
if self.oneshot:
self.pattern_step += 1 # Increment only for one-shot
self.last_update = current_time
self.pattern_step += self.scanner_direction def n_chase(self):
"""
# Change direction if boundaries are reached A theater chase pattern using n1 for on-width and n2 for off-width.
if self.scanner_direction == 1 and self.pattern_step >= self.num_leds: """
self.scanner_direction = -1 current_time = utime.ticks_ms()
self.pattern_step = self.num_leds - 1 # Start moving back from the last LED segment_length = self.n1 + self.n2
elif self.scanner_direction == -1 and self.pattern_step < 0: if segment_length == 0: # Avoid division by zero
self.scanner_direction = 1 self.fill((0,0,0))
self.pattern_step = 0 # Start moving forward from the first LED self.n.write()
self.last_update = current_time self.last_update = current_time
return
for i in range(self.num_leds):
if (i + self.pattern_step) % segment_length < self.n1:
self.n[i] = self.apply_brightness(self.colors[0])
else:
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % segment_length
self.last_update = current_time
def alternating(self):
"""
An alternating pattern where n1 LEDs are ON/OFF and n2 LEDs are OFF/ON globally, without moving.
"""
current_time = utime.ticks_ms()
total_segment_length = self.n1 + self.n2
if total_segment_length == 0:
self.fill((0,0,0))
self.n.write()
self.last_update = current_time
return
# current_phase will alternate between 0 and 1
current_phase = self.pattern_step % 2
for i in range(self.num_leds):
# Position within a single repeating segment (n1 + n2)
pos_in_segment = i % total_segment_length
if current_phase == 0: # State 0: n1 ON, n2 OFF
if pos_in_segment < self.n1:
self.n[i] = self.apply_brightness(self.colors[0]) # n1 is ON
else:
self.n[i] = (0, 0, 0) # n2 is OFF
else: # State 1: n1 OFF, n2 ON
if pos_in_segment < self.n1:
self.n[i] = (0, 0, 0) # n1 is OFF
else:
self.n[i] = self.apply_brightness(self.colors[0]) # n2 is ON
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 2 # Toggle between 0 and 1
self.last_update = current_time
def pulse(self):
if self.pattern_step == 0:
self.fill(self.apply_brightness(self.colors[0]))
self.pattern_step = 1
self.last_update = utime.ticks_ms()
if utime.ticks_diff(utime.ticks_ms(), self.last_update) > self.delay:
self.fill((0, 0, 0))
print(utime.ticks_diff(utime.ticks_ms(), self.last_update))
self.run = False
if __name__ == "__main__":
import time
from machine import WDT
wdt = WDT(timeout=2000) # Enable watchdog with a 2 second timeout
p = Patterns(pin=4, num_leds=60, color1=(255,0,0), color2=(0,0,255), brightness=127, selected="off", delay=100)
print(p.colors, p.brightness)
# tests = [
# ("off", {"duration_ms": 500}),
# ("on", {"duration_ms": 500}),
# ("color_wipe", {"delay": 200, "duration_ms": 1000}),
# ("rainbow_cycle", {"delay": 100, "duration_ms": 2500}),
# ("theater_chase", {"on_width": 3, "off_width": 3, "delay": 1000, "duration_ms": 2500}),
# ("blink", {"delay": 500, "duration_ms": 2000}),
# ("color_transition", {"delay": 150, "colors": [(255,0,0),(0,255,0),(0,0,255)], "duration_ms": 5000}),
# ("flicker", {"delay": 100, "duration_ms": 2000}),
# ("scanner", {"delay": 150, "duration_ms": 2500}),
# ("bidirectional_scanner", {"delay": 50, "duration_ms": 2500}),
# ("fill_range", {"n1": 10, "n2": 20, "delay": 500, "duration_ms": 2000}),
# ("n_chase", {"n1": 5, "n2": 5, "delay": 1000, "duration_ms": 2500}),
# ("alternating", {"n1": 5, "n2": 5, "delay": 500, "duration_ms": 2500}),
# ("pulse", {"delay": 100, "duration_ms": 700}),
# ]
tests = [
("theater_chase", {"on_width": 3, "off_width": 3, "delay": 10000, "duration_ms": 2500}),
("blink", {"delay": 500, "duration_ms": 2000}),
("color_transition", {"delay": 150, "colors": [(255,0,0),(0,255,0),(0,0,255)], "duration_ms": 5000}),
("flicker", {"delay": 100, "duration_ms": 2000}),
("scanner", {"delay": 150, "duration_ms": 2500}),
("bidirectional_scanner", {"delay": 50, "duration_ms": 2500}),
("fill_range", {"n1": 10, "n2": 20, "delay": 500, "duration_ms": 2000}),
("n_chase", {"n1": 5, "n2": 5, "delay": 1000, "duration_ms": 2500}),
("alternating", {"n1": 5, "n2": 5, "delay": 500, "duration_ms": 2500}),
("pulse", {"delay": 100, "duration_ms": 700}),
]
print("\n--- Running pattern self-test ---")
for name, cfg in tests:
print(f"\nPattern: {name}")
# apply simple config helpers
if "delay" in cfg:
p.set_delay(cfg["delay"])
if "on_width" in cfg:
p.set_on_width(cfg["on_width"])
if "off_width" in cfg:
p.set_off_width(cfg["off_width"])
if "n1" in cfg and "n2" in cfg:
p.set_fill_range(cfg["n1"], cfg["n2"])
if "colors" in cfg:
p.set_colors(cfg["colors"])
p.select(name)
# run per configured or computed duration
start = utime.ticks_ms()
duration_ms = cfg["duration_ms"]
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
interval = p.tick()
wdt.feed()
if isinstance(interval, int) and interval > 0:
# sleep a small fraction to reduce busy loop while keeping responsiveness
time.sleep_ms(max(1, interval // 10))
else:
time.sleep_ms(5)
print("\n--- Test routine finished ---")

217
src/patterns_base.py Normal file
View File

@@ -0,0 +1,217 @@
from machine import Pin
from neopixel import NeoPixel
import utime
class PatternBase:
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.pattern_step = 0
self.last_update = utime.ticks_ms()
self.delay = delay
self.brightness = brightness
self.patterns = {}
self.selected = selected
self.run = True
# Ensure colors list always starts with at least two for robust transition handling
self.colors = [color1, color2] if color1 != color2 else [color1, (255, 255, 255)] # Fallback if initial colors are same
if not self.colors: # Ensure at least one color exists
self.colors = [(0, 0, 0)]
self.transition_duration = delay * 50 # Default transition duration
self.hold_duration = delay * 10 # Default hold duration at each color
self.transition_step = 0 # Current step in the transition
self.current_color_idx = 0 # Index of the color currently being held/transitioned from
self.current_color = self.colors[self.current_color_idx] # The actual blended color
self.hold_start_time = utime.ticks_ms() # Time when the current color hold started
# New attributes for scanner patterns (moved from Patterns to PatternBase as they are generic enough)
self.scanner_direction = 1 # 1 for forward, -1 for backward
self.scanner_tail_length = 3 # Number of trailing pixels
# Removed: selected_delay caching
def sync(self):
self.pattern_step=0
self.last_update = utime.ticks_ms() - self.delay
if self.selected == "color_transition":
self.transition_step = 0
self.current_color_idx = 0
self.current_color = self.colors[self.current_color_idx]
self.hold_start_time = utime.ticks_ms() # Reset hold time
# Reset scanner specific variables
self.scanner_direction = 1
# self.tick() # Tick moved to Patterns, as patterns dict is there
def set_pattern_step(self, step):
self.pattern_step = step
def tick(self):
if self.patterns.get(self.selected) and self.run:
# Compute gating interval per pattern based on current delay
interval = None
if self.selected in ("color_wipe", "theater_chase", "blink", "scanner", "fill_range", "n_chase", "alternating"):
interval = self.delay
elif self.selected == "rainbow_cycle":
interval = max(1, int(self.delay // 5))
elif self.selected == "flicker":
interval = max(1, int(self.delay // 5))
elif self.selected == "bidirectional_scanner":
interval = max(1, int(self.delay // 100))
# Patterns intentionally not gated here: off, on, external, pulse, color_transition
if interval is not None:
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) < interval:
return interval
self.patterns[self.selected]()
return interval
return None
def update_num_leds(self, pin, num_leds):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.pattern_step = 0
def set_delay(self, delay):
self.delay = delay
# Update transition duration and hold duration when delay changes
self.transition_duration = self.delay * 50
self.hold_duration = self.delay * 10
# No cached interval
def set_brightness(self, brightness):
self.brightness = brightness
def set_color1(self, color):
if len(self.colors) > 0:
self.colors[0] = color
if self.selected == "color_transition":
# If the first color is changed, potentially reset transition
# to start from this new color if we were about to transition from it
if self.current_color_idx == 0:
self.transition_step = 0
self.current_color = self.colors[0]
self.hold_start_time = utime.ticks_ms()
else:
self.colors.append(color)
def set_color2(self, color):
if len(self.colors) > 1:
self.colors[1] = color
elif len(self.colors) == 1:
self.colors.append(color)
else: # List is empty
self.colors.append((0,0,0)) # Dummy color
self.colors.append(color)
def set_colors(self, colors):
if colors and len(colors) >= 2:
self.colors = colors
if self.selected == "color_transition":
self.sync() # Reset transition if new color list is provided
elif colors and len(colors) == 1:
self.colors = [colors[0], (255,255,255)] # Add a default second color
if self.selected == "color_transition":
print("Warning: 'color_transition' requires at least two colors. Adding a default second color.")
self.sync()
else:
print("Error: set_colors requires a list of at least one color.")
self.colors = [(0,0,0), (255,255,255)] # Fallback
if self.selected == "color_transition":
self.sync()
def set_color(self, num, color):
# Changed: More robust index check
if 0 <= num < len(self.colors):
self.colors[num] = color
# If the changed color is part of the current or next transition,
# restart the transition for smoother updates
if self.selected == "color_transition":
current_from_idx = self.current_color_idx
current_to_idx = (self.current_color_idx + 1) % len(self.colors)
if num == current_from_idx or num == current_to_idx:
# If we change a color involved in the current transition,
# it's best to restart the transition state for smoothness.
self.transition_step = 0
self.current_color_idx = current_from_idx # Stay at the current starting color
self.current_color = self.colors[self.current_color_idx]
self.hold_start_time = utime.ticks_ms() # Reset hold
return True
elif num == len(self.colors): # Allow setting a new color at the end
self.colors.append(color)
return True
return False
def add_color(self, color):
self.colors.append(color)
if self.selected == "color_transition" and len(self.colors) == 2:
# If we just added the second color needed for transition
self.sync()
def del_color(self, num):
# Changed: More robust index check and using del for lists
if 0 <= num < len(self.colors):
del self.colors[num]
# If the color being deleted was part of the current transition,
# re-evaluate the current_color_idx
if self.selected == "color_transition":
if len(self.colors) < 2: # Need at least two colors for transition
print("Warning: Not enough colors for 'color_transition'. Switching to 'on'.")
self.select("on") # Or some other default
else:
# Adjust index if it's out of bounds after deletion or was the one transitioning from
self.current_color_idx %= len(self.colors)
self.transition_step = 0
self.current_color = self.colors[self.current_color_idx]
self.hold_start_time = utime.ticks_ms()
return True
return False
def apply_brightness(self, color, brightness_override=None):
effective_brightness = brightness_override if brightness_override is not None else self.brightness
return tuple(int(c * effective_brightness / 255) for c in color)
def select(self, pattern):
# Removed self.run = True here. It should be handled by Patterns class.
if pattern in self.patterns:
self.selected = pattern
self.sync() # Reset pattern state when selecting a new pattern
if pattern == "color_transition":
if len(self.colors) < 2:
print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.")
self.selected = "on" # Fallback if not enough colors
self.sync() # Re-sync for the new pattern
else:
self.transition_step = 0
self.current_color_idx = 0 # Start from the first color in the list
self.current_color = self.colors[self.current_color_idx]
self.hold_start_time = utime.ticks_ms() # Reset hold timer
self.transition_duration = self.delay * 50 # Initialize transition duration
self.hold_duration = self.delay * 10 # Initialize hold duration
return True
return False
def set(self, i, color):
self.n[i] = color
def write(self):
self.n.write()
def fill(self, color=None):
fill_color = color if color is not None else self.colors[0]
for i in range(self.num_leds):
self.n[i] = fill_color
self.n.write()
def off(self):
self.fill((0, 0, 0))
def on(self):
self.fill(self.apply_brightness(self.colors[0]))

View File

@@ -14,14 +14,19 @@ class Settings(dict):
def set_defaults(self): def set_defaults(self):
self["led_pin"] = 10 self["led_pin"] = 10
self["num_leds"] = 50 self["num_leds"] = 100
self["pattern"] = "on" self["pattern"] = "on"
self["color1"] = "#00ff00" self["color1"] = "#080000"
self["color2"] = "#ff0000" self["color2"] = "#ff0000"
self["delay"] = 100 self["delay"] = 100
self["brightness"] = 10 self["brightness"] = 100
self["on_width"] = 1 # Default on width for theater chase
self["off_width"] = 2 # Default off width for theater chase
self["n1"] = 0 # Default start of fill range
self["n2"] = 58 # Default end of fill range (assuming 59 leds for now)
self["oneshot"] = False # Default one-shot setting
self["color_order"] = "rgb" self["color_order"] = "rgb"
self["name"] = f"led-{ubinascii.hexlify(wifi.get_mac()).decode()}" self["name"] = f"5"
self["ap_password"] = "" self["ap_password"] = ""
self["id"] = 0 self["id"] = 0
@@ -47,7 +52,6 @@ class Settings(dict):
def set_settings(self, data, patterns, save): def set_settings(self, data, patterns, save):
try: try:
print(data)
for key, value in data.items(): for key, value in data.items():
print(key, value) print(key, value)
if key == "colors": if key == "colors":
@@ -70,6 +74,24 @@ class Settings(dict):
elif key == "brightness": elif key == "brightness":
brightness = int(data["brightness"]) brightness = int(data["brightness"])
patterns.set_brightness(brightness) patterns.set_brightness(brightness)
elif key == "on_width":
on_width = int(data["on_width"])
patterns.set_on_width(on_width)
elif key == "off_width":
off_width = int(data["off_width"])
on_width = int(data.get("on_width", self["on_width"]))
patterns.set_on_off_width(on_width, off_width)
elif key == "n1":
n1 = int(data["n1"])
n2 = int(data.get("n2", patterns.n2))
patterns.set_fill_range(n1, n2)
elif key == "n2":
n2 = int(data["n2"])
n1 = int(data.get("n1", patterns.n1))
patterns.set_fill_range(n1, n2)
elif key == "oneshot":
oneshot_value = bool(data["oneshot"])
patterns.set_oneshot(oneshot_value)
elif key == "name": elif key == "name":
self[key] = value self[key] = value
self.save() self.save()
@@ -90,7 +112,8 @@ class Settings(dict):
if save: if save:
self.save() self.save()
return "OK", 200 return "OK", 200
except (KeyError, ValueError): except Exception as e:
print(f"An unexpected error occurred in set_settings: {e}")
return "Bad request", 400 return "Bad request", 400
# Example usage # Example usage

109
src/static/main.css Normal file
View File

@@ -0,0 +1,109 @@
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1 {
text-align: center;
}
form {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
input[type="submit"],
input[type="range"],
input[type="color"] {
width: 100%;
margin-bottom: 10px;
box-sizing: border-box;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 25px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
transition: opacity 0.2s;
}
input[type="range"]:hover {
opacity: 1;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
background: #4caf50;
cursor: pointer;
border-radius: 50%;
}
input[type="range"]::-moz-range-thumb {
width: 25px;
height: 25px;
background: #4caf50;
cursor: pointer;
border-radius: 50%;
}
#pattern_buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
#pattern_buttons button {
flex: 1 0 calc(33.333% - 10px);
padding: 10px;
background-color: #4caf50;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
#pattern_buttons button:hover {
background-color: #45a049;
}
@media (max-width: 480px) {
#pattern_buttons button {
flex: 1 0 calc(50% - 10px);
}
}
#connection-status {
width: 15px;
height: 15px;
border-radius: 50%;
display: inline-block; /* Or block, depending on where you put it */
margin-left: 10px; /* Adjust spacing as needed */
vertical-align: middle; /* Align with nearby text */
background-color: grey; /* Default: Unknown */
}
#connection-status.connecting {
background-color: yellow;
}
#connection-status.open {
background-color: green;
}
#connection-status.closing,
#connection-status.closed {
background-color: red;
}
#color_order_form label,
#color_order_form input[type="radio"] {
/* Ensures they behave as inline elements */
display: inline-block;
/* Adds some space between them for readability */
margin-right: 10px;
vertical-align: middle; /* Aligns them nicely if heights vary */
}

244
src/static/main.js Normal file
View File

@@ -0,0 +1,244 @@
let delayTimeout;
let brightnessTimeout;
let colorTimeout;
let color2Timeout;
let ws; // Variable to hold the WebSocket connection
let connectionStatusElement; // Variable to hold the connection status element
// Function to update the connection status indicator
function updateConnectionStatus(status) {
if (!connectionStatusElement) {
connectionStatusElement = document.getElementById("connection-status");
}
if (connectionStatusElement) {
connectionStatusElement.className = ""; // Clear existing classes
connectionStatusElement.classList.add(status);
// Optionally, you could also update text content based on status
// connectionStatusElement.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
}
// Function to establish WebSocket connection
function connectWebSocket() {
// Determine the WebSocket URL based on the current location
const wsUrl = `ws://${window.location.host}/ws`;
ws = new WebSocket(wsUrl);
updateConnectionStatus("connecting"); // Indicate connecting state
ws.onopen = function (event) {
console.log("WebSocket connection opened:", event);
updateConnectionStatus("open"); // Indicate open state
// Optionally, you could send an initial message here
};
ws.onmessage = function (event) {
console.log("WebSocket message received:", event.data);
};
ws.onerror = function (event) {
console.error("WebSocket error:", event);
updateConnectionStatus("closed"); // Indicate error state (treat as closed)
};
ws.onclose = function (event) {
if (event.wasClean) {
console.log(
`WebSocket connection closed cleanly, code=${event.code}, reason=${event.reason}`,
);
updateConnectionStatus("closed"); // Indicate closed state
} else {
console.error("WebSocket connection died");
updateConnectionStatus("closed"); // Indicate closed state
}
// Attempt to reconnect after a delay
setTimeout(connectWebSocket, 1000);
};
}
// Function to send data over WebSocket
function sendWebSocketData(data) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log("Sending data over WebSocket:", data);
ws.send(JSON.stringify(data));
} else {
console.error("WebSocket is not connected. Cannot send data:", data);
// You might want to queue messages or handle this in a different way
}
}
// Keep the post and get functions for now, they might still be useful
async function post(path, data) {
console.log(`POST to ${path}`, data);
try {
const response = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error("Error during POST request:", error);
}
}
async function get(path) {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error during GET request:", error);
}
}
async function updateColor(event) {
event.preventDefault();
clearTimeout(colorTimeout);
colorTimeout = setTimeout(function () {
const color = document.getElementById("color").value;
sendWebSocketData({ color1: color });
}, 500);
}
async function updateColor2(event) {
event.preventDefault();
clearTimeout(color2Timeout);
color2Timeout = setTimeout(function () {
const color = document.getElementById("color2").value;
sendWebSocketData({ color2: color });
}, 500);
}
async function updatePattern(pattern) {
sendWebSocketData({ pattern: pattern });
}
async function updateBrightness(event) {
event.preventDefault();
clearTimeout(brightnessTimeout);
brightnessTimeout = setTimeout(function () {
const brightness = document.getElementById("brightness").value;
sendWebSocketData({ brightness: brightness });
}, 500);
}
async function updateDelay(event) {
event.preventDefault();
clearTimeout(delayTimeout);
delayTimeout = setTimeout(function () {
const delay = document.getElementById("delay").value;
sendWebSocketData({ delay: delay });
}, 500);
}
async function updateNumLeds(event) {
event.preventDefault();
const numLeds = document.getElementById("num_leds").value;
sendWebSocketData({ num_leds: parseInt(numLeds) });
}
async function updateName(event) {
event.preventDefault();
const name = document.getElementById("name").value;
sendWebSocketData({ name: name });
}
async function updateID(event) {
event.preventDefault();
const id = document.getElementById("id").value;
sendWebSocketData({ id: parseInt(id) });
}
async function updateLedPin(event) {
event.preventDefault();
const ledpin = document.getElementById("led_pin").value;
sendWebSocketData({ led_pin: parseInt(ledpin) });
}
function handleRadioChange(event) {
event.preventDefault();
console.log("Selected color order:", event.target.value);
// Add your specific logic here
if (event.target.value === "rgb") {
console.log("RGB order selected!");
} else if (event.target.value === "rbg") {
console.log("RBG order selected!");
}
sendWebSocketData({ color_order: event.target.value });
}
function createPatternButtons(patterns) {
const container = document.getElementById("pattern_buttons");
container.innerHTML = ""; // Clear previous buttons
patterns.forEach((pattern) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = pattern;
button.value = pattern;
button.addEventListener("click", async function (event) {
event.preventDefault();
await updatePattern(pattern);
});
container.appendChild(button);
});
}
document.addEventListener("DOMContentLoaded", async function () {
// Get the connection status element once the DOM is ready
connectionStatusElement = document.getElementById("connection-status");
// Establish WebSocket connection on page load
connectWebSocket();
document.getElementById("color").addEventListener("input", updateColor);
document.getElementById("color2").addEventListener("input", updateColor2);
document.getElementById("delay").addEventListener("input", updateDelay);
document
.getElementById("brightness")
.addEventListener("input", updateBrightness);
document
.getElementById("num_leds_form")
.addEventListener("submit", updateNumLeds);
document.getElementById("name_form").addEventListener("submit", updateName);
document.getElementById("id_form").addEventListener("submit", updateID);
document
.getElementById("led_pin_form")
.addEventListener("submit", updateLedPin);
document.getElementById("delay").addEventListener("touchend", updateDelay);
document
.getElementById("brightness")
.addEventListener("touchend", updateBrightness);
document.getElementById("rgb").addEventListener("change", handleRadioChange);
document.getElementById("rbg").addEventListener("change", handleRadioChange);
document.querySelectorAll(".pattern_button").forEach((button) => {
console.log(button.value);
button.addEventListener("click", async (event) => {
event.preventDefault();
await updatePattern(button.value);
});
});
});
// Function to toggle the display of the settings menu
function selectSettings() {
const settingsMenu = document.getElementById("settings_menu");
controls = document.getElementById("controls");
settingsMenu.style.display = "block";
controls.style.display = "none";
}
function selectControls() {
const settingsMenu = document.getElementById("settings_menu");
controls = document.getElementById("controls");
settingsMenu.style.display = "none";
controls.style.display = "block";
}

124
src/templates/index.html Normal file
View File

@@ -0,0 +1,124 @@
{% args settings, patterns, mac %}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{settings['name']}}</title>
<script src="static/main.js"></script>
<link rel="stylesheet" href="static/main.css" />
</head>
<body>
<h1>{{settings['name']}}</h1>
<button onclick="selectControls()">Controls</button>
<button onclick="selectSettings()">Settings</button>
<!-- Main LED Controls -->
<div id="controls">
<div id="pattern_buttons">
{% for p in patterns %}
<button class="pattern_button" value="{{p}}">{{p}}</button>
{% endfor %}
<!-- Pattern buttons will be inserted here -->
</div>
<form id="delay_form" method="post" action="/delay">
<label for="delay">Delay:</label>
<input
type="range"
id="delay"
name="delay"
min="1"
max="1000"
value="{{settings['delay']}}"
step="10"
/>
</form>
<form id="brightness_form" method="post" action="/brightness">
<label for="brightness">Brightness:</label>
<input
type="range"
id="brightness"
name="brightness"
min="0"
max="100"
value="{{settings['brightness']}}"
step="1"
/>
</form>
<form id="color_form" method="post" action="/color">
<input
type="color"
id="color"
name="color"
value="{{settings['color1']}}"
/>
</form>
<form id="color2_form" method="post" action="/color2">
<input
type="color"
id="color2"
name="color2"
value="{{settings['color2']}}"
/>
</form>
</div>
<!-- Settings Menu for num_leds, Wi-Fi SSID, and Password -->
<div id="settings_menu" style="display: none">
<h2>Settings</h2>
<form id="name_form" method="post" action="/name">
<label for="name">Name:</label>
<input
type="text"
id="name"
name="num_leds"
value="{{settings['name']}}"
/>
<input type="submit" value="Update Name" />
</form>
<form id="id_form" method="post" action="/id">
<label for="id">ID:</label>
<input
type="text"
id="id"
name="id"
value="{{settings['id']}}"
/>
<input type="submit" value="Update ID" />
</form>
<!-- Separate form for submitting num_leds -->
<form id="num_leds_form" method="post" action="/num_leds">
<label for="num_leds">Number of LEDs:</label>
<input
type="text"
id="num_leds"
name="num_leds"
value="{{settings['num_leds']}}"
/>
<input type="submit" value="Update Number of LEDs" />
</form>
<form id="led_pin_form" method="post" action="/led_pin">
<label for="num_leds">Led pin:</label>
<input
type="text"
id="led_pin"
name="led_pin"
value="{{settings['led_pin']}}"
/>
<input type="submit" value="Update Led Pin" />
</form>
<form id="color_order_form">
<label for="rgb">RGB:</label>
<input type="radio" id="rgb" name="color_order" value="rgb" {{'checked' if settings["color_order"]=="rgb" else ''}} />
<label for="rbg">RBG</label>
<input type="radio" id="rbg" name="color_order" value="rbg" {{'checked' if settings["color_order"]=="rbg" else ''}}/>
</form>
<p>Mac address: {{mac}}</p>
</div>
<div id="connection-status"></div>
</body>
</html>

43
src/web.py Normal file
View File

@@ -0,0 +1,43 @@
from microdot import Microdot, send_file, Response
from microdot.utemplate import Template
from microdot.websocket import with_websocket
import machine
import wifi
import json
def web(settings, patterns):
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
async def index_hnadler(request):
mac = wifi.get_mac().hex()
return Template('/index.html').render(settings=settings, patterns=patterns.patterns.keys(), mac=mac)
@app.route("/static/<path:path>")
def static_handler(request, path):
if '..' in path:
# Directory traversal is not allowed
return 'Not found', 404
return send_file('static/' + path)
@app.post("/settings")
def settings_handler(request):
# Keep the POST handler for compatibility or alternative usage if needed
# For WebSocket updates, the /ws handler is now primary
return settings.set_settings(request.body.decode('utf-8'), patterns)
@app.route("/ws")
@with_websocket
async def ws(request, ws):
while True:
data = await ws.receive()
if data:
# Process the received data
_, status_code = settings.set_settings(json.loads(data), patterns, True)
#await ws.send(status_code)
else:
break
return app

39
src/wifi.py Normal file
View File

@@ -0,0 +1,39 @@
import network
from time import sleep
def connect(ssid, password, ip, gateway):
try:
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
if ssid == "" or password == "":
print("Missing ssid or password")
return None
if ip != "" and gateway != "":
sta_if.ifconfig((ip, '255.255.255.0', gateway, '1.1.1.1'))
print('connecting to network...')
sta_if.active(True)
sta_if.connect(ssid, password)
sleep(0.1)
if sta_if.isconnected():
return sta_if.ifconfig()
return None
return sta_if.ifconfig()
except Exception as e:
print(f"Failed to connect to wifi {e}")
return None
def ap(ssid, password):
ap_if = network.WLAN(network.AP_IF)
ap_mac = ap_if.config('mac')
print(ssid)
ap_if.active(True)
ap_if.config(essid=ssid, password=password)
ap_if.active(False)
ap_if.active(True)
print(ap_if.ifconfig())
def get_mac():
ap_if = network.WLAN(network.AP_IF)
return ap_if.config('mac')