78 Commits

Author SHA1 Message Date
af711bfb98 Remove color order 2025-11-30 23:30:27 +13:00
4d4b91e40d Remove color1 and color2, use colors array instead 2025-11-30 23:08:43 +13:00
b864f166cb Update web interface to use colors array instead of color1/color2 2025-11-30 23:03:45 +13:00
05828833e0 Add support for all RGB color order permutations (RGB, RBG, GRB, GBR, BRG, BGR) 2025-11-30 22:43:19 +13:00
ce1db86fb6 Add tool.py with RGB color order options and debug mode setting 2025-11-30 22:42:33 +13:00
43bd90f137 Update tool: add all RGB color order options and debug mode setting 2025-11-30 22:41:06 +13:00
d45846ec74 Update main.py, tests, and add presets, circle test, and tool directory 2025-11-30 18:35:31 +13:00
6ae6189519 Revert "Add support for all RGB color order permutations (RGB, RBG, GRB, GBR, BRG, BGR)"
This reverts commit f53c57f036.
2025-11-30 18:34:32 +13:00
f53c57f036 Add support for all RGB color order permutations (RGB, RBG, GRB, GBR, BRG, BGR) 2025-11-30 18:26:56 +13:00
47837c00e5 Add GRB 2025-11-30 18:15:37 +13:00
251b27133f Remove presets 2025-11-30 17:45:46 +13:00
10b6e5ee89 Update patterns.py 2025-11-30 17:42:06 +13:00
33aee7a3af Update patterns_base.py 2025-11-30 17:41:32 +13:00
501fd37def Update dev.py 2025-11-30 17:40:43 +13:00
fe2717f51d Add circle pattern 2025-11-24 22:07:39 +13:00
a17380c250 Add n values; remove color1, color2 2025-11-20 18:22:29 +13:00
fb51d65077 Add n_chase 2025-11-20 18:18:30 +13:00
ca80f6a3f5 Add n params 2025-11-20 18:17:42 +13:00
66bfc80771 Remove step 2025-11-20 18:15:19 +13:00
3855e76da1 Update to match the new patterns format 2025-11-20 18:14:30 +13:00
3c3a2a0fb7 Ignore __pycache__ 2025-11-20 18:13:00 +13:00
4dacd8ca38 Add pattern tests 2025-11-20 18:11:54 +13:00
3dae9363e7 Remove unused pattern tests 2025-11-12 19:20:50 +13:00
1962638b81 Add rainbow and transition tests 2025-11-12 19:20:48 +13:00
4f413ee4ff Implement websocket handler 2025-11-12 19:20:42 +13:00
2b0b83f981 Update rainbow: n1 controls step increment 2025-11-12 19:20:33 +13:00
1d82ea6a91 Add auto flag to patterns 2025-11-12 19:20:25 +13:00
f17dd302da refactor: simplify patterns_base class
Extract base Patterns class with async run/stop support
2025-11-06 19:25:07 +13:00
846d574ad6 feat: add pulse pattern
Configurable attack/hold/decay phases via n1/n2/n3. Single-shot when delay=0.
2025-11-06 19:16:51 +13:00
f8851d2e7c add short-key param mapping and set_param() 2025-10-30 21:44:45 +13:00
12e242724e Add pattern test 2025-10-30 21:42:45 +13:00
44cb35d1aa Split into pattern and low level methods 2025-09-05 23:29:18 +12:00
fc080f7796 Add watchfiles 2025-08-26 22:53:48 +12:00
70fe5a0cdc Add watchfiles 2025-08-11 22:15:21 +12:00
2a7b5527a5 Move gc and wdt to function 2025-08-03 19:39:25 +12:00
50545e3170 Remove random patterns 2025-08-03 19:29:10 +12:00
d2826a0f63 Swtich to names isntead of ids 2025-07-12 10:22:17 +12:00
87fc74bb51 Add flicker pattern 2025-07-12 10:21:43 +12:00
03f3f02da8 Remove wifi client 2025-06-19 19:13:13 +12:00
524db5e979 Move espnow to seperate file 2025-06-19 19:05:08 +12:00
279416cded Add set_pattern_step 2025-06-19 19:03:22 +12:00
fbd14f2e16 If no ids run set_settings 2025-06-12 21:32:24 +12:00
1989f6f5c9 Switch to list for colors 2025-06-12 21:29:31 +12:00
a19b1e86f2 Have to save when using espnow 2025-06-08 13:18:28 +12:00
c63e907204 espnow if id is 0 call set_settings 2025-06-04 21:07:15 +12:00
b7920e224f Add color order 2025-06-04 21:02:55 +12:00
42e92dafc8 Add led pin setting 2025-06-04 20:03:51 +12:00
0b6eb9724f Add device ID 2025-06-04 19:54:06 +12:00
55ef5c1580 Move json load out of set_settings 2025-06-04 19:22:09 +12:00
c15f9787a7 Sync after going through all the keys 2025-06-02 00:32:43 +12:00
3d0078f118 Update leds straight away after a sync 2025-06-02 00:18:25 +12:00
9e72dba035 Check all keys before returning 2025-06-02 00:16:58 +12:00
3d7dd754eb Wifi and ESPNOW don't work at the same time 2025-06-02 00:15:31 +12:00
2dd20fa51b Enable garabage collection 2025-05-28 21:19:56 +12:00
d33bd6b0e4 Enable watchdog timer 2025-05-28 21:17:23 +12:00
8902adf18c Fix color transition 2025-05-24 13:09:28 +12:00
9abd425f46 Add wifi 2025-05-22 22:02:49 +12:00
ee28b5805d Change title and h1 to "name" 2025-05-22 22:02:29 +12:00
ec29dbdd01 Add color order 2025-05-19 22:00:35 +12:00
3fa9377438 Add set_settings to Settings class 2025-05-19 21:59:43 +12:00
ec049b52c0 Only check wifi settings if not connected 2025-05-19 19:35:29 +12:00
a009ea85bc Add wifi settings 2025-05-19 19:32:53 +12:00
bd2e6e56cf Check if ssid, password, ip and gateway are "" 2025-05-19 19:31:30 +12:00
37c7280a15 Get ap password from settings 2025-05-19 19:28:11 +12:00
2f10d4cabd Fix rgb order 2025-05-19 19:24:17 +12:00
385dcffe68 Add led pin in settings 2025-05-19 19:22:38 +12:00
fa0578349b Delete index_html.py 2025-05-18 21:28:44 +12:00
4a36ff0da0 Update main.py 2025-05-18 21:28:41 +12:00
bd4046572c Update main.css 2025-05-18 21:28:38 +12:00
fdd299b063 Update main.js 2025-05-18 21:28:34 +12:00
a44ef2d0ad Update index.html 2025-05-18 21:28:29 +12:00
2d1208e223 Update web.py 2025-05-18 21:28:21 +12:00
67279a8f46 Update wifi.py 2025-05-18 21:28:14 +12:00
a52ac3df99 Update Pipfile.lock 2025-05-18 21:27:58 +12:00
c4356cf354 Switch to web socket 2025-05-18 21:26:33 +12:00
0c219e0697 Add websocket settings endpoint 2025-05-12 22:22:05 +12:00
cee8c20176 Always sync 2025-05-12 22:21:11 +12:00
135f6b06f8 Ignore .venv 2025-05-07 19:46:28 +12:00
30 changed files with 3064 additions and 769 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
settings.json settings.json
.venv
__pycache__

View File

@@ -7,8 +7,13 @@ name = "pypi"
mpremote = "*" mpremote = "*"
pyserial = "*" pyserial = "*"
esptool = "*" esptool = "*"
watchfiles = "*"
[dev-packages] [dev-packages]
[requires] [requires]
python_version = "3.12" python_version = "3.12"
[scripts]
dev = 'watchfiles "./dev.py /dev/ttyACM0 src reset follow"'

572
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "8b14bb293b7e7117ffc89c2bc92d7aa2290e8f68be7fc0f073f2b3f7f959ef71" "sha256": "7b8033c15743e27f2589635c75bd0bb86ffc3a725b179d7db9ef200119aa9164"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -16,163 +16,160 @@
] ]
}, },
"default": { "default": {
"argcomplete": { "anyio": {
"hashes": [ "hashes": [
"sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61", "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6",
"sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392" "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"
], ],
"markers": "sys_platform != 'win32'", "markers": "python_version >= '3.9'",
"version": "==3.5.3" "version": "==4.10.0"
}, },
"bitarray": { "bitarray": {
"hashes": [ "hashes": [
"sha256:000df24c183011b5d27c23d79970f49b6762e5bb5aacd25da9c3e9695c693222", "sha256:00628196dd3592972a5183194ab1475dadf9ef2a4cf3fd8c7c184a94934012e8",
"sha256:0027b8f3bb2bba914c79115e96a59b9924aafa1a578223a7c4f0a7242d349842", "sha256:01d6dc548e7fe5c66913c2274f44855b0f8474935acff7811e84fe1f4024c94f",
"sha256:00f9a88c56e373009ac3c73c55205cfbd9683fbd247e2f9a64bae3da78795252", "sha256:056fe779f01a867d572e071c0944ac2f3bf58d8bced326040f0bd060af33a209",
"sha256:041c889e69c847b8a96346650e50f728b747ae176889199c49a3f31ae1de0e23", "sha256:080a7bf55c432abdae74f25dc3dbff407418346aeae1d43e31f65e8ef114f785",
"sha256:0879f839ec8f079fa60c3255966c2e1aa7196699a234d4e5b7898fbc321901b5", "sha256:0956322bf4d5e2293e57600aa929c241edf1e209e94e12483bf58c5c691432db",
"sha256:0b555006a7dea53f6bebc616a4d0249cecbf8f1fadf77860120a2e5dbdc2f167", "sha256:0a6f9e897907757e9c2d722ae6c203d48a04826a14e1495e33935c8583c163a9",
"sha256:0b655c3110e315219e266b2732609fddb0857bc69593de29f3c2ba74b7d3f51a", "sha256:0ac446f557eb28e3f7c65372608810ff073840627e9037e22ed10bd081793a34",
"sha256:0cecaf2981c9cd2054547f651537b4f4939f9fe225d3fc2b77324b597c124e40", "sha256:0b47843f2f288fa746dead4394591a3432a358aaad48240283fa230d6e74b0e7",
"sha256:0e104f9399144fab6a892d379ba1bb4275e56272eb465059beef52a77b4e5ce6", "sha256:11fc8bc65f964c7278deb1b7a69379dab3ecc90095f252deb17365637ebb274d",
"sha256:0ef5c787c8263c082a73219a69eb60a500e157a4ac69d1b8515ad836b0e71fb4", "sha256:129165b68a3e0c2a633ed0d8557cf5ade24a0b37ca97d7805fa6fc5fb73c19d5",
"sha256:12f19ede03e685c5c588ab5ed63167999295ffab5e1126c5fe97d12c0718c18f", "sha256:139963494fc3dd5caee5e38c0a03783ef50be118565e94b1dbb0210770f0b32d",
"sha256:1414a7102a3c4986f241480544f5c99f5d32258fb9b85c9c04e84e48c490ab35", "sha256:157313a124287cbc8a11b55a75def0dd59e68badbc82c2dc2d204dc852742874",
"sha256:147542299f458bdb177f798726e5f7d39ab8491de4182c3c6d9885ed275a3c2b", "sha256:16d0edab54bb9d214319418f65bd15cfc4210ec41a16c3dd0b71e626c803212d",
"sha256:150b7b29c36d9f1a24779aea723fdfc73d1c1c161dc0ea14990da27d4e947092", "sha256:1971050b447023288a2b694a03b400bd5163829cd67b10f19e757fe87cd1161e",
"sha256:153d7c416a70951dcfa73487af05d2f49c632e95602f1620cd9a651fa2033695", "sha256:1c4e75bbf9ade3d2cdf1b607a8b353b17d9b3cf54e88b2a5a773f50ae6f1bfbc",
"sha256:184972c96e1c7e691be60c3792ca1a51dd22b7f25d96ebea502fe3c9b554f25d", "sha256:1c9f36055a89b9517db66eb8e80137126bf629c767ceeade4d004e40bc8bcd99",
"sha256:18abdce7ab5d2104437c39670821cba0b32fdb9b2da9e6d17a4ff295362bd9dc", "sha256:2020102a40edd094c0aa80e09203af71c533c41f76ce3237c99fd194a473ea33",
"sha256:2055206ed653bee0b56628f6a4d248d53e5660228d355bbec0014bdfa27050ae", "sha256:20febc849a1f858e6a57a7d47b323fe9e727c579ddd526d317ad8831748a66a8",
"sha256:20f30373f0af9cb583e4122348cefde93c82865dbcbccc4997108b3d575ece84", "sha256:220d4b8649ef54ac98e5e0e3dd92230247f67270d1524a8b31aa9859007affb0",
"sha256:22b00f65193fafb13aa644e16012c8b49e7d5cbb6bb72825105ff89aadaa01e3", "sha256:22188943a29072b684cd7c99e0b2cfc0af317cea3366c583d820507e6d1f2ed4",
"sha256:251cd5bd47f542893b2b61860eded54f34920ea47fd5bff038d85e7a2f7ae99b", "sha256:222cb27ff05bc0aec72498d075dba1facec49a76a7da45740690cebbe3e81e43",
"sha256:2855cc01ee370f7e6e3ec97eebe44b1453c83fb35080313145e2c8c3c5243afb", "sha256:243825f56b58bef28bfc602992a8c6d09bbc625628c195498d6020120d632a09",
"sha256:2ac67b658fa5426503e9581a3fb44a26a3b346c1abd17105735f07db572195b3", "sha256:25060e7162e44242a449ed1a14a4e94b5aef340812754c443459f19c7954be91",
"sha256:2d9fe3ee51afeb909b68f97e14c6539ace3f4faa99b21012e610bbe7315c388d", "sha256:26691454a6770628882b68fe74e9f84ca2a51512edd49cbb025b14df5a9dd85a",
"sha256:2da91ab3633c66999c2a352f0ca9ae064f553e5fc0eca231d28e7e305b83e942", "sha256:27d13c7b886afc5d2fc49d6e92f9c96b1f0a14dc7b5502520c29f3da7550d401",
"sha256:2dad7ba2af80f9ec1dd988c3aca7992408ec0d0b4c215b65d353d95ab0070b10", "sha256:27eeee915258b105a21a4b0f8aebc5f77bb4dc4fb4063a09dd329fa1fdcbd234",
"sha256:34fc13da3518f14825b239374734fce93c1a9299ed7b558c3ec1d659ec7e4c70", "sha256:29ed022189a7997de46cb9bd4e2e49d6163d4f8d78dea72ac5a0e0293b856810",
"sha256:369b6d457af94af901d632c7e625ca6caf0a7484110fc91c6290ce26bc4f1478", "sha256:2a324e3007afb5c667026f5235b35efe3c4a95f1b83cd93aa9fce67b42f08e7c",
"sha256:37be5482b9df3105bad00fdf7dc65244e449b130867c3879c9db1db7d72e508b", "sha256:2c533c828d0007fac27cf45e5c1a711e5914dd469db5fe6be5f4e606bf2d7f63",
"sha256:3963b80a68aedcd722a9978d261ae53cb9bb6a8129cc29790f0f10ce5aca287a", "sha256:30ba4fba3de1dca653de41c879349ec6ca521d85cff6a7ca5d2fdd8f76c93781",
"sha256:39b38a3d45dac39d528c87b700b81dfd5e8dc8e9e1a102503336310ef837c3fd", "sha256:357e07c827bad01f98d0bd0dfdc722f483febeed39140fd75ffd016a451b60b9",
"sha256:3cd565253889940b4ec4768d24f101d9fe111cad4606fdb203ea16f9797cf9ed", "sha256:3800f3c8c9780f281cf590543fd4b3278fea6988202273a260ecc58136895efb",
"sha256:3d47bc4ff9b0e1624d613563c6fa7b80aebe7863c56c3df5ab238bb7134e8755", "sha256:3d6f3a94abd8b44b2bf346ca81ab2ff41ab9146c53905eedf5178b19d9fe53bf",
"sha256:3fa5d8e4b28388b337face6ce4029be73585651a44866901513df44be9a491ab", "sha256:3eb1390a8b062fe9125e5cc4c5eba990b5d383eec54f2b996e7ce73ac43150f9",
"sha256:42bf1b222c698b467097f58b9f59dc850dfa694dde4e08237407a6a103757aa3", "sha256:407920e9318d94cc6c9611aaa5b5e5963a09f1cbfa17b16b66edea453b3754f4",
"sha256:43b6c7c4f4a7b80e86e24a76f4c6b9b67d03229ea16d7d403520616535c32196", "sha256:42622c42c159ea4535bba7e1e3c97f1fec79505bc6873ae657dc0a8f861c60de",
"sha256:44c3e78b60070389b824d5a654afa1c893df723153c81904088d4922c3cfb6ac", "sha256:4695fcd37478988b1d0a16d5bc0df56dcb677fd5db37f1893d993fd3ebef914b",
"sha256:4683bff52f5a0fd523fb5d3138161ef87611e63968e1fcb6cf4b0c6a86970fe0", "sha256:4798f6744fa2633666e17b4ea8ff70250781b52a25afdbf5ffb5e176c58848f1",
"sha256:47ccf9887bd595d4a0536f2310f0dcf89e17ab83b8befa7dc8727b8017120fda", "sha256:4a5b0d277087a5bf261a607fc6ff4aaffcf80b300cd19b7a1e9754a4649f5fd4",
"sha256:4800c91a14656789d2e67d9513359e23e8a534c8ee1482bb9b517a4cfc845200", "sha256:4e297fd2e58afe17e33dd80c231c3a9d850279a2a8625aed1d39f9be9534809e",
"sha256:4817d73d995bd2b977d9cde6050be8d407791cf1f84c8047fa0bea88c1b815bc", "sha256:507e567aee4806576e20752f22533e8b7ec61e7e75062a7ce9222a0675aa0da6",
"sha256:4839d3b64af51e4b8bb4a602563b98b9faeb34fd6c00ed23d7834e40a9d080fc", "sha256:50d702149747852923be60cae125285eca8d189d4c7d8832c0c958d4071a0f78",
"sha256:4ac2027ca650a7302864ed2528220d6cc6921501b383e9917afc7a2424a1e36d", "sha256:51947a00ae9924584fb14c0c1b1f4c1fd916d9abd6f47582f318ab9c9cb9f3d0",
"sha256:4cb5702dd667f4bb10fed056ffdc4ddaae8193a52cd74cb2cdb54e71f4ef2dd1", "sha256:52328192d454ca2ddad09fbc088872b014c74b22ecdd5164717dc7e6442014fa",
"sha256:53e002ac1073ac70e323a7a4bfa9ab95e7e1a85c79160799e265563f342b1557", "sha256:531e6dfec8058fcf5d69e863b61e6b28e3749b615a4dcc0ab8ad36307c4017fc",
"sha256:545d36332de81e4742a845a80df89530ff193213a50b4cbef937ed5a44c0e5e5", "sha256:54bd71f14a5fa9bae73ef92f2e2be894dc36c7a6d1c4962e5969bd8a9aa39325",
"sha256:572a61fba7e3a710a8324771322fba8488d134034d349dcd036a7aef74723a80", "sha256:552a93be286ca485914777461b384761519db313e0a7f3012dca424c9610a4d5",
"sha256:57d5ef854f8ec434f2ffd9ddcefc25a10848393fe2976e2be2c8c773cf5fef42", "sha256:583b46b3ba44121de5e87e95ae379932dc5fd2e37ebdf2c11a6d7975891425c1",
"sha256:5ddbf71a97ad1d6252e6e93d2d703b624d0a5b77c153b12f9ea87d83e1250e0c", "sha256:5b58a672ec448fb36839a5fc7bf2b2f60df9a97b872d8bd6ca1a28da6126f5c7",
"sha256:5fa4b4d9fa90124b33b251ef74e44e737021f253dc7a9174e1b39f097451f7ca", "sha256:5cfbdccddaa0ff07789e9e180db127906c676e479e05c04830cd458945de3511",
"sha256:628f93e9c2c23930bd1cfe21c634d6c84ec30f45f23e69aefe1fcd262186d7bb", "sha256:5da4939e241301f5e1d18118695e8d2c300be90431b66bd43a00376acec45e1e",
"sha256:648e7ce794928e8d11343b5da8ecc5b910af75a82ea1a4264d5d0a55c3785faa", "sha256:5dd9edcab8979a50c2c4dec6d5b66789fb6f630bb52ab90a4548111075a75e48",
"sha256:656db7bdf1d81ec3b57b3cad7ec7276765964bcfd0eb81c5d1331f385298169c", "sha256:5e304f94c0353f6ae5711533b5793b3a45b17aa2c5b07e656649b0af4e0939b5",
"sha256:666e44b0458bb2894b64264a29f2cc7b5b2cbcc4c5e9cedfe1fdbde37a8e329a", "sha256:60408ec9c0bd76f1fa00d28034429a0316246d31069b982a86aec8d5c99e910a",
"sha256:66a33a537e781eac3a352397ce6b07eedf3a8380ef4a804f8844f3f45e335544", "sha256:62f71b268f14ee6cc3045b95441bfe0518cef1d0b2ffbc6f3e9758f786ff5a03",
"sha256:66d6134b7bb737b88f1d16478ad0927c571387f6054f4afa5557825a4c1b78e2", "sha256:664d462a4c0783fd755fe3440f07b7e46d149859c96caacadf3f28890f19a8de",
"sha256:67a0b56dd02f2713f6f52cacb3f251afd67c94c5f0748026d307d87a81a8e15c", "sha256:66d8b7a89fac6042f7df9ea97d97ed0f5e404281110a882e3babd909161f85b6",
"sha256:6c33129b49196aa7965ac0f16fcde7b6ad8614b606caf01669a0277cef1afe1d", "sha256:6755cfcfa7d8966e704d580c831e39818f85e7b2b7852ad22708973176f0009e",
"sha256:6d2a2ce73f9897268f58857ad6893a1a6680c5a6b28f79d21c7d33285a5ae646", "sha256:679856547f0b27b98811b73756bdf53769c23b045a6f95177cae634daabf1ddf",
"sha256:71ad0139c95c9acf4fb62e203b428f9906157b15eecf3f30dc10b55919225896", "sha256:6841c08b51417f8ffe398b2828fc0593440c99525c868f640e0302476745320b",
"sha256:7814c9924a0b30ecd401f02f082d8697fc5a5be3f8d407efa6e34531ff3c306a", "sha256:68f6e64d4867ee79e25c49d7f35b2b1f04a6d6f778176dcf5b759f3b17a02b2b",
"sha256:787db8da5e9e29be712f7a6bce153c7bc8697ccc2c38633e347bb9c82475d5c9", "sha256:69d2d507c1174330c71c834b5d65e66181ad7b42b0d88b5b31804ee9b4f5dae7",
"sha256:7cb885c043000924554fe2124d13084c8fdae03aec52c4086915cd4cb87fe8be", "sha256:6f0be27d06732e2833b672a8fcc32fa195bdb22161eb88f8890de15e30264a01",
"sha256:7cd021ada988e73d649289cee00428b75564c46d55fbdcb0e3402e504b0ae5ea", "sha256:6f7d2dbe628f3db935622a5b80a5c4d95665cdefc4904372aa3c4d786289477f",
"sha256:7e51e7f8289bf6bb631e1ef2a8f5e9ca287985ff518fe666abbdfdb6a848cb26", "sha256:72760411d60d8d76979a20ed3f15586d824db04668b581b86e61158c2b616db0",
"sha256:7e9eee03f187cef1e54a4545124109ee0afc84398628b4b32ebb4852b4a66393", "sha256:727f7a969416f02ef5c1256541e06f0836fb615022699fa8e2591e85296c5570",
"sha256:7edb83089acbf2c86c8002b96599071931dc4ea5e1513e08306f6f7df879a48b", "sha256:77d2368a06a86a18919c05a9b4b0ee9869f770e6a5f414b0fecc911870fe3974",
"sha256:7f1c24be7519f16a47b7e2ad1a1ef73023d34d8cbe1a3a59b185fc14baabb132", "sha256:79ab1c5f26f23e51d4a44c4397c8a3bf56c306c125dfab6b3eebdfa13d1dca6f",
"sha256:8330912be6cb8e2fbfe8eb69f82dee139d605730cadf8d50882103af9ac83bb4", "sha256:79db23eda81627132327ed292bd813a9af64399b98aaac3d42ad8deeed24cd5e",
"sha256:8a9eb510cde3fa78c2e302bece510bf5ed494ec40e6b082dec753d6e22d5d1b1", "sha256:7c20d6e6cafce5027e7092beb2ac6eec0d71045d6318b34f36e1387a8c8859a3",
"sha256:8c9733d2ff9b7838ac04bf1048baea153174753e6a47312be14c83c6a395424b", "sha256:7e0851a985a7b10f634188117c825ef99d63402555cca5bc32c7bfc5adaf0d6f",
"sha256:904c1d5e3bd24f0c0d37a582d2461312033c91436a6a4f3bdeeceb4bea4a899d", "sha256:7e2e1ff784c2cdfd863bad31985851427f2d2796e445cec85080c7510cba4315",
"sha256:928b8b6dfcd015e1a81334cfdac02815da2a2407854492a80cf8a3a922b04052", "sha256:7f8b12424f8fdf29d1c0749c628bd1530cecfc77374935d096cccc0e4eada232",
"sha256:9502c2230d59a4ace2fddfd770dad8e8b414cbd99517e7e56c55c20997c28b8d", "sha256:81e84054b22babcd6c5cc1eac0de2bfc1054ecdf742720cbfb36efbe89ec6c30",
"sha256:96cf0898f8060b2d3ae491762ae871b071212ded97ff9e1e3a5229e9fefe544c", "sha256:84bb57010a1ab76cf880424a2e0bce8dd26989849d2122ff073aa11bfc271c27",
"sha256:98a4070ddafabddaee70b2aa7cc6286cf73c37984169ab03af1782da2351059a", "sha256:870ed23361e2918ab1ffc23fe0ab293abf3c372a68ee7387456d13da3e213008",
"sha256:9929051feeaf8d948cc0b1c9ce57748079a941a1a15c89f6014edf18adaade84", "sha256:8cf44b012e7493127ce7ca6e469138ac96b3295a117877d5469aabe7c8728d87",
"sha256:996d1b83eb904589f40974538223eaed1ab0f62be8a5105c280b9bd849e685c4", "sha256:8d6c9bc14bacdfbfd51fed85f0576973eaaa7b30d81ef93264f8e22b86a9c9f7",
"sha256:9c6e52005e91803eb4e08c0a08a481fb55ddce97f926bae1f6fa61b3396b5b61", "sha256:8d759cecfa8aab4a1eb4e23b6420126b15c7743e85b33f389916bb98c4ecbb84",
"sha256:9e3727ab63dfb6bde00b281934e2212bb7529ea3006c0031a556a84d2268bea5", "sha256:8ef3f0977c21190f949d5cfd71ded09de87d330c6d98bd5ecb5bb1135d666d0d",
"sha256:a0255bd05ec7165e512c115423a5255a3f301417973d20a80fc5bfc3f3640bcb", "sha256:8f95daf0ce2b24815ddf62667229ba5dfc0cfee43eb43b2549766170d0f24ae9",
"sha256:a2083dc20f0d828a7cdf7a16b20dae56aab0f43dc4f347a3b3039f6577992b03", "sha256:911b4a16dce370657e5b8d8b6ba0fbb50dd5e2b24c4416f4b9e664503d3f0502",
"sha256:a3c36b2fcfebe15ad1c10a90c1d52a42bebe960adcbce340fef867203028fbe7", "sha256:96117212229905da864794df9ea7bd54987c30a5dcbab3432edc3f344231adae",
"sha256:a4f49ac31734fe654a68e2515c0da7f5bbdf2d52755ba09a42ac406f1f08c9d0", "sha256:963cbcf296943f7017470d0b705e63e908f32b4f7dbe43f72c22f6fe1bd9ef66",
"sha256:a667ea05ba1ea81b722682276dbef1d36990f8908cf51e570099fd505a89f931", "sha256:975a118aa019d745f1398613b27fd8789f60a8cea057a00cdc1abedee123ffe6",
"sha256:a754c1464e7b946b1cac7300c582c6fba7d66e535cd1dab76d998ad285ac5a37", "sha256:9930853d451086c4c084d83a87294bdb0c5bc0fa4105a26c487ac09ea62e565b",
"sha256:a817ad70c1aff217530576b4f037dd9b539eb2926603354fcac605d824082ad1", "sha256:99d16862e802e7c50c3b6cdd1bf041b6142335c9c2b426631f731257adfe5a15",
"sha256:aa54c7e1da8cf4be0aab941ea284ec64033ede5d6de3fd47d75e77cafe986e9d", "sha256:9ed4a2852b3de7a64884afcc6936db771707943249a060aec8e551c16361d478",
"sha256:ab37da66a8736ad5a75a58034180e92c41e864da0152b84e71fcc253a2f69cd4", "sha256:9f7796959c9c036a115d34696563f75d4a2912d3b97c15c15f2a36bdd5496ce9",
"sha256:ac06dd72ee1e1b6e312504d06f75220b5894af1fb58f0c20643698f5122aea76", "sha256:a04b7a9017b8d0341ebbe77f61b74df1cf1b714f42b671a06f4912dc93d82597",
"sha256:aca0a9cd376beaccd9f504961de83e776dd209c2de5a4c78dc87a78edf61839b", "sha256:a1b3c4ca3bec8e0ad9d32ce62444c5f3913588124a922629aa7d39357b2adf3f",
"sha256:acc07211a59e2f245e9a06f28fa374d094fb0e71cf5366eef52abbb826ddc81e", "sha256:a290a417608f50137bec731d1f22ff3efebac72845530807a8433b2db9358c95",
"sha256:aef404d5400d95c6ec86664df9924bde667c8865f8e33c9b7bd79823d53b3e5d", "sha256:a33f7c5acf44961f29018b13f0b5f5e1589ac0cfdf75a97c9774cf7ec84d09e0",
"sha256:b1047999f1797c3ea7b7c85261649249c243308dcf3632840d076d18fa72f142", "sha256:a39be79a7c36e9a2e20376261c30deb3cdca86b50f7462ae9ff10a755c6720b9",
"sha256:b7d09ef06ba57bea646144c29764bf6b870fb3c5558ca098191e07b6a1d40bf7", "sha256:a50a66fa34dd7f9dcdbc7602a1b7bf6f9ab030b4f43e892324193423d9ede180",
"sha256:bcf0150ae0bcc4aa97bdfcb231b37bad1a59083c1b5012643b266012bf420e68", "sha256:a5ce1bdee102f7e60c075274df10b892d9ff5183ad6f5f515973eda8903dfe4c",
"sha256:bcf524a087b143ba736aebbb054bb399d49e77cf7c04ed24c728e411adc82bfa", "sha256:a763dd33d6e27c9b4db3f8089a5fa39179a8a3cf48ce702b24a857d7c621333c",
"sha256:beeb79e476d19b91fd6a3439853e4e5ba1b3b475920fa40d62bde719c8af786f", "sha256:a773199dc42b5d02fcd46c8add552da2c4725ce2caa069527c7e27b5b6089e85",
"sha256:bf90aba4cff9e72e24ecdefe33bad608f147a23fa5c97790a5bab0e72fe62b6d", "sha256:aa3c925502bd0b957a96a5619134bcdc0382ef73cffd40bad218ced3586bcf8d",
"sha256:c23286abba0cb509733c6ce8f4013cd951672c332b2e184dbefbd7331cd234c8", "sha256:aeb6db2f4ab54ac21a3851d05130a2aa78a6f6a5f14003f9ae3114fb8b210850",
"sha256:c2945e0390d1329c585c584c6b6d78be017d9c6a1288f9c92006fe907f69cc28", "sha256:af670708e145b048ead87375b899229443f2d0b4af2d1450d7701c74cd932b03",
"sha256:c756a92cf1c1abf01e56a4cc40cb89f0ff9147f2a0be5b557ec436a23ff464d8", "sha256:afa24e5750c9b89ad5a7efef037efe49f4e339f20a94bf678c422c0c71e1207a",
"sha256:c9e9fef0754867d88e948ce8351c9fd7e507d8514e0f242fd67c907b9cdf98b3", "sha256:b02cc1cac9099c0ec72da09593e7fadb1b6cf88a101acc8153592c700d732d80",
"sha256:ca79f02a98cbda1472449d440592a2fe2ad96fe55515a0447fa8864a38017cf8", "sha256:b37c9ea942395de029be270f0eca8c141eb14e8455941495cd3b6f95bbe465f4",
"sha256:cb7302dbcfcb676f0b66f15891f091d0233c4fc23e1d4b9dc9b9e958156e347f", "sha256:b3b521e117ab991d6b3b830656f464b354a42dbea2ca16a0e7d93d573f7ab7ff",
"sha256:cb98d5b6eac4b2cf2a5a69f60a9c499844b8bea207059e9fc45c752436e6bb49", "sha256:b5ad8261f47c2a72d0f676bc40f752db8cfdcab911e970753343836e41d5a9a7",
"sha256:cc83ea003dd75e9ade3291ef0585577dd5524aec0c8c99305c0aaa2a7570d6db", "sha256:b9616ea14917d06736339cf36bb9eaf4eb52110a74136b0dc5eff94e92417d22",
"sha256:ce249ed981f428a8b61538ca82d3875847733d579dd40084ab8246549160f8a4", "sha256:b9a03767c937b621ee267507bc394df97fb2f8f61130f39f2033f3c6c191f124",
"sha256:cf0cc2e91dd38122dec2e6541efa99aafb0a62e118179218181eff720b4b8153", "sha256:b9ae0008cff25e154ef1e3975a1705d344e844ffdeb34c25b007fd48c876e95d",
"sha256:d1a199e6d7c3bad5ba9d0e4dc00dde70ee7d111c9dfc521247fa646ef59fa57e", "sha256:bdd6412c1f38da7565126b174f4e644f362e317ef0560fac1fb9d0c70900ff4d",
"sha256:d1d5abf1d6d910599ac16afdd9a0ed3e24f3b46af57f3070cf2792f236f36e0b", "sha256:bfc417e58f277e949ed662d9cd050ddbb00c0dea8a828abaccc93dc357b7a6d1",
"sha256:d3f761184b93092077c7f6b7dad7bd4e671c1620404a76620da7872ceb576a94", "sha256:c15b9e37bbca59657e4dcc63ad068c821a4676def15f04742c406748a0a11b9c",
"sha256:d756bfeb62ca4fe65d2af7a39249d442c05070c047d03729ad6cd4c2e9b0f0bd", "sha256:c677849947d523a082be6e0b5c9137f443a54e951a1711ef003ec52910c41ece",
"sha256:d8c36ddc1923bcc4c11b9994c54eaae25034812a42400b7b8a86fe6d242166a2", "sha256:c9d247fcc33c90f2758f4162693250341e3f38cd094f64390076ef33ad0887f9",
"sha256:dbe1084935b942fab206e609fa1ed3f46ad1f2612fb4833e177e9b2a5e006c96", "sha256:ca643295bf5441dd38dadf7571ca4b63961820eedbffbe46ceba0893bf226203",
"sha256:dc1937a0ff2671797d35243db4b596329842480d125a65e9fe964bcffaf16dfc", "sha256:ca87f639094c72268e17bc7f57c1225cc38f9e191a489a0134762e3fec402c1a",
"sha256:dfea514e665af278b2e1d4deb542de1cd4f77413bee83dd15ae16175976ea8d5", "sha256:cc060bc17b9de27874997d612e37d52f72092f9b59d1e04284a90ed8113cadca",
"sha256:e008b7b4ce6c7f7a54b250c45c28d4243cc2a3bbfd5298fa7dac92afda229842", "sha256:ccf4a73e07bfbd790443d6b3c1f1447ffda23cc9391e40c035d9b7d3514b57b8",
"sha256:e0e7f24a0b01e6e6a0191c50b06ca8edfdec1988d9d2b264d669d2487f4f4680", "sha256:cf36cadeb9c989f760a13058dbc455e5406ec3d2d247c705c8d4bc6dd1b0fcc6",
"sha256:e15c94d79810c5ab90ddf4d943f71f14332890417be896ca253f21fa3d78d2b1", "sha256:d47e2bdeba4fb1986af2ba395ce51223f4d460e6e77119439e78f2b592cafade",
"sha256:e56ba8be5f17dee0ffa6d6ce85251e062ded2faa3cbd2558659c671e6c3bf96d", "sha256:db78cc5c03b446a43413165aa873e2f408e9fd5ddb45533e7bd3b638bace867c",
"sha256:e89ea59a3ed86a6eb150d016ed28b1bedf892802d0ed32b5659d3199440f3ced", "sha256:dbc5029c61f9ebb2d4c247f13584a0ef0e8e49abb13e56460310821aca3ffcaf",
"sha256:e91d46d12781a14ccb8b284566b14933de4e3b29f8bc5e1c17de7a2001ad3b5b", "sha256:ddb319f869d497ef2d3d56319360b61284a9a1d8b3de3bc936748698acfda6be",
"sha256:ea40e98d751ed4b255db4a88fe8fb743374183f78470b9e9305aab186bf28ede", "sha256:e0e4fdeae6c0a9d886749780ec5dcf469e98f27b312efa93008d03eaa2426fd5",
"sha256:eb27c01b747649afd7e1c342961680893df6d8d81f832a6f04d8c8e03a8a54cc", "sha256:e4c5e7edf1e7bcbde3b52058f171a411e2a24a081b3e951d685dfea4c3c383d5",
"sha256:ec5b0f2d13da53e0975ac15ecbe8badb463bdb0bebaa09457f4df3320421915c", "sha256:e71c9dba78671d38a549e3b2d52514f50e199f9d7e18ed9b0180adeef0d04130",
"sha256:ee040ad3b7dfa05e459713099f16373c1f2a6f68b43cb0575a66718e7a5daef4", "sha256:e997d22e0d1e08c8752f61675a75d93659f7aa4dbeaee54207f8d877817b4a0c",
"sha256:f12cc7c7638074918cdcc7491aff897df921b092ffd877227892d2686e98f876", "sha256:efa5834ba5e6c70b22afdca3894097e5a592d8d483c976359654ba990477799a",
"sha256:f536fc4d1a683025f9caef0bebeafd60384054579ffe0825bb9bd8c59f8c55b8", "sha256:f2d951002b11962b26afb31f758c18ad39771f287b100fa5adb1d09a47eaaf5b",
"sha256:f71f24b58e75a889b9915e3197865302467f13e7390efdea5b6afc7424b3a2ea", "sha256:f3f96f57cea35ba19fd23a20b38fa0dfa3d87d582507129b8c8e314aa298f59b",
"sha256:f75fc0198c955d840b836059bd43e0993edbf119923029ca60c4fc017cefa54a", "sha256:f738051052abc95dc17f9a4c92044294a263fb7f762efdb13e528d419005c0e4",
"sha256:f785af6b7cb07a9b1e5db0dea9ef9e3e8bb3d74874a0a61303eab9c16acc1999", "sha256:f76784355060999c36fa807b59faecb38f5769ae58283d00270835773f95e35b",
"sha256:fbb645477595ce2a0fbb678d1cfd08d3b896e5d56196d40fb9e114eeab9382b3", "sha256:f92462ea3888c99439f58f7561ecd5dd4cf8b8b1b259ccf5376667b8c46ee747",
"sha256:fcef31b062f756ba7eebcd7890c5d5de84b9d64ee877325257bcc9782288564a", "sha256:fefd18b29f3b84a0cdea1d86340219d9871c3b0673a38e722a73a2c39591eaa7"
"sha256:fe606e728842389943a939258809dc5db2de831b1d2e0118515059e87f7bbc1a",
"sha256:fef4e3b3f2084b4dae3e5316b44cda72587dcc81f68b4eb2dbda1b8d15261b61",
"sha256:ffd94b4803811c738e504a4b499fb2f848b2f7412d71e6b517508217c1d7929d"
], ],
"version": "==3.0.0" "version": "==3.6.0"
}, },
"bitstring": { "bitstring": {
"hashes": [ "hashes": [
"sha256:3282a896814813f8fe5fa09dbafac842c57aace1d3bfd94546c6f1ed9aafcbe1", "sha256:69d1587f0ac18dc7d93fc7e80d5f447161a33e57027e726dc18a0a8bacf1711a",
"sha256:81800bc4e00b6508716adbae648e741256355c8dfd19541f76482fb89bee0313" "sha256:a08bc09d3857216d4c0f412a1611056f1cc2b64fd254fb1e8a0afba7cfa1a95a"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.3.0" "version": "==4.3.1"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
@@ -247,54 +244,72 @@
"markers": "platform_python_implementation != 'PyPy'", "markers": "platform_python_implementation != 'PyPy'",
"version": "==1.17.1" "version": "==1.17.1"
}, },
"click": {
"hashes": [
"sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202",
"sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"
],
"markers": "python_version >= '3.10'",
"version": "==8.2.1"
},
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5",
"sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74",
"sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394",
"sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301",
"sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08",
"sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3",
"sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b",
"sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18",
"sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402",
"sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3",
"sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c",
"sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0",
"sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db",
"sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427",
"sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f",
"sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3",
"sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b",
"sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9",
"sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5",
"sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719",
"sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043",
"sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012",
"sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02",
"sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2",
"sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d",
"sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec",
"sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4" "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d",
"sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159",
"sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453",
"sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf",
"sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385",
"sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9",
"sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016",
"sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05",
"sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42",
"sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da",
"sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983"
], ],
"markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'",
"version": "==44.0.0" "version": "==45.0.6"
},
"ecdsa": {
"hashes": [
"sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a",
"sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.19.0"
}, },
"esptool": { "esptool": {
"hashes": [ "hashes": [
"sha256:dc4ef26b659e1a8dcb019147c0ea6d94980b34de99fbe09121c7941c8b254531" "sha256:05cc4732eb2a9a7766c9e3531f7943d76ff0ca06dc9cd308d1d3d0b72f74aac2"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.10'",
"version": "==4.8.1" "version": "==5.0.2"
},
"idna": {
"hashes": [
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
],
"markers": "python_version >= '3.6'",
"version": "==3.10"
}, },
"intelhex": { "intelhex": {
"hashes": [ "hashes": [
@@ -303,14 +318,38 @@
], ],
"version": "==2.3.0" "version": "==2.3.0"
}, },
"markdown-it-py": {
"hashes": [
"sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
"sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
],
"markers": "python_version >= '3.8'",
"version": "==3.0.0"
},
"mdurl": {
"hashes": [
"sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
"sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
],
"markers": "python_version >= '3.7'",
"version": "==0.1.2"
},
"mpremote": { "mpremote": {
"hashes": [ "hashes": [
"sha256:1a3c16d255748cfe54d4a897908651fc8286233173f7c7b2a0e56ae4b9fa940e", "sha256:7f347318fb6d3bb8f89401d399a05efba39b51c74f747cebe92d3c6a9a4ee0b4",
"sha256:d3ae3d0a0ae7713c537be2b6afadd11c7cde5f1750ea1260f6667bb80071b15b" "sha256:daed9b795fdf98edb0c9c4f7f892bf66f075ec5e728bdcc4ab0915abf23d5d17"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.4'", "markers": "python_version >= '3.4'",
"version": "==1.24.1" "version": "==1.26.0"
},
"platformdirs": {
"hashes": [
"sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc",
"sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"
],
"markers": "python_version >= '3.9'",
"version": "==4.3.8"
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
@@ -320,6 +359,14 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.22" "version": "==2.22"
}, },
"pygments": {
"hashes": [
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
],
"markers": "python_version >= '3.8'",
"version": "==2.19.2"
},
"pyserial": { "pyserial": {
"hashes": [ "hashes": [
"sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb",
@@ -394,13 +441,150 @@
], ],
"version": "==1.7.0" "version": "==1.7.0"
}, },
"six": { "rich": {
"hashes": [ "hashes": [
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f",
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_full_version >= '3.8.0'",
"version": "==1.17.0" "version": "==14.1.0"
},
"rich-click": {
"hashes": [
"sha256:c3fa81ed8a671a10de65a9e20abf642cfdac6fdb882db1ef465ee33919fbcfe2",
"sha256:fd98c0ab9ddc1cf9c0b7463f68daf28b4d0033a74214ceb02f761b3ff2af3136"
],
"markers": "python_version >= '3.7'",
"version": "==1.8.9"
},
"sniffio": {
"hashes": [
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
],
"markers": "python_version >= '3.7'",
"version": "==1.3.1"
},
"typing-extensions": {
"hashes": [
"sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36",
"sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"
],
"markers": "python_version >= '3.9'",
"version": "==4.14.1"
},
"watchfiles": {
"hashes": [
"sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a",
"sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f",
"sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6",
"sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3",
"sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7",
"sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a",
"sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259",
"sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297",
"sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1",
"sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c",
"sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a",
"sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b",
"sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb",
"sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc",
"sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b",
"sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339",
"sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9",
"sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df",
"sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb",
"sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4",
"sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5",
"sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc",
"sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c",
"sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8",
"sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433",
"sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12",
"sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30",
"sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0",
"sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86",
"sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c",
"sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5",
"sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866",
"sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb",
"sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2",
"sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e",
"sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575",
"sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f",
"sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a",
"sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f",
"sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d",
"sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277",
"sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9",
"sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf",
"sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92",
"sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72",
"sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b",
"sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68",
"sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa",
"sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc",
"sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b",
"sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd",
"sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4",
"sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7",
"sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792",
"sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9",
"sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0",
"sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297",
"sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef",
"sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179",
"sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d",
"sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea",
"sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5",
"sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee",
"sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82",
"sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011",
"sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e",
"sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4",
"sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf",
"sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db",
"sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20",
"sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4",
"sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575",
"sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa",
"sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c",
"sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f",
"sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f",
"sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267",
"sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018",
"sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2",
"sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d",
"sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd",
"sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47",
"sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb",
"sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29",
"sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147",
"sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8",
"sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670",
"sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587",
"sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97",
"sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c",
"sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5",
"sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e",
"sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e",
"sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6",
"sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc",
"sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e",
"sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8",
"sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895",
"sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7",
"sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432",
"sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc",
"sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633",
"sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f",
"sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77",
"sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12",
"sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==1.1.0"
} }
}, },
"develop": {} "develop": {}

2
dev.py
View File

@@ -28,6 +28,8 @@ for cmd in sys.argv[1:]:
if ser.in_waiting > 0: # Check if there is data in the buffer 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 data = ser.readline().decode('utf-8').strip() # Read and decode the data
print(data) print(data)
case "clean":
subprocess.call(["mpremote", "connect", port, "fs", "rm", ":/settings.json"])

View File

@@ -5,4 +5,5 @@ from settings import Settings
s = Settings() s = Settings()
name = s.get('name', 'led') name = s.get('name', 'led')
wifi.ap(name, '') password = s.get("ap_password", "")
wifi.ap(name, password)

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
import aioespnow import aioespnow
from settings import Settings, set_settings from settings import Settings
from web import web from web import web
from patterns import Patterns from patterns import Patterns
import gc import gc
@@ -8,51 +8,47 @@ import utime
import machine import machine
import time import time
import wifi import wifi
import json
from p2p import p2p
async def main(): async def main():
settings = Settings() settings = Settings()
patterns = Patterns(4, settings["num_leds"], selected=settings["pattern"]) patterns = Patterns(settings["led_pin"], settings["num_leds"], selected=settings["pattern"])
patterns.set_color1(tuple(int(settings["color1"][i:i+2], 16) for i in (1, 5, 3)))
patterns.set_color2(tuple(int(settings["color2"][i:i+2], 16) for i in (1, 5, 3))) patterns.colors = [(8,0,0)]
patterns.set_brightness(int(settings["brightness"]))
patterns.set_delay(int(settings["delay"])) # Initialize WDT only if debug is disabled
wdt = None
if not settings.get("debug", False):
wdt = machine.WDT(timeout=10000)
wdt.feed()
print("Watchdog timer enabled")
else:
print("Debug mode: Watchdog timer disabled")
async def system():
while True:
gc.collect()
if wdt is not None:
for i in range(60):
wdt.feed()
await asyncio.sleep(1)
else:
# If WDT is disabled, just sleep
await asyncio.sleep(60)
w = web(settings, patterns) w = web(settings, patterns)
print(settings) print(settings)
# start the server in a bacakground task # start the server in a bacakground task
print("Starting") print("Starting")
server = asyncio.create_task(w.start_server(host="0.0.0.0", port=80)) server = asyncio.create_task(w.start_server(host="0.0.0.0", port=80))
#wdt = machine.WDT(timeout=10000)
#wdt.feed()
async def tick(): asyncio.create_task(p2p(settings, patterns))
while True: asyncio.create_task(system())
patterns.tick() await patterns.run()
await asyncio.sleep_ms(1)
async def espnow():
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
e.active(True)
async for mac, msg in e:
print(msg)
set_settings(msg, settings, patterns)
asyncio.create_task(tick())
asyncio.create_task(espnow())
while True:
#print(time.localtime())
# gc.collect()
for i in range(60):
#wdt.feed()
await asyncio.sleep_ms(500)
# cleanup before ending the application # cleanup before ending the application
await server await server

16
src/p2p.py Normal file
View File

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

View File

@@ -2,290 +2,446 @@ from machine import Pin
from neopixel import NeoPixel from neopixel import NeoPixel
import utime import utime
import random import random
import _thread
import asyncio
from patterns_base import Patterns as PatternsBase
class Patterns: # Short-key parameter mapping for convenience setters
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100): param_mapping = {
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) "pt": "selected",
self.num_leds = num_leds "pa": "selected",
self.pattern_step = 0 "cl": "colors",
self.last_update = utime.ticks_ms() "br": "brightness",
self.delay = delay "dl": "delay",
self.brightness = brightness "nl": "num_leds",
"co": "color_order",
"lp": "led_pin",
"n1": "n1",
"n2": "n2",
"n3": "n3",
"n4": "n4",
"n5": "n5",
"n6": "n6",
"auto": "auto",
}
class Patterns(PatternsBase):
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
self.step = 0
self.patterns = { self.patterns = {
"off": self.off, "off": self.off,
"on" : self.on, "on" : self.on,
"color_wipe": self.color_wipe_step, "blink": self.blink,
"rainbow_cycle": self.rainbow_cycle_step, "rainbow": self.rainbow,
"theater_chase": self.theater_chase_step, "pulse": self.pulse,
"blink": self.blink_step, "transition": self.transition,
"random_color_wipe": self.random_color_wipe_step, "chase": self.n_chase,
"random_rainbow_cycle": self.random_rainbow_cycle_step, "n_chase": self.n_chase,
"random_theater_chase": self.random_theater_chase_step, "circle": self.circle,
"random_blink": self.random_blink_step,
"color_transition": self.color_transition_step,
"external": None
} }
self.selected = selected
self.color1 = color1
self.color2 = color2
self.transition_duration = 50 # Duration of color transition in milliseconds
self.transition_step = 0
def sync(self):
self.pattern_step=0
self.last_update = utime.ticks_ms()
def tick(self):
if self.patterns[self.selected]:
self.patterns[self.selected]()
def update_num_leds(self, pin, num_leds):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.pattern_step = 0
def set_delay(self, delay):
self.delay = delay
def set_brightness(self, brightness):
self.brightness = brightness
def set_color1(self, color):
print(color)
self.color1 = self.apply_brightness(color)
def set_color2(self, color):
self.color2 = self.apply_brightness(color)
def apply_brightness(self, color):
return tuple(int(c * self.brightness / 255) for c in color)
def select(self, pattern):
if pattern in self.patterns:
self.selected = pattern
return True
return False
def set(self, i, color):
self.n[i] = color
def write(self):
self.n.write()
def fill(self):
for i in range(self.num_leds):
self.n[i] = self.color1
self.n.write()
def off(self):
color = self.color1
self.color1 = (0,0,0)
self.fill()
self.color1 = color
def on(self):
color = self.color1
self.color1 = self.apply_brightness(self.color1)
self.fill()
self.color1 = color
def color_wipe_step(self): def blink(self):
color = self.apply_brightness(self.color1) self.stopped = False
current_time = utime.ticks_ms() self.running = True
if utime.ticks_diff(current_time, self.last_update) >= self.delay: state = True # True = on, False = off
if self.pattern_step < self.num_leds: last_update = utime.ticks_ms()
# Only continue running this pattern while it is the selected one
while self.running and self.selected == "blink":
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= self.delay:
if state:
self.fill(self.apply_brightness(self.colors[0]))
else:
self.fill((0, 0, 0))
state = not state
last_update = current_time
self.running = False
self.stopped = True
def rainbow(self):
self.stopped = False
self.running = True
step = self.step % 256
step_amount = max(1, int(self.n1)) # n1 controls step increment
# If auto is False, run once and update step
if not self.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.write()
# Increment step by n1 for next call
self.step = (step + step_amount) % 256
self.running = False
self.stopped = True
return
# Auto is True: run continuously
last_update = utime.ticks_ms()
# Only continue running this pattern while it is the selected one
while self.running and self.selected == "rainbow":
current_time = utime.ticks_ms()
sleep_ms = max(1, int(self.delay)) # Access delay directly
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
for i in range(self.num_leds): for i in range(self.num_leds):
self.n[i] = (0, 0, 0) rc_index = (i * 256 // self.num_leds) + step
self.n[self.pattern_step] = self.apply_brightness(color) self.n[i] = self.apply_brightness(self.wheel(rc_index & 255))
self.n.write() self.n.write()
self.pattern_step += 1 step = (step + step_amount) % 256
self.step = step
last_update = current_time
self.running = False
self.stopped = True
def pulse(self):
self.stopped = False
self.running = True
self.off()
# Get timing parameters, ensure non-negative
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
# Ensure we have at least one color
if not self.colors:
self.colors = [(255, 255, 255)]
color_index = 0
# Calculate minimum update interval based on LED count
# NeoPixel timing: ~30µs per LED + reset time = ~6ms for 200 LEDs
# Use 10ms minimum to ensure writes complete + overhead
min_write_time_ms = (self.num_leds * 30) // 1000 + 1 # Convert µs to ms, add 1ms overhead
update_interval = max(10, min_write_time_ms + 4) # At least 10ms, add margin for safety
# Only continue running this pattern while it is the selected one
while self.running and self.selected == "pulse":
cycle_start = utime.ticks_ms()
# Get the current color from the cycle
base_color = self.colors[color_index % len(self.colors)]
# Attack phase: fade from 0 to full brightness
if attack_ms > 0:
attack_start = utime.ticks_ms()
last_update = attack_start
while self.running and utime.ticks_diff(utime.ticks_ms(), attack_start) < attack_ms:
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= update_interval:
elapsed = utime.ticks_diff(now, attack_start)
brightness_factor = min(1.0, elapsed / attack_ms)
color = tuple(int(c * brightness_factor) for c in base_color)
self.fill(self.apply_brightness(color))
last_update = now
# Hold phase: maintain full brightness
if hold_ms > 0 and self.running:
self.fill(self.apply_brightness(base_color))
hold_start = utime.ticks_ms()
while self.running and utime.ticks_diff(utime.ticks_ms(), hold_start) < hold_ms:
pass
# Decay phase: fade from full brightness to 0
if decay_ms > 0:
decay_start = utime.ticks_ms()
last_update = decay_start
while self.running and utime.ticks_diff(utime.ticks_ms(), decay_start) < decay_ms:
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= update_interval:
elapsed = utime.ticks_diff(now, decay_start)
brightness_factor = max(0.0, 1.0 - (elapsed / decay_ms))
color = tuple(int(c * brightness_factor) for c in base_color)
self.fill(self.apply_brightness(color))
last_update = now
# Move to next color in the cycle
color_index += 1
# If auto flag is False, run only once and exit
if not self.auto:
break
# Ensure the cycle takes exactly delay milliseconds before restarting
if self.running:
self.off()
delay_ms = int(self.delay) # Access delay directly
wait_until = utime.ticks_add(cycle_start, delay_ms)
while self.running and utime.ticks_diff(wait_until, utime.ticks_ms()) > 0:
pass
self.running = False
self.stopped = True
def transition(self):
"""Transition between colors, taking delay ms between each color"""
self.stopped = False
self.running = True
if not self.colors:
# No colors, turn off
self.off()
self.running = False
self.stopped = True
return
if len(self.colors) == 1:
# Only one color, just stay that color
last_update = utime.ticks_ms()
while self.running and self.selected == "transition":
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= 100:
self.fill(self.apply_brightness(self.colors[0]))
last_update = current_time
self.running = False
self.stopped = True
return
# If auto is False, only transition between color1 and color2
if not self.auto:
if len(self.colors) < 2:
# Need at least 2 colors for transition
self.running = False
self.stopped = True
return
transition_start = utime.ticks_ms()
last_update = transition_start
while self.running:
# Access delay and colors directly for live updates
transition_duration = max(10, int(self.delay)) # At least 10ms
update_interval = max(10, transition_duration // 50) # Update every ~2% of transition
color1 = self.colors[0] if len(self.colors) > 0 else (0, 0, 0)
color2 = self.colors[1] if len(self.colors) > 1 else color1
if utime.ticks_diff(utime.ticks_ms(), transition_start) >= transition_duration:
break
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= update_interval:
# Calculate interpolation factor (0.0 to 1.0)
elapsed = utime.ticks_diff(now, transition_start)
factor = min(1.0, elapsed / transition_duration)
# Interpolate between color1 and color2
interpolated = tuple(
int(color1[i] + (color2[i] - color1[i]) * factor)
for i in range(3)
)
# Apply brightness and fill
self.fill(self.apply_brightness(interpolated))
last_update = now
self.running = False
self.stopped = True
return
# Auto is True: cycle through all colors continuously
color_index = 0
# Auto is True: cycle through all colors continuously
while self.running and self.selected == "transition":
# Access colors directly for live updates
if not self.colors:
break
# Get current and next color
current_color = self.colors[color_index % len(self.colors)]
next_color = self.colors[(color_index + 1) % len(self.colors)]
# Transition from current to next color
transition_start = utime.ticks_ms()
last_update = transition_start
while self.running:
# Access delay directly for live updates
transition_duration = max(10, int(self.delay)) # At least 10ms
update_interval = max(10, transition_duration // 50) # Update every ~2% of transition
if utime.ticks_diff(utime.ticks_ms(), transition_start) >= transition_duration:
break
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= update_interval:
# Calculate interpolation factor (0.0 to 1.0)
elapsed = utime.ticks_diff(now, transition_start)
factor = min(1.0, elapsed / transition_duration)
# Interpolate between colors
interpolated = tuple(
int(current_color[i] + (next_color[i] - current_color[i]) * factor)
for i in range(3)
)
# Apply brightness and fill
self.fill(self.apply_brightness(interpolated))
last_update = now
# Move to next color
color_index = (color_index + 1) % len(self.colors)
self.running = False
self.stopped = True
def n_chase(self):
"""N-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)"""
self.stopped = False
self.running = True
if len(self.colors) < 1:
# Need at least 1 color
self.running = False
self.stopped = True
return
segment_length = 0 # Will be calculated in loop
position = 0 # Current position offset
step_count = 0 # Track which step we're on
last_update = utime.ticks_ms()
# Only continue running this pattern while it is the selected one
# Note: this pattern can be selected as "n_chase" or "chase"
while self.running and self.selected in ("n_chase", "chase"):
# Access colors, delay, and n values directly for live updates
if not self.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]
else: else:
self.pattern_step = 0 color0 = self.colors[0]
self.last_update = current_time color1 = self.colors[1]
def rainbow_cycle_step(self): color0 = self.apply_brightness(color0)
current_time = utime.ticks_ms() color1 = self.apply_brightness(color1)
if utime.ticks_diff(current_time, self.last_update) >= self.delay/5:
def wheel(pos):
if pos < 85:
return (pos * 3, 255 - pos * 3, 0)
elif pos < 170:
pos -= 85
return (255 - pos * 3, 0, pos * 3)
else:
pos -= 170
return (0, pos * 3, 255 - pos * 3)
for i in range(self.num_leds): n1 = max(1, int(self.n1)) # LEDs of color 0
rc_index = (i * 256 // self.num_leds) + self.pattern_step n2 = max(1, int(self.n2)) # LEDs of color 1
self.n[i] = self.apply_brightness(wheel(rc_index & 255)) n3 = int(self.n3) # Step movement on odd steps (can be negative)
self.n.write() n4 = int(self.n4) # Step movement on even steps (can be negative)
self.pattern_step = (self.pattern_step + 1) % 256
self.last_update = current_time
def theater_chase_step(self): segment_length = n1 + n2
current_time = utime.ticks_ms() transition_duration = max(10, int(self.delay))
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
for i in range(self.num_leds):
if (i + self.pattern_step) % 3 == 0:
self.n[i] = self.apply_brightness(self.color1)
else:
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 3
self.last_update = current_time
def blink_step(self): current_time = utime.ticks_ms()
current_time = utime.ticks_ms() if utime.ticks_diff(current_time, last_update) >= transition_duration:
if utime.ticks_diff(current_time, self.last_update) >= self.delay: # Clear all LEDs
if self.pattern_step % 2 == 0: self.n.fill((0, 0, 0))
# Draw repeating pattern starting at position
for i in range(self.num_leds): for i in range(self.num_leds):
self.n[i] = self.apply_brightness(self.color1) # Calculate position in the repeating segment
else: relative_pos = (i - position) % segment_length
for i in range(self.num_leds): if relative_pos < 0:
self.n[i] = (0, 0, 0) relative_pos = (relative_pos + segment_length) % segment_length
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 2 # Determine which color based on position in segment
self.last_update = current_time if relative_pos < n1:
self.n[i] = color0
else:
self.n[i] = color1
def random_color_wipe_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
if self.pattern_step < self.num_leds:
for i in range(self.num_leds):
self.n[i] = (0, 0, 0)
self.n[self.pattern_step] = self.apply_brightness(color)
self.n.write() self.n.write()
self.pattern_step += 1
else:
self.pattern_step = 0
self.last_update = current_time
def random_rainbow_cycle_step(self): # Move position by n3 or n4 on alternate steps
current_time = utime.ticks_ms() if step_count % 2 == 0:
if utime.ticks_diff(current_time, self.last_update) >= self.delay: position = position + n3
def wheel(pos):
if pos < 85:
return (pos * 3, 255 - pos * 3, 0)
elif pos < 170:
pos -= 85
return (255 - pos * 3, 0, pos * 3)
else: else:
pos -= 170 position = position + n4
return (0, pos * 3, 255 - pos * 3)
random_offset = random.randint(0, 255) # Wrap position to keep it reasonable
for i in range(self.num_leds): max_pos = self.num_leds + segment_length
rc_index = (i * 256 // self.num_leds) + self.pattern_step + random_offset position = position % max_pos
self.n[i] = self.apply_brightness(wheel(rc_index & 255)) if position < 0:
self.n.write() position += max_pos
self.pattern_step = (self.pattern_step + 1) % 256
self.last_update = current_time
def random_theater_chase_step(self): step_count += 1
current_time = utime.ticks_ms() last_update = current_time
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
for i in range(self.num_leds):
if (i + self.pattern_step) % 3 == 0:
self.n[i] = self.apply_brightness(color)
else:
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 3
self.last_update = current_time
def random_blink_step(self): self.running = False
current_time = utime.ticks_ms() self.stopped = True
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
if self.pattern_step % 2 == 0:
for i in range(self.num_leds):
self.n[i] = self.apply_brightness(color)
else:
for i in range(self.num_leds):
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 2
self.last_update = current_time
def color_transition_step(self): def circle(self):
current_time = utime.ticks_ms() """Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4"""
if utime.ticks_diff(current_time, self.last_update) >= self.delay: self.stopped = False
# Calculate transition factor based on elapsed time self.running = True
transition_factor = (self.pattern_step * 100) / self.transition_duration head = 0
if transition_factor > 100: tail = 0
transition_factor = 100
color = self.interpolate_color(self.color1, self.color2, transition_factor / 100)
# Apply the interpolated color to all LEDs # Calculate timing
for i in range(self.num_leds): head_rate = max(1, int(self.n1)) # n1 = head moves per second
self.n[i] = self.apply_brightness(color) tail_rate = max(1, int(self.n3)) # n3 = tail moves per second
self.n.write() max_length = max(1, int(self.n2)) # n2 = max length
min_length = max(0, int(self.n4)) # n4 = min length
self.pattern_step += self.delay head_delay = 1000 // head_rate # ms between head movements
if self.pattern_step > self.transition_duration: tail_delay = 1000 // tail_rate # ms between tail movements
self.pattern_step = 0
self.last_update = current_time last_head_move = utime.ticks_ms()
last_tail_move = utime.ticks_ms()
def interpolate_color(self, color1, color2, factor): phase = "growing" # "growing", "shrinking", or "off"
return (
int(color1[0] + (color2[0] - color1[0]) * factor),
int(color1[1] + (color2[1] - color1[1]) * factor),
int(color1[2] + (color2[2] - color1[2]) * factor)
)
def two_steps_forward_one_step_back_step(self): # Only continue running this pattern while it is the selected one
current_time = utime.ticks_ms() while self.running and self.selected == "circle":
if utime.ticks_diff(current_time, self.last_update) >= self.delay: current_time = utime.ticks_ms()
# Move forward 2 steps and backward 1 step
if self.direction == 1: # Moving forward
if self.scanner_position < self.num_leds - 2:
self.scanner_position += 2 # Move forward 2 steps
else:
self.direction = -1 # Change direction to backward
else: # Moving backward
if self.scanner_position > 0:
self.scanner_position -= 1 # Move backward 1 step
else:
self.direction = 1 # Change direction to forward
# Set all LEDs to off # Clear all LEDs
for i in range(self.num_leds): self.n.fill((0, 0, 0))
self.n[i] = (0, 0, 0)
# Set the current position to the color # Calculate segment length
self.n[self.scanner_position] = self.apply_brightness(self.color1) segment_length = (head - tail) % self.num_leds
if segment_length == 0 and head != tail:
segment_length = self.num_leds
# Apply the color transition # Draw segment from tail to head
transition_factor = (self.pattern_step * 100) / self.transition_duration color = self.apply_brightness(self.colors[0])
if transition_factor > 100: for i in range(segment_length + 1):
transition_factor = 100 led_pos = (tail + i) % self.num_leds
color = self.interpolate_color(self.color1, self.color2, transition_factor / 100) self.n[led_pos] = color
self.n[self.scanner_position] = self.apply_brightness(color)
# Move head continuously at n1 LEDs per second
if utime.ticks_diff(current_time, last_head_move) >= head_delay:
head = (head + 1) % self.num_leds
last_head_move = current_time
# Tail behavior based on phase
if phase == "growing":
# Growing phase: tail stays at 0 until max length reached
if segment_length >= max_length:
phase = "shrinking"
elif phase == "shrinking":
# Shrinking phase: move tail forward at n3 LEDs per second
if utime.ticks_diff(current_time, last_tail_move) >= tail_delay:
tail = (tail + 1) % self.num_leds
last_tail_move = current_time
# Check if we've reached min length
current_length = (head - tail) % self.num_leds
if current_length == 0 and head != tail:
current_length = self.num_leds
# For min_length = 0, we need at least 1 LED (the head)
if min_length == 0 and current_length <= 1:
phase = "off" # All LEDs off for 1 step
elif min_length > 0 and current_length <= min_length:
phase = "growing" # Cycle repeats
else: # phase == "off"
# Off phase: all LEDs off for 1 step, then restart
tail = head # Reset tail to head position to start fresh
phase = "growing"
self.n.write() self.n.write()
self.pattern_step += self.delay
if self.pattern_step > self.transition_duration:
self.pattern_step = 0
self.last_update = current_time self.running = False
self.stopped = True
if __name__ == "__main__":
p = Patterns(4, 180)
p.set_color1((255,0,0))
p.set_color2((0,255,0))
#p.set_delay(10)
try:
while True:
for key in p.patterns:
print(key)
p.select(key)
for _ in range(2000):
p.tick()
utime.sleep_ms(1)
except KeyboardInterrupt:
p.fill((0, 0, 0))

167
src/patterns_base.py Normal file
View File

@@ -0,0 +1,167 @@
from machine import Pin
from neopixel import NeoPixel
import utime
import random
import _thread
import asyncio
import json
# Short-key parameter mapping for convenience setters
param_mapping = {
"pt": "selected",
"pa": "selected",
"cl": "colors",
"br": "brightness",
"dl": "delay",
"nl": "num_leds",
"co": "color_order",
"lp": "led_pin",
"n1": "n1",
"n2": "n2",
"n3": "n3",
"n4": "n4",
"n5": "n5",
"n6": "n6",
"auto": "auto",
}
class Patterns:
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", delay=100):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.pattern_step = 0
self.last_update = utime.ticks_ms()
self.delay = delay
self.brightness = brightness
self.auto = False
self.patterns = {}
self.selected = selected
# Ensure colors list always starts with at least two for robust transition handling
self.colors = [color1, color2] if color1 != color2 else [color1, (255, 255, 255)] # Fallback if initial colors are same
if not self.colors: # Ensure at least one color exists
self.colors = [(0, 0, 0)]
self.transition_duration = delay * 50 # Default transition duration
self.hold_duration = delay * 10 # Default hold duration at each color
self.transition_step = 0 # Current step in the transition
self.current_color_idx = 0 # Index of the color currently being held/transitioned from
self.current_color = self.colors[self.current_color_idx] # The actual blended color
self.hold_start_time = utime.ticks_ms() # Time when the current color hold started
# New attributes for scanner patterns
self.scanner_direction = 1 # 1 for forward, -1 for backward
self.scanner_tail_length = 3 # Number of trailing pixels
self.running = False
self.stopped = True
self.n1 = 0
self.n2 = 0
self.n3 = 0
self.n4 = 0
self.n5 = 0
self.n6 = 0
def select(self, pattern):
if pattern in self.patterns:
self.selected = pattern
return True
# If pattern doesn't exist, default to "off"
if "off" in self.patterns:
self.selected = "off"
return False
async def run(self):
await self.stop()
# Ensure we wait a bit more to let the thread fully terminate
# If selected pattern doesn't exist, default to "off"
if self.selected not in self.patterns:
print(f"Pattern {self.selected} not found, defaulting to 'off'")
if "off" in self.patterns:
self.selected = "off"
else:
print("No patterns available")
self.running = False
self.stopped = True
return
print(f"Starting pattern {self.selected}")
_thread.start_new_thread(self.patterns[self.selected], ())
async def stop(self):
if not self.running:
# Already stopped
self.stopped = True
return
self.running = False
start = utime.ticks_ms()
timeout = 2000 # Increased timeout to 2 seconds
while not self.stopped and utime.ticks_diff(utime.ticks_ms(), start) < timeout:
await asyncio.sleep_ms(10) # Check every 10ms instead of 0ms
if not self.stopped:
# Timeout reached, force stop
print("Warning: Pattern did not stop within timeout")
self.stopped = True
def set_param(self, key, value):
if key in param_mapping:
setattr(self, param_mapping[key], value)
return True
print(f"Invalid parameter: {key}")
return False
def update_num_leds(self, pin, num_leds):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.pattern_step = 0
def set_color(self, num, color):
# Changed: More robust index check
if 0 <= num < len(self.colors):
self.colors[num] = color
# If the changed color is part of the current or next transition,
# restart the transition for smoother updates
return True
elif num == len(self.colors): # Allow setting a new color at the end
self.colors.append(color)
return True
return False
def del_color(self, num):
# Changed: More robust index check and using del for lists
if 0 <= num < len(self.colors):
del self.colors[num]
return True
return False
def apply_brightness(self, color, brightness_override=None):
effective_brightness = brightness_override if brightness_override is not None else self.brightness
return tuple(int(c * effective_brightness / 255) for c in color)
def fill(self, color=None):
fill_color = color if color is not None else self.colors[0]
for i in range(self.num_leds):
self.n[i] = fill_color
self.n.write()
def off(self):
self.fill((0, 0, 0))
def on(self):
self.fill(self.apply_brightness(self.colors[0]))
def wheel(self, pos):
if pos < 85:
return (pos * 3, 255 - pos * 3, 0)
elif pos < 170:
pos -= 85
return (255 - pos * 3, 0, pos * 3)
else:
pos -= 170
return (0, pos * 3, 255 - pos * 3)

31
src/presets.py Normal file
View File

@@ -0,0 +1,31 @@
import json
import wifi
import ubinascii
import machine
class Presets(dict):
FILE = "/presets.json"
def __init__(self):
super().__init__()
self.load() # Load settings from file during initialization
def save(self):
try:
j = json.dumps(self)
with open(self.FILE, 'w') as file:
file.write(j)
print("Presets saved successfully.")
except Exception as e:
print(f"Error saving settings: {e}")
def load(self):
try:
with open(self.FILE, 'r') as file:
self.update(json.load(file))
print("Presets loaded successfully.")
except Exception as e:
print(f"Error loading presets")
self.save()

View File

@@ -9,16 +9,19 @@ class Settings(dict):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.load() # Load settings from file during initialization self.load() # Load settings from file during initialization
self.color_order = self.get_color_order(self["color_order"])
def set_defaults(self): def set_defaults(self):
self["led_pin"] = 10
self["num_leds"] = 50 self["num_leds"] = 50
self["pattern"] = "on" self["pattern"] = "on"
self["color1"] = "#00ff00"
self["color2"] = "#ff0000"
self["delay"] = 100 self["delay"] = 100
self["brightness"] = 10 self["brightness"] = 10
self["color_order"] = "rgb"
self["name"] = f"led-{ubinascii.hexlify(wifi.get_mac()).decode()}" self["name"] = f"led-{ubinascii.hexlify(wifi.get_mac()).decode()}"
self["ap_password"] = "" self["ap_password"] = ""
self["id"] = 0
self["debug"] = False
def save(self): def save(self):
try: try:
@@ -38,6 +41,76 @@ class Settings(dict):
except Exception as e: except Exception as e:
print(f"Error loading settings") print(f"Error loading settings")
self.set_defaults() self.set_defaults()
self.save()
async def set_settings(self, data, patterns, save):
try:
print(f"Setting settings: {data}")
for key, value in data.items():
print(key, value)
if key == "colors":
buff = []
for color in value:
buff.append(tuple(int(color[i:i+2], 16) for i in self.color_order))
patterns.colors = buff
elif key == "num_leds":
patterns.update_num_leds(self["led_pin"], value)
elif key == "pattern":
if not patterns.select(value):
return "Pattern doesn't exist", 400
await patterns.run()
elif key == "delay":
delay = int(data["delay"])
patterns.delay = delay
elif key == "brightness":
brightness = int(data["brightness"])
patterns.brightness = brightness
elif key == "n1":
patterns.n1 = value
elif key == "n2":
patterns.n2 = value
elif key == "n3":
patterns.n3 = value
elif key == "n4":
patterns.n4 = value
elif key == "n5":
patterns.n5 = value
elif key == "n6":
patterns.n6 = value
elif key == "name":
self[key] = value
self.save()
machine.reset()
elif key == "color_order":
self["color_order"] = value
self.color_order = self.get_color_order(value)
pass
elif key == "id":
pass
elif key == "led_pin":
patterns.update_num_leds(value, self["num_leds"])
else:
return "Invalid key", 400
self[key] = value
#print(self)
if save:
self.save()
print(self)
return "OK", 200
except (KeyError, ValueError):
return "Bad request", 400
def get_color_order(self, color_order):
"""Convert color order string to tuple of hex string indices."""
color_orders = {
"rgb": (1, 3, 5),
"rbg": (1, 5, 3),
"grb": (3, 1, 5),
"gbr": (3, 5, 1),
"brg": (5, 1, 3),
"bgr": (5, 3, 1)
}
return color_orders.get(color_order.lower(), (1, 3, 5)) # Default to RGB
# Example usage # Example usage
def main(): def main():
@@ -52,42 +125,7 @@ def main():
print(f"Loaded number of LEDs: {new_settings['num_leds']}") print(f"Loaded number of LEDs: {new_settings['num_leds']}")
print(settings) print(settings)
def set_settings(raw_json, settings, patterns):
try:
data = json.loads(raw_json)
print(data)
for key, value in data.items():
print(key, value)
if key == "color1":
patterns.set_color1(tuple(int(value[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
elif key == "color2":
patterns.set_color2(tuple(int(value[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
elif key == "num_leds":
patterns.update_num_leds(4, value)
elif key == "pattern":
if not patterns.select(value):
return "Pattern doesn't exist", 400
elif key == "delay":
delay = int(data["delay"])
patterns.set_delay(delay)
elif key == "brightness":
brightness = int(data["brightness"])
patterns.set_brightness(brightness)
elif key == "name":
settings[key] = value
settings.save()
machine.reset()
elif key == "sync":
patterns.sync()
return "OK", 200
else:
return "Invalid key", 400
settings[key] = value
settings.save()
return "OK", 200
except (KeyError, ValueError):
return "Bad request", 400
# Run the example # Run the example
if __name__ == "__main__": if __name__ == "__main__":

View File

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

View File

@@ -1,8 +1,72 @@
let delayTimeout; let delayTimeout;
let brightnessTimeout; let brightnessTimeout;
let colorTimeout; let colorsTimeout;
let color2Timeout; let ws; // Variable to hold the WebSocket connection
let connectionStatusElement; // Variable to hold the connection status element
// Function to update the connection status indicator
function updateConnectionStatus(status) {
if (!connectionStatusElement) {
connectionStatusElement = document.getElementById("connection-status");
}
if (connectionStatusElement) {
connectionStatusElement.className = ""; // Clear existing classes
connectionStatusElement.classList.add(status);
// Optionally, you could also update text content based on status
// connectionStatusElement.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
}
// Function to establish WebSocket connection
function connectWebSocket() {
// Determine the WebSocket URL based on the current location
const wsUrl = `ws://${window.location.host}/ws`;
ws = new WebSocket(wsUrl);
updateConnectionStatus("connecting"); // Indicate connecting state
ws.onopen = function (event) {
console.log("WebSocket connection opened:", event);
updateConnectionStatus("open"); // Indicate open state
// Optionally, you could send an initial message here
};
ws.onmessage = function (event) {
console.log("WebSocket message received:", event.data);
};
ws.onerror = function (event) {
console.error("WebSocket error:", event);
updateConnectionStatus("closed"); // Indicate error state (treat as closed)
};
ws.onclose = function (event) {
if (event.wasClean) {
console.log(
`WebSocket connection closed cleanly, code=${event.code}, reason=${event.reason}`,
);
updateConnectionStatus("closed"); // Indicate closed state
} else {
console.error("WebSocket connection died");
updateConnectionStatus("closed"); // Indicate closed state
}
// Attempt to reconnect after a delay
setTimeout(connectWebSocket, 1000);
};
}
// Function to send data over WebSocket
function sendWebSocketData(data) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log("Sending data over WebSocket:", data);
ws.send(JSON.stringify(data));
} else {
console.error("WebSocket is not connected. Cannot send data:", data);
// You might want to queue messages or handle this in a different way
}
}
// Keep the post and get functions for now, they might still be useful
async function post(path, data) { async function post(path, data) {
console.log(`POST to ${path}`, data); console.log(`POST to ${path}`, data);
try { try {
@@ -11,7 +75,7 @@ async function post(path, data) {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(data), // Convert data to JSON string body: JSON.stringify(data),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
@@ -27,62 +91,124 @@ async function get(path) {
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
} }
return await response.json(); // Assuming you are expecting JSON response return await response.json();
} catch (error) { } catch (error) {
console.error("Error during GET request:", error); console.error("Error during GET request:", error);
} }
} }
async function updateColor(event) { function updateColors() {
event.preventDefault(); clearTimeout(colorsTimeout);
clearTimeout(colorTimeout); colorsTimeout = setTimeout(function () {
colorTimeout = setTimeout(async function () { const colorInputs = document.querySelectorAll(".color-input");
const color = document.getElementById("color").value; const colors = Array.from(colorInputs).map(input => input.value);
await post("settings", { color1: color }); // Send as JSON sendWebSocketData({ colors: colors });
}, 500); }, 500);
} }
async function updateColor2(event) { function addColorInput(color = "#ff0000") {
event.preventDefault(); const container = document.getElementById("colors_container");
clearTimeout(color2Timeout); const colorDiv = document.createElement("div");
color2Timeout = setTimeout(async function () { colorDiv.className = "color-item";
const color = document.getElementById("color2").value;
await post("/settings", { color2: color }); // Send as JSON const colorInput = document.createElement("input");
}, 500); colorInput.type = "color";
colorInput.className = "color-input";
colorInput.value = color;
colorInput.addEventListener("input", updateColors);
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.textContent = "×";
removeBtn.className = "remove-color-btn";
removeBtn.addEventListener("click", function() {
colorDiv.remove();
updateColors();
});
colorDiv.appendChild(colorInput);
colorDiv.appendChild(removeBtn);
container.appendChild(colorDiv);
}
function initializeColors(initialColors = null) {
const container = document.getElementById("colors_container");
container.innerHTML = "";
// Get initial colors from data attribute or use defaults
if (initialColors === null) {
const colorsData = document.getElementById("colors_container").dataset.colors;
if (colorsData) {
try {
initialColors = JSON.parse(colorsData);
} catch (e) {
initialColors = ["#ff0000", "#00ff00"];
}
} else {
initialColors = ["#ff0000", "#00ff00"];
}
}
if (initialColors.length === 0) {
initialColors = ["#ff0000"];
}
initialColors.forEach(color => addColorInput(color));
} }
async function updatePattern(pattern) { async function updatePattern(pattern) {
await post("/settings", { pattern: pattern }); // Send as JSON sendWebSocketData({ pattern: pattern });
} }
async function updateBrightness(event) { async function updateBrightness(event) {
event.preventDefault(); event.preventDefault();
clearTimeout(brightnessTimeout); clearTimeout(brightnessTimeout);
brightnessTimeout = setTimeout(async function () { brightnessTimeout = setTimeout(function () {
const brightness = document.getElementById("brightness").value; const brightness = document.getElementById("brightness").value;
await post("/settings", { brightness: brightness }); // Send as JSON sendWebSocketData({ brightness: brightness });
}, 500); }, 500);
} }
async function updateDelay(event) { async function updateDelay(event) {
event.preventDefault(); event.preventDefault();
clearTimeout(delayTimeout); clearTimeout(delayTimeout);
delayTimeout = setTimeout(async function () { delayTimeout = setTimeout(function () {
const delay = document.getElementById("delay").value; const delay = document.getElementById("delay").value;
await post("/settings", { delay: delay }); // Send as JSON sendWebSocketData({ delay: delay });
}, 500); }, 500);
} }
async function updateNumLeds(event) { async function updateNumLeds(event) {
event.preventDefault(); event.preventDefault();
const numLeds = document.getElementById("num_leds").value; const numLeds = document.getElementById("num_leds").value;
await post("/settings", { num_leds: parseInt(numLeds) }); // Send as JSON sendWebSocketData({ num_leds: parseInt(numLeds) });
} }
async function updateName(event) { async function updateName(event) {
event.preventDefault(); event.preventDefault();
const name = document.getElementById("name").value; const name = document.getElementById("name").value;
await post("/settings", { name: name }); // Send as JSON sendWebSocketData({ name: name });
}
async function updateID(event) {
event.preventDefault();
const id = document.getElementById("id").value;
sendWebSocketData({ id: parseInt(id) });
}
async function updateLedPin(event) {
event.preventDefault();
const ledpin = document.getElementById("led_pin").value;
sendWebSocketData({ led_pin: parseInt(ledpin) });
}
function handleRadioChange(event) {
event.preventDefault();
console.log("Selected color order:", event.target.value);
// Add your specific logic here
if (event.target.value === "rgb") {
console.log("RGB order selected!");
} else if (event.target.value === "rbg") {
console.log("RBG order selected!");
}
sendWebSocketData({ color_order: event.target.value });
} }
function createPatternButtons(patterns) { function createPatternButtons(patterns) {
@@ -91,7 +217,7 @@ function createPatternButtons(patterns) {
patterns.forEach((pattern) => { patterns.forEach((pattern) => {
const button = document.createElement("button"); const button = document.createElement("button");
button.type = "button"; // Use 'button' instead of 'submit' button.type = "button";
button.textContent = pattern; button.textContent = pattern;
button.value = pattern; button.value = pattern;
button.addEventListener("click", async function (event) { button.addEventListener("click", async function (event) {
@@ -103,8 +229,17 @@ function createPatternButtons(patterns) {
} }
document.addEventListener("DOMContentLoaded", async function () { document.addEventListener("DOMContentLoaded", async function () {
document.getElementById("color").addEventListener("input", updateColor); // Get the connection status element once the DOM is ready
document.getElementById("color2").addEventListener("input", updateColor2); connectionStatusElement = document.getElementById("connection-status");
// Establish WebSocket connection on page load
connectWebSocket();
// Initialize colors palette
initializeColors();
document.getElementById("add_color_btn").addEventListener("click", function() {
addColorInput();
});
document.getElementById("delay").addEventListener("input", updateDelay); document.getElementById("delay").addEventListener("input", updateDelay);
document document
.getElementById("brightness") .getElementById("brightness")
@@ -113,11 +248,17 @@ document.addEventListener("DOMContentLoaded", async function () {
.getElementById("num_leds_form") .getElementById("num_leds_form")
.addEventListener("submit", updateNumLeds); .addEventListener("submit", updateNumLeds);
document.getElementById("name_form").addEventListener("submit", updateName); document.getElementById("name_form").addEventListener("submit", updateName);
document.getElementById("id_form").addEventListener("submit", updateID);
document
.getElementById("led_pin_form")
.addEventListener("submit", updateLedPin);
document.getElementById("delay").addEventListener("touchend", updateDelay); document.getElementById("delay").addEventListener("touchend", updateDelay);
document document
.getElementById("brightness") .getElementById("brightness")
.addEventListener("touchend", updateBrightness); .addEventListener("touchend", updateBrightness);
document.getElementById("rgb").addEventListener("change", handleRadioChange);
document.getElementById("rbg").addEventListener("change", handleRadioChange);
document.querySelectorAll(".pattern_button").forEach((button) => { document.querySelectorAll(".pattern_button").forEach((button) => {
console.log(button.value); console.log(button.value);
button.addEventListener("click", async (event) => { button.addEventListener("click", async (event) => {

View File

@@ -1,15 +1,15 @@
{% args settings, patterns, mac %} {% args settings, patterns, colors_json, mac %}
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LED Control</title> <title>{{settings['name']}}</title>
<script src="static/main.js"></script> <script src="static/main.js"></script>
<link rel="stylesheet" href="static/main.css" /> <link rel="stylesheet" href="static/main.css" />
</head> </head>
<body> <body>
<h1>Control LEDs</h1> <h1>{{settings['name']}}</h1>
<button onclick="selectControls()">Controls</button> <button onclick="selectControls()">Controls</button>
<button onclick="selectSettings()">Settings</button> <button onclick="selectSettings()">Settings</button>
@@ -46,22 +46,13 @@
step="1" step="1"
/> />
</form> </form>
<form id="color_form" method="post" action="/color"> <div id="colors_palette">
<input <label>Colors:</label>
type="color" <div id="colors_container" data-colors='{{colors_json}}'>
id="color" <!-- Color inputs will be added here dynamically -->
name="color" </div>
value="{{settings['color1']}}" <button type="button" id="add_color_btn">+ Add Color</button>
/> </div>
</form>
<form id="color2_form" method="post" action="/color2">
<input
type="color"
id="color2"
name="color2"
value="{{settings['color2']}}"
/>
</form>
</div> </div>
<!-- Settings Menu for num_leds, Wi-Fi SSID, and Password --> <!-- Settings Menu for num_leds, Wi-Fi SSID, and Password -->
@@ -79,6 +70,16 @@
/> />
<input type="submit" value="Update Name" /> <input type="submit" value="Update Name" />
</form> </form>
<form id="id_form" method="post" action="/id">
<label for="id">ID:</label>
<input
type="text"
id="id"
name="id"
value="{{settings['id']}}"
/>
<input type="submit" value="Update ID" />
</form>
<!-- Separate form for submitting num_leds --> <!-- Separate form for submitting num_leds -->
<form id="num_leds_form" method="post" action="/num_leds"> <form id="num_leds_form" method="post" action="/num_leds">
<label for="num_leds">Number of LEDs:</label> <label for="num_leds">Number of LEDs:</label>
@@ -90,7 +91,25 @@
/> />
<input type="submit" value="Update Number of LEDs" /> <input type="submit" value="Update Number of LEDs" />
</form> </form>
<form id="led_pin_form" method="post" action="/led_pin">
<label for="num_leds">Led pin:</label>
<input
type="text"
id="led_pin"
name="led_pin"
value="{{settings['led_pin']}}"
/>
<input type="submit" value="Update Led Pin" />
</form>
<form id="color_order_form">
<label for="rgb">RGB:</label>
<input type="radio" id="rgb" name="color_order" value="rgb" {{'checked' if settings["color_order"]=="rgb" else ''}} />
<label for="rbg">RBG</label>
<input type="radio" id="rbg" name="color_order" value="rbg" {{'checked' if settings["color_order"]=="rbg" else ''}}/>
</form>
<p>Mac address: {{mac}}</p> <p>Mac address: {{mac}}</p>
</div> </div>
<div id="connection-status"></div>
</body> </body>
</html> </html>

View File

@@ -1,94 +0,0 @@
# Autogenerated file
def render(settings, patterns):
yield """<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>LED Control</title>
<script src=\"static/main.js\"></script>
<link rel=\"stylesheet\" href=\"static/main.css\">
</head>
<body>
<h1>Control LEDs</h1>
<button onclick=\"selectControls()\">Controls</button>
<button onclick=\"selectSettings()\">Settings</button>
<!-- Main LED Controls -->
<div id=\"controls\">
<div id=\"pattern_buttons\">
"""
for p in patterns:
yield """ <button class=\"pattern_button\" value=\""""
yield str(p)
yield """\">"""
yield str(p)
yield """</button>
"""
yield """
<!-- Pattern buttons will be inserted here -->
</div>
<form id=\"delay_form\" method=\"post\" action=\"/delay\">
<label for=\"delay\">Delay:</label>
<input type=\"range\" id=\"delay\" name=\"delay\" min=\"1\" max=\"1000\" value=\""""
yield str(settings['delay'])
yield """\" step=\"10\">
</form>
<form id=\"brightness_form\" method=\"post\" action=\"/brightness\">
<label for=\"brightness\">Brightness:</label>
<input type=\"range\" id=\"brightness\" name=\"brightness\" min=\"0\" max=\"100\" value=\""""
yield str(settings['brightness'])
yield """\" step=\"1\">
</form>
<form id=\"color_form\" method=\"post\" action=\"/color\">
<input type=\"color\" id=\"color\" name=\"color\" value=\""""
yield str(settings['color1'])
yield """\">
</form>
<form id=\"color2_form\" method=\"post\" action=\"/color2\">
<input type=\"color\" id=\"color2\" name=\"color2\" value=\""""
yield str(settings['color2'])
yield """\">
</form>
</div>
<!-- Settings Menu for num_leds, Wi-Fi SSID, and Password -->
<div id=\"settings_menu\" style=\"display: none;\">
<h2>Settings</h2>
<!-- Separate form for submitting num_leds -->
<form id=\"num_leds_form\" method=\"post\" action=\"/num_leds\">
<label for=\"num_leds\">Number of LEDs:</label>
<input type=\"text\" id=\"num_leds\" name=\"num_leds\" value=\""""
yield str(settings['num_leds'])
yield """\">
<input type=\"submit\" value=\"Update Number of LEDs\">
</form>
<!-- Form for Wi-Fi SSID and password -->
<form id=\"wifi_form\" method=\"post\" action=\"/wifi_settings\">
<label for=\"ssid\">Wi-Fi SSID:</label>
<input type=\"text\" id=\"ssid\" name=\"ssid\" value=\""""
yield str(settings['wifi']['ssid'])
yield """\">
<br>
<label for=\"password\">Wi-Fi Password:</label>
<input type=\"password\" id=\"password\" name=\"password\">
<br>
<label for=\"ip\">Wi-Fi IP:</label>
<input type=\"ip\" id=\"ip\" name=\"ip\" value=\""""
yield str(settings.get('wifi', {}).get('ip', ''))
yield """\">
<br>
<label for=\"gateway\">Wi-Fi Gateway:</label>
<input type=\"gateway\" id=\"gateway\" name=\"gateway\" value=\""""
yield str(settings.get('wifi', {}).get('gateway', ''))
yield """\">
<br>
<input type=\"submit\" value=\"Save Wi-Fi Settings\">
</form>
</div>
</body>
</html>
"""

View File

@@ -2,7 +2,6 @@ from microdot import Microdot, send_file, Response
from microdot.utemplate import Template from microdot.utemplate import Template
from microdot.websocket import with_websocket from microdot.websocket import with_websocket
import machine import machine
from settings import set_settings
import wifi import wifi
import json import json
@@ -13,7 +12,19 @@ def web(settings, patterns):
@app.route('/') @app.route('/')
async def index_hnadler(request): async def index_hnadler(request):
mac = wifi.get_mac().hex() mac = wifi.get_mac().hex()
return Template('/index.html').render(settings=settings, patterns=patterns.patterns.keys(), mac=mac) # Convert colors from RGB tuples to hex strings for display
colors_hex = []
for color in patterns.colors:
# Convert (R, G, B) tuple to #RRGGBB hex string
colors_hex.append(f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}")
# Convert to JSON string for data attribute
colors_json = json.dumps(colors_hex)
return Template('index.html').render(
settings,
patterns.patterns.keys(),
colors_json,
mac
)
@app.route("/static/<path:path>") @app.route("/static/<path:path>")
def static_handler(request, path): def static_handler(request, path):
@@ -24,18 +35,21 @@ def web(settings, patterns):
@app.post("/settings") @app.post("/settings")
def settings_handler(request): def settings_handler(request):
return set_settings(request.body.decode('utf-8'), settings, patterns) # Keep the POST handler for compatibility or alternative usage if needed
# For WebSocket updates, the /ws handler is now primary
return settings.set_settings(request.body.decode('utf-8'), patterns)
@app.route("/ws")
@app.route("/external")
@with_websocket @with_websocket
async def ws(request, ws): async def ws(request, ws):
patterns.select("external")
while True: while True:
data = await ws.receive() data = await ws.receive()
print(data) if data:
for i in range(min(patterns.num_leds, int(len(data)/3))):
patterns.set(i, (data[i*3], data[i*3+1], data[i*3+2])) # Process the received data
patterns.write() _, status_code = await settings.set_settings(json.loads(data), patterns, True)
#await ws.send(status_code)
else:
break
return app return app

View File

@@ -1,18 +1,16 @@
import network import network
from machine import Pin
from time import sleep from time import sleep
import ubinascii
from settings import Settings
def connect(ssid, password, ip, gateway): def connect(ssid, password, ip, gateway):
if ssid is None or password is None:
print("Missing ssid or password")
return None
try: try:
sta_if = network.WLAN(network.STA_IF) sta_if = network.WLAN(network.STA_IF)
if ip is not None and gateway is not None:
sta_if.ifconfig((ip, '255.255.255.0', gateway, '1.1.1.1'))
if not sta_if.isconnected(): if not sta_if.isconnected():
if ssid == "" or password == "":
print("Missing ssid or password")
return None
if ip != "" and gateway != "":
sta_if.ifconfig((ip, '255.255.255.0', gateway, '1.1.1.1'))
print('connecting to network...') print('connecting to network...')
sta_if.active(True) sta_if.active(True)
sta_if.connect(ssid, password) sta_if.connect(ssid, password)
@@ -26,21 +24,16 @@ def connect(ssid, password, ip, gateway):
return None return None
def ap(password): def ap(ssid, password):
ap_if = network.WLAN(network.AP_IF) ap_if = network.WLAN(network.AP_IF)
ap_mac = ap_if.config('mac') ap_mac = ap_if.config('mac')
ssid = f"led-{ubinascii.hexlify(ap_mac).decode()}"
print(ssid) print(ssid)
ap_if.active(True) ap_if.active(True)
ap_if.config(essid=ssid, password="qwerty1234") ap_if.config(essid=ssid, password=password)
ap_if.active(False) ap_if.active(False)
ap_if.active(True) ap_if.active(True)
print(ap_if.ifconfig()) print(ap_if.ifconfig())
def get_mac():
ap_if = network.WLAN(network.AP_IF)
return ap_if.config('mac')

63
test/circle.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
Circle test: n1=50, n2=100, n3=200, n4=0 (Red)
Runs forever
Run with: mpremote run test/circle.py
"""
import patterns
import utime
import _thread
from settings import Settings
from machine import WDT
print("Starting Circle Test: n1=50, n2=100, n3=200, n4=0 (Red)")
print("Press Ctrl+C to stop")
# Load settings
settings = Settings()
# Initialize patterns using settings
p = patterns.Patterns(
pin=settings["led_pin"],
num_leds=settings["num_leds"],
brightness=255,
delay=2000
)
# Configure test parameters
p.n1 = 50 # Head moves 50 LEDs/second
p.n2 = 100 # Max length 100 LEDs
p.n3 = 200 # Tail moves 200 LEDs/second
p.n4 = 0 # Min length 0 LEDs
p.colors = [(255, 0, 0)] # Red
print(f"LED Pin: {settings['led_pin']}")
print(f"LEDs: {settings['num_leds']}")
print(f"Brightness: {p.brightness}")
print(f"Parameters: n1={p.n1}, n2={p.n2}, n3={p.n3}, n4={p.n4}")
print(f"Color: {p.colors[0]}")
# Initialize watchdog timer
wdt = WDT(timeout=10000)
wdt.feed()
# Start pattern
p.select("circle")
if p.selected in p.patterns:
_thread.start_new_thread(p.patterns[p.selected], ())
print("Pattern started. Running forever...")
else:
print(f"Pattern {p.selected} not found")
# Run forever
try:
while True:
wdt.feed()
utime.sleep_ms(100)
except KeyboardInterrupt:
print("\nStopping...")
p.running = False
p.off()
print("LEDs turned off")

55
test/patterns.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
import uasyncio as asyncio
from machine import WDT
from settings import Settings
from patterns import Patterns
async def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num)
p.load()
print(p)
p.save()
# print(p)
# wdt = WDT(timeout=10000)
# # Baseline params
# p.set_param("br", 64)
# p.set_param("dl", 500)
# p.set_param("cl", [(255, 0, 0), (0, 0, 255)])
# p.set_param("n1", 200)
# p.set_param("n2", 200)
# p.set_param("n3", 1)
# p.set_param("n4", 1)
# for name, fn in p.patterns.items():
# if fn is None:
# continue
# print(name)
# p.set_param("pt", name)
# task = asyncio.create_task(p.run())
# end = asyncio.get_event_loop().time() + 2.0
# while asyncio.get_event_loop().time() < end:
# wdt.feed()
# await asyncio.sleep_ms(10)
# p.stopped = True
# await task
# p.stopped = False
# p.set_param("pt", "off")
# task = asyncio.create_task(p.run())
# await asyncio.sleep_ms(200)
# p.stopped = True
# await task
if __name__ == "__main__":
asyncio.run(main())

32
test/patterns/blink.py Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import uasyncio as asyncio
import utime
from machine import WDT
from settings import Settings
from patterns import Patterns
async 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)
p.set_param("br", 64)
p.set_param("dl", 200)
p.set_param("cl", [(255, 0, 0), (0, 0, 255)])
p.select("blink")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 1500:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
if __name__ == "__main__":
asyncio.run(main())

132
test/patterns/circle.py Normal file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python3
import uasyncio as asyncio
import utime
from machine import WDT
from settings import Settings
from patterns import Patterns
async 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)
# 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")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 5 seconds to see full cycle
while utime.ticks_diff(utime.ticks_ms(), start) < 5000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# 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.stopped = False
p.set_param("n1", 20) # Head moves 20 LEDs/second (slow)
p.set_param("n2", 50) # Max length 50 LEDs
p.set_param("n3", 100) # Tail moves 100 LEDs/second (fast)
p.set_param("n4", 0) # Min length 0 LEDs
p.set_param("cl", [(0, 255, 0)]) # Green
p.select("circle")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 5000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# 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.stopped = False
p.set_param("n1", 100) # Head moves 100 LEDs/second (fast)
p.set_param("n2", 30) # Max length 30 LEDs
p.set_param("n3", 20) # Tail moves 20 LEDs/second (slow)
p.set_param("n4", 0) # Min length 0 LEDs
p.set_param("cl", [(0, 0, 255)]) # Blue
p.select("circle")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 5000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# 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.stopped = False
p.set_param("n1", 50) # Head moves 50 LEDs/second
p.set_param("n2", 40) # Max length 40 LEDs
p.set_param("n3", 100) # Tail moves 100 LEDs/second
p.set_param("n4", 10) # Min length 10 LEDs (never fully disappears)
p.set_param("cl", [(255, 255, 0)]) # Yellow
p.select("circle")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 5000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# 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.stopped = False
p.set_param("n1", 200) # Head moves 200 LEDs/second (very fast)
p.set_param("n2", 20) # Max length 20 LEDs
p.set_param("n3", 200) # Tail moves 200 LEDs/second (very fast)
p.set_param("n4", 0) # Min length 0 LEDs
p.set_param("cl", [(255, 0, 255)]) # Magenta
p.select("circle")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# 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.stopped = False
p.set_param("n1", 10) # Head moves 10 LEDs/second (very slow)
p.set_param("n2", 25) # Max length 25 LEDs
p.set_param("n3", 10) # Tail moves 10 LEDs/second (very slow)
p.set_param("n4", 0) # Min length 0 LEDs
p.set_param("cl", [(0, 255, 255)]) # Cyan
p.select("circle")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 5000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Cleanup
print("Test complete, turning off")
p.stopped = False
p.select("off")
task = asyncio.create_task(p.run())
await asyncio.sleep_ms(100)
await p.stop()
await task
if __name__ == "__main__":
asyncio.run(main())

144
test/patterns/n_chase.py Normal file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
import uasyncio as asyncio
import utime
from machine import WDT
from settings import Settings
from patterns import Patterns
async 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)
# Test 1: Basic n_chase (n1=5, n2=5, n3=1, n4=1)
print("Test 1: Basic n_chase (n1=5, n2=5, n3=1, n4=1)")
p.set_param("br", 255)
p.set_param("dl", 200)
p.set_param("n1", 5) # 5 LEDs color0
p.set_param("n2", 5) # 5 LEDs color1
p.set_param("n3", 1) # Move 1 forward on even steps
p.set_param("n4", 1) # Move 1 forward on odd steps
p.set_param("cl", [(255, 0, 0), (0, 255, 0)]) # Red and Green
p.select("n_chase")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 2: Forward and backward (n3=2, n4=-1)
print("Test 2: Forward and backward (n3=2, n4=-1)")
p.stopped = False
p.set_param("n1", 3)
p.set_param("n2", 3)
p.set_param("n3", 2) # Move 2 forward on even steps
p.set_param("n4", -1) # Move 1 backward on odd steps
p.set_param("dl", 150)
p.set_param("cl", [(0, 0, 255), (255, 255, 0)]) # Blue and Yellow
p.select("n_chase")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 3: Large segments (n1=10, n2=5)
print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)")
p.stopped = False
p.set_param("n1", 10) # 10 LEDs color0
p.set_param("n2", 5) # 5 LEDs color1
p.set_param("n3", 3) # Move 3 forward
p.set_param("n4", 3) # Move 3 forward
p.set_param("dl", 200)
p.set_param("cl", [(255, 128, 0), (128, 0, 255)]) # Orange and Purple
p.select("n_chase")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 4: Fast movement (n3=5, n4=5)
print("Test 4: Fast movement (n3=5, n4=5)")
p.stopped = False
p.set_param("n1", 4)
p.set_param("n2", 4)
p.set_param("n3", 5) # Move 5 forward
p.set_param("n4", 5) # Move 5 forward
p.set_param("dl", 100)
p.set_param("cl", [(255, 0, 255), (0, 255, 255)]) # Magenta and Cyan
p.select("n_chase")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 5: Backward movement (n3=-2, n4=-2)
print("Test 5: Backward movement (n3=-2, n4=-2)")
p.stopped = False
p.set_param("n1", 6)
p.set_param("n2", 4)
p.set_param("n3", -2) # Move 2 backward
p.set_param("n4", -2) # Move 2 backward
p.set_param("dl", 200)
p.set_param("cl", [(255, 255, 255), (0, 0, 0)]) # White and Black
p.select("n_chase")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 6: Alternating forward/backward (n3=3, n4=-2)
print("Test 6: Alternating forward/backward (n3=3, n4=-2)")
p.stopped = False
p.set_param("n1", 5)
p.set_param("n2", 5)
p.set_param("n3", 3) # Move 3 forward on even steps
p.set_param("n4", -2) # Move 2 backward on odd steps
p.set_param("dl", 250)
p.set_param("cl", [(255, 0, 0), (0, 255, 0)]) # Red and Green
p.select("n_chase")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 4000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Cleanup
print("Test complete, turning off")
p.stopped = False
p.select("off")
task = asyncio.create_task(p.run())
await asyncio.sleep_ms(100)
await p.stop()
await task
if __name__ == "__main__":
asyncio.run(main())

26
test/patterns/off.py Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import uasyncio as asyncio
from machine import WDT
from settings import Settings
from patterns import Patterns
async 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)
p.select("off")
task = asyncio.create_task(p.run())
wdt.feed()
await asyncio.sleep_ms(200)
p.stopped = True
await task
if __name__ == "__main__":
asyncio.run(main())

34
test/patterns/on.py Normal file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3
import uasyncio as asyncio
from machine import WDT
from settings import Settings
from patterns import Patterns
async 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)
p.set_param("br", 64)
p.set_param("dl", 120)
p.set_param("cl", [(255, 0, 0), (0, 0, 255)])
p.select("on")
task = asyncio.create_task(p.run())
await asyncio.sleep_ms(800)
p.stopped = True
await task
p.stopped = False
p.select("off")
task = asyncio.create_task(p.run())
await asyncio.sleep_ms(100)
p.stopped = True
await task
if __name__ == "__main__":
asyncio.run(main())

160
test/patterns/pulse.py Normal file
View File

@@ -0,0 +1,160 @@
#!/usr/bin/env python3
import uasyncio as asyncio
import utime
from machine import WDT
from settings import Settings
from patterns import Patterns
async 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)
# Test 1: Basic pulse with attack, hold, and decay
print("Test 1: Basic pulse pattern")
p.set_param("br", 255)
p.set_param("dl", 1000) # 1 second delay between pulses
p.set_param("auto", True) # Run continuously
p.set_param("cl", [(255, 255, 255), (255, 255, 255)])
p.set_param("n1", 200) # Attack: 200ms
p.set_param("n2", 200) # Hold: 200ms
p.set_param("n3", 200) # Decay: 200ms
p.select("pulse")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 3 seconds to see multiple pulse cycles
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 2: Fast pulse with shorter delay
print("Test 2: Fast pulse pattern")
p.stopped = False
p.set_param("dl", 500) # 500ms delay between pulses
p.set_param("auto", True) # Run continuously
p.set_param("n1", 100) # Attack: 100ms
p.set_param("n2", 100) # Hold: 100ms
p.set_param("n3", 100) # Decay: 100ms
p.select("pulse")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 2 seconds
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 3: Colored pulse
print("Test 3: Colored pulse pattern")
p.stopped = False
p.set_param("dl", 800)
p.set_param("auto", True) # Run continuously
p.set_param("cl", [(255, 0, 0), (0, 0, 255)]) # Red pulse
p.set_param("n1", 150)
p.set_param("n2", 150)
p.set_param("n3", 150)
p.select("pulse")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 2 seconds
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 4: Verify delay restart timing
print("Test 4: Testing delay restart timing")
p.stopped = False
p.set_param("dl", 500) # 500ms delay
p.set_param("auto", True) # Run continuously
p.set_param("n1", 100) # Total attack+hold+decay = 300ms, should wait 200ms more
p.set_param("n2", 100)
p.set_param("n3", 100)
p.select("pulse")
task = asyncio.create_task(p.run())
# Monitor pulse cycles
cycle_count = 0
last_cycle_time = utime.ticks_ms()
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
# Check if we're near the start of a new cycle (LEDs off)
# This is a simplified check - in practice you'd monitor LED state
p.stopped = True
await task
# Test 5: Single-shot pulse (auto=False)
print("Test 5: Single-shot pulse (auto=False)")
p.stopped = False
p.set_param("dl", 500) # Delay between pulses
p.set_param("auto", False) # Run only once
p.set_param("cl", [(0, 255, 0), (0, 255, 0)]) # Green pulse
p.set_param("n1", 150) # Attack: 150ms
p.set_param("n2", 150) # Hold: 150ms
p.set_param("n3", 150) # Decay: 150ms
p.select("pulse")
task = asyncio.create_task(p.run())
# The pulse should complete once and then stop
# Total time should be ~450ms (attack + hold + decay)
# Wait a bit longer to verify it doesn't repeat
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 1000:
wdt.feed()
await asyncio.sleep_ms(10)
# Task should have completed on its own (not stopped manually)
# Verify it's stopped
if not p.stopped:
print("Warning: Pulse should have stopped automatically with auto=False")
p.stopped = True
await task
# Test 6: Pulse cycles through colors
print("Test 6: Pulse cycles through colors")
p.stopped = False
p.set_param("dl", 300) # cycle interval
p.set_param("auto", True) # Run continuously
p.set_param("cl", [
(255, 0, 0), # red
(0, 255, 0), # green
(0, 0, 255), # blue
(255, 255, 0), # yellow
])
p.set_param("n1", 50)
p.set_param("n2", 0)
p.set_param("n3", 50)
p.select("pulse")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run long enough to observe multiple color cycles
while utime.ticks_diff(utime.ticks_ms(), start) < 10000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Cleanup
print("Test complete, turning off")
p.stopped = False
p.select("off")
task = asyncio.create_task(p.run())
await asyncio.sleep_ms(100)
p.stopped = True
await task
if __name__ == "__main__":
asyncio.run(main())

167
test/patterns/rainbow.py Normal file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
import uasyncio as asyncio
import utime
from machine import WDT
from settings import Settings
from patterns import Patterns
async 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)
# 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) # Run continuously
p.select("rainbow")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 3 seconds to see rainbow animation
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 2: Fast rainbow
print("Test 2: Fast rainbow (low delay, n1=1)")
p.stopped = False
p.set_param("dl", 50) # Faster animation
p.set_param("n1", 1)
p.set_param("auto", True)
p.select("rainbow")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 3: Slow rainbow
print("Test 3: Slow rainbow (high delay, n1=1)")
p.stopped = False
p.set_param("dl", 500) # Slower animation
p.set_param("n1", 1)
p.set_param("auto", True)
p.select("rainbow")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 4: Low brightness rainbow
print("Test 4: Low brightness rainbow (n1=1)")
p.stopped = False
p.set_param("br", 64) # Low brightness
p.set_param("dl", 100)
p.set_param("n1", 1)
p.set_param("auto", True)
p.select("rainbow")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 5: Single-step rainbow (auto=False)
print("Test 5: Single-step rainbow (auto=False, n1=1)")
p.stopped = False
p.set_param("br", 255)
p.set_param("dl", 100)
p.set_param("n1", 1)
p.set_param("auto", False) # Run once per call
p.set_param("step", 0) # Reset step
p.select("rainbow")
# Call rainbow multiple times to see step progression
for i in range(10):
task = asyncio.create_task(p.run())
await task
await asyncio.sleep_ms(100) # Small delay between steps
wdt.feed()
# Test 6: Verify step updates correctly
print("Test 6: Verify step updates (auto=False, n1=1)")
p.stopped = False
p.set_param("n1", 1)
initial_step = p.step
p.select("rainbow")
task = asyncio.create_task(p.run())
await task
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.stopped = False
p.set_param("br", 255)
p.set_param("dl", 100)
p.set_param("n1", 5) # Step increment of 5 (5x faster)
p.set_param("auto", True)
p.select("rainbow")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 8: Very fast step increment (n1=10)
print("Test 8: Very fast rainbow (n1=10, auto=True)")
p.stopped = False
p.set_param("n1", 10) # Step increment of 10 (10x faster)
p.set_param("auto", True)
p.select("rainbow")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
await asyncio.sleep_ms(10)
await p.stop()
await task
# Test 9: Verify n1 controls step increment (auto=False)
print("Test 9: Verify n1 step increment (auto=False, n1=5)")
p.stopped = False
p.set_param("n1", 5) # Step increment of 5
p.set_param("auto", False)
p.set_param("step", 0) # Reset step
initial_step = p.step
p.select("rainbow")
task = asyncio.create_task(p.run())
await task
final_step = p.step
expected_step = (initial_step + 5) % 256
print(f"Step updated from {initial_step} to {final_step} (expected: {expected_step})")
if final_step == expected_step:
print("✓ n1 step increment working correctly")
else:
print(f"✗ Step increment mismatch! Expected {expected_step}, got {final_step}")
# Cleanup
print("Test complete, turning off")
p.stopped = False
p.select("off")
task = asyncio.create_task(p.run())
await asyncio.sleep_ms(100)
await p.stop()
await task
if __name__ == "__main__":
asyncio.run(main())

165
test/patterns/transition.py Normal file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
import uasyncio as asyncio
import utime
from machine import WDT
from settings import Settings
from patterns import Patterns
async 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)
# Test 1: Basic transition with 2 colors (auto=True, cycles continuously)
print("Test 1: Basic transition (2 colors, 1000ms delay, auto=True)")
p.set_param("br", 255)
p.set_param("dl", 1000) # 1 second transition time
p.set_param("auto", True) # Cycle continuously
p.set_param("cl", [(255, 0, 0), (0, 255, 0)]) # Red to Green
p.select("transition")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 5 seconds to see multiple transitions
while utime.ticks_diff(utime.ticks_ms(), start) < 5000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 2: Fast transition (auto=True, cycles continuously)
print("Test 2: Fast transition (500ms delay, auto=True)")
p.stopped = False
p.set_param("dl", 500) # 500ms transition time
p.set_param("auto", True) # Cycle continuously
p.set_param("cl", [(0, 0, 255), (255, 255, 0)]) # Blue to Yellow
p.select("transition")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 3 seconds
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 3: Multiple colors transition (auto=True, cycles continuously)
print("Test 3: Multiple colors transition (3 colors, auto=True)")
p.stopped = False
p.set_param("dl", 800)
p.set_param("auto", True) # Cycle continuously
p.set_param("cl", [
(255, 0, 0), # Red
(0, 255, 0), # Green
(0, 0, 255), # Blue
])
p.select("transition")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 8 seconds to see full cycles
while utime.ticks_diff(utime.ticks_ms(), start) < 8000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 4: Single color (should just stay that color)
print("Test 4: Single color (should stay that color)")
p.stopped = False
p.set_param("dl", 1000)
p.set_param("cl", [(255, 128, 0)]) # Orange
p.select("transition")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 3 seconds
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 5: Many colors transition (auto=True, cycles continuously)
print("Test 5: Many colors transition (5 colors, auto=True)")
p.stopped = False
p.set_param("dl", 600)
p.set_param("auto", True) # Cycle continuously
p.set_param("cl", [
(255, 0, 0), # Red
(255, 128, 0), # Orange
(255, 255, 0), # Yellow
(0, 255, 0), # Green
(0, 0, 255), # Blue
])
p.select("transition")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 10 seconds to see multiple cycles
while utime.ticks_diff(utime.ticks_ms(), start) < 10000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 6: Low brightness transition (auto=True, cycles continuously)
print("Test 6: Low brightness transition (auto=True)")
p.stopped = False
p.set_param("br", 64) # Low brightness
p.set_param("dl", 1000)
p.set_param("auto", True) # Cycle continuously
p.set_param("cl", [(255, 0, 0), (0, 255, 0)])
p.select("transition")
task = asyncio.create_task(p.run())
start = utime.ticks_ms()
# Run for 3 seconds
while utime.ticks_diff(utime.ticks_ms(), start) < 3000:
wdt.feed()
await asyncio.sleep_ms(10)
p.stopped = True
await task
# Test 7: Single-shot transition (auto=False, only color1 to color2)
print("Test 7: Single-shot transition (auto=False, color1 to color2 only)")
p.stopped = False
p.set_param("br", 255)
p.set_param("dl", 1000) # 1 second transition
p.set_param("auto", False) # Run only once
p.set_param("cl", [
(255, 0, 0), # Red (color1)
(0, 255, 0), # Green (color2)
(0, 0, 255), # Blue (should be ignored)
(255, 255, 0), # Yellow (should be ignored)
])
p.select("transition")
task = asyncio.create_task(p.run())
# The transition should complete once (color1 to color2) and then stop
# Total time should be ~1000ms
# Wait a bit longer to verify it doesn't continue
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
await asyncio.sleep_ms(10)
# Task should have completed on its own (not stopped manually)
# Verify it's stopped
if not p.stopped:
print("Warning: Transition should have stopped automatically with auto=False")
p.stopped = True
await task
# Cleanup
print("Test complete, turning off")
p.stopped = False
p.select("off")
task = asyncio.create_task(p.run())
await asyncio.sleep_ms(100)
p.stopped = True
await task
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Test for saving and loading patterns
Run with: mpremote run test/test_patterns_save_load.py
"""
import json
import uasyncio as asyncio
from settings import Settings
from patterns import Patterns
async def test_patterns_save_load():
"""Test saving and loading patterns"""
print("Testing patterns save and load functionality...")
# Test 1: Initialize patterns and check initial state
print("\nTest 1: Initialize patterns")
s = Settings()
pin = s.get("led_pin", 10)
num_leds = s.get("num_leds", 30)
p1 = Patterns(pin=pin, num_leds=num_leds)
print(f"Initial patterns count: {len(p1.patterns)}")
print(f"Available patterns: {list(p1.patterns.keys())}")
print(f"Selected pattern: {p1.selected}")
# Test 2: Try to save patterns (will fail because patterns contain functions)
print("\nTest 2: Attempt to save patterns")
try:
result = p1.save()
if result:
print("✓ Patterns saved successfully")
else:
print("✗ Patterns save failed (expected - patterns contain functions)")
except Exception as e:
print(f"✗ Exception during save: {e}")
# Test 3: Try to load patterns
print("\nTest 3: Attempt to load patterns")
try:
result = p1.load()
if result:
print("✓ Patterns loaded successfully")
print(f"Patterns after load: {list(p1.patterns.keys())}")
else:
print("✗ Patterns load failed")
except Exception as e:
print(f"✗ Exception during load: {e}")
# Test 4: Test with empty patterns dict (simulating custom patterns)
print("\nTest 4: Test save/load with empty patterns dict")
p2 = Patterns(pin=pin, num_leds=num_leds)
# Store original patterns
original_patterns = p2.patterns.copy()
# Clear patterns to test save/load with empty dict
p2.patterns = {}
try:
result = p2.save()
if result:
print("✓ Empty patterns dict saved successfully")
else:
print("✗ Failed to save empty patterns dict")
except Exception as e:
print(f"✗ Exception saving empty patterns: {e}")
# Try to load
p3 = Patterns(pin=pin, num_leds=num_leds)
p3.patterns = {} # Start with empty
try:
result = p3.load()
if result:
print("✓ Patterns loaded successfully")
print(f"Patterns count after load: {len(p3.patterns)}")
else:
print("✗ Failed to load patterns")
except Exception as e:
print(f"✗ Exception loading patterns: {e}")
# Restore original patterns
p2.patterns = original_patterns
p3.patterns = original_patterns
# Test 5: Verify patterns object state
print("\nTest 5: Verify patterns object state")
print(f"Patterns object type: {type(p1)}")
print(f"Has save method: {hasattr(p1, 'save')}")
print(f"Has load method: {hasattr(p1, 'load')}")
print(f"PATTERNS_FILE: {p1.PATTERNS_FILE}")
# Test 6: Test pattern selection persists
print("\nTest 6: Test pattern selection")
test_pattern = "rainbow"
if test_pattern in p1.patterns:
p1.select(test_pattern)
print(f"Selected pattern: {p1.selected}")
if p1.selected == test_pattern:
print("✓ Pattern selection works")
else:
print(f"✗ Pattern selection failed. Expected '{test_pattern}', got '{p1.selected}'")
else:
print(f"Pattern '{test_pattern}' not available")
print("\n=== Patterns Save/Load test complete ===")
if __name__ == "__main__":
asyncio.run(test_patterns_save_load())

112
test/test_save_load.py Normal file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
Test for saving and loading settings
Run with: mpremote run test/test_save_load.py
"""
import json
import os
from settings import Settings
from patterns import Patterns
def test_save_load():
"""Test saving and loading settings"""
print("Testing save and load functionality...")
# Test 1: Save settings
print("\nTest 1: Save settings")
settings1 = Settings()
# Modify some settings
original_num_leds = settings1.get("num_leds", 50)
original_pattern = settings1.get("pattern", "off")
original_brightness = settings1.get("brightness", 127)
settings1["num_leds"] = 100
settings1["pattern"] = "rainbow"
settings1["brightness"] = 200
settings1["delay"] = 150
settings1["color1"] = "#ff0000"
settings1["color2"] = "#00ff00"
print(f"Original num_leds: {original_num_leds}")
print(f"Setting num_leds to: {settings1['num_leds']}")
print(f"Setting pattern to: {settings1['pattern']}")
print(f"Setting brightness to: {settings1['brightness']}")
# Save settings
settings1.save()
print("Settings saved")
# Test 2: Load settings
print("\nTest 2: Load settings")
settings2 = Settings()
# Verify loaded values
print(f"Loaded num_leds: {settings2['num_leds']}")
print(f"Loaded pattern: {settings2['pattern']}")
print(f"Loaded brightness: {settings2['brightness']}")
print(f"Loaded delay: {settings2.get('delay', 'not set')}")
print(f"Loaded color1: {settings2.get('color1', 'not set')}")
print(f"Loaded color2: {settings2.get('color2', 'not set')}")
# Verify values match
if settings2["num_leds"] == 100:
print("✓ num_leds saved and loaded correctly")
else:
print(f"✗ num_leds mismatch! Expected 100, got {settings2['num_leds']}")
if settings2["pattern"] == "rainbow":
print("✓ pattern saved and loaded correctly")
else:
print(f"✗ pattern mismatch! Expected 'rainbow', got '{settings2['pattern']}'")
if settings2["brightness"] == 200:
print("✓ brightness saved and loaded correctly")
else:
print(f"✗ brightness mismatch! Expected 200, got {settings2['brightness']}")
# Test 3: Test with patterns
print("\nTest 3: Test pattern persistence")
pin = settings2.get("led_pin", 10)
num_leds = settings2["num_leds"]
patterns = Patterns(pin=pin, num_leds=num_leds, selected=settings2["pattern"])
patterns.set_brightness(settings2["brightness"])
patterns.set_delay(settings2["delay"])
print(f"Pattern selected: {patterns.selected}")
print(f"Pattern brightness: {patterns.brightness}")
print(f"Pattern delay: {patterns.delay}")
if patterns.selected == settings2["pattern"]:
print("✓ Pattern selection persisted")
else:
print(f"✗ Pattern mismatch! Expected '{settings2['pattern']}', got '{patterns.selected}'")
# Test 4: Restore original settings
print("\nTest 4: Restore original settings")
settings3 = Settings()
settings3["num_leds"] = original_num_leds
settings3["pattern"] = original_pattern
settings3["brightness"] = original_brightness
settings3.save()
print(f"Restored num_leds to: {original_num_leds}")
print(f"Restored pattern to: {original_pattern}")
print(f"Restored brightness to: {original_brightness}")
# Verify restoration
settings4 = Settings()
if settings4["num_leds"] == original_num_leds:
print("✓ Settings restored correctly")
else:
print(f"✗ Restoration failed! Expected {original_num_leds}, got {settings4['num_leds']}")
print("\n=== Save/Load test complete ===")
if __name__ == "__main__":
test_save_load()

335
tool.py Executable file
View File

@@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""
LED Bar Configuration Tool
A tkinter GUI for downloading, editing, and uploading settings.json to/from MicroPython devices via mpremote.
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import json
import subprocess
import os
import tempfile
import serial
from pathlib import Path
class LEDConfigTool:
def __init__(self, root):
self.root = root
self.root.title("LED Bar Configuration Tool")
self.root.geometry("600x700")
self.settings = {}
self.temp_file = None
# Create main frame
main_frame = ttk.Frame(root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Title
title_label = ttk.Label(main_frame, text="LED Bar Configuration", font=("Arial", 16, "bold"))
title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20))
# Device connection section
device_frame = ttk.LabelFrame(main_frame, text="Device Connection", padding="10")
device_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
ttk.Label(device_frame, text="Device:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
self.device_entry = ttk.Entry(device_frame, width=30)
self.device_entry.insert(0, "/dev/ttyACM0") # Default device
self.device_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
ttk.Button(device_frame, text="Download Settings", command=self.download_settings).grid(row=0, column=2)
# Settings section
settings_frame = ttk.LabelFrame(main_frame, text="Settings", padding="10")
settings_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
# Create scrollable frame for settings
canvas = tk.Canvas(settings_frame, height=400)
scrollbar = ttk.Scrollbar(settings_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Settings fields
self.setting_widgets = {}
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"]),
]
for idx, config in enumerate(settings_config):
key = config[0]
label_text = config[1]
field_type = config[2]
ttk.Label(scrollable_frame, text=f"{label_text}:").grid(row=idx, column=0, sticky=tk.W, padx=(0, 10), pady=5)
if field_type == "number":
widget = ttk.Entry(scrollable_frame, width=20)
elif field_type == "choice":
widget = ttk.Combobox(scrollable_frame, width=17, values=config[3], state="readonly")
elif field_type == "color":
widget = ttk.Entry(scrollable_frame, width=20)
else: # text
widget = ttk.Entry(scrollable_frame, width=20)
widget.grid(row=idx, column=1, sticky=(tk.W, tk.E), pady=5)
self.setting_widgets[key] = widget
canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
settings_frame.grid_rowconfigure(0, weight=1)
settings_frame.grid_columnconfigure(0, weight=1)
# Buttons section
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0))
ttk.Button(button_frame, text="Load from File", command=self.load_from_file).grid(row=0, column=0, padx=5)
ttk.Button(button_frame, text="Save to File", command=self.save_to_file).grid(row=0, column=1, padx=5)
ttk.Button(button_frame, text="Upload Settings", command=self.upload_settings).grid(row=0, column=2, padx=5)
# Status bar
self.status_label = ttk.Label(main_frame, text="Ready", relief=tk.SUNKEN, anchor=tk.W)
self.status_label.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
# Configure grid weights
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(2, weight=1)
device_frame.columnconfigure(1, weight=1)
def update_status(self, message):
"""Update the status bar message."""
self.status_label.config(text=message)
self.root.update_idletasks()
def download_settings(self):
"""Download settings.json from the device using mpremote."""
device = self.device_entry.get().strip()
if not device:
messagebox.showerror("Error", "Please specify a device")
return
self.update_status("Downloading settings...")
try:
# Create temporary file
self.temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
temp_path = self.temp_file.name
self.temp_file.close()
# Download file using mpremote
cmd = ["mpremote", "connect", device, "cp", ":/settings.json", temp_path]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
raise Exception(f"mpremote error: {result.stderr}")
# Load the downloaded file
with open(temp_path, 'r') as f:
self.settings = json.load(f)
# Update UI with loaded settings
self.update_ui_from_settings()
self.update_status(f"Settings downloaded successfully from {device}")
messagebox.showinfo("Success", "Settings downloaded successfully!")
except subprocess.TimeoutExpired:
self.update_status("Error: Connection timeout")
messagebox.showerror("Error", "Connection timeout. Check device connection.")
except FileNotFoundError:
self.update_status("Error: mpremote not found")
messagebox.showerror("Error", "mpremote not found. Please install it:\npip install mpremote")
except Exception as e:
self.update_status(f"Error: {str(e)}")
messagebox.showerror("Error", f"Failed to download settings:\n{str(e)}")
finally:
# Clean up temp file
if self.temp_file and os.path.exists(temp_path):
try:
os.unlink(temp_path)
except:
pass
def upload_settings(self):
"""Upload settings.json to the device using mpremote."""
device = self.device_entry.get().strip()
if not device:
messagebox.showerror("Error", "Please specify a device")
return
if not self.settings:
messagebox.showerror("Error", "No settings to upload. Please download or load settings first.")
return
self.update_status("Uploading settings...")
try:
# Get current settings from UI
self.update_settings_from_ui()
# Create temporary file with current settings
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
temp_path = temp_file.name
json.dump(self.settings, temp_file, indent=2)
temp_file.close()
# Upload file using mpremote
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 Exception(f"mpremote error: {result.stderr}")
# Reset the device
self.update_status("Resetting device...")
try:
with serial.Serial(device, baudrate=115200) as ser:
ser.write(b'\x03\x03\x04')
except Exception as e:
# If serial reset fails, try mpremote method as fallback
reset_cmd = ["mpremote", "connect", device, "exec", "import machine; machine.reset()"]
subprocess.run(reset_cmd, capture_output=True, text=True, timeout=5)
self.update_status(f"Settings uploaded and device reset on {device}")
messagebox.showinfo("Success", "Settings uploaded successfully and device reset!")
except subprocess.TimeoutExpired:
self.update_status("Error: Connection timeout")
messagebox.showerror("Error", "Connection timeout. Check device connection.")
except FileNotFoundError:
self.update_status("Error: mpremote not found")
messagebox.showerror("Error", "mpremote not found. Please install it:\npip install mpremote")
except Exception as e:
self.update_status(f"Error: {str(e)}")
messagebox.showerror("Error", f"Failed to upload settings:\n{str(e)}")
finally:
# Clean up temp file
if os.path.exists(temp_path):
try:
os.unlink(temp_path)
except:
pass
def load_from_file(self):
"""Load settings from a local JSON file."""
file_path = filedialog.askopenfilename(
title="Load Settings",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if not file_path:
return
try:
with open(file_path, 'r') as f:
self.settings = json.load(f)
self.update_ui_from_settings()
self.update_status(f"Settings loaded from {os.path.basename(file_path)}")
messagebox.showinfo("Success", "Settings loaded successfully!")
except Exception as e:
self.update_status(f"Error: {str(e)}")
messagebox.showerror("Error", f"Failed to load settings:\n{str(e)}")
def save_to_file(self):
"""Save current settings to a local JSON file."""
if not self.settings:
messagebox.showerror("Error", "No settings to save. Please download or load settings first.")
return
file_path = filedialog.asksaveasfilename(
title="Save Settings",
defaultextension=".json",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if not file_path:
return
try:
# Get current settings from UI
self.update_settings_from_ui()
with open(file_path, 'w') as f:
json.dump(self.settings, f, indent=2)
self.update_status(f"Settings saved to {os.path.basename(file_path)}")
messagebox.showinfo("Success", "Settings saved successfully!")
except Exception as e:
self.update_status(f"Error: {str(e)}")
messagebox.showerror("Error", f"Failed to save settings:\n{str(e)}")
def update_ui_from_settings(self):
"""Update UI widgets with current settings values."""
for key, widget in self.setting_widgets.items():
if key in self.settings:
value = self.settings[key]
if isinstance(widget, ttk.Combobox):
# For debug, convert boolean to string
if key == "debug":
widget.set(str(value))
else:
widget.set(str(value))
else:
widget.delete(0, tk.END)
widget.insert(0, str(value))
def update_settings_from_ui(self):
"""Update settings dictionary from UI widget values."""
for key, widget in self.setting_widgets.items():
value = widget.get().strip()
if value:
# Try to convert to appropriate type
if key in ["led_pin", "num_leds", "delay", "brightness", "id", "n1", "n2", "n3", "n4", "n5", "n6"]:
try:
self.settings[key] = int(value)
except ValueError:
pass # Keep as string if conversion fails
elif key == "debug":
# Convert string "True"/"False" to boolean
self.settings[key] = value == "True"
else:
self.settings[key] = value
elif key in self.settings:
# Keep existing value if widget is empty
pass
def main():
root = tk.Tk()
app = LEDConfigTool(root)
root.mainloop()
if __name__ == "__main__":
main()