Refactor patterns to use preset-based API and fix initialization order

- Fix initialization order: initialize self.presets before calling self.select()
- Separate add() and edit() methods: add() creates new presets, edit() updates existing ones
- Update all test files to use add() instead of edit() for creating presets
- Add comprehensive auto/manual mode test
- Remove tool.py (moved to led-tool project)
This commit is contained in:
2026-01-25 23:23:14 +13:00
parent f4ef415b5a
commit b7d2f52fc3
16 changed files with 1593 additions and 1105 deletions

View File

@@ -20,3 +20,4 @@ python_version = "3"
[scripts] [scripts]
dev = 'watchfiles "./dev.py /dev/ttyACM0 src reset follow"' dev = 'watchfiles "./dev.py /dev/ttyACM0 src reset follow"'
web = "uvicorn tool:app --host 0.0.0.0 --port 8080" web = "uvicorn tool:app --host 0.0.0.0 --port 8080"
install = "pipenv install"

948
Pipfile.lock generated Normal file
View File

@@ -0,0 +1,948 @@
{
"_meta": {
"hash": {
"sha256": "1d0184b0df68796cc30d8a808f27b6a5d447b3e1f8af0633b2a543d14f0ab829"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"annotated-doc": {
"hashes": [
"sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320",
"sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"
],
"markers": "python_version >= '3.8'",
"version": "==0.0.4"
},
"annotated-types": {
"hashes": [
"sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53",
"sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"
],
"markers": "python_version >= '3.8'",
"version": "==0.7.0"
},
"anyio": {
"hashes": [
"sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0",
"sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb"
],
"markers": "python_version >= '3.9'",
"version": "==4.12.0"
},
"asgiref": {
"hashes": [
"sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4",
"sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==3.11.0"
},
"bitarray": {
"hashes": [
"sha256:004d518fa410e6da43386d20e07b576a41eb417ac67abf9f30fa75e125697199",
"sha256:014df8a9430276862392ac5d471697de042367996c49f32d0008585d2c60755a",
"sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e",
"sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3",
"sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e",
"sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4",
"sha256:0f8069a807a3e6e3c361ce302ece4bf1c3b49962c1726d1d56587e8f48682861",
"sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5",
"sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521",
"sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d",
"sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55",
"sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9",
"sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce",
"sha256:1a926fa554870642607fd10e66ee25b75fdd9a7ca4bbffa93d424e4ae2bf734a",
"sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9",
"sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e",
"sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b",
"sha256:239578587b9c29469ab61149dda40a2fe714a6a4eca0f8ff9ea9439ec4b7bc30",
"sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6",
"sha256:26714898eb0d847aac8af94c4441c9cb50387847d0fe6b9fc4217c086cd68b80",
"sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11",
"sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f",
"sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25",
"sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77",
"sha256:2fe8c54b15a9cd4f93bc2aaceab354ec65af93370aa1496ba2f9c537a4855ee0",
"sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125",
"sha256:31a4ad2b730128e273f1c22300da3e3631f125703e4fee0ac44d385abfb15671",
"sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de",
"sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860",
"sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe",
"sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d",
"sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc",
"sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df",
"sha256:46cf239856b87fe1c86dfbb3d459d840a8b1649e7922b1e0bfb6b6464692644a",
"sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8",
"sha256:4902f4ecd5fcb6a5f482d7b0ae1c16c21f26fc5279b3b6127363d13ad8e7a9d9",
"sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe",
"sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607",
"sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf",
"sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee",
"sha256:5338a313f998e1be7267191b7caaae82563b4a2b42b393561055412a34042caa",
"sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954",
"sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a",
"sha256:58a01ea34057463f7a98a4d6ff40160f65f945e924fec08a5b39e327e372875d",
"sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428",
"sha256:5c5a8a83df95e51f7a7c2b083eaea134cbed39fc42c6aeb2e764ddb7ccccd43e",
"sha256:5f2fb10518f6b365f5b720e43a529c3b2324ca02932f609631a44edb347d8d54",
"sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5",
"sha256:6d70fa9c6d2e955bde8cd327ffc11f2cc34bc21944e5571a46ca501e7eadef24",
"sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f",
"sha256:720963fee259291a88348ae9735d9deb5d334e84a016244f61c89f5a49aa400a",
"sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b",
"sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70",
"sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7",
"sha256:7f14d6b303e55bd7d19b28309ef8014370e84a3806c5e452e078e7df7344d97a",
"sha256:7f65bd5d4cdb396295b6aa07f84ca659ac65c5c68b53956a6d95219e304b0ada",
"sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541",
"sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b",
"sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4",
"sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2",
"sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd",
"sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646",
"sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89",
"sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa",
"sha256:94652da1a4ca7cfb69c15dd6986b205e0bd9c63a05029c3b48b4201085f527bd",
"sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1",
"sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb",
"sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220",
"sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c",
"sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310",
"sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2",
"sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e",
"sha256:a358277122456666a8b2a0b9aa04f1b89d34e8aa41d08a6557d693e6abb6667c",
"sha256:a60da2f9efbed355edb35a1fb6829148676786c829fad708bb6bb47211b3593a",
"sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a",
"sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594",
"sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8",
"sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52",
"sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20",
"sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8",
"sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6",
"sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9",
"sha256:cbba763d99de0255a3e4938f25a8579930ac8aa089233cb2fb2ed7d04d4aff02",
"sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425",
"sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d",
"sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2",
"sha256:d2dbe8a3baf2d842e342e8acb06ae3844765d38df67687c144cdeb71f1bcb5d7",
"sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4",
"sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096",
"sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d",
"sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149",
"sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b",
"sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35",
"sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773",
"sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d",
"sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6",
"sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741",
"sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f",
"sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8",
"sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f",
"sha256:f8d3417db5e14a6789073b21ae44439a755289477901901bae378a57b905e148",
"sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8",
"sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97",
"sha256:ff1863f037dad765ef5963efc2e37d399ac023e192a6f2bb394e2377d023cefe"
],
"version": "==3.8.0"
},
"bitstring": {
"hashes": [
"sha256:69d1587f0ac18dc7d93fc7e80d5f447161a33e57027e726dc18a0a8bacf1711a",
"sha256:a08bc09d3857216d4c0f412a1611056f1cc2b64fd254fb1e8a0afba7cfa1a95a"
],
"markers": "python_version >= '3.8'",
"version": "==4.3.1"
},
"blinker": {
"hashes": [
"sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf",
"sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"
],
"markers": "python_version >= '3.9'",
"version": "==1.9.0"
},
"cffi": {
"hashes": [
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
"sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
"sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
"sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
"sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
"sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
"sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
"sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
"sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
"sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
"sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
"sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
"sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
"sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
"sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
"sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
"sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
"sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
"sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
"sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
"sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
"sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
"sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
"sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
"sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
"sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
"sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
"sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
"sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
"sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
"sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
"sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
"sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
"sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
"sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
"sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
"sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
"sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
"sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
"sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
"sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
"sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
"sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
"sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
"sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
"sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
"sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
"sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
"sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
"sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
"sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
"sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
"sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
"sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
"sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
"sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
"sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
"sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
"sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
"sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
"sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
"sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
"sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
"sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
"sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
"sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
"sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
],
"markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'",
"version": "==2.0.0"
},
"click": {
"hashes": [
"sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a",
"sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"
],
"markers": "python_version >= '3.10'",
"version": "==8.3.1"
},
"cryptography": {
"hashes": [
"sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217",
"sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d",
"sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc",
"sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71",
"sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971",
"sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a",
"sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926",
"sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc",
"sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d",
"sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b",
"sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20",
"sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044",
"sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3",
"sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715",
"sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4",
"sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506",
"sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f",
"sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0",
"sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683",
"sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3",
"sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21",
"sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91",
"sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c",
"sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8",
"sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df",
"sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c",
"sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb",
"sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7",
"sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04",
"sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db",
"sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459",
"sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea",
"sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914",
"sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717",
"sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9",
"sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac",
"sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32",
"sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec",
"sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1",
"sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb",
"sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac",
"sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665",
"sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e",
"sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb",
"sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5",
"sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936",
"sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de",
"sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372",
"sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54",
"sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422",
"sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849",
"sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c",
"sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963",
"sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"
],
"markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'",
"version": "==46.0.3"
},
"esptool": {
"hashes": [
"sha256:2ea9bcd7eb263d380a4fe0170856a10e4c65e3f38c757ebdc73584c8dd8322da"
],
"index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==5.1.0"
},
"fastapi": {
"hashes": [
"sha256:0503b7b7bc71bc98f7c90c9117d21fdf6147c0d74703011b87936becc86985c1",
"sha256:624d384d7cda7c096449c889fc776a0571948ba14c3c929fa8e9a78cd0b0a6a8"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==0.123.10"
},
"flask": {
"hashes": [
"sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87",
"sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==3.1.2"
},
"h11": {
"hashes": [
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
],
"markers": "python_version >= '3.8'",
"version": "==0.16.0"
},
"idna": {
"hashes": [
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
],
"markers": "python_version >= '3.8'",
"version": "==3.11"
},
"intelhex": {
"hashes": [
"sha256:87cc5225657524ec6361354be928adfd56bcf2a3dcc646c40f8f094c39c07db4",
"sha256:892b7361a719f4945237da8ccf754e9513db32f5628852785aea108dcd250093"
],
"version": "==2.3.0"
},
"itsdangerous": {
"hashes": [
"sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef",
"sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"
],
"markers": "python_version >= '3.8'",
"version": "==2.2.0"
},
"jinja2": {
"hashes": [
"sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
"sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.6"
},
"markdown-it-py": {
"hashes": [
"sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147",
"sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"
],
"markers": "python_version >= '3.10'",
"version": "==4.0.0"
},
"markupsafe": {
"hashes": [
"sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f",
"sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a",
"sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf",
"sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19",
"sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf",
"sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c",
"sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175",
"sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219",
"sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb",
"sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6",
"sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab",
"sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26",
"sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1",
"sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce",
"sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218",
"sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634",
"sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695",
"sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad",
"sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73",
"sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c",
"sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe",
"sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa",
"sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559",
"sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa",
"sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37",
"sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758",
"sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f",
"sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8",
"sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d",
"sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c",
"sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97",
"sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a",
"sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19",
"sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9",
"sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9",
"sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc",
"sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2",
"sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4",
"sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354",
"sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50",
"sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698",
"sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9",
"sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b",
"sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc",
"sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115",
"sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e",
"sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485",
"sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f",
"sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12",
"sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025",
"sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009",
"sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d",
"sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b",
"sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a",
"sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5",
"sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f",
"sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d",
"sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1",
"sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287",
"sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6",
"sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f",
"sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581",
"sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed",
"sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b",
"sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c",
"sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026",
"sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8",
"sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676",
"sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6",
"sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e",
"sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d",
"sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d",
"sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01",
"sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7",
"sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419",
"sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795",
"sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1",
"sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5",
"sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d",
"sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42",
"sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe",
"sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda",
"sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e",
"sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737",
"sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523",
"sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591",
"sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc",
"sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a",
"sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"
],
"markers": "python_version >= '3.9'",
"version": "==3.0.3"
},
"mdurl": {
"hashes": [
"sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
"sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
],
"markers": "python_version >= '3.7'",
"version": "==0.1.2"
},
"mpremote": {
"hashes": [
"sha256:39251644305be718c52bc5965315adc4ae824901750abf6a3fb63683234df05c",
"sha256:61a39bf5af502e1ec56d1b28bf067766c3a0daea9d7487934cb472e378a12fe1"
],
"index": "pypi",
"markers": "python_version >= '3.4'",
"version": "==1.26.1"
},
"platformdirs": {
"hashes": [
"sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda",
"sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"
],
"markers": "python_version >= '3.10'",
"version": "==4.5.1"
},
"pycparser": {
"hashes": [
"sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
"sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
],
"markers": "implementation_name != 'PyPy'",
"version": "==2.23"
},
"pydantic": {
"hashes": [
"sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49",
"sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"
],
"markers": "python_version >= '3.9'",
"version": "==2.12.5"
},
"pydantic-core": {
"hashes": [
"sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90",
"sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740",
"sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504",
"sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84",
"sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33",
"sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c",
"sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0",
"sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e",
"sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0",
"sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a",
"sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34",
"sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2",
"sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3",
"sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815",
"sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14",
"sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba",
"sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375",
"sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf",
"sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963",
"sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1",
"sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808",
"sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553",
"sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1",
"sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2",
"sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5",
"sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470",
"sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2",
"sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b",
"sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660",
"sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c",
"sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093",
"sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5",
"sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594",
"sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008",
"sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a",
"sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a",
"sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd",
"sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284",
"sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586",
"sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869",
"sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294",
"sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f",
"sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66",
"sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51",
"sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc",
"sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97",
"sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a",
"sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d",
"sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9",
"sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c",
"sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07",
"sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36",
"sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e",
"sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05",
"sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e",
"sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941",
"sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3",
"sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612",
"sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3",
"sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b",
"sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe",
"sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146",
"sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11",
"sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60",
"sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd",
"sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b",
"sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c",
"sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a",
"sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460",
"sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1",
"sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf",
"sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf",
"sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858",
"sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2",
"sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9",
"sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2",
"sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3",
"sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6",
"sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770",
"sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d",
"sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc",
"sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23",
"sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26",
"sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa",
"sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8",
"sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d",
"sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3",
"sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d",
"sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034",
"sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9",
"sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1",
"sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56",
"sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b",
"sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c",
"sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a",
"sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e",
"sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9",
"sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5",
"sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a",
"sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556",
"sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e",
"sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49",
"sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2",
"sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9",
"sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b",
"sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc",
"sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb",
"sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0",
"sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8",
"sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82",
"sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69",
"sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b",
"sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c",
"sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75",
"sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5",
"sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f",
"sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad",
"sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b",
"sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7",
"sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425",
"sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"
],
"markers": "python_version >= '3.9'",
"version": "==2.41.5"
},
"pygments": {
"hashes": [
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
],
"markers": "python_version >= '3.8'",
"version": "==2.19.2"
},
"pyserial": {
"hashes": [
"sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb",
"sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"
],
"index": "pypi",
"version": "==3.5"
},
"pyyaml": {
"hashes": [
"sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c",
"sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a",
"sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3",
"sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956",
"sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6",
"sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c",
"sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65",
"sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a",
"sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0",
"sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b",
"sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1",
"sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6",
"sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7",
"sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e",
"sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007",
"sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310",
"sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4",
"sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9",
"sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295",
"sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea",
"sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0",
"sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e",
"sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac",
"sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9",
"sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7",
"sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35",
"sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb",
"sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b",
"sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69",
"sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5",
"sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b",
"sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c",
"sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369",
"sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd",
"sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824",
"sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198",
"sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065",
"sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c",
"sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c",
"sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764",
"sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196",
"sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b",
"sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00",
"sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac",
"sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8",
"sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e",
"sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28",
"sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3",
"sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5",
"sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4",
"sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b",
"sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf",
"sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5",
"sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702",
"sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8",
"sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788",
"sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da",
"sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d",
"sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc",
"sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c",
"sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba",
"sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f",
"sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917",
"sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5",
"sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26",
"sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f",
"sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b",
"sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be",
"sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c",
"sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3",
"sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6",
"sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926",
"sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"
],
"markers": "python_version >= '3.8'",
"version": "==6.0.3"
},
"reedsolo": {
"hashes": [
"sha256:2b6a3e402a1ee3e1eea3f932f81e6c0b7bbc615588074dca1dbbcdeb055002bd",
"sha256:c1359f02742751afe0f1c0de9f0772cc113835aa2855d2db420ea24393c87732"
],
"version": "==1.7.0"
},
"rich": {
"hashes": [
"sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4",
"sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"
],
"markers": "python_full_version >= '3.8.0'",
"version": "==14.2.0"
},
"rich-click": {
"hashes": [
"sha256:af73dc68e85f3bebb80ce302a642b9fe3b65f3df0ceb42eb9a27c467c1b678c8",
"sha256:d70f39938bcecaf5543e8750828cbea94ef51853f7d0e174cda1e10543767389"
],
"markers": "python_version >= '3.8'",
"version": "==1.9.4"
},
"starlette": {
"hashes": [
"sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca",
"sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca"
],
"markers": "python_version >= '3.10'",
"version": "==0.50.0"
},
"typing-extensions": {
"hashes": [
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
],
"markers": "python_version >= '3.9'",
"version": "==4.15.0"
},
"typing-inspection": {
"hashes": [
"sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7",
"sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"
],
"markers": "python_version >= '3.9'",
"version": "==0.4.2"
},
"uvicorn": {
"hashes": [
"sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02",
"sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==0.38.0"
},
"watchfiles": {
"hashes": [
"sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c",
"sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43",
"sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510",
"sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0",
"sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2",
"sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b",
"sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18",
"sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219",
"sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3",
"sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4",
"sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803",
"sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94",
"sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6",
"sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce",
"sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099",
"sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae",
"sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4",
"sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43",
"sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd",
"sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10",
"sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374",
"sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051",
"sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d",
"sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34",
"sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49",
"sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7",
"sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844",
"sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77",
"sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b",
"sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741",
"sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e",
"sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33",
"sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42",
"sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab",
"sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc",
"sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5",
"sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da",
"sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e",
"sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05",
"sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a",
"sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d",
"sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701",
"sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863",
"sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2",
"sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101",
"sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02",
"sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b",
"sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6",
"sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb",
"sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620",
"sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957",
"sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6",
"sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d",
"sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956",
"sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef",
"sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261",
"sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02",
"sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af",
"sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9",
"sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21",
"sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336",
"sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d",
"sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c",
"sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31",
"sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81",
"sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9",
"sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff",
"sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2",
"sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e",
"sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc",
"sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404",
"sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01",
"sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18",
"sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3",
"sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606",
"sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04",
"sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3",
"sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14",
"sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c",
"sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82",
"sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610",
"sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0",
"sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150",
"sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5",
"sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c",
"sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a",
"sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b",
"sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d",
"sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70",
"sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70",
"sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f",
"sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24",
"sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e",
"sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be",
"sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5",
"sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e",
"sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f",
"sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88",
"sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb",
"sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849",
"sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d",
"sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c",
"sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44",
"sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac",
"sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428",
"sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b",
"sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5",
"sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa",
"sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==1.1.1"
},
"werkzeug": {
"hashes": [
"sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905",
"sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e"
],
"markers": "python_version >= '3.9'",
"version": "==3.1.4"
}
},
"develop": {}
}

153
dev.py
View File

@@ -3,130 +3,51 @@
import subprocess import subprocess
import serial import serial
import sys import sys
import glob
def upload_src(port): print(sys.argv)
# Extract port (first arg if it's not a command)
commands = ["src", "lib", "ls", "reset", "follow", "db"]
port = None
if len(sys.argv) > 1 and sys.argv[1] not in commands:
port = sys.argv[1]
for cmd in sys.argv[1:]:
print(cmd)
match cmd:
case "src":
if port:
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":" ], cwd="src") subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":" ], cwd="src")
else:
def upload_lib(port): print("Error: Port required for 'src' command")
case "lib":
if port:
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":" ]) subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":" ])
else:
def list_files(port): print("Error: Port required for 'lib' command")
case "ls":
if port:
subprocess.call(["mpremote", "connect", port, "fs", "ls", ":" ]) subprocess.call(["mpremote", "connect", port, "fs", "ls", ":" ])
else:
def reset_device(port): print("Error: Port required for 'ls' command")
case "reset":
if port:
with serial.Serial(port, baudrate=115200) as ser: with serial.Serial(port, baudrate=115200) as ser:
ser.write(b'\x03\x03\x04') ser.write(b'\x03\x03\x04')
else:
def follow_serial(port): print("Error: Port required for 'reset' command")
case "follow":
if port:
with serial.Serial(port, baudrate=115200) as ser: with serial.Serial(port, baudrate=115200) as ser:
while True: while True:
if ser.in_waiting > 0: if ser.in_waiting > 0: # Check if there is data in the buffer
data = ser.readline().decode('utf-8').strip() data = ser.readline().decode('utf-8').strip() # Read and decode the data
print(data) print(data)
def clean_settings(port):
subprocess.call(["mpremote", "connect", port, "fs", "rm", ":/settings.json"])
def flash_firmware(port):
# Find MicroPython firmware binary
firmware_files = glob.glob("*.bin")
if not firmware_files:
print("Error: No .bin firmware file found in current directory")
print("Please download MicroPython firmware and place it in the project directory")
sys.exit(1)
firmware = firmware_files[0]
if len(firmware_files) > 1:
print(f"Warning: Multiple .bin files found, using: {firmware}")
print(f"Flashing MicroPython firmware: {firmware}")
print("Erasing flash...")
subprocess.call(["esptool.py", "--port", port, "erase_flash"])
print(f"Writing firmware to flash...")
subprocess.call([
"esptool.py",
"--port", port,
"--baud", "460800",
"write_flash", "0",
firmware
])
print("Flash complete!")
def main():
port = "/dev/ttyACM0"
commands = []
i = 1
# Parse arguments manually to preserve order
while i < len(sys.argv):
arg = sys.argv[i]
if arg in ["-p", "--port"]:
if i + 1 < len(sys.argv):
port = sys.argv[i + 1]
i += 2
else: else:
print(f"Error: {arg} requires a port argument") print("Error: Port required for 'follow' command")
sys.exit(1) case "db":
elif arg in ["-s", "--src"]: if port:
commands.append(("src", upload_src)) subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "db", ":" ])
i += 1
elif arg in ["-r", "--reset"]:
commands.append(("reset", reset_device))
i += 1
elif arg in ["-f", "--follow"]:
commands.append(("follow", follow_serial))
i += 1
elif arg == "--lib":
commands.append(("lib", upload_lib))
i += 1
elif arg == "--ls":
commands.append(("ls", list_files))
i += 1
elif arg == "--clean":
commands.append(("clean", clean_settings))
i += 1
elif arg == "--flash":
commands.append(("flash", flash_firmware))
i += 1
elif arg in ["-h", "--help"]:
print("LED Driver development tools")
print("\nUsage:")
print(" ./dev.py [-p PORT] [FLAGS...]")
print("\nFlags:")
print(" -p, --port PORT Serial port (default: /dev/ttyACM0)")
print(" -s, --src Upload src directory")
print(" -r, --reset Reset device")
print(" -f, --follow Follow serial output")
print(" --lib Upload lib directory")
print(" --ls List files on device")
print(" --clean Remove settings.json from device")
print(" --flash Flash MicroPython firmware")
print("\nExamples:")
print(" ./dev.py -p /dev/ttyACM0 -s -r -f")
print(" ./dev.py --flash -s -r")
sys.exit(0)
else: else:
print(f"Error: Unknown argument: {arg}") print("Error: Port required for 'db' command")
print("Use -h or --help for usage information")
sys.exit(1)
# Execute commands in the order they were given
if not commands:
print("No commands specified. Use -h or --help for usage information.")
sys.exit(1)
for cmd_name, cmd_func in commands:
if cmd_name == "reset":
print("Resetting device...")
elif cmd_name == "follow":
print("Following serial output (Ctrl+C to exit)...")
elif cmd_name == "flash":
pass # flash_firmware prints its own messages
else:
print(f"{cmd_name.capitalize()}...")
cmd_func(port)
if __name__ == "__main__":
main()

4
install.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# Install script - runs pipenv install
pipenv install "$@"

View File

@@ -1,7 +1,7 @@
import utime import utime
from patterns_base import atternsBase from patterns_base import Patterns_Base
class Patterns(PatternsBase): class Patterns(Patterns_Base):
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", delay=100): def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", delay=100):
super().__init__(pin, num_leds, color1, color2, brightness, selected, delay) super().__init__(pin, num_leds, color1, color2, brightness, selected, delay)
self.auto = True self.auto = True
@@ -18,15 +18,16 @@ class Patterns(PatternsBase):
} }
def blink(self): def blink(self, preset):
state = True # True = on, False = off state = True # True = on, False = off
last_update = utime.ticks_ms() last_update = utime.ticks_ms()
while True: while True:
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= self.delay: if utime.ticks_diff(current_time, last_update) >= preset.delay:
if state: if state:
self.fill(self.apply_brightness(self.colors[0])) color = preset.colors[0] if preset.colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.brightness))
else: else:
self.fill((0, 0, 0)) self.fill((0, 0, 0))
state = not state state = not state
@@ -35,15 +36,15 @@ class Patterns(PatternsBase):
yield yield
def rainbow(self): def rainbow(self, preset):
step = self.step % 256 step = self.step % 256
step_amount = max(1, int(self.n1)) # n1 controls step increment step_amount = max(1, int(preset.n1)) # n1 controls step increment
# If auto is False, run a single step and then stop # If auto is False, run a single step and then stop
if not self.auto: if not preset.auto:
for i in range(self.num_leds): for i in range(self.num_leds):
rc_index = (i * 256 // self.num_leds) + step rc_index = (i * 256 // self.num_leds) + step
self.n[i] = self.apply_brightness(self.wheel(rc_index & 255)) self.n[i] = self.apply_brightness(self.wheel(rc_index & 255), preset.brightness)
self.n.write() self.n.write()
# Increment step by n1 for next manual call # Increment step by n1 for next manual call
self.step = (step + step_amount) % 256 self.step = (step + step_amount) % 256
@@ -55,11 +56,11 @@ class Patterns(PatternsBase):
while True: while True:
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
sleep_ms = max(1, int(self.delay)) # Access delay directly sleep_ms = max(1, int(preset.delay)) # Get delay from preset
if utime.ticks_diff(current_time, last_update) >= sleep_ms: if utime.ticks_diff(current_time, last_update) >= sleep_ms:
for i in range(self.num_leds): for i in range(self.num_leds):
rc_index = (i * 256 // self.num_leds) + step rc_index = (i * 256 // self.num_leds) + step
self.n[i] = self.apply_brightness(self.wheel(rc_index & 255)) self.n[i] = self.apply_brightness(self.wheel(rc_index & 255), preset.brightness)
self.n.write() self.n.write()
step = (step + step_amount) % 256 step = (step + step_amount) % 256
self.step = step self.step = step
@@ -68,23 +69,24 @@ class Patterns(PatternsBase):
yield yield
def pulse(self): def pulse(self, preset):
self.off() self.off()
# Ensure we have at least one color # Get colors from preset
if not self.colors: colors = preset.colors
self.colors = [(255, 255, 255)] if not colors:
colors = [(255, 255, 255)]
color_index = 0 color_index = 0
cycle_start = utime.ticks_ms() cycle_start = utime.ticks_ms()
# State machine based pulse using a single generator loop # State machine based pulse using a single generator loop
while True: while True:
# Read current timing parameters each cycle so they can be changed live # Read current timing parameters from preset
attack_ms = max(0, int(self.n1)) # Attack time in ms attack_ms = max(0, int(preset.n1)) # Attack time in ms
hold_ms = max(0, int(self.n2)) # Hold time in ms hold_ms = max(0, int(preset.n2)) # Hold time in ms
decay_ms = max(0, int(self.n3)) # Decay time in ms decay_ms = max(0, int(preset.n3)) # Decay time in ms
delay_ms = max(0, int(self.delay)) delay_ms = max(0, int(preset.delay))
total_ms = attack_ms + hold_ms + decay_ms + delay_ms total_ms = attack_ms + hold_ms + decay_ms + delay_ms
if total_ms <= 0: if total_ms <= 0:
@@ -93,22 +95,22 @@ class Patterns(PatternsBase):
now = utime.ticks_ms() now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, cycle_start) elapsed = utime.ticks_diff(now, cycle_start)
base_color = self.colors[color_index % len(self.colors)] base_color = colors[color_index % len(colors)]
if elapsed < attack_ms and attack_ms > 0: if elapsed < attack_ms and attack_ms > 0:
# Attack: fade 0 -> 1 # Attack: fade 0 -> 1
factor = elapsed / attack_ms factor = elapsed / attack_ms
color = tuple(int(c * factor) for c in base_color) color = tuple(int(c * factor) for c in base_color)
self.fill(self.apply_brightness(color)) self.fill(self.apply_brightness(color, preset.brightness))
elif elapsed < attack_ms + hold_ms: elif elapsed < attack_ms + hold_ms:
# Hold: full brightness # Hold: full brightness
self.fill(self.apply_brightness(base_color)) self.fill(self.apply_brightness(base_color, preset.brightness))
elif elapsed < attack_ms + hold_ms + decay_ms and decay_ms > 0: elif elapsed < attack_ms + hold_ms + decay_ms and decay_ms > 0:
# Decay: fade 1 -> 0 # Decay: fade 1 -> 0
dec_elapsed = elapsed - attack_ms - hold_ms dec_elapsed = elapsed - attack_ms - hold_ms
factor = max(0.0, 1.0 - (dec_elapsed / decay_ms)) factor = max(0.0, 1.0 - (dec_elapsed / decay_ms))
color = tuple(int(c * factor) for c in base_color) color = tuple(int(c * factor) for c in base_color)
self.fill(self.apply_brightness(color)) self.fill(self.apply_brightness(color, preset.brightness))
elif elapsed < total_ms: elif elapsed < total_ms:
# Delay phase: LEDs off between pulses # Delay phase: LEDs off between pulses
self.fill((0, 0, 0)) self.fill((0, 0, 0))
@@ -116,7 +118,7 @@ class Patterns(PatternsBase):
# End of cycle, move to next color and restart timing # End of cycle, move to next color and restart timing
color_index += 1 color_index += 1
cycle_start = now cycle_start = now
if not self.auto: if not preset.auto:
break break
# Skip drawing this tick, start next cycle # Skip drawing this tick, start next cycle
yield yield
@@ -125,17 +127,18 @@ class Patterns(PatternsBase):
# Yield once per tick # Yield once per tick
yield yield
def transition(self): def transition(self, preset):
"""Transition between colors, blending over `delay` ms.""" """Transition between colors, blending over `delay` ms."""
if not self.colors: colors = preset.colors
if not colors:
self.off() self.off()
yield yield
return return
# Only one color: just keep it on # Only one color: just keep it on
if len(self.colors) == 1: if len(colors) == 1:
while True: while True:
self.fill(self.apply_brightness(self.colors[0])) self.fill(self.apply_brightness(colors[0], preset.brightness))
yield yield
return return
@@ -143,25 +146,25 @@ class Patterns(PatternsBase):
start_time = utime.ticks_ms() start_time = utime.ticks_ms()
while True: while True:
if not self.colors: if not colors:
break break
# Get current and next color based on live list # Get current and next color based on live list
c1 = self.colors[color_index % len(self.colors)] c1 = colors[color_index % len(colors)]
c2 = self.colors[(color_index + 1) % len(self.colors)] c2 = colors[(color_index + 1) % len(colors)]
duration = max(10, int(self.delay)) # At least 10ms duration = max(10, int(preset.delay)) # At least 10ms
now = utime.ticks_ms() now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, start_time) elapsed = utime.ticks_diff(now, start_time)
if elapsed >= duration: if elapsed >= duration:
# End of this transition step # End of this transition step
if not self.auto and color_index >= 0: if not preset.auto and color_index >= 0:
# One-shot: transition from first to second color only # One-shot: transition from first to second color only
self.fill(self.apply_brightness(c2)) self.fill(self.apply_brightness(c2, preset.brightness))
break break
# Auto: move to next pair # Auto: move to next pair
color_index = (color_index + 1) % len(self.colors) color_index = (color_index + 1) % len(colors)
start_time = now start_time = now
yield yield
continue continue
@@ -171,14 +174,15 @@ class Patterns(PatternsBase):
interpolated = tuple( interpolated = tuple(
int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3) int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3)
) )
self.fill(self.apply_brightness(interpolated)) self.fill(self.apply_brightness(interpolated, preset.brightness))
yield yield
def chase(self): def chase(self, preset):
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating. """Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating.
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)""" Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)"""
if len(self.colors) < 1: colors = preset.colors
if len(colors) < 1:
# Need at least 1 color # Need at least 1 color
return return
@@ -189,27 +193,27 @@ class Patterns(PatternsBase):
last_update = utime.ticks_ms() last_update = utime.ticks_ms()
while True: while True:
# Access colors, delay, and n values directly for live updates # Access colors, delay, and n values from preset
if not self.colors: if not colors:
break break
# If only one color provided, use it for both colors # If only one color provided, use it for both colors
if len(self.colors) < 2: if len(colors) < 2:
color0 = self.colors[0] color0 = colors[0]
color1 = self.colors[0] color1 = colors[0]
else: else:
color0 = self.colors[0] color0 = colors[0]
color1 = self.colors[1] color1 = colors[1]
color0 = self.apply_brightness(color0) color0 = self.apply_brightness(color0, preset.brightness)
color1 = self.apply_brightness(color1) color1 = self.apply_brightness(color1, preset.brightness)
n1 = max(1, int(self.n1)) # LEDs of color 0 n1 = max(1, int(preset.n1)) # LEDs of color 0
n2 = max(1, int(self.n2)) # LEDs of color 1 n2 = max(1, int(preset.n2)) # LEDs of color 1
n3 = int(self.n3) # Step movement on odd steps (can be negative) n3 = int(preset.n3) # Step movement on odd steps (can be negative)
n4 = int(self.n4) # Step movement on even steps (can be negative) n4 = int(preset.n4) # Step movement on even steps (can be negative)
segment_length = n1 + n2 segment_length = n1 + n2
transition_duration = max(10, int(self.delay)) transition_duration = max(10, int(preset.delay))
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= transition_duration: if utime.ticks_diff(current_time, last_update) >= transition_duration:
@@ -249,16 +253,16 @@ class Patterns(PatternsBase):
# Yield once per tick so other logic can run # Yield once per tick so other logic can run
yield yield
def circle(self): def circle(self, preset):
"""Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4""" """Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4"""
head = 0 head = 0
tail = 0 tail = 0
# Calculate timing # Calculate timing from preset
head_rate = max(1, int(self.n1)) # n1 = head moves per second head_rate = max(1, int(preset.n1)) # n1 = head moves per second
tail_rate = max(1, int(self.n3)) # n3 = tail moves per second tail_rate = max(1, int(preset.n3)) # n3 = tail moves per second
max_length = max(1, int(self.n2)) # n2 = max length max_length = max(1, int(preset.n2)) # n2 = max length
min_length = max(0, int(self.n4)) # n4 = min length min_length = max(0, int(preset.n4)) # n4 = min length
head_delay = 1000 // head_rate # ms between head movements head_delay = 1000 // head_rate # ms between head movements
tail_delay = 1000 // tail_rate # ms between tail movements tail_delay = 1000 // tail_rate # ms between tail movements
@@ -268,6 +272,9 @@ class Patterns(PatternsBase):
phase = "growing" # "growing", "shrinking", or "off" phase = "growing" # "growing", "shrinking", or "off"
colors = preset.colors
color = self.apply_brightness(colors[0] if colors else (255, 255, 255), preset.brightness)
while True: while True:
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
@@ -280,7 +287,6 @@ class Patterns(PatternsBase):
segment_length = self.num_leds segment_length = self.num_leds
# Draw segment from tail to head # Draw segment from tail to head
color = self.apply_brightness(self.colors[0])
for i in range(segment_length + 1): for i in range(segment_length + 1):
led_pos = (tail + i) % self.num_leds led_pos = (tail + i) % self.num_leds
self.n[led_pos] = color self.n[led_pos] = color

View File

@@ -24,6 +24,31 @@ param_mapping = {
"auto": "auto", "auto": "auto",
} }
class Preset:
def __init__(self, data):
# Set default values for all preset attributes
self.pattern = "off"
self.delay = 100
self.brightness = 127
self.colors = [(255, 255, 255)]
self.auto = True
self.n1 = 0
self.n2 = 0
self.n3 = 0
self.n4 = 0
self.n5 = 0
self.n6 = 0
# Override defaults with provided data
self.edit(data)
def edit(self, data=None):
if not data:
return False
for key, value in data.items():
setattr(self, key, value)
return True
class Patterns_Base: class Patterns_Base:
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", delay=100): def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", delay=100):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
@@ -46,8 +71,27 @@ class Patterns_Base:
self.n6 = 0 self.n6 = 0
self.generator = None self.generator = None
self.presets = {}
self.select(self.selected) self.select(self.selected)
def edit(self, name, data):
if name in self.presets:
self.presets[name].edit(data)
return True
return False
def add(self, name, data):
self.presets[name] = Preset(data)
return
def delete(self, name):
if name in self.presets:
del self.presets[name]
return True
return False
def tick(self): def tick(self):
if self.generator is None: if self.generator is None:
@@ -57,13 +101,14 @@ class Patterns_Base:
except StopIteration: except StopIteration:
self.generator = None self.generator = None
def select(self, pattern): def select(self, preset_name):
if pattern in self.patterns: if preset_name in self.presets:
self.selected = pattern preset = self.presets[preset_name]
self.generator = self.patterns[pattern]() if preset.pattern in self.patterns:
print(f"Selected pattern: {pattern}") self.generator = self.patterns[preset.pattern](preset)
self.selected = preset_name # Store the preset name, not the object
return True return True
# If pattern doesn't exist, default to "off" # If preset doesn't exist or pattern not found, default to "off"
return False return False
def set_param(self, key, value): def set_param(self, key, value):
@@ -108,11 +153,13 @@ class Patterns_Base:
self.n[i] = fill_color self.n[i] = fill_color
self.n.write() self.n.write()
def off(self): def off(self, preset=None):
self.fill((0, 0, 0)) self.fill((0, 0, 0))
def on(self): def on(self, preset):
self.fill(self.apply_brightness(self.colors[0])) colors = preset.colors
color = colors[0] if colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.brightness))
@@ -127,4 +174,3 @@ class Patterns_Base:
pos -= 170 pos -= 170
return (0, pos * 3, 255 - pos * 3) return (0, pos * 3, 255 - pos * 3)

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from patterns import Patterns
def run_for(p, wdt, duration_ms):
"""Run pattern for specified duration."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
print("=" * 50)
print("Testing Auto and Manual Modes")
print("=" * 50)
# Test 1: Rainbow in AUTO mode (continuous)
print("\nTest 1: Rainbow pattern in AUTO mode (should run continuously)")
p.add("rainbow_auto", {
"pattern": "rainbow",
"brightness": 128,
"delay": 50,
"n1": 2,
"auto": True
})
p.select("rainbow_auto")
print("Running rainbow_auto for 3 seconds...")
run_for(p, wdt, 3000)
print("✓ Auto mode: Pattern ran continuously")
# Test 2: Rainbow in MANUAL mode (one step per tick)
print("\nTest 2: Rainbow pattern in MANUAL mode (one step per tick)")
p.add("rainbow_manual", {
"pattern": "rainbow",
"brightness": 128,
"delay": 50,
"n1": 2,
"auto": False
})
p.select("rainbow_manual")
print("Calling tick() 5 times (should advance 5 steps)...")
for i in range(5):
p.tick()
utime.sleep_ms(100) # Small delay to see changes
print(f" Tick {i+1}: generator={'active' if p.generator is not None else 'stopped'}")
# Check if generator stopped after one cycle
if p.generator is None:
print("✓ Manual mode: Generator stopped after one step (as expected)")
else:
print("⚠ Manual mode: Generator still active (may need multiple ticks)")
# Test 3: Pulse in AUTO mode (continuous cycles)
print("\nTest 3: Pulse pattern in AUTO mode (should pulse continuously)")
p.add("pulse_auto", {
"pattern": "pulse",
"brightness": 128,
"delay": 100,
"n1": 500, # Attack
"n2": 200, # Hold
"n3": 500, # Decay
"colors": [(255, 0, 0)],
"auto": True
})
p.select("pulse_auto")
print("Running pulse_auto for 3 seconds...")
run_for(p, wdt, 3000)
print("✓ Auto mode: Pulse ran continuously")
# Test 4: Pulse in MANUAL mode (one cycle then stop)
print("\nTest 4: Pulse pattern in MANUAL mode (one cycle then stop)")
p.add("pulse_manual", {
"pattern": "pulse",
"brightness": 128,
"delay": 100,
"n1": 300, # Attack
"n2": 200, # Hold
"n3": 300, # Decay
"colors": [(0, 255, 0)],
"auto": False
})
p.select("pulse_manual")
print("Running pulse_manual until generator stops...")
tick_count = 0
max_ticks = 200 # Safety limit
while p.generator is not None and tick_count < max_ticks:
p.tick()
tick_count += 1
utime.sleep_ms(10)
if p.generator is None:
print(f"✓ Manual mode: Pulse completed one cycle after {tick_count} ticks")
else:
print(f"⚠ Manual mode: Pulse still running after {tick_count} ticks")
# Test 5: Transition in AUTO mode (continuous transitions)
print("\nTest 5: Transition pattern in AUTO mode (continuous transitions)")
p.add("transition_auto", {
"pattern": "transition",
"brightness": 128,
"delay": 500,
"colors": [(255, 0, 0), (0, 255, 0), (0, 0, 255)],
"auto": True
})
p.select("transition_auto")
print("Running transition_auto for 3 seconds...")
run_for(p, wdt, 3000)
print("✓ Auto mode: Transition ran continuously")
# Test 6: Transition in MANUAL mode (one transition then stop)
print("\nTest 6: Transition pattern in MANUAL mode (one transition then stop)")
p.add("transition_manual", {
"pattern": "transition",
"brightness": 128,
"delay": 500,
"colors": [(255, 0, 0), (0, 255, 0)],
"auto": False
})
p.select("transition_manual")
print("Running transition_manual until generator stops...")
tick_count = 0
max_ticks = 200
while p.generator is not None and tick_count < max_ticks:
p.tick()
tick_count += 1
utime.sleep_ms(10)
if p.generator is None:
print(f"✓ Manual mode: Transition completed after {tick_count} ticks")
else:
print(f"⚠ Manual mode: Transition still running after {tick_count} ticks")
# Test 7: Switching between auto and manual modes
print("\nTest 7: Switching between auto and manual modes")
p.add("switch_test", {
"pattern": "rainbow",
"brightness": 128,
"delay": 50,
"n1": 2,
"auto": True
})
p.select("switch_test")
print("Running in auto mode for 1 second...")
run_for(p, wdt, 1000)
# Switch to manual mode by editing the preset
print("Switching to manual mode...")
p.edit("switch_test", {"auto": False})
p.select("switch_test") # Re-select to apply changes
print("Calling tick() 3 times in manual mode...")
for i in range(3):
p.tick()
utime.sleep_ms(100)
print(f" Tick {i+1}: generator={'active' if p.generator is not None else 'stopped'}")
# Switch back to auto mode
print("Switching back to auto mode...")
p.edit("switch_test", {"auto": True})
p.select("switch_test")
print("Running in auto mode for 1 second...")
run_for(p, wdt, 1000)
print("✓ Successfully switched between auto and manual modes")
# Cleanup
print("\nCleaning up...")
p.add("cleanup_off", {"pattern": "off"})
p.select("cleanup_off")
p.tick()
utime.sleep_ms(100)
print("\n" + "=" * 50)
print("All tests completed!")
print("=" * 50)
if __name__ == "__main__":
main()

View File

@@ -12,10 +12,15 @@ def main():
p = Patterns(pin=pin, num_leds=num) p = Patterns(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
p.set_param("br", 64)
p.set_param("dl", 200) # Create blink preset
p.set_param("cl", [(255, 0, 0), (0, 0, 255)]) p.add("test_blink", {
p.select("blink") "pattern": "blink",
"brightness": 64,
"delay": 200,
"colors": [(255, 0, 0), (0, 0, 255)]
})
p.select("test_blink")
start = utime.ticks_ms() start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 1500: while utime.ticks_diff(utime.ticks_ms(), start) < 1500:

View File

@@ -24,74 +24,93 @@ def main():
# Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1) # Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)
print("Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)") print("Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)")
p.set_param("br", 255) p.add("chase1", {
p.set_param("dl", 200) "pattern": "chase",
p.set_param("n1", 5) "brightness": 255,
p.set_param("n2", 5) "delay": 200,
p.set_param("n3", 1) "n1": 5,
p.set_param("n4", 1) "n2": 5,
p.set_param("cl", [(255, 0, 0), (0, 255, 0)]) "n3": 1,
p.select("chase") "n4": 1,
"colors": [(255, 0, 0), (0, 255, 0)]
})
p.select("chase1")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Test 2: Forward and backward (n3=2, n4=-1) # Test 2: Forward and backward (n3=2, n4=-1)
print("Test 2: Forward and backward (n3=2, n4=-1)") print("Test 2: Forward and backward (n3=2, n4=-1)")
p.set_param("n1", 3) p.add("chase2", {
p.set_param("n2", 3) "pattern": "chase",
p.set_param("n3", 2) "n1": 3,
p.set_param("n4", -1) "n2": 3,
p.set_param("dl", 150) "n3": 2,
p.set_param("cl", [(0, 0, 255), (255, 255, 0)]) "n4": -1,
p.select("chase") "delay": 150,
"colors": [(0, 0, 255), (255, 255, 0)]
})
p.select("chase2")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Test 3: Large segments (n1=10, n2=5) # Test 3: Large segments (n1=10, n2=5)
print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)") print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)")
p.set_param("n1", 10) p.add("chase3", {
p.set_param("n2", 5) "pattern": "chase",
p.set_param("n3", 3) "n1": 10,
p.set_param("n4", 3) "n2": 5,
p.set_param("dl", 200) "n3": 3,
p.set_param("cl", [(255, 128, 0), (128, 0, 255)]) "n4": 3,
p.select("chase") "delay": 200,
"colors": [(255, 128, 0), (128, 0, 255)]
})
p.select("chase3")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Test 4: Fast movement (n3=5, n4=5) # Test 4: Fast movement (n3=5, n4=5)
print("Test 4: Fast movement (n3=5, n4=5)") print("Test 4: Fast movement (n3=5, n4=5)")
p.set_param("n1", 4) p.add("chase4", {
p.set_param("n2", 4) "pattern": "chase",
p.set_param("n3", 5) "n1": 4,
p.set_param("n4", 5) "n2": 4,
p.set_param("dl", 100) "n3": 5,
p.set_param("cl", [(255, 0, 255), (0, 255, 255)]) "n4": 5,
p.select("chase") "delay": 100,
"colors": [(255, 0, 255), (0, 255, 255)]
})
p.select("chase4")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
# Test 5: Backward movement (n3=-2, n4=-2) # Test 5: Backward movement (n3=-2, n4=-2)
print("Test 5: Backward movement (n3=-2, n4=-2)") print("Test 5: Backward movement (n3=-2, n4=-2)")
p.set_param("n1", 6) p.add("chase5", {
p.set_param("n2", 4) "pattern": "chase",
p.set_param("n3", -2) "n1": 6,
p.set_param("n4", -2) "n2": 4,
p.set_param("dl", 200) "n3": -2,
p.set_param("cl", [(255, 255, 255), (0, 0, 0)]) "n4": -2,
p.select("chase") "delay": 200,
"colors": [(255, 255, 255), (0, 0, 0)]
})
p.select("chase5")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Test 6: Alternating forward/backward (n3=3, n4=-2) # Test 6: Alternating forward/backward (n3=3, n4=-2)
print("Test 6: Alternating forward/backward (n3=3, n4=-2)") print("Test 6: Alternating forward/backward (n3=3, n4=-2)")
p.set_param("n1", 5) p.add("chase6", {
p.set_param("n2", 5) "pattern": "chase",
p.set_param("n3", 3) "n1": 5,
p.set_param("n4", -2) "n2": 5,
p.set_param("dl", 250) "n3": 3,
p.set_param("cl", [(255, 0, 0), (0, 255, 0)]) "n4": -2,
p.select("chase") "delay": 250,
"colors": [(255, 0, 0), (0, 255, 0)]
})
p.select("chase6")
run_for(p, wdt, 4000) run_for(p, wdt, 4000)
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.select("off") p.add("cleanup_off", {"pattern": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100) run_for(p, wdt, 100)

View File

@@ -24,68 +24,87 @@ def main():
# Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0) # Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)
print("Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)") print("Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)")
p.set_param("br", 255) p.add("circle1", {
p.set_param("n1", 50) # Head moves 50 LEDs/second "pattern": "circle",
p.set_param("n2", 100) # Max length 100 LEDs "brightness": 255,
p.set_param("n3", 200) # Tail moves 200 LEDs/second "n1": 50, # Head moves 50 LEDs/second
p.set_param("n4", 0) # Min length 0 LEDs "n2": 100, # Max length 100 LEDs
p.set_param("cl", [(255, 0, 0)]) # Red "n3": 200, # Tail moves 200 LEDs/second
p.select("circle") "n4": 0, # Min length 0 LEDs
"colors": [(255, 0, 0)] # Red
})
p.select("circle1")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
# Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0) # Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)
print("Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)") print("Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)")
p.set_param("n1", 20) p.add("circle2", {
p.set_param("n2", 50) "pattern": "circle",
p.set_param("n3", 100) "n1": 20,
p.set_param("n4", 0) "n2": 50,
p.set_param("cl", [(0, 255, 0)]) # Green "n3": 100,
p.select("circle") "n4": 0,
"colors": [(0, 255, 0)] # Green
})
p.select("circle2")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
# Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0) # Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)
print("Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)") print("Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)")
p.set_param("n1", 100) p.add("circle3", {
p.set_param("n2", 30) "pattern": "circle",
p.set_param("n3", 20) "n1": 100,
p.set_param("n4", 0) "n2": 30,
p.set_param("cl", [(0, 0, 255)]) # Blue "n3": 20,
p.select("circle") "n4": 0,
"colors": [(0, 0, 255)] # Blue
})
p.select("circle3")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
# Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10) # Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)
print("Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)") print("Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)")
p.set_param("n1", 50) p.add("circle4", {
p.set_param("n2", 40) "pattern": "circle",
p.set_param("n3", 100) "n1": 50,
p.set_param("n4", 10) "n2": 40,
p.set_param("cl", [(255, 255, 0)]) # Yellow "n3": 100,
p.select("circle") "n4": 10,
"colors": [(255, 255, 0)] # Yellow
})
p.select("circle4")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
# Test 5: Very fast (n1=200, n2=20, n3=200, n4=0) # Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)
print("Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)") print("Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)")
p.set_param("n1", 200) p.add("circle5", {
p.set_param("n2", 20) "pattern": "circle",
p.set_param("n3", 200) "n1": 200,
p.set_param("n4", 0) "n2": 20,
p.set_param("cl", [(255, 0, 255)]) # Magenta "n3": 200,
p.select("circle") "n4": 0,
"colors": [(255, 0, 255)] # Magenta
})
p.select("circle5")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Test 6: Very slow (n1=10, n2=25, n3=10, n4=0) # Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)
print("Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)") print("Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)")
p.set_param("n1", 10) p.add("circle6", {
p.set_param("n2", 25) "pattern": "circle",
p.set_param("n3", 10) "n1": 10,
p.set_param("n4", 0) "n2": 25,
p.set_param("cl", [(0, 255, 255)]) # Cyan "n3": 10,
p.select("circle") "n4": 0,
"colors": [(0, 255, 255)] # Cyan
})
p.select("circle6")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.select("off") p.add("cleanup_off", {"pattern": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100) run_for(p, wdt, 100)

View File

@@ -12,7 +12,10 @@ def main():
p = Patterns(pin=pin, num_leds=num) p = Patterns(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
p.select("off")
# Create an "off" preset
p.add("test_off", {"pattern": "off"})
p.select("test_off")
start = utime.ticks_ms() start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 200: while utime.ticks_diff(utime.ticks_ms(), start) < 200:

View File

@@ -12,12 +12,18 @@ def main():
p = Patterns(pin=pin, num_leds=num) p = Patterns(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
p.set_param("br", 64)
p.set_param("dl", 120) # Create presets for on and off
p.set_param("cl", [(255, 0, 0), (0, 0, 255)]) p.add("test_on", {
"pattern": "on",
"brightness": 64,
"delay": 120,
"colors": [(255, 0, 0), (0, 0, 255)]
})
p.add("test_off", {"pattern": "off"})
# ON phase # ON phase
p.select("on") p.select("test_on")
start = utime.ticks_ms() start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 800: while utime.ticks_diff(utime.ticks_ms(), start) < 800:
wdt.feed() wdt.feed()
@@ -25,7 +31,7 @@ def main():
utime.sleep_ms(10) utime.sleep_ms(10)
# OFF phase # OFF phase
p.select("off") p.select("test_off")
start = utime.ticks_ms() start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 100: while utime.ticks_diff(utime.ticks_ms(), start) < 100:
wdt.feed() wdt.feed()

View File

@@ -24,52 +24,65 @@ def main():
# Test 1: Simple single-color pulse # Test 1: Simple single-color pulse
print("Test 1: Single-color pulse (attack=500, hold=500, decay=500, delay=500)") print("Test 1: Single-color pulse (attack=500, hold=500, decay=500, delay=500)")
p.set_param("br", 255) p.add("pulse1", {
p.set_param("cl", [(255, 0, 0)]) # Red "pattern": "pulse",
p.set_param("n1", 500) # attack ms "brightness": 255,
p.set_param("n2", 500) # hold ms "colors": [(255, 0, 0)],
p.set_param("n3", 500) # decay ms "n1": 500, # attack ms
p.set_param("dl", 500) # delay ms between pulses "n2": 500, # hold ms
p.set_param("auto", True) "n3": 500, # decay ms
p.select("pulse") "delay": 500, # delay ms between pulses
"auto": True
})
p.select("pulse1")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
# Test 2: Faster pulse # Test 2: Faster pulse
print("Test 2: Fast pulse (attack=100, hold=100, decay=100, delay=100)") print("Test 2: Fast pulse (attack=100, hold=100, decay=100, delay=100)")
p.set_param("n1", 100) p.add("pulse2", {
p.set_param("n2", 100) "pattern": "pulse",
p.set_param("n3", 100) "n1": 100,
p.set_param("dl", 100) "n2": 100,
p.set_param("cl", [(0, 255, 0)]) # Green "n3": 100,
p.select("pulse") "delay": 100,
"colors": [(0, 255, 0)]
})
p.select("pulse2")
run_for(p, wdt, 4000) run_for(p, wdt, 4000)
# Test 3: Multi-color pulse cycle # Test 3: Multi-color pulse cycle
print("Test 3: Multi-color pulse (red -> green -> blue)") print("Test 3: Multi-color pulse (red -> green -> blue)")
p.set_param("n1", 300) p.add("pulse3", {
p.set_param("n2", 300) "pattern": "pulse",
p.set_param("n3", 300) "n1": 300,
p.set_param("dl", 200) "n2": 300,
p.set_param("cl", [(255, 0, 0), (0, 255, 0), (0, 0, 255)]) "n3": 300,
p.set_param("auto", True) "delay": 200,
p.select("pulse") "colors": [(255, 0, 0), (0, 255, 0), (0, 0, 255)],
"auto": True
})
p.select("pulse3")
run_for(p, wdt, 6000) run_for(p, wdt, 6000)
# Test 4: One-shot pulse (auto=False) # Test 4: One-shot pulse (auto=False)
print("Test 4: Single pulse, auto=False") print("Test 4: Single pulse, auto=False")
p.set_param("n1", 400) p.add("pulse4", {
p.set_param("n2", 0) "pattern": "pulse",
p.set_param("n3", 400) "n1": 400,
p.set_param("dl", 0) "n2": 0,
p.set_param("cl", [(255, 255, 255)]) "n3": 400,
p.set_param("auto", False) "delay": 0,
p.select("pulse") "colors": [(255, 255, 255)],
"auto": False
})
p.select("pulse4")
# Run long enough to allow one full pulse cycle # Run long enough to allow one full pulse cycle
run_for(p, wdt, 1500) run_for(p, wdt, 1500)
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.select("off") p.add("cleanup_off", {"pattern": "off"})
p.select("cleanup_off")
run_for(p, wdt, 200) run_for(p, wdt, 200)

View File

@@ -24,47 +24,62 @@ def main():
# Test 1: Basic rainbow with auto=True (continuous) # Test 1: Basic rainbow with auto=True (continuous)
print("Test 1: Basic rainbow (auto=True, n1=1)") print("Test 1: Basic rainbow (auto=True, n1=1)")
p.set_param("br", 255) p.add("rainbow1", {
p.set_param("dl", 100) # Delay affects animation speed "pattern": "rainbow",
p.set_param("n1", 1) # Step increment of 1 "brightness": 255,
p.set_param("auto", True) "delay": 100,
p.select("rainbow") "n1": 1,
"auto": True
})
p.select("rainbow1")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Test 2: Fast rainbow # Test 2: Fast rainbow
print("Test 2: Fast rainbow (low delay, n1=1)") print("Test 2: Fast rainbow (low delay, n1=1)")
p.set_param("dl", 50) p.add("rainbow2", {
p.set_param("n1", 1) "pattern": "rainbow",
p.set_param("auto", True) "delay": 50,
p.select("rainbow") "n1": 1,
"auto": True
})
p.select("rainbow2")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
# Test 3: Slow rainbow # Test 3: Slow rainbow
print("Test 3: Slow rainbow (high delay, n1=1)") print("Test 3: Slow rainbow (high delay, n1=1)")
p.set_param("dl", 500) p.add("rainbow3", {
p.set_param("n1", 1) "pattern": "rainbow",
p.set_param("auto", True) "delay": 500,
p.select("rainbow") "n1": 1,
"auto": True
})
p.select("rainbow3")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Test 4: Low brightness rainbow # Test 4: Low brightness rainbow
print("Test 4: Low brightness rainbow (n1=1)") print("Test 4: Low brightness rainbow (n1=1)")
p.set_param("br", 64) p.add("rainbow4", {
p.set_param("dl", 100) "pattern": "rainbow",
p.set_param("n1", 1) "brightness": 64,
p.set_param("auto", True) "delay": 100,
p.select("rainbow") "n1": 1,
"auto": True
})
p.select("rainbow4")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
# Test 5: Single-step rainbow (auto=False) # Test 5: Single-step rainbow (auto=False)
print("Test 5: Single-step rainbow (auto=False, n1=1)") print("Test 5: Single-step rainbow (auto=False, n1=1)")
p.set_param("br", 255) p.add("rainbow5", {
p.set_param("dl", 100) "pattern": "rainbow",
p.set_param("n1", 1) "brightness": 255,
p.set_param("auto", False) "delay": 100,
"n1": 1,
"auto": False
})
p.step = 0 p.step = 0
for i in range(10): for i in range(10):
p.select("rainbow") p.select("rainbow5")
# One tick advances the generator one frame when auto=False # One tick advances the generator one frame when auto=False
p.tick() p.tick()
utime.sleep_ms(100) utime.sleep_ms(100)
@@ -72,37 +87,49 @@ def main():
# Test 6: Verify step updates correctly # Test 6: Verify step updates correctly
print("Test 6: Verify step updates (auto=False, n1=1)") print("Test 6: Verify step updates (auto=False, n1=1)")
p.set_param("n1", 1) p.add("rainbow6", {
p.set_param("auto", False) "pattern": "rainbow",
"n1": 1,
"auto": False
})
initial_step = p.step initial_step = p.step
p.select("rainbow") p.select("rainbow6")
p.tick() p.tick()
final_step = p.step final_step = p.step
print(f"Step updated from {initial_step} to {final_step} (expected increment: 1)") print(f"Step updated from {initial_step} to {final_step} (expected increment: 1)")
# Test 7: Fast step increment (n1=5) # Test 7: Fast step increment (n1=5)
print("Test 7: Fast rainbow (n1=5, auto=True)") print("Test 7: Fast rainbow (n1=5, auto=True)")
p.set_param("br", 255) p.add("rainbow7", {
p.set_param("dl", 100) "pattern": "rainbow",
p.set_param("n1", 5) "brightness": 255,
p.set_param("auto", True) "delay": 100,
p.select("rainbow") "n1": 5,
"auto": True
})
p.select("rainbow7")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
# Test 8: Very fast step increment (n1=10) # Test 8: Very fast step increment (n1=10)
print("Test 8: Very fast rainbow (n1=10, auto=True)") print("Test 8: Very fast rainbow (n1=10, auto=True)")
p.set_param("n1", 10) p.add("rainbow8", {
p.set_param("auto", True) "pattern": "rainbow",
p.select("rainbow") "n1": 10,
"auto": True
})
p.select("rainbow8")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
# Test 9: Verify n1 controls step increment (auto=False) # Test 9: Verify n1 controls step increment (auto=False)
print("Test 9: Verify n1 step increment (auto=False, n1=5)") print("Test 9: Verify n1 step increment (auto=False, n1=5)")
p.set_param("n1", 5) p.add("rainbow9", {
p.set_param("auto", False) "pattern": "rainbow",
"n1": 5,
"auto": False
})
p.step = 0 p.step = 0
initial_step = p.step initial_step = p.step
p.select("rainbow") p.select("rainbow9")
p.tick() p.tick()
final_step = p.step final_step = p.step
expected_step = (initial_step + 5) % 256 expected_step = (initial_step + 5) % 256
@@ -114,7 +141,8 @@ def main():
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.select("off") p.add("cleanup_off", {"pattern": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100) run_for(p, wdt, 100)

View File

@@ -24,41 +24,54 @@ def main():
# Test 1: Simple two-color transition # Test 1: Simple two-color transition
print("Test 1: Two-color transition (red <-> blue, delay=1000)") print("Test 1: Two-color transition (red <-> blue, delay=1000)")
p.set_param("br", 255) p.add("transition1", {
p.set_param("dl", 1000) # transition duration "pattern": "transition",
p.set_param("cl", [(255, 0, 0), (0, 0, 255)]) "brightness": 255,
p.set_param("auto", True) "delay": 1000, # transition duration
p.select("transition") "colors": [(255, 0, 0), (0, 0, 255)],
"auto": True
})
p.select("transition1")
run_for(p, wdt, 6000) run_for(p, wdt, 6000)
# Test 2: Multi-color transition # Test 2: Multi-color transition
print("Test 2: Multi-color transition (red -> green -> blue -> white)") print("Test 2: Multi-color transition (red -> green -> blue -> white)")
p.set_param("dl", 800) p.add("transition2", {
p.set_param("cl", [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)]) "pattern": "transition",
p.set_param("auto", True) "delay": 800,
p.select("transition") "colors": [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)],
"auto": True
})
p.select("transition2")
run_for(p, wdt, 8000) run_for(p, wdt, 8000)
# Test 3: One-shot transition (auto=False) # Test 3: One-shot transition (auto=False)
print("Test 3: One-shot transition (auto=False)") print("Test 3: One-shot transition (auto=False)")
p.set_param("dl", 1000) p.add("transition3", {
p.set_param("cl", [(255, 0, 0), (0, 255, 0)]) "pattern": "transition",
p.set_param("auto", False) "delay": 1000,
p.select("transition") "colors": [(255, 0, 0), (0, 255, 0)],
"auto": False
})
p.select("transition3")
# Run long enough for a single transition step # Run long enough for a single transition step
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
# Test 4: Single-color behavior (should just stay on) # Test 4: Single-color behavior (should just stay on)
print("Test 4: Single-color transition (should hold color)") print("Test 4: Single-color transition (should hold color)")
p.set_param("cl", [(0, 0, 255)]) p.add("transition4", {
p.set_param("dl", 500) "pattern": "transition",
p.set_param("auto", True) "colors": [(0, 0, 255)],
p.select("transition") "delay": 500,
"auto": True
})
p.select("transition4")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.select("off") p.add("cleanup_off", {"pattern": "off"})
p.select("cleanup_off")
run_for(p, wdt, 200) run_for(p, wdt, 200)

734
tool.py
View File

@@ -1,734 +0,0 @@
#!/usr/bin/env python3
"""
LED Bar Configuration Web App
Flask-based web UI for downloading, editing, and uploading settings.json
to/from MicroPython devices via mpremote.
"""
import json
import tempfile
import subprocess
import os
from pathlib import Path
from flask import (
Flask,
render_template_string,
request,
redirect,
url_for,
flash,
)
app = Flask(__name__)
app.secret_key = "change-me-in-production"
SETTINGS_CONFIG = [
("led_pin", "LED Pin", "number"),
("num_leds", "Number of LEDs", "number"),
("color_order", "Color Order", "choice", ["rgb", "rbg", "grb", "gbr", "brg", "bgr"]),
("name", "Device Name", "text"),
("pattern", "Pattern", "text"),
("delay", "Delay (ms)", "number"),
("brightness", "Brightness", "number"),
("n1", "N1", "number"),
("n2", "N2", "number"),
("n3", "N3", "number"),
("n4", "N4", "number"),
("n5", "N5", "number"),
("n6", "N6", "number"),
("ap_password", "AP Password", "text"),
("id", "ID", "number"),
("debug", "Debug Mode", "choice", ["True", "False"]),
]
def _run_mpremote_copy(from_device: bool, device: str, temp_path: str) -> None:
if from_device:
cmd = ["mpremote", "connect", device, "cp", ":/settings.json", temp_path]
else:
cmd = ["mpremote", "connect", device, "cp", temp_path, ":/settings.json"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
raise RuntimeError(f"mpremote error: {result.stderr.strip() or result.stdout.strip()}")
def download_settings(device: str) -> dict:
"""Download settings.json from the device using mpremote."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
temp_path = temp_file.name
temp_file.close()
try:
_run_mpremote_copy(from_device=True, device=device, temp_path=temp_path)
with open(temp_path, "r", encoding="utf-8") as f:
return json.load(f)
finally:
if os.path.exists(temp_path):
try:
os.unlink(temp_path)
except OSError:
pass
def upload_settings(device: str, settings: dict) -> None:
"""Upload settings.json to the device using mpremote and reset device."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
temp_path = temp_file.name
try:
json.dump(settings, temp_file, indent=2)
temp_file.close()
_run_mpremote_copy(from_device=False, device=device, temp_path=temp_path)
# Reset device (best effort)
try:
import serial # type: ignore
with serial.Serial(device, baudrate=115200) as ser:
ser.write(b"\x03\x03\x04")
except Exception:
reset_cmd = [
"mpremote",
"connect",
device,
"exec",
"import machine; machine.reset()",
]
try:
subprocess.run(reset_cmd, capture_output=True, text=True, timeout=5)
except subprocess.TimeoutExpired:
pass
finally:
if os.path.exists(temp_path):
try:
os.unlink(temp_path)
except OSError:
pass
def parse_settings_from_form(form) -> dict:
settings = {}
for cfg in SETTINGS_CONFIG:
key = cfg[0]
raw = (form.get(key) or "").strip()
if raw == "":
continue
if key in ["led_pin", "num_leds", "delay", "brightness", "id", "n1", "n2", "n3", "n4", "n5", "n6"]:
try:
settings[key] = int(raw)
except ValueError:
settings[key] = raw
elif key == "debug":
settings[key] = raw == "True"
else:
settings[key] = raw
return settings
TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>LED Bar Configuration</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
color-scheme: dark;
--bg: #0b1020;
--bg-alt: #141b2f;
--accent: #3b82f6;
--accent-soft: rgba(59, 130, 246, 0.15);
--border: #1f2937;
--text: #e5e7eb;
--muted: #9ca3af;
--danger: #f97373;
--radius-lg: 14px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top, #1f2937 0, #020617 55%);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.shell {
width: 100%;
max-width: 960px;
background: linear-gradient(145deg, #020617 0, #020617 40%, #030712 100%);
border-radius: 24px;
border: 1px solid rgba(148, 163, 184, 0.25);
box-shadow:
0 0 0 1px rgba(15, 23, 42, 0.9),
0 45px 80px rgba(15, 23, 42, 0.95),
0 0 80px rgba(37, 99, 235, 0.3);
overflow: hidden;
}
header {
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
background: radial-gradient(circle at top left, rgba(37, 99, 235, 0.4), transparent 55%);
}
header h1 {
font-size: 1.15rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
header h1 span.badge {
font-size: 0.65rem;
padding: 0.15rem 0.45rem;
border-radius: 999px;
border: 1px solid rgba(96, 165, 250, 0.6);
background: rgba(37, 99, 235, 0.15);
color: #bfdbfe;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.chip-row {
display: flex;
gap: 0.6rem;
align-items: center;
font-size: 0.7rem;
color: var(--muted);
}
.chip {
padding: 0.15rem 0.6rem;
border-radius: 999px;
border: 1px solid rgba(55, 65, 81, 0.9);
background: rgba(15, 23, 42, 0.9);
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
main {
padding: 1.25rem 1.5rem 1.5rem;
display: grid;
grid-template-columns: minmax(0, 2.2fr) minmax(0, 1.2fr);
gap: 1rem;
}
@media (max-width: 800px) {
main {
grid-template-columns: minmax(0, 1fr);
}
}
.card {
background: radial-gradient(circle at top left, rgba(37, 99, 235, 0.18), transparent 55%);
border-radius: var(--radius-lg);
border: 1px solid rgba(31, 41, 55, 0.95);
padding: 1rem;
position: relative;
overflow: hidden;
}
.card::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at 0 0, rgba(59, 130, 246, 0.4), transparent 55%),
radial-gradient(circle at 100% 0, rgba(236, 72, 153, 0.28), transparent 55%);
opacity: 0.55;
mix-blend-mode: screen;
}
.card > * { position: relative; z-index: 1; }
.card-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.75rem;
}
.card-header h2 {
font-size: 0.9rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #cbd5f5;
}
.card-header span.sub {
font-size: 0.7rem;
color: var(--muted);
}
.field-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.6rem 0.75rem;
}
label {
display: block;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 0.2rem;
}
input, select {
width: 100%;
padding: 0.4rem 0.55rem;
border-radius: 999px;
border: 1px solid rgba(31, 41, 55, 0.95);
background: rgba(15, 23, 42, 0.92);
color: var(--text);
font-size: 0.8rem;
outline: none;
transition: border-color 0.14s ease, box-shadow 0.14s ease, background 0.14s ease;
}
input:focus, select:focus {
border-color: rgba(59, 130, 246, 0.95);
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.65), 0 0 25px rgba(37, 99, 235, 0.6);
background: rgba(15, 23, 42, 0.98);
}
.device-row {
display: flex;
gap: 0.55rem;
margin-top: 0.2rem;
}
.device-row input {
flex: 1;
border-radius: 999px;
}
.btn {
border-radius: 999px;
border: 1px solid transparent;
padding: 0.4rem 0.9rem;
font-size: 0.78rem;
font-weight: 500;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
cursor: pointer;
background: linear-gradient(135deg, #2563eb, #4f46e5);
color: white;
box-shadow:
0 10px 25px rgba(37, 99, 235, 0.55),
0 0 0 1px rgba(15, 23, 42, 0.95);
white-space: nowrap;
transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease, opacity 0.1s ease;
}
.btn-secondary {
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.95), rgba(17, 24, 39, 0.98));
border-color: rgba(55, 65, 81, 0.9);
color: var(--text);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.85);
}
.btn-ghost {
background: transparent;
border-color: rgba(55, 65, 81, 0.8);
color: var(--muted);
box-shadow: none;
}
.btn:hover {
transform: translateY(-1px);
box-shadow:
0 20px 40px rgba(37, 99, 235, 0.75),
0 0 0 1px rgba(191, 219, 254, 0.45);
opacity: 0.97;
}
.btn:active {
transform: translateY(0);
box-shadow:
0 10px 20px rgba(15, 23, 42, 0.9),
0 0 0 1px rgba(30, 64, 175, 0.9);
}
.btn-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.9rem;
}
.status {
margin-top: 0.65rem;
font-size: 0.75rem;
color: var(--muted);
display: flex;
align-items: center;
gap: 0.45rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: #22c55e;
box-shadow: 0 0 14px rgba(34, 197, 94, 0.95);
}
.status.error .status-dot {
background: var(--danger);
box-shadow: 0 0 14px rgba(248, 113, 113, 0.95);
}
.flash-container {
position: fixed;
right: 1.4rem;
bottom: 1.4rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 320px;
z-index: 40;
}
.flash {
padding: 0.55rem 0.75rem;
border-radius: 12px;
font-size: 0.78rem;
backdrop-filter: blur(18px);
background: radial-gradient(circle at top left, rgba(37, 99, 235, 0.8), rgba(15, 23, 42, 0.96));
border: 1px solid rgba(96, 165, 250, 0.8);
color: #e5f0ff;
box-shadow:
0 22px 40px rgba(15, 23, 42, 0.95),
0 0 30px rgba(37, 99, 235, 0.7);
}
.flash.error {
background: radial-gradient(circle at top left, rgba(248, 113, 113, 0.85), rgba(15, 23, 42, 0.96));
border-color: rgba(248, 113, 113, 0.8);
}
.flash small {
display: block;
color: rgba(226, 232, 240, 0.8);
margin-top: 0.15rem;
}
.pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
border: 1px solid rgba(55, 65, 81, 0.9);
font-size: 0.7rem;
color: var(--muted);
margin-top: 0.3rem;
}
</style>
</head>
<body>
<div class="shell">
<header>
<div>
<h1>
<span>LED Bar Configuration</span>
<span class="badge">Web Console</span>
</h1>
<div class="chip-row">
<span class="chip">
<span style="width: 6px; height: 6px; border-radius: 999px; background: #22c55e; box-shadow: 0 0 12px rgba(34, 197, 94, 0.95);"></span>
<span>Raspberry Pi · MicroPython</span>
</span>
<span class="chip">settings.json live editor</span>
</div>
</div>
<div class="chip-row">
<span class="chip">Device: {{ device or "/dev/ttyACM0" }}</span>
</div>
</header>
<main>
<section class="card">
<div class="card-header">
<div>
<h2>Device Connection</h2>
<span class="sub">Connect to your MicroPython LED controller and sync configuration</span>
</div>
</div>
<form method="post" action="{{ url_for('handle_action') }}">
<label for="device">Serial / mpremote device</label>
<div class="device-row">
<input
id="device"
name="device"
type="text"
value="{{ device or '/dev/ttyACM0' }}"
placeholder="/dev/ttyACM0"
required
/>
<button class="btn" type="submit" name="action" value="download">
⬇ Download
</button>
<button class="btn btn-secondary" type="submit" name="action" value="upload">
⬆ Upload
</button>
</div>
<div class="status {% if status_type == 'error' %}error{% endif %}">
<span class="status-dot"></span>
<span>{{ status or "Ready" }}</span>
</div>
<div class="pill">
<span>Tip:</span>
<span>Download from device → tweak parameters → Upload and reboot.</span>
</div>
<hr style="border: none; border-top: 1px solid rgba(31, 41, 55, 0.9); margin: 0.9rem 0 0.7rem;" />
<div class="card-header">
<div>
<h2>LED Settings</h2>
<span class="sub">Edit all fields before uploading back to your controller</span>
</div>
</div>
<div class="field-grid">
{% for field in settings_config %}
{% set key, label, field_type = field[0], field[1], field[2] %}
<div>
<label for="{{ key }}">{{ label }}</label>
{% if field_type == 'choice' %}
{% set choices = field[3] %}
<select id="{{ key }}" name="{{ key }}">
<option value=""></option>
{% for choice in choices %}
{% if key == 'debug' %}
{% set selected = 'selected' if (settings.get(key) is sameas true and choice == 'True') or (settings.get(key) is sameas false and choice == 'False') else '' %}
{% else %}
{% set selected = 'selected' if settings.get(key) == choice else '' %}
{% endif %}
<option value="{{ choice }}" {{ selected }}>{{ choice }}</option>
{% endfor %}
</select>
{% else %}
<input
id="{{ key }}"
name="{{ key }}"
type="text"
value="{{ settings.get(key, '') }}"
/>
{% endif %}
</div>
{% endfor %}
</div>
<div class="btn-row">
<button class="btn btn-secondary" type="submit" name="action" value="clear">
Reset form
</button>
<button class="btn btn-ghost" type="submit" name="action" value="from_json">
Paste JSON…
</button>
</div>
</form>
</section>
<section class="card">
<div class="card-header">
<div>
<h2>Raw JSON</h2>
<span class="sub">For advanced editing, paste or copy the full settings.json</span>
</div>
</div>
<form method="post" action="{{ url_for('handle_action') }}">
<input type="hidden" name="device" value="{{ device or '/dev/ttyACM0' }}" />
<label for="raw_json">settings.json</label>
<textarea
id="raw_json"
name="raw_json"
rows="16"
style="
width: 100%;
resize: vertical;
padding: 0.65rem 0.75rem;
border-radius: 12px;
border: 1px solid rgba(31, 41, 55, 0.95);
background: rgba(15, 23, 42, 0.96);
color: var(--text);
font-size: 0.78rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
outline: none;
"
>{{ raw_json }}</textarea>
<div class="btn-row" style="margin-top: 0.75rem;">
<button class="btn btn-secondary" type="submit" name="action" value="to_form">
Use JSON for form
</button>
<button class="btn btn-ghost" type="submit" name="action" value="pretty">
Pretty-print
</button>
</div>
</form>
</section>
</main>
</div>
<div class="flash-container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {% if category == 'error' %}error{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</body>
</html>
"""
@app.route("/", methods=["GET"])
def index():
return render_template_string(
TEMPLATE,
device="/dev/ttyACM0",
settings={},
settings_config=SETTINGS_CONFIG,
status="Ready",
status_type="ok",
raw_json="{}",
)
@app.route("/", methods=["POST"])
def handle_action():
action = request.form.get("action") or ""
device = (request.form.get("device") or "/dev/ttyACM0").strip()
raw_json = (request.form.get("raw_json") or "").strip()
settings = {}
status = "Ready"
status_type = "ok"
if action == "download":
if not device:
flash("Please specify a device.", "error")
status, status_type = "Missing device.", "error"
else:
try:
settings = download_settings(device)
raw_json = json.dumps(settings, indent=2)
flash(f"Settings downloaded from {device}.", "success")
status = f"Settings downloaded from {device}"
except subprocess.TimeoutExpired:
flash("Connection timeout. Check device connection.", "error")
status, status_type = "Connection timeout.", "error"
except FileNotFoundError:
flash("mpremote not found. Install with: pip install mpremote", "error")
status, status_type = "mpremote not found.", "error"
except Exception as exc: # pylint: disable=broad-except
flash(f"Failed to download settings: {exc}", "error")
status, status_type = "Download failed.", "error"
elif action == "upload":
if not device:
flash("Please specify a device.", "error")
status, status_type = "Missing device.", "error"
else:
# Take current form fields as source of truth, falling back to JSON if present
if raw_json:
try:
settings = json.loads(raw_json)
except json.JSONDecodeError:
flash("Raw JSON is invalid; using form values instead.", "error")
settings = {}
form_settings = parse_settings_from_form(request.form)
settings.update(form_settings)
if not settings:
flash("No settings to upload. Download or provide settings first.", "error")
status, status_type = "No settings to upload.", "error"
else:
try:
upload_settings(device, settings)
raw_json = json.dumps(settings, indent=2)
flash(f"Settings uploaded and device reset on {device}.", "success")
status = f"Settings uploaded and device reset on {device}"
except subprocess.TimeoutExpired:
flash("Connection timeout. Check device connection.", "error")
status, status_type = "Connection timeout.", "error"
except FileNotFoundError:
flash("mpremote not found. Install with: pip install mpremote", "error")
status, status_type = "mpremote not found.", "error"
except Exception as exc: # pylint: disable=broad-except
flash(f"Failed to upload settings: {exc}", "error")
status, status_type = "Upload failed.", "error"
elif action == "from_json":
# No-op here, JSON is just edited in the side panel
form_settings = parse_settings_from_form(request.form)
settings.update(form_settings)
if raw_json:
try:
settings.update(json.loads(raw_json))
flash("JSON merged into form values.", "success")
status = "JSON merged into form."
except json.JSONDecodeError:
flash("Invalid JSON; keeping previous form values.", "error")
status, status_type = "JSON parse error.", "error"
elif action == "to_form":
if raw_json:
try:
settings = json.loads(raw_json)
flash("Form fields updated from JSON.", "success")
status = "Form fields updated from JSON."
except json.JSONDecodeError:
flash("Invalid JSON; could not update form fields.", "error")
status, status_type = "JSON parse error.", "error"
elif action == "pretty":
if raw_json:
try:
parsed = json.loads(raw_json)
raw_json = json.dumps(parsed, indent=2)
settings = parsed if isinstance(parsed, dict) else {}
flash("JSON pretty-printed.", "success")
status = "JSON pretty-printed."
except json.JSONDecodeError:
flash("Invalid JSON; cannot pretty-print.", "error")
status, status_type = "JSON parse error.", "error"
elif action == "clear":
settings = {}
raw_json = "{}"
flash("Form cleared.", "success")
status = "Form cleared."
else:
# Unknown / initial action: just reflect form values back
settings = parse_settings_from_form(request.form)
if raw_json and not settings:
try:
settings = json.loads(raw_json)
except json.JSONDecodeError:
pass
return render_template_string(
TEMPLATE,
device=device,
settings=settings,
settings_config=SETTINGS_CONFIG,
status=status,
status_type=status_type,
raw_json=raw_json or json.dumps(settings or {}, indent=2),
)
def main() -> None:
# Bind to all interfaces so you can reach it from your LAN:
# python web_app.py
# Then open: http://<pi-ip>:5000/
app.run(host="0.0.0.0", port=5000, debug=False)
if __name__ == "__main__":
main()