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]
dev = 'watchfiles "./dev.py /dev/ttyACM0 src reset follow"'
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": {}
}

165
dev.py
View File

@@ -3,130 +3,51 @@
import subprocess
import serial
import sys
import glob
def upload_src(port):
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":"], cwd="src")
print(sys.argv)
def upload_lib(port):
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":"])
# 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]
def list_files(port):
subprocess.call(["mpremote", "connect", port, "fs", "ls", ":"])
def reset_device(port):
with serial.Serial(port, baudrate=115200) as ser:
ser.write(b'\x03\x03\x04')
def follow_serial(port):
with serial.Serial(port, baudrate=115200) as ser:
while True:
if ser.in_waiting > 0:
data = ser.readline().decode('utf-8').strip()
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
for cmd in sys.argv[1:]:
print(cmd)
match cmd:
case "src":
if port:
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":" ], cwd="src")
else:
print(f"Error: {arg} requires a port argument")
sys.exit(1)
elif arg in ["-s", "--src"]:
commands.append(("src", upload_src))
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:
print(f"Error: Unknown argument: {arg}")
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()
print("Error: Port required for 'src' command")
case "lib":
if port:
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":" ])
else:
print("Error: Port required for 'lib' command")
case "ls":
if port:
subprocess.call(["mpremote", "connect", port, "fs", "ls", ":" ])
else:
print("Error: Port required for 'ls' command")
case "reset":
if port:
with serial.Serial(port, baudrate=115200) as ser:
ser.write(b'\x03\x03\x04')
else:
print("Error: Port required for 'reset' command")
case "follow":
if port:
with serial.Serial(port, baudrate=115200) as ser:
while True:
if ser.in_waiting > 0: # Check if there is data in the buffer
data = ser.readline().decode('utf-8').strip() # Read and decode the data
print(data)
else:
print("Error: Port required for 'follow' command")
case "db":
if port:
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "db", ":" ])
else:
print("Error: Port required for 'db' command")

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

View File

@@ -24,6 +24,31 @@ param_mapping = {
"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:
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)
@@ -46,8 +71,27 @@ class Patterns_Base:
self.n6 = 0
self.generator = None
self.presets = {}
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):
if self.generator is None:
@@ -57,13 +101,14 @@ class Patterns_Base:
except StopIteration:
self.generator = None
def select(self, pattern):
if pattern in self.patterns:
self.selected = pattern
self.generator = self.patterns[pattern]()
print(f"Selected pattern: {pattern}")
return True
# If pattern doesn't exist, default to "off"
def select(self, preset_name):
if preset_name in self.presets:
preset = self.presets[preset_name]
if preset.pattern in self.patterns:
self.generator = self.patterns[preset.pattern](preset)
self.selected = preset_name # Store the preset name, not the object
return True
# If preset doesn't exist or pattern not found, default to "off"
return False
def set_param(self, key, value):
@@ -108,11 +153,13 @@ class Patterns_Base:
self.n[i] = fill_color
self.n.write()
def off(self):
def off(self, preset=None):
self.fill((0, 0, 0))
def on(self):
self.fill(self.apply_brightness(self.colors[0]))
def on(self, preset):
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
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)
wdt = WDT(timeout=10000)
p.set_param("br", 64)
p.set_param("dl", 200)
p.set_param("cl", [(255, 0, 0), (0, 0, 255)])
p.select("blink")
# Create blink preset
p.add("test_blink", {
"pattern": "blink",
"brightness": 64,
"delay": 200,
"colors": [(255, 0, 0), (0, 0, 255)]
})
p.select("test_blink")
start = utime.ticks_ms()
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)
print("Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)")
p.set_param("br", 255)
p.set_param("dl", 200)
p.set_param("n1", 5)
p.set_param("n2", 5)
p.set_param("n3", 1)
p.set_param("n4", 1)
p.set_param("cl", [(255, 0, 0), (0, 255, 0)])
p.select("chase")
p.add("chase1", {
"pattern": "chase",
"brightness": 255,
"delay": 200,
"n1": 5,
"n2": 5,
"n3": 1,
"n4": 1,
"colors": [(255, 0, 0), (0, 255, 0)]
})
p.select("chase1")
run_for(p, wdt, 3000)
# 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.set_param("n2", 3)
p.set_param("n3", 2)
p.set_param("n4", -1)
p.set_param("dl", 150)
p.set_param("cl", [(0, 0, 255), (255, 255, 0)])
p.select("chase")
p.add("chase2", {
"pattern": "chase",
"n1": 3,
"n2": 3,
"n3": 2,
"n4": -1,
"delay": 150,
"colors": [(0, 0, 255), (255, 255, 0)]
})
p.select("chase2")
run_for(p, wdt, 3000)
# Test 3: Large segments (n1=10, n2=5)
print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)")
p.set_param("n1", 10)
p.set_param("n2", 5)
p.set_param("n3", 3)
p.set_param("n4", 3)
p.set_param("dl", 200)
p.set_param("cl", [(255, 128, 0), (128, 0, 255)])
p.select("chase")
p.add("chase3", {
"pattern": "chase",
"n1": 10,
"n2": 5,
"n3": 3,
"n4": 3,
"delay": 200,
"colors": [(255, 128, 0), (128, 0, 255)]
})
p.select("chase3")
run_for(p, wdt, 3000)
# Test 4: Fast movement (n3=5, n4=5)
print("Test 4: Fast movement (n3=5, n4=5)")
p.set_param("n1", 4)
p.set_param("n2", 4)
p.set_param("n3", 5)
p.set_param("n4", 5)
p.set_param("dl", 100)
p.set_param("cl", [(255, 0, 255), (0, 255, 255)])
p.select("chase")
p.add("chase4", {
"pattern": "chase",
"n1": 4,
"n2": 4,
"n3": 5,
"n4": 5,
"delay": 100,
"colors": [(255, 0, 255), (0, 255, 255)]
})
p.select("chase4")
run_for(p, wdt, 2000)
# Test 5: Backward movement (n3=-2, n4=-2)
print("Test 5: Backward movement (n3=-2, n4=-2)")
p.set_param("n1", 6)
p.set_param("n2", 4)
p.set_param("n3", -2)
p.set_param("n4", -2)
p.set_param("dl", 200)
p.set_param("cl", [(255, 255, 255), (0, 0, 0)])
p.select("chase")
p.add("chase5", {
"pattern": "chase",
"n1": 6,
"n2": 4,
"n3": -2,
"n4": -2,
"delay": 200,
"colors": [(255, 255, 255), (0, 0, 0)]
})
p.select("chase5")
run_for(p, wdt, 3000)
# 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.set_param("n2", 5)
p.set_param("n3", 3)
p.set_param("n4", -2)
p.set_param("dl", 250)
p.set_param("cl", [(255, 0, 0), (0, 255, 0)])
p.select("chase")
p.add("chase6", {
"pattern": "chase",
"n1": 5,
"n2": 5,
"n3": 3,
"n4": -2,
"delay": 250,
"colors": [(255, 0, 0), (0, 255, 0)]
})
p.select("chase6")
run_for(p, wdt, 4000)
# Cleanup
print("Test complete, turning off")
p.select("off")
p.add("cleanup_off", {"pattern": "off"})
p.select("cleanup_off")
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)
print("Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)")
p.set_param("br", 255)
p.set_param("n1", 50) # Head moves 50 LEDs/second
p.set_param("n2", 100) # Max length 100 LEDs
p.set_param("n3", 200) # Tail moves 200 LEDs/second
p.set_param("n4", 0) # Min length 0 LEDs
p.set_param("cl", [(255, 0, 0)]) # Red
p.select("circle")
p.add("circle1", {
"pattern": "circle",
"brightness": 255,
"n1": 50, # Head moves 50 LEDs/second
"n2": 100, # Max length 100 LEDs
"n3": 200, # Tail moves 200 LEDs/second
"n4": 0, # Min length 0 LEDs
"colors": [(255, 0, 0)] # Red
})
p.select("circle1")
run_for(p, wdt, 5000)
# 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.set_param("n2", 50)
p.set_param("n3", 100)
p.set_param("n4", 0)
p.set_param("cl", [(0, 255, 0)]) # Green
p.select("circle")
p.add("circle2", {
"pattern": "circle",
"n1": 20,
"n2": 50,
"n3": 100,
"n4": 0,
"colors": [(0, 255, 0)] # Green
})
p.select("circle2")
run_for(p, wdt, 5000)
# 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.set_param("n2", 30)
p.set_param("n3", 20)
p.set_param("n4", 0)
p.set_param("cl", [(0, 0, 255)]) # Blue
p.select("circle")
p.add("circle3", {
"pattern": "circle",
"n1": 100,
"n2": 30,
"n3": 20,
"n4": 0,
"colors": [(0, 0, 255)] # Blue
})
p.select("circle3")
run_for(p, wdt, 5000)
# 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.set_param("n2", 40)
p.set_param("n3", 100)
p.set_param("n4", 10)
p.set_param("cl", [(255, 255, 0)]) # Yellow
p.select("circle")
p.add("circle4", {
"pattern": "circle",
"n1": 50,
"n2": 40,
"n3": 100,
"n4": 10,
"colors": [(255, 255, 0)] # Yellow
})
p.select("circle4")
run_for(p, wdt, 5000)
# 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.set_param("n2", 20)
p.set_param("n3", 200)
p.set_param("n4", 0)
p.set_param("cl", [(255, 0, 255)]) # Magenta
p.select("circle")
p.add("circle5", {
"pattern": "circle",
"n1": 200,
"n2": 20,
"n3": 200,
"n4": 0,
"colors": [(255, 0, 255)] # Magenta
})
p.select("circle5")
run_for(p, wdt, 3000)
# 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.set_param("n2", 25)
p.set_param("n3", 10)
p.set_param("n4", 0)
p.set_param("cl", [(0, 255, 255)]) # Cyan
p.select("circle")
p.add("circle6", {
"pattern": "circle",
"n1": 10,
"n2": 25,
"n3": 10,
"n4": 0,
"colors": [(0, 255, 255)] # Cyan
})
p.select("circle6")
run_for(p, wdt, 5000)
# Cleanup
print("Test complete, turning off")
p.select("off")
p.add("cleanup_off", {"pattern": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)

View File

@@ -12,7 +12,10 @@ def main():
p = Patterns(pin=pin, num_leds=num)
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()
while utime.ticks_diff(utime.ticks_ms(), start) < 200:

View File

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

View File

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

View File

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

View File

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