1 Commits

Author SHA1 Message Date
b2077c0199 Improve ESP-NOW messaging and tab defaults
- Use shared ESPNOW payload limit and message splitting
- Expand default tab names and add flash/build artifacts.

Made-with: Cursor
2026-03-14 02:41:08 +13:00
24 changed files with 873 additions and 658 deletions

View File

@@ -12,7 +12,6 @@ watchfiles = "*"
requests = "*" requests = "*"
selenium = "*" selenium = "*"
adafruit-ampy = "*" adafruit-ampy = "*"
microdot = "*"
[dev-packages] [dev-packages]
@@ -22,5 +21,4 @@ python_version = "3.12"
[scripts] [scripts]
web = "python /home/pi/led-controller/tests/web.py" web = "python /home/pi/led-controller/tests/web.py"
watch = "python -m watchfiles 'python tests/web.py' src tests" watch = "python -m watchfiles 'python tests/web.py' src tests"
install = "pipenv install" install = "pipenv install"
run = "sh -c 'cd src && python main.py'"

499
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "12b64c3bf5857d958f790f2416072408e2244631242ba2598210d89df330e184" "sha256": "c963cd52164ac13fda5e6f3c5975bc14db6cea03ad4973de02ad91a0ab10d2ea"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -32,6 +32,14 @@
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==4.12.1" "version": "==4.12.1"
}, },
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"markers": "python_version >= '3.5'",
"version": "==1.10"
},
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11",
@@ -151,19 +159,19 @@
}, },
"bitstring": { "bitstring": {
"hashes": [ "hashes": [
"sha256:e682ac522bb63e041d16cbc9d0ca86a4f00194db16d0847c7efe066f836b2e37", "sha256:69d1587f0ac18dc7d93fc7e80d5f447161a33e57027e726dc18a0a8bacf1711a",
"sha256:feac49524fcf3ef27e6081e86f02b10d2adf6c3773bf22fbe0e7eea9534bc737" "sha256:a08bc09d3857216d4c0f412a1611056f1cc2b64fd254fb1e8a0afba7cfa1a95a"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.4.0" "version": "==4.3.1"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c",
"sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==2026.2.25" "version": "==2026.1.4"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
@@ -257,122 +265,122 @@
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4", "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad",
"sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93",
"sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394",
"sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89",
"sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc",
"sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86",
"sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63",
"sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d",
"sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f",
"sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8",
"sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0",
"sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505",
"sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161",
"sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2", "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af",
"sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe", "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152",
"sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318",
"sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72",
"sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4",
"sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e",
"sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3",
"sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf", "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576",
"sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139", "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c",
"sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770", "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1",
"sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8",
"sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918", "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1",
"sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2",
"sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7", "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44",
"sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26",
"sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88",
"sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016",
"sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede",
"sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf",
"sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a",
"sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659", "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc",
"sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0",
"sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9", "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84",
"sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9", "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db",
"sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1",
"sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d", "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7",
"sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed",
"sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8",
"sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133",
"sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e",
"sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99", "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef",
"sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14",
"sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2",
"sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0",
"sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca", "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d",
"sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c", "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828",
"sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c", "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f",
"sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf",
"sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6",
"sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328",
"sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090",
"sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa",
"sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381",
"sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c",
"sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb",
"sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a", "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc",
"sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e", "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a",
"sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec",
"sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123", "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc",
"sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac",
"sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc", "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e",
"sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313",
"sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569",
"sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3",
"sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d",
"sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525",
"sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894",
"sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3",
"sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9",
"sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294", "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a",
"sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22", "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9",
"sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14",
"sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8", "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25",
"sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2", "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50",
"sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf",
"sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242", "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1",
"sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3",
"sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac",
"sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e",
"sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815",
"sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c",
"sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6",
"sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6",
"sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e",
"sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4",
"sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84",
"sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69",
"sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969", "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15",
"sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191",
"sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0",
"sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897",
"sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd",
"sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2",
"sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794",
"sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d",
"sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193", "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074",
"sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3",
"sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224",
"sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95", "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838",
"sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a",
"sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d",
"sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98", "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d",
"sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f",
"sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8",
"sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490",
"sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966",
"sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9",
"sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3",
"sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947", "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e",
"sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3" "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==3.4.5" "version": "==3.4.4"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@@ -384,65 +392,66 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa",
"sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc",
"sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da",
"sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255",
"sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2",
"sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485",
"sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0",
"sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d",
"sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616",
"sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947",
"sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0",
"sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908",
"sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81",
"sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc",
"sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd",
"sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b",
"sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019",
"sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7",
"sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b",
"sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973",
"sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b",
"sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5",
"sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80",
"sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef",
"sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0",
"sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b",
"sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e",
"sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c",
"sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2",
"sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af",
"sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4",
"sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab",
"sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82",
"sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3",
"sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59",
"sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da",
"sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061",
"sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085",
"sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b",
"sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263",
"sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e",
"sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829",
"sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4",
"sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c",
"sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f",
"sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095",
"sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32",
"sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976",
"sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87" "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822"
], ],
"markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'",
"version": "==46.0.5" "version": "==46.0.4"
}, },
"esptool": { "esptool": {
"hashes": [ "hashes": [
"sha256:9c355b7d6331cc92979cc710ae5c41f59830d1ea29ec24c467c6005a092c06d6" "sha256:2ea9bcd7eb263d380a4fe0170856a10e4c65e3f38c757ebdc73584c8dd8322da"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.2.0" "markers": "python_version >= '3.10'",
"version": "==5.1.0"
}, },
"h11": { "h11": {
"hashes": [ "hashes": [
@@ -460,6 +469,14 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==3.11" "version": "==3.11"
}, },
"importlib-metadata": {
"hashes": [
"sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb",
"sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"
],
"markers": "python_version >= '3.9'",
"version": "==8.7.1"
},
"intelhex": { "intelhex": {
"hashes": [ "hashes": [
"sha256:87cc5225657524ec6361354be928adfd56bcf2a3dcc646c40f8f094c39c07db4", "sha256:87cc5225657524ec6361354be928adfd56bcf2a3dcc646c40f8f094c39c07db4",
@@ -483,22 +500,23 @@
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==0.1.2" "version": "==0.1.2"
}, },
"microdot": {
"hashes": [
"sha256:363e3ebfc80b7e0415779848c9332e4e7fb7bd365ee54d3620abffe42ed82946",
"sha256:abfb82ca31cc430174e4761cc7356adc4bff00ea758d437c2b258883dc63f464"
],
"index": "pypi",
"version": "==2.6.0"
},
"mpremote": { "mpremote": {
"hashes": [ "hashes": [
"sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4", "sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4",
"sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5" "sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.4'",
"version": "==1.27.0" "version": "==1.27.0"
}, },
"mypy-extensions": {
"hashes": [
"sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505",
"sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"
],
"markers": "python_version >= '3.8'",
"version": "==1.1.0"
},
"outcome": { "outcome": {
"hashes": [ "hashes": [
"sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8",
@@ -507,13 +525,21 @@
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==1.3.0.post0" "version": "==1.3.0.post0"
}, },
"packaging": {
"hashes": [
"sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4",
"sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"
],
"markers": "python_version >= '3.8'",
"version": "==26.0"
},
"platformdirs": { "platformdirs": {
"hashes": [ "hashes": [
"sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda",
"sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868" "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"
], ],
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==4.9.4" "version": "==4.5.1"
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
@@ -533,11 +559,12 @@
}, },
"pyjwt": { "pyjwt": {
"hashes": [ "hashes": [
"sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623",
"sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b" "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.12.1" "markers": "python_version >= '3.9'",
"version": "==2.11.0"
}, },
"pyserial": { "pyserial": {
"hashes": [ "hashes": [
@@ -557,11 +584,11 @@
}, },
"python-dotenv": { "python-dotenv": {
"hashes": [ "hashes": [
"sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6",
"sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"
], ],
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.9'",
"version": "==1.2.2" "version": "==1.2.1"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
@@ -655,15 +682,16 @@
"sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==2.32.5" "version": "==2.32.5"
}, },
"rich": { "rich": {
"hashes": [ "hashes": [
"sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69",
"sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b" "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"
], ],
"markers": "python_full_version >= '3.8.0'", "markers": "python_full_version >= '3.8.0'",
"version": "==14.3.3" "version": "==14.3.2"
}, },
"rich-click": { "rich-click": {
"hashes": [ "hashes": [
@@ -675,11 +703,12 @@
}, },
"selenium": { "selenium": {
"hashes": [ "hashes": [
"sha256:003e971f805231ad63e671783a2b91a299355d10cefb9de964c36ff3819115aa", "sha256:a88f5905d88ad0b84991c2386ea39e2bbde6d6c334be38df5842318ba98eaa8c",
"sha256:b8ccde8d2e7642221ca64af184a92c19eee6accf2e27f20f30472f5efae18eb1" "sha256:c8823fc02e2c771d9ad9a0cf899cee7de1a57a6697e3d0b91f67566129f2b729"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.41.0" "markers": "python_version >= '3.10'",
"version": "==4.40.0"
}, },
"sniffio": { "sniffio": {
"hashes": [ "hashes": [
@@ -696,50 +725,20 @@
], ],
"version": "==2.4.0" "version": "==2.4.0"
}, },
"tibs": {
"hashes": [
"sha256:01ea5258bdf942d21560dc07d532082cd04f07cfef65fedd58ae84f7d0d2562a",
"sha256:0a7ce857ef05c59dc61abadc31c4b9b1e3c62f9e5fb29217988c308936aea71e",
"sha256:130bc68ff500fc8185677df7a97350b5d5339e6ba7e325bc3031337f6424ede7",
"sha256:173dfbecb2309edd9771f453580c88cf251e775613461566b23dbd756b3d54cb",
"sha256:1906729038b85c3b4c040aa28a456d85bc976d0c5007177350eb73374ffa0fd0",
"sha256:1b56583db148e5094d781c3d746815dbcbb6378c6f813c8ce291efd4ab21da8b",
"sha256:1d5521cc6768bfa6282a0c591ba06b079ab91b5c7d5696925ad2abac59779a54",
"sha256:1f95d5db62960205a1e9eba73ce67dc14e7366ae080cd4e5b6f005ebd90faf02",
"sha256:29480bf03e3372a5f9cc59ea0541f76f8efd696d4f0d214715e94247c342a037",
"sha256:2a618de62004d9217d2d2ab0f7f9bbdd098c12642dc01f07b3fb00f0b5f3131a",
"sha256:42725200f1b02687ed6e6a1c01e0ec150dc829d21d901ffc74cc0ac4d821f57f",
"sha256:477608f9b87e24a22ab6d50b81da04a5cb59bfa49598ff7ec5165035a18fb392",
"sha256:4b7510235379368b7523f624d46e0680f3706e3a3965877a6583cdcb598b8bac",
"sha256:501728d096e10d9a165aa526743d47418a6bbfd7b084fa47ecb22be7641d3edb",
"sha256:63255749f937c5e6fedcc7d54e7bd359aef711017e6855f373b0510a14ee2215",
"sha256:6a9feed5931b881809a950eca0e01e757113e2383a2af06a3e6982f110c869e2",
"sha256:76746f01b3db9dbd802f5e615f11f68df7a29ecef521b082dca53f3fa7d0084f",
"sha256:77103a9f1af72ac4cf5006828d0fb21578d19ce55fd990e9a1c8e46fd549561f",
"sha256:7d6592ed93c6748acd39df484c1ee24d40ee247c2a20ca38ba03363506fd24f3",
"sha256:847709c108800ad6a45efaf9a040628278956938a4897f7427a2587013dc3b98",
"sha256:859f05315ffb307d3474c505d694f3a547f00730a024c982f5f60316a5505b3c",
"sha256:a61d36155f8ab8642e1b6744e13822f72050fc7ec4f86ec6965295afa04949e2",
"sha256:a883ca13a922a66b2c1326a9c188123a574741a72510a4bf52fd6f97db191e44",
"sha256:ac0aa2aae38f7325c91c261ce1d18f769c4c7033c98d6ea3ea5534585cf16452",
"sha256:ace018a057459e3dccd06a4aae1c5c8cd57e352b263dcef534ae39bf3e03b5cf",
"sha256:ad61df93b50f875b277ab736c5d37b6bce56f9abce489a22f4e02d9daa2966e3",
"sha256:b9535dc7b7484904a58b51bd8e64da7efbf1d8466ff7e84ed1d78f4ddc561c99",
"sha256:d4f3ff613d486650816bc5516760c0382a2cc0ca8aeddd8914d011bc3b81d9a2",
"sha256:e13b9c7ff2604b0146772025e1ac6f85c8c625bf6ac73736ff671eaf357dda41",
"sha256:f5eea45851c960628a2bd29847765d55e19a687c5374456ad2c8cf6410eb1efa",
"sha256:f70bd250769381c73110d6f24feaf8b6fcd44f680b3cb28a20ea06db3d04fb6f"
],
"markers": "python_version >= '3.8'",
"version": "==0.5.7"
},
"trio": { "trio": {
"hashes": [ "hashes": [
"sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b", "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b",
"sha256:a29b92b73f09d4b48ed249acd91073281a7f1063f09caba5dc70465b5c7aa970" "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"
], ],
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==0.33.0" "version": "==0.32.0"
},
"trio-typing": {
"hashes": [
"sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3",
"sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264"
],
"version": "==0.10.0"
}, },
"trio-websocket": { "trio-websocket": {
"hashes": [ "hashes": [
@@ -749,6 +748,20 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==0.12.2" "version": "==0.12.2"
}, },
"types-certifi": {
"hashes": [
"sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f",
"sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a"
],
"version": "==2021.10.8.3"
},
"types-urllib3": {
"hashes": [
"sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f",
"sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"
],
"version": "==1.26.25.14"
},
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
@@ -758,6 +771,9 @@
"version": "==4.15.0" "version": "==4.15.0"
}, },
"urllib3": { "urllib3": {
"extras": [
"socks"
],
"hashes": [ "hashes": [
"sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed",
"sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"
@@ -878,6 +894,7 @@
"sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf" "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==1.1.1" "version": "==1.1.1"
}, },
"websocket-client": { "websocket-client": {
@@ -895,6 +912,14 @@
], ],
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==1.3.2" "version": "==1.3.2"
},
"zipp": {
"hashes": [
"sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e",
"sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"
],
"markers": "python_version >= '3.9'",
"version": "==3.23.0"
} }
}, },
"develop": {} "develop": {}

View File

@@ -1,6 +1,2 @@
# led-controller # led-controller
## Run on port 80 without root
Run once: `sudo scripts/setup-port80.sh`. Then start the app with: `pipenv run run`.

BIN
build_static/app.js.gz Normal file

Binary file not shown.

37
build_static/styles.css Normal file
View File

@@ -0,0 +1,37 @@
/* General tab styles */
.tabs {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
margin: 0 10px;
cursor: pointer;
background-color: #f1f1f1;
border: 1px solid #ccc;
border-radius: 4px;
transition: background-color 0.3s ease;
}
.tab:hover {
background-color: #ddd;
}
.tab.active {
background-color: #ccc;
}
.tab-content {
display: flex;
justify-content: center;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}

BIN
build_static/styles.css.gz Normal file

Binary file not shown.

View File

@@ -2,7 +2,7 @@
"1": { "1": {
"name": "default", "name": "default",
"names": [ "names": [
"1","2","3","4","5","6","7","8" "a","b","c","d","e","f","g","h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"
], ],
"presets": [ "presets": [
[ [

View File

@@ -1,112 +0,0 @@
# Benchmark: LRU eviction vs add-then-remove-after-use on ESP32.
# Run on device: mpremote run esp32/benchmark_peers.py
# (add/del_peer are timed; send() may fail if no peer is listening - timing still valid)
import espnow
import network
import time
BROADCAST = b"\xff\xff\xff\xff\xff\xff"
MAX_PEERS = 20
ITERATIONS = 50
PAYLOAD = b"x" * 32 # small payload
network.WLAN(network.STA_IF).active(True)
esp = espnow.ESPNow()
esp.active(True)
esp.add_peer(BROADCAST)
# Build 19 dummy MACs so we have 20 peers total (broadcast + 19).
def mac(i):
return bytes([0, 0, 0, 0, 0, i])
peers_list = [mac(i) for i in range(1, 20)]
for p in peers_list:
esp.add_peer(p)
# One "new" MAC we'll add/remove.
new_mac = bytes([0, 0, 0, 0, 0, 99])
def bench_lru():
"""LRU: ensure_peer (evict oldest + add new), send, update last_used."""
last_used = {BROADCAST: time.ticks_ms()}
for p in peers_list:
last_used[p] = time.ticks_ms()
# Pre-remove one so we have 19; ensure_peer(new) will add 20th.
esp.del_peer(peers_list[-1])
last_used.pop(peers_list[-1], None)
# Now 19 peers. Each iteration: ensure_peer(new) -> add_peer(new), send, update.
# Next iter: ensure_peer(new) -> already there, just send. So we need to force
# eviction each time: use a different "new" each time so we always evict+add.
t0 = time.ticks_us()
for i in range(ITERATIONS):
addr = bytes([0, 0, 0, 0, 0, 50 + (i % 30)]) # 30 different "new" MACs
peers = esp.get_peers()
peer_macs = [p[0] for p in peers]
if addr not in peer_macs:
if len(peer_macs) >= MAX_PEERS:
oldest_mac = None
oldest_ts = time.ticks_ms()
for m in peer_macs:
if m == BROADCAST:
continue
ts = last_used.get(m, 0)
if ts <= oldest_ts:
oldest_ts = ts
oldest_mac = m
if oldest_mac is not None:
esp.del_peer(oldest_mac)
last_used.pop(oldest_mac, None)
esp.add_peer(addr)
esp.send(addr, PAYLOAD)
last_used[addr] = time.ticks_ms()
t1 = time.ticks_us()
return time.ticks_diff(t1, t0)
def bench_add_then_remove():
"""Add peer, send, del_peer (remove after use). At 20 we must del one first."""
# Start full: 20 peers. To add new we del any one, add new, send, del new.
victim = peers_list[0]
t0 = time.ticks_us()
for i in range(ITERATIONS):
esp.del_peer(victim) # make room
esp.add_peer(new_mac)
esp.send(new_mac, PAYLOAD)
esp.del_peer(new_mac)
esp.add_peer(victim) # put victim back so we're at 20 again
t1 = time.ticks_us()
return time.ticks_diff(t1, t0)
def bench_send_existing():
"""Baseline: send to existing peer only (no add/del)."""
t0 = time.ticks_us()
for _ in range(ITERATIONS):
esp.send(peers_list[0], PAYLOAD)
t1 = time.ticks_us()
return time.ticks_diff(t1, t0)
print("ESP-NOW peer benchmark ({} iterations)".format(ITERATIONS))
print()
# Baseline: send to existing peer
try:
us = bench_send_existing()
print("Send to existing peer only: {:>8} us total {:>7.1f} us/iter".format(us, us / ITERATIONS))
except Exception as e:
print("Send existing failed:", e)
print()
# LRU: evict oldest then add new, send
try:
us = bench_lru()
print("LRU (evict oldest + add + send): {:>8} us total {:>7.1f} us/iter".format(us, us / ITERATIONS))
except Exception as e:
print("LRU failed:", e)
print()
# Add then remove after use
try:
us = bench_add_then_remove()
print("Add then remove after use: {:>8} us total {:>7.1f} us/iter".format(us, us / ITERATIONS))
except Exception as e:
print("Add-then-remove failed:", e)
print()
print("Done.")

View File

@@ -1,63 +0,0 @@
# Serial-to-ESP-NOW bridge: receives from Pi on UART, forwards to ESP-NOW peers.
# Wire format: first 6 bytes = destination MAC, rest = payload. Address is always 6 bytes.
from machine import Pin, UART
import espnow
import network
import time
UART_BAUD = 912000
BROADCAST = b"\xff\xff\xff\xff\xff\xff"
MAX_PEERS = 20
network.WLAN(network.STA_IF).active(True)
esp = espnow.ESPNow()
esp.active(True)
esp.add_peer(BROADCAST)
uart = UART(1, UART_BAUD, tx=Pin(21), rx=Pin(6))
# Track last send time per peer for LRU eviction (remove oldest when at limit).
last_used = {BROADCAST: time.ticks_ms()}
# ESP_ERR_ESPNOW_EXIST: peer already registered (ignore when adding).
ESP_ERR_ESPNOW_EXIST = -12395
def ensure_peer(addr):
"""Ensure addr is in the peer list. When at 20 peers, remove the oldest-used (LRU)."""
peers = esp.get_peers()
peer_macs = [p[0] for p in peers]
if addr in peer_macs:
return
if len(peer_macs) >= MAX_PEERS:
# Remove the peer we used least recently (oldest).
oldest_mac = None
oldest_ts = time.ticks_ms()
for mac in peer_macs:
if mac == BROADCAST:
continue
ts = last_used.get(mac, 0)
if ts <= oldest_ts:
oldest_ts = ts
oldest_mac = mac
if oldest_mac is not None:
esp.del_peer(oldest_mac)
last_used.pop(oldest_mac, None)
try:
esp.add_peer(addr)
except OSError as e:
if e.args[0] != ESP_ERR_ESPNOW_EXIST:
raise
while True:
if uart.any():
data = uart.read()
if not data or len(data) < 6:
continue
addr = data[:6]
payload = data[6:]
ensure_peer(addr)
esp.send(addr, payload)
last_used[addr] = time.ticks_ms()

244
flash.sh Executable file
View File

@@ -0,0 +1,244 @@
#!/usr/bin/env sh
set -eu
# Environment variables:
# PORT - serial port (default: /dev/ttyUSB0)
# BAUD - baud rate (default: 460800)
# FIRMWARE - local path to firmware .bin
# FW_URL - URL to download firmware if FIRMWARE not provided or missing
PORT=${PORT:-}
BAUD=${BAUD:-460800}
CHIP=${CHIP:-esp32} # esp32 | esp32c3
# Map chip-specific settings
ESPT_CHIP="$CHIP"
FLASH_OFFSET=0x1000
DEFAULT_DOWNLOAD_PAGE="https://micropython.org/download/ESP32/"
BOARD_ID="ESP32_GENERIC"
BOARD_PAGE="https://micropython.org/download/${BOARD_ID}/"
case "$CHIP" in
esp32c3)
ESPT_CHIP="esp32c3"
FLASH_OFFSET=0x0
DEFAULT_DOWNLOAD_PAGE="https://micropython.org/download/ESP32C3/"
BOARD_ID="ESP32_GENERIC_C3"
BOARD_PAGE="https://micropython.org/download/${BOARD_ID}/"
;;
esp32)
ESPT_CHIP="esp32"
FLASH_OFFSET=0x1000
DEFAULT_DOWNLOAD_PAGE="https://micropython.org/download/ESP32/"
BOARD_ID="ESP32_GENERIC"
BOARD_PAGE="https://micropython.org/download/${BOARD_ID}/"
;;
*)
echo "Unsupported CHIP: $CHIP (supported: esp32, esp32c3)" >&2
exit 1
;;
esac
# Download-only mode: fetch the appropriate firmware and exit
if [ -n "${DOWNLOAD_ONLY:-}" ]; then
# Prefer resolving latest if nothing provided
if [ -z "${FIRMWARE:-}" ] && [ -z "${FW_URL:-}" ]; then
LATEST=1
fi
if ! resolve_firmware; then
echo "Failed to resolve firmware for CHIP=$CHIP" >&2
exit 1
fi
echo "$FIRMWARE"
exit 0
fi
# Helper: resolve the latest firmware URL for a given board pattern with multiple fallbacks
resolve_latest_url() {
board_pattern="$1" # e.g., ESP32_GENERIC_C3-.*\.bin
# Candidate pages to try in order
pages="${BOARD_PAGE} ${DOWNLOAD_PAGE:-$DEFAULT_DOWNLOAD_PAGE} https://micropython.org/download/ https://micropython.org/resources/firmware/"
for page in $pages; do
echo "Trying to resolve latest from $page" >&2
html=$(curl -fsSL -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' -e 'https://micropython.org/download/' "$page" || true)
[ -z "$html" ] && continue
# Prefer matching the board pattern
url=$(printf "%s" "$html" \
| sed -n 's/.*href=\"\([^\"]*\.bin\)\".*/\1/p' \
| grep -E "$board_pattern" \
| head -n1)
if [ -n "$url" ]; then
case "$url" in
http*) echo "$url"; return 0 ;;
/*) echo "https://micropython.org$url"; return 0 ;;
*) echo "$page$url"; return 0 ;;
esac
fi
done
return 1
}
# If LATEST is set and neither FIRMWARE nor FW_URL are provided, auto-detect latest URL
if [ -n "${LATEST:-}" ] && [ -z "${FIRMWARE:-}" ] && [ -z "${FW_URL:-}" ]; then
# Default board identifiers for each chip
case "$CHIP" in
esp32c3) BOARD_ID="ESP32_GENERIC_C3" ;;
esp32) BOARD_ID="ESP32_GENERIC" ;;
*) BOARD_ID="ESP32_GENERIC" ;;
esac
pattern="${BOARD_ID}-.*\\.bin"
echo "Resolving latest firmware for $BOARD_ID"
if FW_URL=$(resolve_latest_url "$pattern"); then
export FW_URL
echo "Latest firmware resolved to: $FW_URL"
else
echo "Failed to resolve latest firmware for pattern $pattern" >&2
exit 1
fi
fi
# Resolve firmware path, downloading if needed
resolve_firmware() {
if [ -z "${FIRMWARE:-}" ]; then
if [ -n "${FW_URL:-}" ] || [ -n "${LATEST:-}" ]; then
# If FW_URL still unset, resolve latest using board-specific pattern
if [ -z "${FW_URL:-}" ]; then
case "$CHIP" in
esp32c3) BOARD_ID="ESP32_GENERIC_C3" ;;
esp32) BOARD_ID="ESP32_GENERIC" ;;
*) BOARD_ID="ESP32_GENERIC" ;;
esac
pattern="${BOARD_ID}-.*\\.bin"
echo "Resolving latest firmware for $BOARD_ID"
if ! FW_URL=$(resolve_latest_url "$pattern"); then
echo "Failed to resolve latest firmware for pattern $pattern" >&2
exit 1
fi
fi
mkdir -p .cache
FIRMWARE=".cache/$(basename "$FW_URL")"
if [ ! -f "$FIRMWARE" ]; then
echo "Downloading firmware from $FW_URL to $FIRMWARE"
curl -L --fail -o "$FIRMWARE" "$FW_URL"
else
echo "Firmware already downloaded at $FIRMWARE"
fi
else
# Default fallback: fetch latest using board-specific pattern
case "$CHIP" in
esp32c3) BOARD_ID="ESP32_GENERIC_C3" ;;
esp32) BOARD_ID="ESP32_GENERIC" ;;
*) BOARD_ID="ESP32_GENERIC" ;;
esac
pattern="${BOARD_ID}-.*\\.bin"
echo "No FIRMWARE or FW_URL specified. Auto-fetching latest for $BOARD_ID"
if ! FW_URL=$(resolve_latest_url "$pattern"); then
echo "Failed to resolve latest firmware for pattern $pattern" >&2
exit 1
fi
mkdir -p .cache
FIRMWARE=".cache/$(basename "$FW_URL")"
if [ ! -f "$FIRMWARE" ]; then
echo "Downloading firmware from $FW_URL to $FIRMWARE"
curl -L --fail -o "$FIRMWARE" "$FW_URL"
else
echo "Firmware already downloaded at $FIRMWARE"
fi
fi
else
if [ ! -f "$FIRMWARE" ]; then
if [ -n "${FW_URL:-}" ]; then
mkdir -p "$(dirname "$FIRMWARE")"
echo "Firmware not found at $FIRMWARE. Downloading from $FW_URL"
curl -L --fail -o "$FIRMWARE" "$FW_URL"
else
echo "Firmware file not found: $FIRMWARE. Provide FW_URL to download automatically." >&2
exit 1
fi
fi
fi
}
# Auto-detect PORT if not specified
if [ -z "$PORT" ]; then
candidates="$(ls /dev/tty/ACM* /dev/tty/USB* 2>/dev/null || true)"
# Some systems expose without /dev/tty/ prefix patterns; try common Linux paths
[ -z "$candidates" ] && candidates="$(ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null || true)"
# Prefer ACM (often for C3) then USB
PORT=$(printf "%s\n" $candidates | grep -E "/dev/ttyACM[0-9]+" | head -n1 || true)
[ -z "$PORT" ] && PORT=$(printf "%s\n" $candidates | grep -E "/dev/ttyUSB[0-9]+" | head -n1 || true)
if [ -z "$PORT" ]; then
echo "No serial port detected. Connect the board and set PORT=/dev/ttyACM0 (or /dev/ttyUSB0)." >&2
exit 1
fi
echo "Auto-detected PORT=$PORT"
fi
# Preflight: ensure port exists
if [ ! -e "$PORT" ]; then
echo "Port $PORT does not exist. Detected candidates:" >&2
ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null || true
exit 1
fi
ESPL="python -m esptool"
detect_chip() {
# Try to detect actual connected chip using esptool and override if needed
out=$($ESPL --port "$PORT" --baud "$BAUD" chip_id 2>&1 || true)
case "$out" in
*"ESP32-C3"*) DETECTED_CHIP=esp32c3 ;;
*"ESP32"*) DETECTED_CHIP=esp32 ;;
*) DETECTED_CHIP="" ;;
esac
if [ -n "$DETECTED_CHIP" ] && [ "$DETECTED_CHIP" != "$ESPT_CHIP" ]; then
echo "Detected chip $DETECTED_CHIP differs from requested $ESPT_CHIP. Using detected chip."
ESPT_CHIP="$DETECTED_CHIP"
case "$ESPT_CHIP" in
esp32c3) FLASH_OFFSET=0x0 ;;
esp32) FLASH_OFFSET=0x1000 ;;
esac
fi
}
detect_chip
# Now that we know the actual chip, resolve the correct firmware for it
resolve_firmware
# Validate firmware matches detected chip; if not, auto-correct by fetching the right image
EXPECTED_BOARD_ID="ESP32_GENERIC"
case "$ESPT_CHIP" in
esp32c3) EXPECTED_BOARD_ID="ESP32_GENERIC_C3" ;;
esp32) EXPECTED_BOARD_ID="ESP32_GENERIC" ;;
esac
FW_BASENAME="$(basename "$FIRMWARE")"
case "$FW_BASENAME" in
${EXPECTED_BOARD_ID}-*.bin) : ;; # ok
*)
echo "Firmware $FW_BASENAME does not match detected chip ($ESPT_CHIP). Fetching correct image for $EXPECTED_BOARD_ID..."
pattern="${EXPECTED_BOARD_ID}-.*\\.bin"
if ! FW_URL=$(resolve_latest_url "$pattern"); then
echo "Failed to resolve a firmware matching $EXPECTED_BOARD_ID" >&2
exit 1
fi
mkdir -p .cache
FIRMWARE=".cache/$(basename "$FW_URL")"
if [ ! -f "$FIRMWARE" ]; then
echo "Downloading firmware from $FW_URL to $FIRMWARE"
curl -L --fail -o "$FIRMWARE" "$FW_URL"
else
echo "Firmware already downloaded at $FIRMWARE"
fi
;;
esac
$ESPL --chip "$ESPT_CHIP" --port "$PORT" --baud "$BAUD" erase_flash
echo "Writing firmware $FIRMWARE to $FLASH_OFFSET..."
$ESPL --chip "$ESPT_CHIP" --port "$PORT" --baud "$BAUD" write_flash -z "$FLASH_OFFSET" "$FIRMWARE"
echo "Done."

View File

@@ -1,35 +0,0 @@
#!/usr/bin/env bash
# Allow the app to bind to port 80 without root.
# Run once: sudo scripts/setup-port80.sh (from repo root)
# Or: scripts/setup-port80.sh (will prompt for sudo only for setcap)
set -e
cd "$(dirname "$0")/.."
REPO_ROOT="$(pwd)"
# If run under sudo, use the invoking user's pipenv so the venv is found
if [ -n "$SUDO_USER" ]; then
VENV="$(sudo -u "$SUDO_USER" bash -c "cd '$REPO_ROOT' && pipenv --venv" 2>/dev/null)" || true
else
VENV="$(pipenv --venv 2>/dev/null)" || true
fi
if [ -z "$VENV" ]; then
echo "Run 'pipenv install' first, then run this script again."
exit 1
fi
PYTHON="${VENV}/bin/python3"
if [ ! -f "$PYTHON" ]; then
PYTHON="${VENV}/bin/python"
fi
if [ ! -f "$PYTHON" ]; then
echo "Python not found in venv: $VENV"
exit 1
fi
# Use the real binary (setcap can fail on symlinks or some filesystems)
REAL_PYTHON="$(readlink -f "$PYTHON" 2>/dev/null)" || REAL_PYTHON="$PYTHON"
if sudo setcap 'cap_net_bind_service=+ep' "$REAL_PYTHON" 2>/dev/null; then
echo "OK: port 80 enabled for $REAL_PYTHON"
echo "Start the app with: pipenv run run"
else
echo "setcap failed on $REAL_PYTHON"
exit 1
fi

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
# Test the app on port 80. Run after: sudo scripts/setup-port80.sh
# Usage: ./scripts/test-port80.sh
set -e
cd "$(dirname "$0")/.."
APP_URL="${APP_URL:-http://127.0.0.1:80}"
echo "Starting app on port 80 in background..."
pipenv run run &
PID=$!
trap "kill $PID 2>/dev/null; exit" EXIT
echo "Waiting for server to start..."
for i in 1 2 3 4 5 6 7 8 9 10; do
if curl -s -o /dev/null -w "%{http_code}" "$APP_URL/" 2>/dev/null | grep -q 200; then
echo "Server is up."
break
fi
sleep 1
done
echo "Requesting $APP_URL/ ..."
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$APP_URL/")
if [ "$CODE" = "200" ]; then
echo "OK: GET / returned HTTP $CODE"
curl -s "$APP_URL/" | head -5
echo "..."
exit 0
else
echo "FAIL: GET / returned HTTP $CODE (expected 200)"
exit 1
fi

View File

@@ -1,6 +1,8 @@
# Boot script (ESP only; no-op on Pi) import settings
import settings # noqa: F401 import util.wifi as wifi
from settings import Settings from settings import Settings
s = Settings() s = Settings()
# AP setup was here when running on ESP; Pi uses system networking.
name = s.get('name', 'led-controller')
wifi.ap(name, '')

View File

@@ -2,8 +2,8 @@ from microdot import Microdot
from microdot.session import with_session from microdot.session import with_session
from models.preset import Preset from models.preset import Preset
from models.profile import Profile from models.profile import Profile
from models.transport import get_current_sender from models.espnow import ESPNow
from util.espnow_message import build_message, build_preset_dict from util.espnow_message import build_message, build_preset_dict, ESPNOW_MAX_PAYLOAD_BYTES
import asyncio import asyncio
import json import json
@@ -110,13 +110,16 @@ async def delete_preset(request, id, session):
@with_session @with_session
async def send_presets(request, session): async def send_presets(request, session):
""" """
Send one or more presets to the LED driver (via serial transport). Send one or more presets over ESPNow.
Body JSON: Body JSON:
{"preset_ids": ["1", "2", ...]} or {"ids": ["1", "2", ...]} {"preset_ids": ["1", "2", ...]} or {"ids": ["1", "2", ...]}
The controller looks up each preset, converts to API format, chunks into The controller:
<= 240-byte messages, and sends them over the configured transport. - looks up each preset in the Preset model
- converts them to API-compliant format
- splits into <= 240-byte ESPNow messages
- sends each message to all configured ESPNow peers.
""" """
try: try:
data = request.json or {} data = request.json or {}
@@ -129,8 +132,6 @@ async def send_presets(request, session):
save_flag = data.get('save', True) save_flag = data.get('save', True)
save_flag = bool(save_flag) save_flag = bool(save_flag)
default_id = data.get('default') default_id = data.get('default')
# Optional 12-char hex MAC to send to one device; omit for default (e.g. broadcast).
destination_mac = data.get('destination_mac') or data.get('to')
# Build API-compliant preset map keyed by preset ID, include name # Build API-compliant preset map keyed by preset ID, include name
current_profile_id = get_current_profile_id(session) current_profile_id = get_current_profile_id(session)
@@ -152,17 +153,16 @@ async def send_presets(request, session):
if default_id is not None and str(default_id) not in presets_by_name: if default_id is not None and str(default_id) not in presets_by_name:
default_id = None default_id = None
sender = get_current_sender() # Use shared ESPNow singleton
if not sender: esp = ESPNow()
return json.dumps({"error": "Transport not configured"}), 503, {'Content-Type': 'application/json'}
async def send_chunk(chunk_presets): async def send_chunk(chunk_presets):
# Include save flag so the led-driver can persist when desired. # Include save flag so the led-driver can persist when desired.
msg = build_message(presets=chunk_presets, save=save_flag, default=default_id) msg = build_message(presets=chunk_presets, save=save_flag, default=default_id)
await sender.send(msg, addr=destination_mac) await esp.send(msg)
MAX_BYTES = 240 MAX_BYTES = ESPNOW_MAX_PAYLOAD_BYTES
send_delay_s = 0.1 SEND_DELAY_MS = 100
entries = list(presets_by_name.items()) entries = list(presets_by_name.items())
total_presets = len(entries) total_presets = len(entries)
messages_sent = 0 messages_sent = 0
@@ -182,8 +182,8 @@ async def send_presets(request, session):
try: try:
await send_chunk(batch) await send_chunk(batch)
except Exception: except Exception:
return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'} return json.dumps({"error": "ESP-NOW send failed"}), 503, {'Content-Type': 'application/json'}
await asyncio.sleep(send_delay_s) await asyncio.sleep_ms(SEND_DELAY_MS)
messages_sent += 1 messages_sent += 1
batch = {name: preset_obj} batch = {name: preset_obj}
last_msg = build_message(presets=batch, save=save_flag, default=default_id) last_msg = build_message(presets=batch, save=save_flag, default=default_id)
@@ -192,12 +192,12 @@ async def send_presets(request, session):
try: try:
await send_chunk(batch) await send_chunk(batch)
except Exception: except Exception:
return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'} return json.dumps({"error": "ESP-NOW send failed"}), 503, {'Content-Type': 'application/json'}
await asyncio.sleep(send_delay_s) await asyncio.sleep_ms(SEND_DELAY_MS)
messages_sent += 1 messages_sent += 1
return json.dumps({ return json.dumps({
"message": "Presets sent", "message": "Presets sent via ESPNow",
"presets_sent": total_presets, "presets_sent": total_presets,
"messages_sent": messages_sent "messages_sent": messages_sent
}), 200, {'Content-Type': 'application/json'} }), 200, {'Content-Type': 'application/json'}

View File

@@ -1,5 +1,6 @@
from microdot import Microdot, send_file from microdot import Microdot, send_file
from settings import Settings from settings import Settings
import util.wifi as wifi
import json import json
controller = Microdot() controller = Microdot()
@@ -14,18 +15,19 @@ async def get_settings(request):
@controller.get('/wifi/ap') @controller.get('/wifi/ap')
async def get_ap_config(request): async def get_ap_config(request):
"""Get saved AP configuration (Pi: no in-device AP).""" """Get Access Point configuration."""
config = { config = wifi.get_ap_config()
'saved_ssid': settings.get('wifi_ap_ssid'), if config:
'saved_password': settings.get('wifi_ap_password'), # Also get saved settings
'saved_channel': settings.get('wifi_ap_channel'), config['saved_ssid'] = settings.get('wifi_ap_ssid')
'active': False, config['saved_password'] = settings.get('wifi_ap_password')
} config['saved_channel'] = settings.get('wifi_ap_channel')
return json.dumps(config), 200, {'Content-Type': 'application/json'} return json.dumps(config), 200, {'Content-Type': 'application/json'}
return json.dumps({"error": "Failed to get AP config"}), 500
@controller.post('/wifi/ap') @controller.post('/wifi/ap')
async def configure_ap(request): async def configure_ap(request):
"""Save AP configuration to settings (Pi: no in-device AP).""" """Configure Access Point."""
try: try:
data = request.json data = request.json
ssid = data.get('ssid') ssid = data.get('ssid')
@@ -41,14 +43,18 @@ async def configure_ap(request):
if channel < 1 or channel > 11: if channel < 1 or channel > 11:
return json.dumps({"error": "Channel must be between 1 and 11"}), 400 return json.dumps({"error": "Channel must be between 1 and 11"}), 400
# Save to settings
settings['wifi_ap_ssid'] = ssid settings['wifi_ap_ssid'] = ssid
settings['wifi_ap_password'] = password settings['wifi_ap_password'] = password
if channel is not None: if channel is not None:
settings['wifi_ap_channel'] = channel settings['wifi_ap_channel'] = channel
settings.save() settings.save()
# Configure AP
wifi.ap(ssid, password, channel)
return json.dumps({ return json.dumps({
"message": "AP settings saved", "message": "AP configured successfully",
"ssid": ssid, "ssid": ssid,
"channel": channel "channel": channel
}), 200, {'Content-Type': 'application/json'} }), 200, {'Content-Type': 'application/json'}

View File

@@ -1,11 +1,14 @@
import asyncio import asyncio
import gc
import json import json
import os import machine
from machine import Pin
from microdot import Microdot, send_file from microdot import Microdot, send_file
from microdot.websocket import with_websocket from microdot.websocket import with_websocket
from microdot.session import Session from microdot.session import Session
from settings import Settings from settings import Settings
import aioespnow
import controllers.preset as preset import controllers.preset as preset
import controllers.profile as profile import controllers.profile as profile
import controllers.group as group import controllers.group as group
@@ -15,7 +18,8 @@ import controllers.palette as palette
import controllers.scene as scene import controllers.scene as scene
import controllers.pattern as pattern import controllers.pattern as pattern
import controllers.settings as settings_controller import controllers.settings as settings_controller
from models.transport import get_sender, set_sender from models.espnow import ESPNow
from util.espnow_message import split_espnow_message
async def main(port=80): async def main(port=80):
@@ -23,9 +27,8 @@ async def main(port=80):
print(settings) print(settings)
print("Starting") print("Starting")
# Initialize transport (serial to ESP32 bridge) # Initialize ESPNow singleton (config + peers)
sender = get_sender(settings) esp = ESPNow()
set_sender(sender)
app = Microdot() app = Microdot()
@@ -56,7 +59,7 @@ async def main(port=80):
app.mount(pattern.controller, '/patterns') app.mount(pattern.controller, '/patterns')
app.mount(settings_controller.controller, '/settings') app.mount(settings_controller.controller, '/settings')
# Serve index.html at root (cwd is src/ when run via pipenv run run) # Serve index.html at root
@app.route('/') @app.route('/')
def index(request): def index(request):
"""Serve the main web UI.""" """Serve the main web UI."""
@@ -89,25 +92,27 @@ async def main(port=80):
data = await ws.receive() data = await ws.receive()
print(data) print(data)
if data: if data:
# Debug: log incoming WebSocket data
try: try:
parsed = json.loads(data) parsed = json.loads(data)
print("WS received JSON:", parsed) print("WS received JSON:", parsed)
# Optional "to": 12-char hex MAC; rest is payload (sent with that address). except Exception:
addr = parsed.pop("to", None) print("WS received raw:", data)
payload = json.dumps(parsed) if parsed else data
await sender.send(payload, addr=addr) # Forward JSON over ESPNow; split into multiple frames if > 250 bytes
except json.JSONDecodeError: try:
# Not JSON: send raw with default address
try: try:
await sender.send(data) parsed = json.loads(data)
except Exception: chunks = split_espnow_message(parsed)
try: except (json.JSONDecodeError, ValueError):
await ws.send(json.dumps({"error": "Send failed"})) chunks = [data]
except Exception: for i, chunk in enumerate(chunks):
pass if i > 0:
await asyncio.sleep_ms(100)
await esp.send(chunk)
except Exception: except Exception:
try: try:
await ws.send(json.dumps({"error": "Send failed"})) await ws.send(json.dumps({"error": "ESP-NOW send failed"}))
except Exception: except Exception:
pass pass
else: else:
@@ -117,11 +122,25 @@ async def main(port=80):
server = asyncio.create_task(app.start_server(host="0.0.0.0", port=port)) server = asyncio.create_task(app.start_server(host="0.0.0.0", port=port))
#wdt = machine.WDT(timeout=10000)
#wdt.feed()
# Initialize heartbeat LED (XIAO ESP32S3 built-in LED on GPIO 21)
led = Pin(15, Pin.OUT)
led_state = False
while True: while True:
await asyncio.sleep(30) gc.collect()
for i in range(60):
#wdt.feed()
# Heartbeat: toggle LED every 500 ms
led.value(not led.value())
await asyncio.sleep_ms(500)
# cleanup before ending the application # cleanup before ending the application
if __name__ == "__main__": if __name__ == "__main__":
import os asyncio.run(main())
port = int(os.environ.get("PORT", 80))
asyncio.run(main(port=port))

69
src/models/espnow.py Normal file
View File

@@ -0,0 +1,69 @@
import network
import aioespnow
class ESPNow:
"""
Singleton ESPNow helper:
- Manages a single AIOESPNow instance
- Adds a single broadcast-like peer
- Exposes async send(data) to send to that peer.
"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if getattr(self, "_initialized", False):
return
# ESP-NOW requires a WiFi interface to be active (STA or AP). Activate STA
# so ESP-NOW has an interface to use; we don't need to connect to an AP.
try:
sta = network.WLAN(network.STA_IF)
sta.active(True)
except Exception as e:
print("ESPNow: STA active failed:", e)
self._esp = aioespnow.AIOESPNow()
self._esp.active(True)
try:
self._esp.add_peer(b"\xff\xff\xff\xff\xff\xff")
except Exception:
# Ignore add_peer failures (e.g. duplicate)
pass
self._initialized = True
async def send(self, data):
"""
Async send to the broadcast peer.
- data: bytes or str (JSON)
"""
if isinstance(data, str):
payload = data.encode()
else:
payload = data
# Debug: show what we're sending and its size
try:
preview = payload.decode('utf-8')
except Exception:
preview = str(payload)
if len(preview) > 200:
preview = preview[:200] + "...(truncated)"
print("ESPNow.send len=", len(payload), "payload=", preview)
try:
await self._esp.asend(b"\xff\xff\xff\xff\xff\xff", payload)
except Exception as e:
print("ESPNow.send error:", e)
raise

View File

@@ -1,15 +1,5 @@
import json import json
import os import os
import traceback
# DB directory: project root / db (writable without root)
def _db_dir():
try:
# src/models/model.py -> project root
base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
return os.path.join(base, "db")
except Exception:
return "db"
class Model(dict): class Model(dict):
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
@@ -23,13 +13,13 @@ class Model(dict):
if hasattr(self, '_initialized'): if hasattr(self, '_initialized'):
return return
db_dir = _db_dir() # Create /db directory if it doesn't exist (MicroPython compatible)
try: try:
os.makedirs(db_dir, exist_ok=True) os.mkdir("/db")
except OSError: except OSError:
pass pass # Directory already exists, which is fine
self.class_name = self.__class__.__name__ self.class_name = self.__class__.__name__
self.file = os.path.join(db_dir, f"{self.class_name.lower()}.json") self.file = f"/db/{self.class_name.lower()}.json"
super().__init__() super().__init__()
self.load() # Load settings from file during initialization self.load() # Load settings from file during initialization
@@ -47,11 +37,11 @@ class Model(dict):
def save(self): def save(self):
try: try:
db_dir = os.path.dirname(self.file) # Ensure directory exists
try: try:
os.makedirs(db_dir, exist_ok=True) os.mkdir("/db")
except OSError: except OSError:
pass pass # Directory already exists
j = json.dumps(self) j = json.dumps(self)
with open(self.file, 'w') as file: with open(self.file, 'w') as file:
file.write(j) file.write(j)
@@ -64,7 +54,8 @@ class Model(dict):
print(f"{self.class_name} saved successfully to {self.file}") print(f"{self.class_name} saved successfully to {self.file}")
except Exception as e: except Exception as e:
print(f"Error saving {self.class_name} to {self.file}: {e}") print(f"Error saving {self.class_name} to {self.file}: {e}")
traceback.print_exception(type(e), e, e.__traceback__) import sys
sys.print_exception(e)
def load(self): def load(self):
try: try:

View File

@@ -1,12 +0,0 @@
class Serial:
def __init__(self, port, baudrate):
self.port = port
self.baudrate = baudrate
self.uart = UART(1, baudrate, tx=Pin(21), rx=Pin(6))
def send(self, data):
self.uart.write(data)
def receive(self):
return self.uart.read()

View File

@@ -1,66 +0,0 @@
import asyncio
import json
# Default: broadcast (6 bytes). Pi always sends 6-byte address + payload to ESP32.
BROADCAST_MAC = bytes.fromhex("ffffffffffff")
def _encode_payload(data):
if isinstance(data, str):
return data.encode()
if isinstance(data, dict):
return json.dumps(data).encode()
return data
def _parse_mac(addr):
"""Convert 12-char hex string or 6-byte bytes to 6-byte MAC."""
if addr is None or addr == b"":
return BROADCAST_MAC
if isinstance(addr, bytes) and len(addr) == 6:
return addr
if isinstance(addr, str) and len(addr) == 12:
return bytes.fromhex(addr)
return BROADCAST_MAC
async def _to_thread(func, *args):
to_thread = getattr(asyncio, "to_thread", None)
if to_thread:
return await to_thread(func, *args)
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, func, *args)
class SerialSender:
def __init__(self, port, baudrate, default_addr=None):
import serial
self._serial = serial.Serial(port, baudrate=baudrate, timeout=1)
self._default_addr = _parse_mac(default_addr)
async def send(self, data, addr=None):
mac = _parse_mac(addr) if addr is not None else self._default_addr
payload = _encode_payload(data)
await _to_thread(self._serial.write, mac + payload)
return True
_current_sender = None
def set_sender(sender):
global _current_sender
_current_sender = sender
def get_current_sender():
return _current_sender
def get_sender(settings):
port = settings.get("serial_port", "/dev/ttyS0")
baudrate = settings.get("serial_baudrate", 912000)
default_addr = settings.get("serial_destination_mac", "ffffffffffff")
return SerialSender(port, baudrate, default_addr=default_addr)

39
src/p2p.py Normal file
View File

@@ -0,0 +1,39 @@
import network
import aioespnow
import asyncio
import json
from time import sleep
class P2P:
def __init__(self):
network.WLAN(network.STA_IF).active(True)
self.broadcast = bytes.fromhex("ffffffffffff")
self.e = aioespnow.AIOESPNow()
self.e.active(True)
try:
self.e.add_peer(self.broadcast)
except:
pass
async def send(self, data):
# Convert data to bytes if it's a string or dict
if isinstance(data, str):
payload = data.encode()
elif isinstance(data, dict):
payload = json.dumps(data).encode()
else:
payload = data # Assume it's already bytes
# Use asend for async sending - returns boolean indicating success
result = await self.e.asend(self.broadcast, payload)
return result
async def main():
p = P2P()
await p.send(json.dumps({"dj": {"p": "on", "colors": ["#ff0000"]}}))
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -2,23 +2,11 @@ import json
import os import os
import binascii import binascii
def _settings_path():
"""Path to settings.json in project root (writable without root)."""
try:
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, "settings.json")
except Exception:
return "settings.json"
class Settings(dict): class Settings(dict):
SETTINGS_FILE = None # Set in __init__ from _settings_path() SETTINGS_FILE = "/settings.json"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
if Settings.SETTINGS_FILE is None:
Settings.SETTINGS_FILE = _settings_path()
self.load() # Load settings from file during initialization self.load() # Load settings from file during initialization
def generate_secret_key(self): def generate_secret_key(self):

View File

@@ -1,23 +1,27 @@
""" """
Message builder for LED driver API communication. ESPNow message builder utility for LED driver communication.
Builds JSON messages according to the LED driver API specification This module provides utilities to build ESPNow messages according to the API specification.
for sending presets and select commands over the transport (e.g. serial). ESPNow has a 250-byte payload limit; messages larger than that must be split into multiple
frames.
""" """
import json import json
# ESPNow payload limit (bytes). Messages larger than this must be split.
ESPNOW_MAX_PAYLOAD_BYTES = 240
def build_message(presets=None, select=None, save=False, default=None): def build_message(presets=None, select=None, save=False, default=None):
""" """
Build an API message (presets and/or select) as a JSON string. Build an ESPNow message according to the API specification.
Args: Args:
presets: Dictionary mapping preset names to preset objects, or None presets: Dictionary mapping preset names to preset objects, or None
select: Dictionary mapping device names to select lists, or None select: Dictionary mapping device names to select lists, or None
Returns: Returns:
JSON string ready to send over the transport JSON string ready to send via ESPNow
Example: Example:
message = build_message( message = build_message(
@@ -55,6 +59,82 @@ def build_message(presets=None, select=None, save=False, default=None):
return json.dumps(message) return json.dumps(message)
def split_espnow_message(msg_dict, max_bytes=None):
"""
Split a message dict into one or more JSON strings each within ESPNow payload limit.
If the message fits in max_bytes, returns a single-element list. Otherwise splits
"select" and/or "presets" into multiple messages (other keys like v, b, default, save
are included only in the first message).
Args:
msg_dict: Full message as a dict (e.g. from json.loads).
max_bytes: Max payload size in bytes (default ESPNOW_MAX_PAYLOAD_BYTES).
Returns:
List of JSON strings, each <= max_bytes, to send in order.
"""
if max_bytes is None:
max_bytes = ESPNOW_MAX_PAYLOAD_BYTES
single = json.dumps(msg_dict)
if len(single) <= max_bytes:
return [single]
# Keys to attach only to the first message we emit
first_only = {k: msg_dict[k] for k in ("b", "default", "save") if k in msg_dict}
out = []
def emit(chunk_dict, is_first):
m = {"v": msg_dict.get("v", "1")}
if is_first and first_only:
m.update(first_only)
m.update(chunk_dict)
s = json.dumps(m)
if len(s) > max_bytes:
raise ValueError(f"Chunk still too large ({len(s)} > {max_bytes})")
out.append(s)
def chunk_dict(key, items_dict):
if not items_dict:
return
items = list(items_dict.items())
i = 0
first = True
while i < len(items):
chunk = {}
while i < len(items):
k, v = items[i]
trial = dict(chunk)
trial[k] = v
trial_msg = {"v": msg_dict.get("v", "1"), key: trial}
if first_only and first:
trial_msg.update(first_only)
if len(json.dumps(trial_msg)) <= max_bytes:
chunk[k] = v
i += 1
else:
if not chunk:
# Single entry too large; send as-is and hope receiver accepts
chunk[k] = v
i += 1
break
if chunk:
emit({key: chunk}, first)
first = False
if not chunk:
break
if "select" in msg_dict:
chunk_dict("select", msg_dict["select"])
if "presets" in msg_dict:
chunk_dict("presets", msg_dict["presets"])
if not out:
# Fallback: emit one message even if over limit (receiver may reject)
out = [single]
return out
def build_select_message(device_name, preset_name, step=None): def build_select_message(device_name, preset_name, step=None):
""" """
Build a select message for a single device. Build a select message for a single device.

42
src/util/wifi.py Normal file
View File

@@ -0,0 +1,42 @@
import network
def ap(ssid, password, channel=None):
ap_if = network.WLAN(network.AP_IF)
ap_mac = ap_if.config('mac')
print(ssid)
ap_if.active(True)
if channel is not None:
ap_if.config(essid=ssid, password=password, channel=channel)
else:
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')
def get_ap_config():
"""Get current AP configuration."""
try:
ap_if = network.WLAN(network.AP_IF)
if ap_if.active():
config = ap_if.ifconfig()
return {
'ssid': ap_if.config('essid'),
'channel': ap_if.config('channel'),
'ip': config[0] if config else None,
'active': True
}
return {
'ssid': None,
'channel': None,
'ip': None,
'active': False
}
except Exception as e:
print(f"Error getting AP config: {e}")
return None