From ac9fca8d4ba5300a9d2ed29b717042404e7ea7e2 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 15 Mar 2026 17:16:07 +1300 Subject: [PATCH] Pi port: serial transport, addressed ESP-NOW bridge, port 80 - Run app on Raspberry Pi: serial to ESP32 bridge at 912000 baud, /dev/ttyS0 - Remove ESP-NOW/MicroPython-only code from src (espnow, p2p, wifi, machine/Pin) - Transport: always send 6-byte MAC + payload; optional to/destination_mac in API and WebSocket - Settings and model DB use project paths (no root); fix sys.print_exception for CPython - Preset/settings controllers use get_current_sender(); template paths for cwd=src - Pipfile: run from src, PORT from env; scripts for port 80 (setcap) and test - ESP32 bridge: receive 6-byte addr + payload, LRU peer management (20 max), handle ESP_ERR_ESPNOW_EXIST - Add esp32/main.py, esp32/benchmark_peers.py, scripts/setup-port80.sh, scripts/test-port80.sh Made-with: Cursor --- Pipfile | 4 +- Pipfile.lock | 499 +++++++++++++++++------------------- README.md | 4 + esp32/benchmark_peers.py | 112 ++++++++ esp32/main.py | 63 +++++ scripts/setup-port80.sh | 35 +++ scripts/test-port80.sh | 33 +++ src/boot.py | 8 +- src/controllers/preset.py | 32 +-- src/controllers/settings.py | 26 +- src/main.py | 58 ++--- src/models/espnow.py | 69 ----- src/models/model.py | 27 +- src/models/serial.py | 12 + src/models/transport.py | 66 +++++ src/p2p.py | 39 --- src/settings.py | 14 +- src/util/espnow_message.py | 13 +- src/util/wifi.py | 42 --- 19 files changed, 656 insertions(+), 500 deletions(-) create mode 100644 esp32/benchmark_peers.py create mode 100644 esp32/main.py create mode 100755 scripts/setup-port80.sh create mode 100755 scripts/test-port80.sh delete mode 100644 src/models/espnow.py create mode 100644 src/models/serial.py create mode 100644 src/models/transport.py delete mode 100644 src/p2p.py delete mode 100644 src/util/wifi.py diff --git a/Pipfile b/Pipfile index 4c5662c..b8cd450 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ watchfiles = "*" requests = "*" selenium = "*" adafruit-ampy = "*" +microdot = "*" [dev-packages] @@ -21,4 +22,5 @@ python_version = "3.12" [scripts] web = "python /home/pi/led-controller/tests/web.py" watch = "python -m watchfiles 'python tests/web.py' src tests" -install = "pipenv install" \ No newline at end of file +install = "pipenv install" +run = "sh -c 'cd src && python main.py'" diff --git a/Pipfile.lock b/Pipfile.lock index 50e4a49..377865c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c963cd52164ac13fda5e6f3c5975bc14db6cea03ad4973de02ad91a0ab10d2ea" + "sha256": "12b64c3bf5857d958f790f2416072408e2244631242ba2598210d89df330e184" }, "pipfile-spec": 6, "requires": { @@ -32,14 +32,6 @@ "markers": "python_version >= '3.9'", "version": "==4.12.1" }, - "async-generator": { - "hashes": [ - "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", - "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" - ], - "markers": "python_version >= '3.5'", - "version": "==1.10" - }, "attrs": { "hashes": [ "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", @@ -159,19 +151,19 @@ }, "bitstring": { "hashes": [ - "sha256:69d1587f0ac18dc7d93fc7e80d5f447161a33e57027e726dc18a0a8bacf1711a", - "sha256:a08bc09d3857216d4c0f412a1611056f1cc2b64fd254fb1e8a0afba7cfa1a95a" + "sha256:e682ac522bb63e041d16cbc9d0ca86a4f00194db16d0847c7efe066f836b2e37", + "sha256:feac49524fcf3ef27e6081e86f02b10d2adf6c3773bf22fbe0e7eea9534bc737" ], "markers": "python_version >= '3.8'", - "version": "==4.3.1" + "version": "==4.4.0" }, "certifi": { "hashes": [ - "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", - "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" ], "markers": "python_version >= '3.7'", - "version": "==2026.1.4" + "version": "==2026.2.25" }, "cffi": { "hashes": [ @@ -265,122 +257,122 @@ }, "charset-normalizer": { "hashes": [ - "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", - "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", - "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", - "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", - "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", - "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", - "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", - "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", - "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", - "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", - "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", - "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", - "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", - "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", - "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", - "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", - "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", - "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", - "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", - "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", - "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", - "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", - "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", - "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", - "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", - "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", - "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", - "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", - "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", - "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", - "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", - "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", - "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", - "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", - "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", - "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", - "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", - "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", - "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", - "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", - "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", - "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", - "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", - "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", - "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", - "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", - "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", - "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", - "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", - "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", - "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", - "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", - "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", - "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", - "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", - "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", - "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", - "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", - "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", - "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", - "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", - "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", - "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", - "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", - "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", - "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", - "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", - "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", - "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", - "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", - "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", - "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", - "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", - "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", - "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", - "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", - "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", - "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", - "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", - "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", - "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", - "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", - "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", - "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", - "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", - "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", - "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", - "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", - "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", - "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", - "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", - "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", - "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", - "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", - "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", - "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", - "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", - "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", - "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", - "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", - "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", - "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", - "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", - "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", - "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", - "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", - "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", - "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", - "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", - "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", - "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", - "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", - "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4", + "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", + "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", + "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", + "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", + "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", + "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", + "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", + "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", + "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", + "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", + "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", + "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", + "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2", + "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe", + "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", + "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", + "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", + "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", + "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", + "sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf", + "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139", + "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770", + "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", + "sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918", + "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", + "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7", + "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", + "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", + "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", + "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", + "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", + "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", + "sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659", + "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", + "sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9", + "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9", + "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", + "sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d", + "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", + "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", + "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", + "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", + "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99", + "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", + "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", + "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", + "sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca", + "sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c", + "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c", + "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", + "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", + "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", + "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", + "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", + "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", + "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", + "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", + "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a", + "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e", + "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", + "sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123", + "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", + "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc", + "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", + "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", + "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", + "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", + "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", + "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", + "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", + "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", + "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294", + "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22", + "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", + "sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8", + "sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2", + "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", + "sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242", + "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", + "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", + "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", + "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", + "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", + "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", + "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", + "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", + "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", + "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", + "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", + "sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969", + "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", + "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", + "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", + "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", + "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", + "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", + "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", + "sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193", + "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", + "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", + "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95", + "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", + "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", + "sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98", + "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", + "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", + "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", + "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", + "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", + "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", + "sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947", + "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3" ], "markers": "python_version >= '3.7'", - "version": "==3.4.4" + "version": "==3.4.5" }, "click": { "hashes": [ @@ -392,66 +384,65 @@ }, "cryptography": { "hashes": [ - "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", - "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", - "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", - "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", - "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", - "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", - "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", - "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", - "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", - "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", - "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", - "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", - "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", - "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", - "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", - "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", - "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", - "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", - "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", - "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", - "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", - "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", - "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", - "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", - "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", - "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", - "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", - "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", - "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", - "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", - "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", - "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", - "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", - "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", - "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", - "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", - "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", - "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", - "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", - "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", - "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", - "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", - "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", - "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", - "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", - "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", - "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", - "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", - "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822" + "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", + "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", + "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", + "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", + "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", + "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", + "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", + "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", + "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", + "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", + "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", + "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", + "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", + "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", + "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", + "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", + "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", + "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", + "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", + "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", + "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", + "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", + "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", + "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", + "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", + "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", + "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", + "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", + "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", + "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", + "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", + "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", + "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", + "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", + "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", + "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", + "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", + "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", + "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", + "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", + "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", + "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", + "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", + "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", + "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", + "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", + "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", + "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", + "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.4" + "version": "==46.0.5" }, "esptool": { "hashes": [ - "sha256:2ea9bcd7eb263d380a4fe0170856a10e4c65e3f38c757ebdc73584c8dd8322da" + "sha256:9c355b7d6331cc92979cc710ae5c41f59830d1ea29ec24c467c6005a092c06d6" ], "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==5.1.0" + "version": "==5.2.0" }, "h11": { "hashes": [ @@ -469,14 +460,6 @@ "markers": "python_version >= '3.8'", "version": "==3.11" }, - "importlib-metadata": { - "hashes": [ - "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", - "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151" - ], - "markers": "python_version >= '3.9'", - "version": "==8.7.1" - }, "intelhex": { "hashes": [ "sha256:87cc5225657524ec6361354be928adfd56bcf2a3dcc646c40f8f094c39c07db4", @@ -500,23 +483,22 @@ "markers": "python_version >= '3.7'", "version": "==0.1.2" }, + "microdot": { + "hashes": [ + "sha256:363e3ebfc80b7e0415779848c9332e4e7fb7bd365ee54d3620abffe42ed82946", + "sha256:abfb82ca31cc430174e4761cc7356adc4bff00ea758d437c2b258883dc63f464" + ], + "index": "pypi", + "version": "==2.6.0" + }, "mpremote": { "hashes": [ "sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4", "sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5" ], "index": "pypi", - "markers": "python_version >= '3.4'", "version": "==1.27.0" }, - "mypy-extensions": { - "hashes": [ - "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", - "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" - ], - "markers": "python_version >= '3.8'", - "version": "==1.1.0" - }, "outcome": { "hashes": [ "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", @@ -525,21 +507,13 @@ "markers": "python_version >= '3.7'", "version": "==1.3.0.post0" }, - "packaging": { - "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" - ], - "markers": "python_version >= '3.8'", - "version": "==26.0" - }, "platformdirs": { "hashes": [ - "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", - "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31" + "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", + "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868" ], "markers": "python_version >= '3.10'", - "version": "==4.5.1" + "version": "==4.9.4" }, "pycparser": { "hashes": [ @@ -559,12 +533,11 @@ }, "pyjwt": { "hashes": [ - "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", - "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469" + "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", + "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==2.11.0" + "version": "==2.12.1" }, "pyserial": { "hashes": [ @@ -584,11 +557,11 @@ }, "python-dotenv": { "hashes": [ - "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", - "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61" + "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", + "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" ], - "markers": "python_version >= '3.9'", - "version": "==1.2.1" + "markers": "python_version >= '3.10'", + "version": "==1.2.2" }, "pyyaml": { "hashes": [ @@ -682,16 +655,15 @@ "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" ], "index": "pypi", - "markers": "python_version >= '3.9'", "version": "==2.32.5" }, "rich": { "hashes": [ - "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", - "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8" + "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", + "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b" ], "markers": "python_full_version >= '3.8.0'", - "version": "==14.3.2" + "version": "==14.3.3" }, "rich-click": { "hashes": [ @@ -703,12 +675,11 @@ }, "selenium": { "hashes": [ - "sha256:a88f5905d88ad0b84991c2386ea39e2bbde6d6c334be38df5842318ba98eaa8c", - "sha256:c8823fc02e2c771d9ad9a0cf899cee7de1a57a6697e3d0b91f67566129f2b729" + "sha256:003e971f805231ad63e671783a2b91a299355d10cefb9de964c36ff3819115aa", + "sha256:b8ccde8d2e7642221ca64af184a92c19eee6accf2e27f20f30472f5efae18eb1" ], "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==4.40.0" + "version": "==4.41.0" }, "sniffio": { "hashes": [ @@ -725,20 +696,50 @@ ], "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": { "hashes": [ - "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", - "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5" + "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b", + "sha256:a29b92b73f09d4b48ed249acd91073281a7f1063f09caba5dc70465b5c7aa970" ], "markers": "python_version >= '3.10'", - "version": "==0.32.0" - }, - "trio-typing": { - "hashes": [ - "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", - "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264" - ], - "version": "==0.10.0" + "version": "==0.33.0" }, "trio-websocket": { "hashes": [ @@ -748,20 +749,6 @@ "markers": "python_version >= '3.8'", "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": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", @@ -771,9 +758,6 @@ "version": "==4.15.0" }, "urllib3": { - "extras": [ - "socks" - ], "hashes": [ "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" @@ -894,7 +878,6 @@ "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf" ], "index": "pypi", - "markers": "python_version >= '3.9'", "version": "==1.1.1" }, "websocket-client": { @@ -912,14 +895,6 @@ ], "markers": "python_version >= '3.10'", "version": "==1.3.2" - }, - "zipp": { - "hashes": [ - "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", - "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" - ], - "markers": "python_version >= '3.9'", - "version": "==3.23.0" } }, "develop": {} diff --git a/README.md b/README.md index d8322e4..4c2ba8f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # led-controller +## Run on port 80 without root + +Run once: `sudo scripts/setup-port80.sh`. Then start the app with: `pipenv run run`. + diff --git a/esp32/benchmark_peers.py b/esp32/benchmark_peers.py new file mode 100644 index 0000000..49f4f6e --- /dev/null +++ b/esp32/benchmark_peers.py @@ -0,0 +1,112 @@ +# 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.") diff --git a/esp32/main.py b/esp32/main.py new file mode 100644 index 0000000..26c8bb5 --- /dev/null +++ b/esp32/main.py @@ -0,0 +1,63 @@ +# 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() \ No newline at end of file diff --git a/scripts/setup-port80.sh b/scripts/setup-port80.sh new file mode 100755 index 0000000..cfb5a75 --- /dev/null +++ b/scripts/setup-port80.sh @@ -0,0 +1,35 @@ +#!/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 diff --git a/scripts/test-port80.sh b/scripts/test-port80.sh new file mode 100755 index 0000000..4e559bb --- /dev/null +++ b/scripts/test-port80.sh @@ -0,0 +1,33 @@ +#!/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 diff --git a/src/boot.py b/src/boot.py index f61a927..0cc6e07 100644 --- a/src/boot.py +++ b/src/boot.py @@ -1,8 +1,6 @@ -import settings -import util.wifi as wifi +# Boot script (ESP only; no-op on Pi) +import settings # noqa: F401 from settings import Settings s = Settings() - -name = s.get('name', 'led-controller') -wifi.ap(name, '') +# AP setup was here when running on ESP; Pi uses system networking. diff --git a/src/controllers/preset.py b/src/controllers/preset.py index 059b83e..c31a90e 100644 --- a/src/controllers/preset.py +++ b/src/controllers/preset.py @@ -2,7 +2,7 @@ from microdot import Microdot from microdot.session import with_session from models.preset import Preset from models.profile import Profile -from models.espnow import ESPNow +from models.transport import get_current_sender from util.espnow_message import build_message, build_preset_dict import asyncio import json @@ -110,16 +110,13 @@ async def delete_preset(request, id, session): @with_session async def send_presets(request, session): """ - Send one or more presets over ESPNow. + Send one or more presets to the LED driver (via serial transport). Body JSON: {"preset_ids": ["1", "2", ...]} or {"ids": ["1", "2", ...]} - The controller: - - 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. + The controller looks up each preset, converts to API format, chunks into + <= 240-byte messages, and sends them over the configured transport. """ try: data = request.json or {} @@ -132,6 +129,8 @@ async def send_presets(request, session): save_flag = data.get('save', True) save_flag = bool(save_flag) 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 current_profile_id = get_current_profile_id(session) @@ -153,16 +152,17 @@ async def send_presets(request, session): if default_id is not None and str(default_id) not in presets_by_name: default_id = None - # Use shared ESPNow singleton - esp = ESPNow() + sender = get_current_sender() + if not sender: + return json.dumps({"error": "Transport not configured"}), 503, {'Content-Type': 'application/json'} async def send_chunk(chunk_presets): # Include save flag so the led-driver can persist when desired. msg = build_message(presets=chunk_presets, save=save_flag, default=default_id) - await esp.send(msg) + await sender.send(msg, addr=destination_mac) MAX_BYTES = 240 - SEND_DELAY_MS = 100 + send_delay_s = 0.1 entries = list(presets_by_name.items()) total_presets = len(entries) messages_sent = 0 @@ -182,8 +182,8 @@ async def send_presets(request, session): try: await send_chunk(batch) except Exception: - return json.dumps({"error": "ESP-NOW send failed"}), 503, {'Content-Type': 'application/json'} - await asyncio.sleep_ms(SEND_DELAY_MS) + return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'} + await asyncio.sleep(send_delay_s) messages_sent += 1 batch = {name: preset_obj} last_msg = build_message(presets=batch, save=save_flag, default=default_id) @@ -192,12 +192,12 @@ async def send_presets(request, session): try: await send_chunk(batch) except Exception: - return json.dumps({"error": "ESP-NOW send failed"}), 503, {'Content-Type': 'application/json'} - await asyncio.sleep_ms(SEND_DELAY_MS) + return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'} + await asyncio.sleep(send_delay_s) messages_sent += 1 return json.dumps({ - "message": "Presets sent via ESPNow", + "message": "Presets sent", "presets_sent": total_presets, "messages_sent": messages_sent }), 200, {'Content-Type': 'application/json'} diff --git a/src/controllers/settings.py b/src/controllers/settings.py index 43de919..8f05ed0 100644 --- a/src/controllers/settings.py +++ b/src/controllers/settings.py @@ -1,6 +1,5 @@ from microdot import Microdot, send_file from settings import Settings -import util.wifi as wifi import json controller = Microdot() @@ -15,19 +14,18 @@ async def get_settings(request): @controller.get('/wifi/ap') async def get_ap_config(request): - """Get Access Point configuration.""" - config = wifi.get_ap_config() - if config: - # Also get saved settings - config['saved_ssid'] = settings.get('wifi_ap_ssid') - 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({"error": "Failed to get AP config"}), 500 + """Get saved AP configuration (Pi: no in-device AP).""" + config = { + 'saved_ssid': settings.get('wifi_ap_ssid'), + 'saved_password': settings.get('wifi_ap_password'), + 'saved_channel': settings.get('wifi_ap_channel'), + 'active': False, + } + return json.dumps(config), 200, {'Content-Type': 'application/json'} @controller.post('/wifi/ap') async def configure_ap(request): - """Configure Access Point.""" + """Save AP configuration to settings (Pi: no in-device AP).""" try: data = request.json ssid = data.get('ssid') @@ -43,18 +41,14 @@ async def configure_ap(request): if channel < 1 or channel > 11: return json.dumps({"error": "Channel must be between 1 and 11"}), 400 - # Save to settings settings['wifi_ap_ssid'] = ssid settings['wifi_ap_password'] = password if channel is not None: settings['wifi_ap_channel'] = channel settings.save() - # Configure AP - wifi.ap(ssid, password, channel) - return json.dumps({ - "message": "AP configured successfully", + "message": "AP settings saved", "ssid": ssid, "channel": channel }), 200, {'Content-Type': 'application/json'} diff --git a/src/main.py b/src/main.py index f6d95d6..4386f84 100644 --- a/src/main.py +++ b/src/main.py @@ -1,14 +1,11 @@ import asyncio -import gc import json -import machine -from machine import Pin +import os from microdot import Microdot, send_file from microdot.websocket import with_websocket from microdot.session import Session from settings import Settings -import aioespnow import controllers.preset as preset import controllers.profile as profile import controllers.group as group @@ -18,7 +15,7 @@ import controllers.palette as palette import controllers.scene as scene import controllers.pattern as pattern import controllers.settings as settings_controller -from models.espnow import ESPNow +from models.transport import get_sender, set_sender async def main(port=80): @@ -26,8 +23,9 @@ async def main(port=80): print(settings) print("Starting") - # Initialize ESPNow singleton (config + peers) - esp = ESPNow() + # Initialize transport (serial to ESP32 bridge) + sender = get_sender(settings) + set_sender(sender) app = Microdot() @@ -58,7 +56,7 @@ async def main(port=80): app.mount(pattern.controller, '/patterns') app.mount(settings_controller.controller, '/settings') - # Serve index.html at root + # Serve index.html at root (cwd is src/ when run via pipenv run run) @app.route('/') def index(request): """Serve the main web UI.""" @@ -91,19 +89,25 @@ async def main(port=80): data = await ws.receive() print(data) if data: - # Debug: log incoming WebSocket data try: parsed = json.loads(data) print("WS received JSON:", parsed) - except Exception: - print("WS received raw:", data) - - # Forward raw JSON payload over ESPNow to configured peers - try: - await esp.send(data) + # Optional "to": 12-char hex MAC; rest is payload (sent with that address). + addr = parsed.pop("to", None) + payload = json.dumps(parsed) if parsed else data + await sender.send(payload, addr=addr) + except json.JSONDecodeError: + # Not JSON: send raw with default address + try: + await sender.send(data) + except Exception: + try: + await ws.send(json.dumps({"error": "Send failed"})) + except Exception: + pass except Exception: try: - await ws.send(json.dumps({"error": "ESP-NOW send failed"})) + await ws.send(json.dumps({"error": "Send failed"})) except Exception: pass else: @@ -113,25 +117,11 @@ async def main(port=80): 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: - 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) + await asyncio.sleep(30) # cleanup before ending the application if __name__ == "__main__": - asyncio.run(main()) + import os + port = int(os.environ.get("PORT", 80)) + asyncio.run(main(port=port)) diff --git a/src/models/espnow.py b/src/models/espnow.py deleted file mode 100644 index 357dda0..0000000 --- a/src/models/espnow.py +++ /dev/null @@ -1,69 +0,0 @@ -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 - diff --git a/src/models/model.py b/src/models/model.py index ccb7a3e..43aa625 100644 --- a/src/models/model.py +++ b/src/models/model.py @@ -1,5 +1,15 @@ import json 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): def __new__(cls, *args, **kwargs): @@ -13,13 +23,13 @@ class Model(dict): if hasattr(self, '_initialized'): return - # Create /db directory if it doesn't exist (MicroPython compatible) + db_dir = _db_dir() try: - os.mkdir("/db") + os.makedirs(db_dir, exist_ok=True) except OSError: - pass # Directory already exists, which is fine + pass self.class_name = self.__class__.__name__ - self.file = f"/db/{self.class_name.lower()}.json" + self.file = os.path.join(db_dir, f"{self.class_name.lower()}.json") super().__init__() self.load() # Load settings from file during initialization @@ -37,11 +47,11 @@ class Model(dict): def save(self): try: - # Ensure directory exists + db_dir = os.path.dirname(self.file) try: - os.mkdir("/db") + os.makedirs(db_dir, exist_ok=True) except OSError: - pass # Directory already exists + pass j = json.dumps(self) with open(self.file, 'w') as file: file.write(j) @@ -54,8 +64,7 @@ class Model(dict): print(f"{self.class_name} saved successfully to {self.file}") except Exception as e: print(f"Error saving {self.class_name} to {self.file}: {e}") - import sys - sys.print_exception(e) + traceback.print_exception(type(e), e, e.__traceback__) def load(self): try: diff --git a/src/models/serial.py b/src/models/serial.py new file mode 100644 index 0000000..2517c27 --- /dev/null +++ b/src/models/serial.py @@ -0,0 +1,12 @@ +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() + \ No newline at end of file diff --git a/src/models/transport.py b/src/models/transport.py new file mode 100644 index 0000000..30abca4 --- /dev/null +++ b/src/models/transport.py @@ -0,0 +1,66 @@ +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) diff --git a/src/p2p.py b/src/p2p.py deleted file mode 100644 index e5d2464..0000000 --- a/src/p2p.py +++ /dev/null @@ -1,39 +0,0 @@ -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()) diff --git a/src/settings.py b/src/settings.py index b10e61a..9d9f9cc 100644 --- a/src/settings.py +++ b/src/settings.py @@ -2,11 +2,23 @@ import json import os 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): - SETTINGS_FILE = "/settings.json" + SETTINGS_FILE = None # Set in __init__ from _settings_path() def __init__(self): super().__init__() + if Settings.SETTINGS_FILE is None: + Settings.SETTINGS_FILE = _settings_path() self.load() # Load settings from file during initialization def generate_secret_key(self): diff --git a/src/util/espnow_message.py b/src/util/espnow_message.py index a377e2b..c7176cc 100644 --- a/src/util/espnow_message.py +++ b/src/util/espnow_message.py @@ -1,7 +1,8 @@ """ -ESPNow message builder utility for LED driver communication. +Message builder for LED driver API communication. -This module provides utilities to build ESPNow messages according to the API specification. +Builds JSON messages according to the LED driver API specification +for sending presets and select commands over the transport (e.g. serial). """ import json @@ -9,14 +10,14 @@ import json def build_message(presets=None, select=None, save=False, default=None): """ - Build an ESPNow message according to the API specification. - + Build an API message (presets and/or select) as a JSON string. + Args: presets: Dictionary mapping preset names to preset objects, or None select: Dictionary mapping device names to select lists, or None - + Returns: - JSON string ready to send via ESPNow + JSON string ready to send over the transport Example: message = build_message( diff --git a/src/util/wifi.py b/src/util/wifi.py deleted file mode 100644 index 72756cc..0000000 --- a/src/util/wifi.py +++ /dev/null @@ -1,42 +0,0 @@ -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