Compare commits

...

8 Commits

Author SHA1 Message Date
a06d526ad5 Update Pipfile.lock.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:39:32 +13:00
d82fd9e47c Persist global brightness settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:39:29 +13:00
39390b2311 Add patterns package.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:08:18 +13:00
3080548f47 Add preset persistence and startup default.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 18:48:44 +13:00
7cc0a3b7d7 Remove unused preset parameter mapping.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 17:18:48 +13:00
43957adb28 Rename patterns module to presets
Rename the driver module and update imports so tests and main entry use the new presets naming, while moving Preset to its own file.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 11:40:04 +13:00
f35d8f7084 Add global brightness support to driver
Handle per-device global brightness via ESPNow messages and apply it alongside per-preset brightness in all patterns.
2026-01-29 00:02:28 +13:00
337e8c9906 Use shortened preset fields in driver
Switch led-driver patterns and main loop to use compact preset keys (p, d, b, c, a, n1..n6) and remove unused settings defaults.
2026-01-28 23:28:54 +13:00
24 changed files with 979 additions and 792 deletions

174
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "1d0184b0df68796cc30d8a808f27b6a5d447b3e1f8af0633b2a543d14f0ab829" "sha256": "921fc0268aaeb27ac977902942dd25f0f84ea35bcbbee0412a4d7c801652eb67"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -34,20 +34,11 @@
}, },
"anyio": { "anyio": {
"hashes": [ "hashes": [
"sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703",
"sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb" "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==4.12.0" "version": "==4.12.1"
},
"asgiref": {
"hashes": [
"sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4",
"sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==3.11.0"
}, },
"bitarray": { "bitarray": {
"hashes": [ "hashes": [
@@ -274,63 +265,58 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa",
"sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc",
"sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da",
"sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255",
"sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2",
"sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485",
"sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0",
"sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d",
"sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616",
"sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947",
"sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0",
"sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908",
"sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81",
"sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc",
"sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd",
"sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b",
"sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019",
"sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7",
"sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b",
"sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973",
"sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b",
"sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5",
"sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80",
"sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef",
"sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0",
"sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b",
"sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e",
"sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c",
"sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2",
"sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af",
"sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4",
"sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab",
"sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82",
"sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3",
"sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59",
"sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da",
"sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061",
"sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085",
"sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b",
"sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263",
"sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e",
"sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829",
"sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4",
"sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c",
"sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f",
"sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095",
"sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32",
"sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976",
"sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822"
"sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422",
"sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849",
"sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c",
"sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963",
"sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"
], ],
"markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'",
"version": "==46.0.3" "version": "==46.0.4"
}, },
"esptool": { "esptool": {
"hashes": [ "hashes": [
@@ -342,12 +328,12 @@
}, },
"fastapi": { "fastapi": {
"hashes": [ "hashes": [
"sha256:0503b7b7bc71bc98f7c90c9117d21fdf6147c0d74703011b87936becc86985c1", "sha256:c8cdf7c2182c9a06bf9cfa3329819913c189dc86389b90d5709892053582db29",
"sha256:624d384d7cda7c096449c889fc776a0571948ba14c3c929fa8e9a78cd0b0a6a8" "sha256:ed99383fd96063447597d5aa2a9ec3973be198e3b4fc10c55f15c62efdb21c60"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.9'",
"version": "==0.123.10" "version": "==0.128.3"
}, },
"flask": { "flask": {
"hashes": [ "hashes": [
@@ -510,12 +496,12 @@
}, },
"mpremote": { "mpremote": {
"hashes": [ "hashes": [
"sha256:39251644305be718c52bc5965315adc4ae824901750abf6a3fb63683234df05c", "sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4",
"sha256:61a39bf5af502e1ec56d1b28bf067766c3a0daea9d7487934cb472e378a12fe1" "sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.4'", "markers": "python_version >= '3.4'",
"version": "==1.26.1" "version": "==1.27.0"
}, },
"platformdirs": { "platformdirs": {
"hashes": [ "hashes": [
@@ -527,11 +513,11 @@
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
"sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29",
"sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934" "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"
], ],
"markers": "implementation_name != 'PyPy'", "markers": "implementation_name != 'PyPy'",
"version": "==2.23" "version": "==3.0"
}, },
"pydantic": { "pydantic": {
"hashes": [ "hashes": [
@@ -772,27 +758,27 @@
}, },
"rich": { "rich": {
"hashes": [ "hashes": [
"sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69",
"sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd" "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"
], ],
"markers": "python_full_version >= '3.8.0'", "markers": "python_full_version >= '3.8.0'",
"version": "==14.2.0" "version": "==14.3.2"
}, },
"rich-click": { "rich-click": {
"hashes": [ "hashes": [
"sha256:af73dc68e85f3bebb80ce302a642b9fe3b65f3df0ceb42eb9a27c467c1b678c8", "sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc",
"sha256:d70f39938bcecaf5543e8750828cbea94ef51853f7d0e174cda1e10543767389" "sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.9.4" "version": "==1.9.7"
}, },
"starlette": { "starlette": {
"hashes": [ "hashes": [
"sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74",
"sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca" "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933"
], ],
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==0.50.0" "version": "==0.52.1"
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
@@ -812,12 +798,12 @@
}, },
"uvicorn": { "uvicorn": {
"hashes": [ "hashes": [
"sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea",
"sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d" "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.10'",
"version": "==0.38.0" "version": "==0.40.0"
}, },
"watchfiles": { "watchfiles": {
"hashes": [ "hashes": [
@@ -937,11 +923,11 @@
}, },
"werkzeug": { "werkzeug": {
"hashes": [ "hashes": [
"sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc",
"sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e" "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==3.1.4" "version": "==3.1.5"
} }
}, },
"develop": {} "develop": {}

View File

@@ -26,7 +26,7 @@ MicroPython-based LED driver application for ESP32 microcontrollers.
led-driver/ led-driver/
├── src/ ├── src/
│ ├── main.py # Main application code │ ├── main.py # Main application code
│ ├── patterns.py # LED pattern implementations (includes Preset and Patterns classes) │ ├── presets.py # LED pattern implementations (includes Preset and Presets classes)
│ ├── settings.py # Settings management │ ├── settings.py # Settings management
│ └── p2p.py # Peer-to-peer communication │ └── p2p.py # Peer-to-peer communication
├── test/ # Pattern tests ├── test/ # Pattern tests

View File

@@ -1,18 +1,26 @@
from settings import Settings from settings import Settings
from machine import WDT from machine import WDT
from espnow import ESPNow from espnow import ESPNow
import utime
import network import network
from patterns import Patterns from presets import Presets
from utils import convert_and_reorder_colors from utils import convert_and_reorder_colors
import json import json
settings = Settings() settings = Settings()
print(settings) print(settings)
patterns = Patterns(settings["led_pin"], settings["num_leds"], selected=settings["pattern"]) presets = Presets(settings["led_pin"], settings["num_leds"])
presets.load()
presets.b = settings.get("brightness", 255)
startup_preset = settings.get("startup_preset")
if startup_preset:
presets.select(startup_preset)
print(f"Selected startup preset: {startup_preset}")
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
wdt.feed() wdt.feed()
last_brightness_save = 0
sta_if = network.WLAN(network.STA_IF) sta_if = network.WLAN(network.STA_IF)
sta_if.active(True) sta_if.active(True)
@@ -24,23 +32,42 @@ e.active(True)
while True: while True:
wdt.feed() wdt.feed()
patterns.tick() presets.tick()
if e.any(): if e.any():
host, msg = e.recv() host, msg = e.recv()
data = json.loads(msg) data = json.loads(msg)
if data["v"] != "1": # Only handle messages with the expected version.
if data.get("v") != "1":
continue continue
# print(data)
# Global brightness (0255) for this device
if "b" in data:
try:
presets.b = max(0, min(255, int(data["b"])))
settings["brightness"] = presets.b
now = utime.ticks_ms()
if utime.ticks_diff(now, last_brightness_save) >= 500:
settings.save()
last_brightness_save = now
except (TypeError, ValueError):
pass
if "presets" in data: if "presets" in data:
for name, preset_data in data["presets"].items(): for id, preset_data in data["presets"].items():
# Convert hex color strings to RGB tuples and reorder based on device color order # Convert hex color strings to RGB tuples and reorder based on device color order
if "colors" in preset_data: if "c" in preset_data:
preset_data["colors"] = convert_and_reorder_colors(preset_data["colors"], settings) preset_data["c"] = convert_and_reorder_colors(preset_data["c"], settings)
patterns.edit(name, preset_data) presets.edit(id, preset_data)
print(f"Edited preset {id}: {preset_data.get('name', '')}")
if settings.get("name") in data.get("select", {}): if settings.get("name") in data.get("select", {}):
select_list = data["select"][settings.get("name")] select_list = data["select"][settings.get("name")]
# Select value is always a list: ["preset_name"] or ["preset_name", step] # Select value is always a list: ["preset_name"] or ["preset_name", step]
if select_list: if select_list:
preset_name = select_list[0] preset_name = select_list[0]
step = select_list[1] if len(select_list) > 1 else None step = select_list[1] if len(select_list) > 1 else None
patterns.select(preset_name, step=step) presets.select(preset_name, step=step)
if "default" in data:
settings["startup_preset"] = data["default"]
print(f"Set startup preset to: {data['default']}")
settings.save()
if "save" in data:
presets.save()

View File

@@ -1,500 +0,0 @@
from machine import Pin
from neopixel import NeoPixel
import utime
# 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 Preset:
def __init__(self, data):
# Set default values for all preset attributes
self.pattern = "off"
self.delay = 100
self.brightness = 127
self.colors = [(255, 255, 255)]
self.auto = True
self.n1 = 0
self.n2 = 0
self.n3 = 0
self.n4 = 0
self.n5 = 0
self.n6 = 0
# Override defaults with provided data
self.edit(data)
def edit(self, data=None):
if not data:
return False
for key, value in data.items():
setattr(self, key, value)
return True
class Patterns:
def __init__(self, pin, num_leds, brightness=127, selected="off", delay=100):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.brightness = brightness
self.step = 0
self.selected = selected
self.generator = None
self.presets = {}
# Register all pattern methods
self.patterns = {
"off": self.off,
"on": self.on,
"blink": self.blink,
"rainbow": self.rainbow,
"pulse": self.pulse,
"transition": self.transition,
"chase": self.chase,
"circle": self.circle,
}
self.select(self.selected)
def edit(self, name, data):
"""Create or update a preset with the given name."""
if name in self.presets:
# Update existing preset
self.presets[name].edit(data)
else:
# Create new preset
self.presets[name] = Preset(data)
return True
def delete(self, name):
if name in self.presets:
del self.presets[name]
return True
return False
def tick(self):
if self.generator is None:
return
try:
next(self.generator)
except StopIteration:
self.generator = None
def select(self, preset_name, step=None):
if preset_name in self.presets:
preset = self.presets[preset_name]
if preset.pattern in self.patterns:
# Set step value if explicitly provided
if step is not None:
self.step = step
elif preset.pattern == "off" or self.selected != preset_name:
self.step = 0
self.generator = self.patterns[preset.pattern](preset)
self.selected = preset_name # Store the preset name, not the object
return True
# If preset doesn't exist or pattern not found, default to "off"
return False
def set_param(self, key, value):
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
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 (0, 0, 0)
for i in range(self.num_leds):
self.n[i] = fill_color
self.n.write()
def off(self, preset=None):
self.fill((0, 0, 0))
def on(self, preset):
colors = preset.colors
color = colors[0] if colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.brightness))
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)
def blink(self, preset):
state = True # True = on, False = off
last_update = utime.ticks_ms()
while True:
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= preset.delay:
if state:
color = preset.colors[0] if preset.colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.brightness))
else:
self.fill((0, 0, 0))
state = not state
last_update = current_time
# Yield once per tick so other logic can run
yield
def rainbow(self, preset):
step = self.step % 256
step_amount = max(1, int(preset.n1)) # n1 controls step increment
# If auto is False, run a single step and then stop
if not preset.auto:
for i in range(self.num_leds):
rc_index = (i * 256 // self.num_leds) + step
self.n[i] = self.apply_brightness(self.wheel(rc_index & 255), preset.brightness)
self.n.write()
# Increment step by n1 for next manual call
self.step = (step + step_amount) % 256
# Allow tick() to advance the generator once
yield
return
last_update = utime.ticks_ms()
while True:
current_time = utime.ticks_ms()
sleep_ms = max(1, int(preset.delay)) # Get delay from preset
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
for i in range(self.num_leds):
rc_index = (i * 256 // self.num_leds) + step
self.n[i] = self.apply_brightness(self.wheel(rc_index & 255), preset.brightness)
self.n.write()
step = (step + step_amount) % 256
self.step = step
last_update = current_time
# Yield once per tick so other logic can run
yield
def pulse(self, preset):
self.off()
# Get colors from preset
colors = preset.colors
if not colors:
colors = [(255, 255, 255)]
color_index = 0
cycle_start = utime.ticks_ms()
# State machine based pulse using a single generator loop
while True:
# Read current timing parameters from preset
attack_ms = max(0, int(preset.n1)) # Attack time in ms
hold_ms = max(0, int(preset.n2)) # Hold time in ms
decay_ms = max(0, int(preset.n3)) # Decay time in ms
delay_ms = max(0, int(preset.delay))
total_ms = attack_ms + hold_ms + decay_ms + delay_ms
if total_ms <= 0:
total_ms = 1
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, cycle_start)
base_color = colors[color_index % len(colors)]
if elapsed < attack_ms and attack_ms > 0:
# Attack: fade 0 -> 1
factor = elapsed / attack_ms
color = tuple(int(c * factor) for c in base_color)
self.fill(self.apply_brightness(color, preset.brightness))
elif elapsed < attack_ms + hold_ms:
# Hold: full brightness
self.fill(self.apply_brightness(base_color, preset.brightness))
elif elapsed < attack_ms + hold_ms + decay_ms and decay_ms > 0:
# Decay: fade 1 -> 0
dec_elapsed = elapsed - attack_ms - hold_ms
factor = max(0.0, 1.0 - (dec_elapsed / decay_ms))
color = tuple(int(c * factor) for c in base_color)
self.fill(self.apply_brightness(color, preset.brightness))
elif elapsed < total_ms:
# Delay phase: LEDs off between pulses
self.fill((0, 0, 0))
else:
# End of cycle, move to next color and restart timing
color_index += 1
cycle_start = now
if not preset.auto:
break
# Skip drawing this tick, start next cycle
yield
continue
# Yield once per tick
yield
def transition(self, preset):
"""Transition between colors, blending over `delay` ms."""
colors = preset.colors
if not colors:
self.off()
yield
return
# Only one color: just keep it on
if len(colors) == 1:
while True:
self.fill(self.apply_brightness(colors[0], preset.brightness))
yield
return
color_index = 0
start_time = utime.ticks_ms()
while True:
if not colors:
break
# Get current and next color based on live list
c1 = colors[color_index % len(colors)]
c2 = colors[(color_index + 1) % len(colors)]
duration = max(10, int(preset.delay)) # At least 10ms
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, start_time)
if elapsed >= duration:
# End of this transition step
if not preset.auto:
# One-shot: transition from first to second color only
self.fill(self.apply_brightness(c2, preset.brightness))
break
# Auto: move to next pair
color_index = (color_index + 1) % len(colors)
start_time = now
yield
continue
# Interpolate between c1 and c2
factor = elapsed / duration
interpolated = tuple(
int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3)
)
self.fill(self.apply_brightness(interpolated, preset.brightness))
yield
def chase(self, preset):
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating.
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)"""
colors = preset.colors
if len(colors) < 1:
# Need at least 1 color
return
# Access colors, delay, and n values from preset
if not colors:
return
# If only one color provided, use it for both colors
if len(colors) < 2:
color0 = colors[0]
color1 = colors[0]
else:
color0 = colors[0]
color1 = colors[1]
color0 = self.apply_brightness(color0, preset.brightness)
color1 = self.apply_brightness(color1, preset.brightness)
n1 = max(1, int(preset.n1)) # LEDs of color 0
n2 = max(1, int(preset.n2)) # LEDs of color 1
n3 = int(preset.n3) # Step movement on even steps (can be negative)
n4 = int(preset.n4) # Step movement on odd steps (can be negative)
segment_length = n1 + n2
# Calculate position from step_count
step_count = self.step
# Position alternates: step 0 adds n3, step 1 adds n4, step 2 adds n3, etc.
if step_count % 2 == 0:
# Even steps: (step_count//2) pairs of (n3+n4) plus one extra n3
position = (step_count // 2) * (n3 + n4) + n3
else:
# Odd steps: ((step_count+1)//2) pairs of (n3+n4)
position = ((step_count + 1) // 2) * (n3 + n4)
# Wrap position to keep it reasonable
max_pos = self.num_leds + segment_length
position = position % max_pos
if position < 0:
position += max_pos
# If auto is False, run a single step and then stop
if not preset.auto:
# Clear all LEDs
self.n.fill((0, 0, 0))
# Draw repeating pattern starting at position
for i in range(self.num_leds):
# Calculate position in the repeating segment
relative_pos = (i - position) % segment_length
if relative_pos < 0:
relative_pos = (relative_pos + segment_length) % segment_length
# Determine which color based on position in segment
if relative_pos < n1:
self.n[i] = color0
else:
self.n[i] = color1
self.n.write()
# Increment step for next beat
self.step = step_count + 1
# Allow tick() to advance the generator once
yield
return
# Auto mode: continuous loop
last_update = utime.ticks_ms()
transition_duration = max(10, int(preset.delay))
while True:
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= transition_duration:
# Calculate current position from step_count
if step_count % 2 == 0:
position = (step_count // 2) * (n3 + n4) + n3
else:
position = ((step_count + 1) // 2) * (n3 + n4)
# Wrap position
max_pos = self.num_leds + segment_length
position = position % max_pos
if position < 0:
position += max_pos
# Clear all LEDs
self.n.fill((0, 0, 0))
# Draw repeating pattern starting at position
for i in range(self.num_leds):
# Calculate position in the repeating segment
relative_pos = (i - position) % segment_length
if relative_pos < 0:
relative_pos = (relative_pos + segment_length) % segment_length
# Determine which color based on position in segment
if relative_pos < n1:
self.n[i] = color0
else:
self.n[i] = color1
self.n.write()
# Increment step
step_count += 1
self.step = step_count
last_update = current_time
# Yield once per tick so other logic can run
yield
def circle(self, preset):
"""Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4"""
head = 0
tail = 0
# Calculate timing from preset
head_rate = max(1, int(preset.n1)) # n1 = head moves per second
tail_rate = max(1, int(preset.n3)) # n3 = tail moves per second
max_length = max(1, int(preset.n2)) # n2 = max length
min_length = max(0, int(preset.n4)) # n4 = min length
head_delay = 1000 // head_rate # ms between head movements
tail_delay = 1000 // tail_rate # ms between tail movements
last_head_move = utime.ticks_ms()
last_tail_move = utime.ticks_ms()
phase = "growing" # "growing", "shrinking", or "off"
colors = preset.colors
color = self.apply_brightness(colors[0] if colors else (255, 255, 255), preset.brightness)
while True:
current_time = utime.ticks_ms()
# Clear all LEDs
self.n.fill((0, 0, 0))
# Calculate segment length
segment_length = (head - tail) % self.num_leds
if segment_length == 0 and head != tail:
segment_length = self.num_leds
# Draw segment from tail to head
for i in range(segment_length + 1):
led_pos = (tail + i) % self.num_leds
self.n[led_pos] = 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()
# Yield once per tick so other logic can run
yield

6
src/patterns/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
from .blink import Blink
from .rainbow import Rainbow
from .pulse import Pulse
from .transition import Transition
from .chase import Chase
from .circle import Circle

33
src/patterns/blink.py Normal file
View File

@@ -0,0 +1,33 @@
import utime
class Blink:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""Blink pattern: toggles LEDs on/off using preset delay, cycling through colors."""
# Use provided colors, or default to white if none
colors = preset.c if preset.c else [(255, 255, 255)]
color_index = 0
state = True # True = on, False = off
last_update = utime.ticks_ms()
while True:
current_time = utime.ticks_ms()
# Re-read delay each loop so live updates to preset.d take effect
delay_ms = max(1, int(preset.d))
if utime.ticks_diff(current_time, last_update) >= delay_ms:
if state:
base_color = colors[color_index % len(colors)]
color = self.driver.apply_brightness(base_color, preset.b)
self.driver.fill(color)
# Advance to next color for the next "on" phase
color_index += 1
else:
# "Off" phase: turn all LEDs off
self.driver.fill((0, 0, 0))
state = not state
last_update = current_time
# Yield once per tick so other logic can run
yield

124
src/patterns/chase.py Normal file
View File

@@ -0,0 +1,124 @@
import utime
class Chase:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating.
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)"""
colors = preset.c
if len(colors) < 1:
# Need at least 1 color
return
# Access colors, delay, and n values from preset
if not colors:
return
# If only one color provided, use it for both colors
if len(colors) < 2:
color0 = colors[0]
color1 = colors[0]
else:
color0 = colors[0]
color1 = colors[1]
color0 = self.driver.apply_brightness(color0, preset.b)
color1 = self.driver.apply_brightness(color1, preset.b)
n1 = max(1, int(preset.n1)) # LEDs of color 0
n2 = max(1, int(preset.n2)) # LEDs of color 1
n3 = int(preset.n3) # Step movement on even steps (can be negative)
n4 = int(preset.n4) # Step movement on odd steps (can be negative)
segment_length = n1 + n2
# Calculate position from step_count
step_count = self.driver.step
# Position alternates: step 0 adds n3, step 1 adds n4, step 2 adds n3, etc.
if step_count % 2 == 0:
# Even steps: (step_count//2) pairs of (n3+n4) plus one extra n3
position = (step_count // 2) * (n3 + n4) + n3
else:
# Odd steps: ((step_count+1)//2) pairs of (n3+n4)
position = ((step_count + 1) // 2) * (n3 + n4)
# Wrap position to keep it reasonable
max_pos = self.driver.num_leds + segment_length
position = position % max_pos
if position < 0:
position += max_pos
# If auto is False, run a single step and then stop
if not preset.a:
# Clear all LEDs
self.driver.n.fill((0, 0, 0))
# Draw repeating pattern starting at position
for i in range(self.driver.num_leds):
# Calculate position in the repeating segment
relative_pos = (i - position) % segment_length
if relative_pos < 0:
relative_pos = (relative_pos + segment_length) % segment_length
# Determine which color based on position in segment
if relative_pos < n1:
self.driver.n[i] = color0
else:
self.driver.n[i] = color1
self.driver.n.write()
# Increment step for next beat
self.driver.step = step_count + 1
# Allow tick() to advance the generator once
yield
return
# Auto mode: continuous loop
# Use transition_duration for timing and force the first update to happen immediately
transition_duration = max(10, int(preset.d))
last_update = utime.ticks_ms() - transition_duration
while True:
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= transition_duration:
# Calculate current position from step_count
if step_count % 2 == 0:
position = (step_count // 2) * (n3 + n4) + n3
else:
position = ((step_count + 1) // 2) * (n3 + n4)
# Wrap position
max_pos = self.driver.num_leds + segment_length
position = position % max_pos
if position < 0:
position += max_pos
# Clear all LEDs
self.driver.n.fill((0, 0, 0))
# Draw repeating pattern starting at position
for i in range(self.driver.num_leds):
# Calculate position in the repeating segment
relative_pos = (i - position) % segment_length
if relative_pos < 0:
relative_pos = (relative_pos + segment_length) % segment_length
# Determine which color based on position in segment
if relative_pos < n1:
self.driver.n[i] = color0
else:
self.driver.n[i] = color1
self.driver.n.write()
# Increment step
step_count += 1
self.driver.step = step_count
last_update = current_time
# Yield once per tick so other logic can run
yield

96
src/patterns/circle.py Normal file
View File

@@ -0,0 +1,96 @@
import utime
class Circle:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4"""
head = 0
tail = 0
# Calculate timing from preset
head_rate = max(1, int(preset.n1)) # n1 = head moves per second
tail_rate = max(1, int(preset.n3)) # n3 = tail moves per second
max_length = max(1, int(preset.n2)) # n2 = max length
min_length = max(0, int(preset.n4)) # n4 = min length
head_delay = 1000 // head_rate # ms between head movements
tail_delay = 1000 // tail_rate # ms between tail movements
last_head_move = utime.ticks_ms()
last_tail_move = utime.ticks_ms()
phase = "growing" # "growing", "shrinking", or "off"
# Support up to two colors (like chase). If only one color is provided,
# use black for the second; if none, default to white.
colors = preset.c
if not colors:
base0 = base1 = (255, 255, 255)
elif len(colors) == 1:
base0 = colors[0]
base1 = (0, 0, 0)
else:
base0 = colors[0]
base1 = colors[1]
color0 = self.driver.apply_brightness(base0, preset.b)
color1 = self.driver.apply_brightness(base1, preset.b)
while True:
current_time = utime.ticks_ms()
# Background: use second color during the "off" phase, otherwise clear to black
if phase == "off":
self.driver.n.fill(color1)
else:
self.driver.n.fill((0, 0, 0))
# Calculate segment length
segment_length = (head - tail) % self.driver.num_leds
if segment_length == 0 and head != tail:
segment_length = self.driver.num_leds
# Draw segment from tail to head as a solid color (no per-LED alternation)
current_color = color0
for i in range(segment_length + 1):
led_pos = (tail + i) % self.driver.num_leds
self.driver.n[led_pos] = current_color
# Move head continuously at n1 LEDs per second
if utime.ticks_diff(current_time, last_head_move) >= head_delay:
head = (head + 1) % self.driver.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.driver.num_leds
last_tail_move = current_time
# Check if we've reached min length
current_length = (head - tail) % self.driver.num_leds
if current_length == 0 and head != tail:
current_length = self.driver.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: second color fills the ring for 1 step, then restart
tail = head # Reset tail to head position to start fresh
phase = "growing"
self.driver.n.write()
# Yield once per tick so other logic can run
yield

64
src/patterns/pulse.py Normal file
View File

@@ -0,0 +1,64 @@
import utime
class Pulse:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
self.driver.off()
# Get colors from preset
colors = preset.c
if not colors:
colors = [(255, 255, 255)]
color_index = 0
cycle_start = utime.ticks_ms()
# State machine based pulse using a single generator loop
while True:
# Read current timing parameters from preset
attack_ms = max(0, int(preset.n1)) # Attack time in ms
hold_ms = max(0, int(preset.n2)) # Hold time in ms
decay_ms = max(0, int(preset.n3)) # Decay time in ms
delay_ms = max(0, int(preset.d))
total_ms = attack_ms + hold_ms + decay_ms + delay_ms
if total_ms <= 0:
total_ms = 1
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, cycle_start)
base_color = colors[color_index % len(colors)]
if elapsed < attack_ms and attack_ms > 0:
# Attack: fade 0 -> 1
factor = elapsed / attack_ms
color = tuple(int(c * factor) for c in base_color)
self.driver.fill(self.driver.apply_brightness(color, preset.b))
elif elapsed < attack_ms + hold_ms:
# Hold: full brightness
self.driver.fill(self.driver.apply_brightness(base_color, preset.b))
elif elapsed < attack_ms + hold_ms + decay_ms and decay_ms > 0:
# Decay: fade 1 -> 0
dec_elapsed = elapsed - attack_ms - hold_ms
factor = max(0.0, 1.0 - (dec_elapsed / decay_ms))
color = tuple(int(c * factor) for c in base_color)
self.driver.fill(self.driver.apply_brightness(color, preset.b))
elif elapsed < total_ms:
# Delay phase: LEDs off between pulses
self.driver.fill((0, 0, 0))
else:
# End of cycle, move to next color and restart timing
color_index += 1
cycle_start = now
if not preset.a:
break
# Skip drawing this tick, start next cycle
yield
continue
# Yield once per tick
yield

51
src/patterns/rainbow.py Normal file
View File

@@ -0,0 +1,51 @@
import utime
class Rainbow:
def __init__(self, driver):
self.driver = driver
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)
def run(self, preset):
step = self.driver.step % 256
step_amount = max(1, int(preset.n1)) # n1 controls step increment
# If auto is False, run a single step and then stop
if not preset.a:
for i in range(self.driver.num_leds):
rc_index = (i * 256 // self.driver.num_leds) + step
self.driver.n[i] = self.driver.apply_brightness(self._wheel(rc_index & 255), preset.b)
self.driver.n.write()
# Increment step by n1 for next manual call
self.driver.step = (step + step_amount) % 256
# Allow tick() to advance the generator once
yield
return
last_update = utime.ticks_ms()
while True:
current_time = utime.ticks_ms()
sleep_ms = max(1, int(preset.d)) # Get delay from preset
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
for i in range(self.driver.num_leds):
rc_index = (i * 256 // self.driver.num_leds) + step
self.driver.n[i] = self.driver.apply_brightness(
self._wheel(rc_index & 255),
preset.b,
)
self.driver.n.write()
step = (step + step_amount) % 256
self.driver.step = step
last_update = current_time
# Yield once per tick so other logic can run
yield

View File

@@ -0,0 +1,57 @@
import utime
class Transition:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""Transition between colors, blending over `delay` ms."""
colors = preset.c
if not colors:
self.driver.off()
yield
return
# Only one color: just keep it on
if len(colors) == 1:
while True:
self.driver.fill(self.driver.apply_brightness(colors[0], preset.b))
yield
return
color_index = 0
start_time = utime.ticks_ms()
while True:
if not colors:
break
# Get current and next color based on live list
c1 = colors[color_index % len(colors)]
c2 = colors[(color_index + 1) % len(colors)]
duration = max(10, int(preset.d)) # At least 10ms
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, start_time)
if elapsed >= duration:
# End of this transition step
if not preset.a:
# One-shot: transition from first to second color only
self.driver.fill(self.driver.apply_brightness(c2, preset.b))
break
# Auto: move to next pair
color_index = (color_index + 1) % len(colors)
start_time = now
yield
continue
# Interpolate between c1 and c2
factor = elapsed / duration
interpolated = tuple(
int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3)
)
self.driver.fill(self.driver.apply_brightness(interpolated, preset.b))
yield

79
src/preset.py Normal file
View File

@@ -0,0 +1,79 @@
class Preset:
def __init__(self, data):
# Set default values for all preset attributes
self.p = "off"
self.d = 100
self.b = 127
self.c = [(255, 255, 255)]
self.a = True
self.n1 = 0
self.n2 = 0
self.n3 = 0
self.n4 = 0
self.n5 = 0
self.n6 = 0
# Override defaults with provided data
self.edit(data)
def edit(self, data=None):
if not data:
return False
for key, value in data.items():
setattr(self, key, value)
return True
@property
def pattern(self):
return self.p
@pattern.setter
def pattern(self, value):
self.p = value
@property
def delay(self):
return self.d
@delay.setter
def delay(self, value):
self.d = value
@property
def brightness(self):
return self.b
@brightness.setter
def brightness(self, value):
self.b = value
@property
def colors(self):
return self.c
@colors.setter
def colors(self, value):
self.c = value
@property
def auto(self):
return self.a
@auto.setter
def auto(self, value):
self.a = value
def to_dict(self):
return {
"p": self.p,
"d": self.d,
"b": self.b,
"c": self.c,
"a": self.a,
"n1": self.n1,
"n2": self.n2,
"n3": self.n3,
"n4": self.n4,
"n5": self.n5,
"n6": self.n6,
}

122
src/presets.py Normal file
View File

@@ -0,0 +1,122 @@
from machine import Pin
from neopixel import NeoPixel
from preset import Preset
from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle
import json
class Presets:
def __init__(self, pin, num_leds):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.step = 0
# Global brightness (0255), controlled via ESPNow {"b": <value>}
self.b = 255
self.generator = None
self.presets = {}
self.selected = None
# Register all pattern methods
self.patterns = {
"off": self.off,
"on": self.on,
"blink": Blink(self).run,
"rainbow": Rainbow(self).run,
"pulse": Pulse(self).run,
"transition": Transition(self).run,
"chase": Chase(self).run,
"circle": Circle(self).run,
}
def save(self):
"""Save the presets to a file."""
with open("presets.json", "w") as f:
json.dump({name: preset.to_dict() for name, preset in self.presets.items()}, f)
return True
def load(self):
"""Load presets from a file."""
try:
with open("presets.json", "r") as f:
data = json.load(f)
except OSError:
# Create an empty presets file if missing
self.presets = {}
self.save()
return True
self.presets = {}
for name, preset_data in data.items():
if "c" in preset_data:
preset_data["c"] = [tuple(color) for color in preset_data["c"]]
self.presets[name] = Preset(preset_data)
if self.presets:
print("Loaded presets:")
#for name in sorted(self.presets.keys()):
# print(f" {name}: {self.presets[name].to_dict()}")
return True
def edit(self, name, data):
"""Create or update a preset with the given name."""
if name in self.presets:
# Update existing preset
self.presets[name].edit(data)
else:
# Create new preset
self.presets[name] = Preset(data)
return True
def delete(self, name):
if name in self.presets:
del self.presets[name]
return True
return False
def tick(self):
if self.generator is None:
return
try:
next(self.generator)
except StopIteration:
self.generator = None
def select(self, preset_name, step=None):
if preset_name in self.presets:
preset = self.presets[preset_name]
if preset.p in self.patterns:
# Set step value if explicitly provided
if step is not None:
self.step = step
elif preset.p == "off" or self.selected != preset_name:
self.step = 0
self.generator = self.patterns[preset.p](preset)
self.selected = preset_name # Store the preset name, not the object
return True
# If preset doesn't exist or pattern not found, default to "off"
return False
def update_num_leds(self, pin, num_leds):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
def apply_brightness(self, color, brightness_override=None):
# Combine per-preset brightness (override) with global brightness self.b
local = brightness_override if brightness_override is not None else 255
# Scale preset brightness by global brightness
effective_brightness = int(local * self.b / 255)
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 (0, 0, 0)
for i in range(self.num_leds):
self.n[i] = fill_color
self.n.write()
def off(self, preset=None):
self.fill((0, 0, 0))
def on(self, preset):
colors = preset.c
color = colors[0] if colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.b))

View File

@@ -14,14 +14,13 @@ class Settings(dict):
def set_defaults(self): def set_defaults(self):
self["led_pin"] = 10 self["led_pin"] = 10
self["num_leds"] = 50 self["num_leds"] = 50
self["pattern"] = "on"
self["delay"] = 100
self["brightness"] = 10
self["color_order"] = "rgb" self["color_order"] = "rgb"
self["name"] = f"led-{ubinascii.hexlify(network.WLAN(network.AP_IF).config('mac')).decode()}" self["name"] = f"led-{ubinascii.hexlify(network.WLAN(network.AP_IF).config('mac')).decode()}"
self["ap_password"] = ""
self["id"] = 0
self["debug"] = False self["debug"] = False
self["startup_preset"] = None
self["brightness"] = 255
def save(self): def save(self):
try: try:

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def run_for(p, wdt, duration_ms): def run_for(p, wdt, duration_ms):
@@ -19,7 +19,7 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
print("=" * 50) print("=" * 50)
@@ -29,11 +29,11 @@ def main():
# Test 1: Rainbow in AUTO mode (continuous) # Test 1: Rainbow in AUTO mode (continuous)
print("\nTest 1: Rainbow pattern in AUTO mode (should run continuously)") print("\nTest 1: Rainbow pattern in AUTO mode (should run continuously)")
p.edit("rainbow_auto", { p.edit("rainbow_auto", {
"pattern": "rainbow", "p": "rainbow",
"brightness": 128, "b": 128,
"delay": 50, "d": 50,
"n1": 2, "n1": 2,
"auto": True "a": True,
}) })
p.select("rainbow_auto") p.select("rainbow_auto")
print("Running rainbow_auto for 3 seconds...") print("Running rainbow_auto for 3 seconds...")
@@ -43,11 +43,11 @@ def main():
# Test 2: Rainbow in MANUAL mode (one step per tick) # Test 2: Rainbow in MANUAL mode (one step per tick)
print("\nTest 2: Rainbow pattern in MANUAL mode (one step per tick)") print("\nTest 2: Rainbow pattern in MANUAL mode (one step per tick)")
p.edit("rainbow_manual", { p.edit("rainbow_manual", {
"pattern": "rainbow", "p": "rainbow",
"brightness": 128, "b": 128,
"delay": 50, "d": 50,
"n1": 2, "n1": 2,
"auto": False "a": False,
}) })
p.select("rainbow_manual") p.select("rainbow_manual")
print("Calling tick() 5 times (should advance 5 steps)...") print("Calling tick() 5 times (should advance 5 steps)...")
@@ -65,14 +65,14 @@ def main():
# Test 3: Pulse in AUTO mode (continuous cycles) # Test 3: Pulse in AUTO mode (continuous cycles)
print("\nTest 3: Pulse pattern in AUTO mode (should pulse continuously)") print("\nTest 3: Pulse pattern in AUTO mode (should pulse continuously)")
p.edit("pulse_auto", { p.edit("pulse_auto", {
"pattern": "pulse", "p": "pulse",
"brightness": 128, "b": 128,
"delay": 100, "d": 100,
"n1": 500, # Attack "n1": 500, # Attack
"n2": 200, # Hold "n2": 200, # Hold
"n3": 500, # Decay "n3": 500, # Decay
"colors": [(255, 0, 0)], "c": [(255, 0, 0)],
"auto": True "a": True,
}) })
p.select("pulse_auto") p.select("pulse_auto")
print("Running pulse_auto for 3 seconds...") print("Running pulse_auto for 3 seconds...")
@@ -82,14 +82,14 @@ def main():
# Test 4: Pulse in MANUAL mode (one cycle then stop) # Test 4: Pulse in MANUAL mode (one cycle then stop)
print("\nTest 4: Pulse pattern in MANUAL mode (one cycle then stop)") print("\nTest 4: Pulse pattern in MANUAL mode (one cycle then stop)")
p.edit("pulse_manual", { p.edit("pulse_manual", {
"pattern": "pulse", "p": "pulse",
"brightness": 128, "b": 128,
"delay": 100, "d": 100,
"n1": 300, # Attack "n1": 300, # Attack
"n2": 200, # Hold "n2": 200, # Hold
"n3": 300, # Decay "n3": 300, # Decay
"colors": [(0, 255, 0)], "c": [(0, 255, 0)],
"auto": False "a": False,
}) })
p.select("pulse_manual") p.select("pulse_manual")
print("Running pulse_manual until generator stops...") print("Running pulse_manual until generator stops...")
@@ -108,11 +108,11 @@ def main():
# Test 5: Transition in AUTO mode (continuous transitions) # Test 5: Transition in AUTO mode (continuous transitions)
print("\nTest 5: Transition pattern in AUTO mode (continuous transitions)") print("\nTest 5: Transition pattern in AUTO mode (continuous transitions)")
p.edit("transition_auto", { p.edit("transition_auto", {
"pattern": "transition", "p": "transition",
"brightness": 128, "b": 128,
"delay": 500, "d": 500,
"colors": [(255, 0, 0), (0, 255, 0), (0, 0, 255)], "c": [(255, 0, 0), (0, 255, 0), (0, 0, 255)],
"auto": True "a": True,
}) })
p.select("transition_auto") p.select("transition_auto")
print("Running transition_auto for 3 seconds...") print("Running transition_auto for 3 seconds...")
@@ -122,11 +122,11 @@ def main():
# Test 6: Transition in MANUAL mode (one transition then stop) # Test 6: Transition in MANUAL mode (one transition then stop)
print("\nTest 6: Transition pattern in MANUAL mode (one transition then stop)") print("\nTest 6: Transition pattern in MANUAL mode (one transition then stop)")
p.edit("transition_manual", { p.edit("transition_manual", {
"pattern": "transition", "p": "transition",
"brightness": 128, "b": 128,
"delay": 500, "d": 500,
"colors": [(255, 0, 0), (0, 255, 0)], "c": [(255, 0, 0), (0, 255, 0)],
"auto": False "a": False,
}) })
p.select("transition_manual") p.select("transition_manual")
print("Running transition_manual until generator stops...") print("Running transition_manual until generator stops...")
@@ -145,11 +145,11 @@ def main():
# Test 7: Switching between auto and manual modes # Test 7: Switching between auto and manual modes
print("\nTest 7: Switching between auto and manual modes") print("\nTest 7: Switching between auto and manual modes")
p.edit("switch_test", { p.edit("switch_test", {
"pattern": "rainbow", "p": "rainbow",
"brightness": 128, "b": 128,
"delay": 50, "d": 50,
"n1": 2, "n1": 2,
"auto": True "a": True,
}) })
p.select("switch_test") p.select("switch_test")
print("Running in auto mode for 1 second...") print("Running in auto mode for 1 second...")
@@ -157,7 +157,7 @@ def main():
# Switch to manual mode by editing the preset # Switch to manual mode by editing the preset
print("Switching to manual mode...") print("Switching to manual mode...")
p.edit("switch_test", {"auto": False}) p.edit("switch_test", {"a": False})
p.select("switch_test") # Re-select to apply changes p.select("switch_test") # Re-select to apply changes
print("Calling tick() 3 times in manual mode...") print("Calling tick() 3 times in manual mode...")
@@ -168,7 +168,7 @@ def main():
# Switch back to auto mode # Switch back to auto mode
print("Switching back to auto mode...") print("Switching back to auto mode...")
p.edit("switch_test", {"auto": True}) p.edit("switch_test", {"a": True})
p.select("switch_test") p.select("switch_test")
print("Running in auto mode for 1 second...") print("Running in auto mode for 1 second...")
run_for(p, wdt, 1000) run_for(p, wdt, 1000)
@@ -176,7 +176,7 @@ def main():
# Cleanup # Cleanup
print("\nCleaning up...") print("\nCleaning up...")
p.edit("cleanup_off", {"pattern": "off"}) p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off") p.select("cleanup_off")
p.tick() p.tick()
utime.sleep_ms(100) utime.sleep_ms(100)

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def main(): def main():
@@ -10,15 +10,15 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
# Create blink preset # Create blink preset (use short-key fields: p=pattern, b=brightness, d=delay, c=colors)
p.edit("test_blink", { p.edit("test_blink", {
"pattern": "blink", "p": "blink",
"brightness": 64, "b": 64,
"delay": 200, "d": 200,
"colors": [(255, 0, 0), (0, 0, 255)] "c": [(255, 0, 0), (0, 0, 255)],
}) })
p.select("test_blink") p.select("test_blink")

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def run_for(p, wdt, ms): def run_for(p, wdt, ms):
@@ -19,20 +19,20 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
# Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1) # Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)
print("Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)") print("Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)")
p.edit("chase1", { p.edit("chase1", {
"pattern": "chase", "p": "chase",
"brightness": 255, "b": 255,
"delay": 200, "d": 200,
"n1": 5, "n1": 5,
"n2": 5, "n2": 5,
"n3": 1, "n3": 1,
"n4": 1, "n4": 1,
"colors": [(255, 0, 0), (0, 255, 0)] "c": [(255, 0, 0), (0, 255, 0)],
}) })
p.select("chase1") p.select("chase1")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
@@ -40,13 +40,13 @@ def main():
# Test 2: Forward and backward (n3=2, n4=-1) # Test 2: Forward and backward (n3=2, n4=-1)
print("Test 2: Forward and backward (n3=2, n4=-1)") print("Test 2: Forward and backward (n3=2, n4=-1)")
p.edit("chase2", { p.edit("chase2", {
"pattern": "chase", "p": "chase",
"n1": 3, "n1": 3,
"n2": 3, "n2": 3,
"n3": 2, "n3": 2,
"n4": -1, "n4": -1,
"delay": 150, "d": 150,
"colors": [(0, 0, 255), (255, 255, 0)] "c": [(0, 0, 255), (255, 255, 0)],
}) })
p.select("chase2") p.select("chase2")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
@@ -54,13 +54,13 @@ def main():
# Test 3: Large segments (n1=10, n2=5) # Test 3: Large segments (n1=10, n2=5)
print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)") print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)")
p.edit("chase3", { p.edit("chase3", {
"pattern": "chase", "p": "chase",
"n1": 10, "n1": 10,
"n2": 5, "n2": 5,
"n3": 3, "n3": 3,
"n4": 3, "n4": 3,
"delay": 200, "d": 200,
"colors": [(255, 128, 0), (128, 0, 255)] "c": [(255, 128, 0), (128, 0, 255)],
}) })
p.select("chase3") p.select("chase3")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
@@ -68,13 +68,13 @@ def main():
# Test 4: Fast movement (n3=5, n4=5) # Test 4: Fast movement (n3=5, n4=5)
print("Test 4: Fast movement (n3=5, n4=5)") print("Test 4: Fast movement (n3=5, n4=5)")
p.edit("chase4", { p.edit("chase4", {
"pattern": "chase", "p": "chase",
"n1": 4, "n1": 4,
"n2": 4, "n2": 4,
"n3": 5, "n3": 5,
"n4": 5, "n4": 5,
"delay": 100, "d": 100,
"colors": [(255, 0, 255), (0, 255, 255)] "c": [(255, 0, 255), (0, 255, 255)],
}) })
p.select("chase4") p.select("chase4")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
@@ -82,13 +82,13 @@ def main():
# Test 5: Backward movement (n3=-2, n4=-2) # Test 5: Backward movement (n3=-2, n4=-2)
print("Test 5: Backward movement (n3=-2, n4=-2)") print("Test 5: Backward movement (n3=-2, n4=-2)")
p.edit("chase5", { p.edit("chase5", {
"pattern": "chase", "p": "chase",
"n1": 6, "n1": 6,
"n2": 4, "n2": 4,
"n3": -2, "n3": -2,
"n4": -2, "n4": -2,
"delay": 200, "d": 200,
"colors": [(255, 255, 255), (0, 0, 0)] "c": [(255, 255, 255), (0, 0, 0)],
}) })
p.select("chase5") p.select("chase5")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
@@ -96,13 +96,13 @@ def main():
# Test 6: Alternating forward/backward (n3=3, n4=-2) # Test 6: Alternating forward/backward (n3=3, n4=-2)
print("Test 6: Alternating forward/backward (n3=3, n4=-2)") print("Test 6: Alternating forward/backward (n3=3, n4=-2)")
p.edit("chase6", { p.edit("chase6", {
"pattern": "chase", "p": "chase",
"n1": 5, "n1": 5,
"n2": 5, "n2": 5,
"n3": 3, "n3": 3,
"n4": -2, "n4": -2,
"delay": 250, "d": 250,
"colors": [(255, 0, 0), (0, 255, 0)] "c": [(255, 0, 0), (0, 255, 0)],
}) })
p.select("chase6") p.select("chase6")
run_for(p, wdt, 4000) run_for(p, wdt, 4000)
@@ -110,14 +110,14 @@ def main():
# Test 7: Manual mode - advance one step per beat # Test 7: Manual mode - advance one step per beat
print("Test 7: Manual mode chase (auto=False, n3=2, n4=1)") print("Test 7: Manual mode chase (auto=False, n3=2, n4=1)")
p.edit("chase_manual", { p.edit("chase_manual", {
"pattern": "chase", "p": "chase",
"n1": 4, "n1": 4,
"n2": 4, "n2": 4,
"n3": 2, "n3": 2,
"n4": 1, "n4": 1,
"delay": 200, "d": 200,
"colors": [(255, 255, 0), (0, 255, 255)], "c": [(255, 255, 0), (0, 255, 255)],
"auto": False "a": False,
}) })
p.step = 0 # Reset step counter p.step = 0 # Reset step counter
print(" Advancing pattern with 10 beats (select + tick)...") print(" Advancing pattern with 10 beats (select + tick)...")
@@ -131,12 +131,12 @@ def main():
# Test 8: Verify step increments correctly in manual mode # Test 8: Verify step increments correctly in manual mode
print("Test 8: Verify step increments (auto=False)") print("Test 8: Verify step increments (auto=False)")
p.edit("chase_manual2", { p.edit("chase_manual2", {
"pattern": "chase", "p": "chase",
"n1": 3, "n1": 3,
"n2": 3, "n2": 3,
"n3": 1, "n3": 1,
"n4": 1, "n4": 1,
"auto": False "a": False,
}) })
p.step = 0 p.step = 0
initial_step = p.step initial_step = p.step
@@ -151,7 +151,7 @@ def main():
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.edit("cleanup_off", {"pattern": "off"}) p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off") p.select("cleanup_off")
run_for(p, wdt, 100) run_for(p, wdt, 100)

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def run_for(p, wdt, ms): def run_for(p, wdt, ms):
@@ -19,19 +19,19 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
# Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0) # Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)
print("Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)") print("Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)")
p.edit("circle1", { p.edit("circle1", {
"pattern": "circle", "p": "circle",
"brightness": 255, "b": 255,
"n1": 50, # Head moves 50 LEDs/second "n1": 50, # Head moves 50 LEDs/second
"n2": 100, # Max length 100 LEDs "n2": 100, # Max length 100 LEDs
"n3": 200, # Tail moves 200 LEDs/second "n3": 200, # Tail moves 200 LEDs/second
"n4": 0, # Min length 0 LEDs "n4": 0, # Min length 0 LEDs
"colors": [(255, 0, 0)] # Red "c": [(255, 0, 0)], # Red
}) })
p.select("circle1") p.select("circle1")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
@@ -39,12 +39,12 @@ def main():
# Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0) # Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)
print("Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)") print("Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)")
p.edit("circle2", { p.edit("circle2", {
"pattern": "circle", "p": "circle",
"n1": 20, "n1": 20,
"n2": 50, "n2": 50,
"n3": 100, "n3": 100,
"n4": 0, "n4": 0,
"colors": [(0, 255, 0)] # Green "c": [(0, 255, 0)], # Green
}) })
p.select("circle2") p.select("circle2")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
@@ -52,12 +52,12 @@ def main():
# Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0) # Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)
print("Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)") print("Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)")
p.edit("circle3", { p.edit("circle3", {
"pattern": "circle", "p": "circle",
"n1": 100, "n1": 100,
"n2": 30, "n2": 30,
"n3": 20, "n3": 20,
"n4": 0, "n4": 0,
"colors": [(0, 0, 255)] # Blue "c": [(0, 0, 255)], # Blue
}) })
p.select("circle3") p.select("circle3")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
@@ -65,12 +65,12 @@ def main():
# Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10) # Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)
print("Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)") print("Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)")
p.edit("circle4", { p.edit("circle4", {
"pattern": "circle", "p": "circle",
"n1": 50, "n1": 50,
"n2": 40, "n2": 40,
"n3": 100, "n3": 100,
"n4": 10, "n4": 10,
"colors": [(255, 255, 0)] # Yellow "c": [(255, 255, 0)], # Yellow
}) })
p.select("circle4") p.select("circle4")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
@@ -78,12 +78,12 @@ def main():
# Test 5: Very fast (n1=200, n2=20, n3=200, n4=0) # Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)
print("Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)") print("Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)")
p.edit("circle5", { p.edit("circle5", {
"pattern": "circle", "p": "circle",
"n1": 200, "n1": 200,
"n2": 20, "n2": 20,
"n3": 200, "n3": 200,
"n4": 0, "n4": 0,
"colors": [(255, 0, 255)] # Magenta "c": [(255, 0, 255)], # Magenta
}) })
p.select("circle5") p.select("circle5")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
@@ -91,19 +91,19 @@ def main():
# Test 6: Very slow (n1=10, n2=25, n3=10, n4=0) # Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)
print("Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)") print("Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)")
p.edit("circle6", { p.edit("circle6", {
"pattern": "circle", "p": "circle",
"n1": 10, "n1": 10,
"n2": 25, "n2": 25,
"n3": 10, "n3": 10,
"n4": 0, "n4": 0,
"colors": [(0, 255, 255)] # Cyan "c": [(0, 255, 255)], # Cyan
}) })
p.select("circle6") p.select("circle6")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.edit("cleanup_off", {"pattern": "off"}) p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off") p.select("cleanup_off")
run_for(p, wdt, 100) run_for(p, wdt, 100)

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def main(): def main():
@@ -10,11 +10,11 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
# Create an "off" preset # Create an "off" preset (use short-key field `p` for pattern)
p.edit("test_off", {"pattern": "off"}) p.edit("test_off", {"p": "off"})
p.select("test_off") p.select("test_off")
start = utime.ticks_ms() start = utime.ticks_ms()

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def main(): def main():
@@ -10,17 +10,19 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
# Create presets for on and off # Create presets for on and off using the short-key fields that Presets expects
# Preset fields:
# p = pattern name, b = brightness, d = delay, c = list of (r,g,b) colors
p.edit("test_on", { p.edit("test_on", {
"pattern": "on", "p": "on",
"brightness": 64, "b": 64,
"delay": 120, "d": 120,
"colors": [(255, 0, 0), (0, 0, 255)] "c": [(255, 0, 0), (0, 0, 255)],
}) })
p.edit("test_off", {"pattern": "off"}) p.edit("test_off", {"p": "off"})
# ON phase # ON phase
p.select("test_on") p.select("test_on")

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def run_for(p, wdt, ms): def run_for(p, wdt, ms):
@@ -19,20 +19,20 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
# Test 1: Simple single-color pulse # Test 1: Simple single-color pulse
print("Test 1: Single-color pulse (attack=500, hold=500, decay=500, delay=500)") print("Test 1: Single-color pulse (attack=500, hold=500, decay=500, delay=500)")
p.edit("pulse1", { p.edit("pulse1", {
"pattern": "pulse", "p": "pulse",
"brightness": 255, "b": 255,
"colors": [(255, 0, 0)], "c": [(255, 0, 0)],
"n1": 500, # attack ms "n1": 500, # attack ms
"n2": 500, # hold ms "n2": 500, # hold ms
"n3": 500, # decay ms "n3": 500, # decay ms
"delay": 500, # delay ms between pulses "d": 500, # delay ms between pulses
"auto": True "a": True,
}) })
p.select("pulse1") p.select("pulse1")
run_for(p, wdt, 5000) run_for(p, wdt, 5000)
@@ -40,12 +40,12 @@ def main():
# Test 2: Faster pulse # Test 2: Faster pulse
print("Test 2: Fast pulse (attack=100, hold=100, decay=100, delay=100)") print("Test 2: Fast pulse (attack=100, hold=100, decay=100, delay=100)")
p.edit("pulse2", { p.edit("pulse2", {
"pattern": "pulse", "p": "pulse",
"n1": 100, "n1": 100,
"n2": 100, "n2": 100,
"n3": 100, "n3": 100,
"delay": 100, "d": 100,
"colors": [(0, 255, 0)] "c": [(0, 255, 0)],
}) })
p.select("pulse2") p.select("pulse2")
run_for(p, wdt, 4000) run_for(p, wdt, 4000)
@@ -53,13 +53,13 @@ def main():
# Test 3: Multi-color pulse cycle # Test 3: Multi-color pulse cycle
print("Test 3: Multi-color pulse (red -> green -> blue)") print("Test 3: Multi-color pulse (red -> green -> blue)")
p.edit("pulse3", { p.edit("pulse3", {
"pattern": "pulse", "p": "pulse",
"n1": 300, "n1": 300,
"n2": 300, "n2": 300,
"n3": 300, "n3": 300,
"delay": 200, "d": 200,
"colors": [(255, 0, 0), (0, 255, 0), (0, 0, 255)], "c": [(255, 0, 0), (0, 255, 0), (0, 0, 255)],
"auto": True "a": True,
}) })
p.select("pulse3") p.select("pulse3")
run_for(p, wdt, 6000) run_for(p, wdt, 6000)
@@ -67,13 +67,13 @@ def main():
# Test 4: One-shot pulse (auto=False) # Test 4: One-shot pulse (auto=False)
print("Test 4: Single pulse, auto=False") print("Test 4: Single pulse, auto=False")
p.edit("pulse4", { p.edit("pulse4", {
"pattern": "pulse", "p": "pulse",
"n1": 400, "n1": 400,
"n2": 0, "n2": 0,
"n3": 400, "n3": 400,
"delay": 0, "d": 0,
"colors": [(255, 255, 255)], "c": [(255, 255, 255)],
"auto": False "a": False,
}) })
p.select("pulse4") p.select("pulse4")
# Run long enough to allow one full pulse cycle # Run long enough to allow one full pulse cycle
@@ -81,7 +81,7 @@ def main():
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.edit("cleanup_off", {"pattern": "off"}) p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off") p.select("cleanup_off")
run_for(p, wdt, 200) run_for(p, wdt, 200)

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def run_for(p, wdt, ms): def run_for(p, wdt, ms):
@@ -19,17 +19,17 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
# Test 1: Basic rainbow with auto=True (continuous) # Test 1: Basic rainbow with auto=True (continuous)
print("Test 1: Basic rainbow (auto=True, n1=1)") print("Test 1: Basic rainbow (auto=True, n1=1)")
p.edit("rainbow1", { p.edit("rainbow1", {
"pattern": "rainbow", "p": "rainbow",
"brightness": 255, "b": 255,
"delay": 100, "d": 100,
"n1": 1, "n1": 1,
"auto": True "a": True,
}) })
p.select("rainbow1") p.select("rainbow1")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
@@ -37,10 +37,10 @@ def main():
# Test 2: Fast rainbow # Test 2: Fast rainbow
print("Test 2: Fast rainbow (low delay, n1=1)") print("Test 2: Fast rainbow (low delay, n1=1)")
p.edit("rainbow2", { p.edit("rainbow2", {
"pattern": "rainbow", "p": "rainbow",
"delay": 50, "d": 50,
"n1": 1, "n1": 1,
"auto": True "a": True,
}) })
p.select("rainbow2") p.select("rainbow2")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
@@ -48,10 +48,10 @@ def main():
# Test 3: Slow rainbow # Test 3: Slow rainbow
print("Test 3: Slow rainbow (high delay, n1=1)") print("Test 3: Slow rainbow (high delay, n1=1)")
p.edit("rainbow3", { p.edit("rainbow3", {
"pattern": "rainbow", "p": "rainbow",
"delay": 500, "d": 500,
"n1": 1, "n1": 1,
"auto": True "a": True,
}) })
p.select("rainbow3") p.select("rainbow3")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
@@ -59,11 +59,11 @@ def main():
# Test 4: Low brightness rainbow # Test 4: Low brightness rainbow
print("Test 4: Low brightness rainbow (n1=1)") print("Test 4: Low brightness rainbow (n1=1)")
p.edit("rainbow4", { p.edit("rainbow4", {
"pattern": "rainbow", "p": "rainbow",
"brightness": 64, "b": 64,
"delay": 100, "d": 100,
"n1": 1, "n1": 1,
"auto": True "a": True,
}) })
p.select("rainbow4") p.select("rainbow4")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
@@ -71,11 +71,11 @@ def main():
# Test 5: Single-step rainbow (auto=False) # Test 5: Single-step rainbow (auto=False)
print("Test 5: Single-step rainbow (auto=False, n1=1)") print("Test 5: Single-step rainbow (auto=False, n1=1)")
p.edit("rainbow5", { p.edit("rainbow5", {
"pattern": "rainbow", "p": "rainbow",
"brightness": 255, "b": 255,
"delay": 100, "d": 100,
"n1": 1, "n1": 1,
"auto": False "a": False,
}) })
p.step = 0 p.step = 0
for i in range(10): for i in range(10):
@@ -88,9 +88,9 @@ def main():
# Test 6: Verify step updates correctly # Test 6: Verify step updates correctly
print("Test 6: Verify step updates (auto=False, n1=1)") print("Test 6: Verify step updates (auto=False, n1=1)")
p.edit("rainbow6", { p.edit("rainbow6", {
"pattern": "rainbow", "p": "rainbow",
"n1": 1, "n1": 1,
"auto": False "a": False,
}) })
initial_step = p.step initial_step = p.step
p.select("rainbow6") p.select("rainbow6")
@@ -101,11 +101,11 @@ def main():
# Test 7: Fast step increment (n1=5) # Test 7: Fast step increment (n1=5)
print("Test 7: Fast rainbow (n1=5, auto=True)") print("Test 7: Fast rainbow (n1=5, auto=True)")
p.edit("rainbow7", { p.edit("rainbow7", {
"pattern": "rainbow", "p": "rainbow",
"brightness": 255, "b": 255,
"delay": 100, "d": 100,
"n1": 5, "n1": 5,
"auto": True "a": True,
}) })
p.select("rainbow7") p.select("rainbow7")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
@@ -113,9 +113,9 @@ def main():
# Test 8: Very fast step increment (n1=10) # Test 8: Very fast step increment (n1=10)
print("Test 8: Very fast rainbow (n1=10, auto=True)") print("Test 8: Very fast rainbow (n1=10, auto=True)")
p.edit("rainbow8", { p.edit("rainbow8", {
"pattern": "rainbow", "p": "rainbow",
"n1": 10, "n1": 10,
"auto": True "a": True,
}) })
p.select("rainbow8") p.select("rainbow8")
run_for(p, wdt, 2000) run_for(p, wdt, 2000)
@@ -123,9 +123,9 @@ def main():
# Test 9: Verify n1 controls step increment (auto=False) # Test 9: Verify n1 controls step increment (auto=False)
print("Test 9: Verify n1 step increment (auto=False, n1=5)") print("Test 9: Verify n1 step increment (auto=False, n1=5)")
p.edit("rainbow9", { p.edit("rainbow9", {
"pattern": "rainbow", "p": "rainbow",
"n1": 5, "n1": 5,
"auto": False "a": False,
}) })
p.step = 0 p.step = 0
initial_step = p.step initial_step = p.step
@@ -141,7 +141,7 @@ def main():
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.edit("cleanup_off", {"pattern": "off"}) p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off") p.select("cleanup_off")
run_for(p, wdt, 100) run_for(p, wdt, 100)

View File

@@ -2,7 +2,7 @@
import utime import utime
from machine import WDT from machine import WDT
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
def run_for(p, wdt, ms): def run_for(p, wdt, ms):
@@ -19,17 +19,17 @@ def main():
pin = s.get("led_pin", 10) pin = s.get("led_pin", 10)
num = s.get("num_leds", 30) num = s.get("num_leds", 30)
p = Patterns(pin=pin, num_leds=num) p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000) wdt = WDT(timeout=10000)
# Test 1: Simple two-color transition # Test 1: Simple two-color transition
print("Test 1: Two-color transition (red <-> blue, delay=1000)") print("Test 1: Two-color transition (red <-> blue, delay=1000)")
p.edit("transition1", { p.edit("transition1", {
"pattern": "transition", "p": "transition",
"brightness": 255, "b": 255,
"delay": 1000, # transition duration "d": 1000, # transition duration
"colors": [(255, 0, 0), (0, 0, 255)], "c": [(255, 0, 0), (0, 0, 255)],
"auto": True "a": True,
}) })
p.select("transition1") p.select("transition1")
run_for(p, wdt, 6000) run_for(p, wdt, 6000)
@@ -37,10 +37,10 @@ def main():
# Test 2: Multi-color transition # Test 2: Multi-color transition
print("Test 2: Multi-color transition (red -> green -> blue -> white)") print("Test 2: Multi-color transition (red -> green -> blue -> white)")
p.edit("transition2", { p.edit("transition2", {
"pattern": "transition", "p": "transition",
"delay": 800, "d": 800,
"colors": [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)], "c": [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)],
"auto": True "a": True,
}) })
p.select("transition2") p.select("transition2")
run_for(p, wdt, 8000) run_for(p, wdt, 8000)
@@ -48,10 +48,10 @@ def main():
# Test 3: One-shot transition (auto=False) # Test 3: One-shot transition (auto=False)
print("Test 3: One-shot transition (auto=False)") print("Test 3: One-shot transition (auto=False)")
p.edit("transition3", { p.edit("transition3", {
"pattern": "transition", "p": "transition",
"delay": 1000, "d": 1000,
"colors": [(255, 0, 0), (0, 255, 0)], "c": [(255, 0, 0), (0, 255, 0)],
"auto": False "a": False,
}) })
p.select("transition3") p.select("transition3")
# Run long enough for a single transition step # Run long enough for a single transition step
@@ -60,17 +60,17 @@ def main():
# Test 4: Single-color behavior (should just stay on) # Test 4: Single-color behavior (should just stay on)
print("Test 4: Single-color transition (should hold color)") print("Test 4: Single-color transition (should hold color)")
p.edit("transition4", { p.edit("transition4", {
"pattern": "transition", "p": "transition",
"colors": [(0, 0, 255)], "c": [(0, 0, 255)],
"delay": 500, "d": 500,
"auto": True "a": True,
}) })
p.select("transition4") p.select("transition4")
run_for(p, wdt, 3000) run_for(p, wdt, 3000)
# Cleanup # Cleanup
print("Test complete, turning off") print("Test complete, turning off")
p.edit("cleanup_off", {"pattern": "off"}) p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off") p.select("cleanup_off")
run_for(p, wdt, 200) run_for(p, wdt, 200)

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Test ESPNow receive functionality - runs on MicroPython device.""" """Test ESPNow receive functionality - runs on MicroPython device."""
import json import json
import os
import utime import utime
from settings import Settings from settings import Settings
from patterns import Patterns from presets import Presets
from utils import convert_and_reorder_colors from utils import convert_and_reorder_colors
@@ -93,7 +94,7 @@ def test_version_check():
"""Test that messages with wrong version are rejected.""" """Test that messages with wrong version are rejected."""
print("Test 1: Version check") print("Test 1: Version check")
settings = Settings() settings = Settings()
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -119,7 +120,7 @@ def test_preset_creation():
"""Test preset creation from ESPNow messages.""" """Test preset creation from ESPNow messages."""
print("\nTest 2: Preset creation") print("\nTest 2: Preset creation")
settings = Settings() settings = Settings()
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -164,7 +165,7 @@ def test_color_conversion():
print("\nTest 3: Color conversion") print("\nTest 3: Color conversion")
settings = Settings() settings = Settings()
settings["color_order"] = "rgb" # Default RGB order settings["color_order"] = "rgb" # Default RGB order
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -190,7 +191,7 @@ def test_color_conversion():
# Test GRB order # Test GRB order
settings["color_order"] = "grb" settings["color_order"] = "grb"
patterns2 = Patterns(settings["led_pin"], settings["num_leds"]) patterns2 = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow2 = MockESPNow() mock_espnow2 = MockESPNow()
msg2 = { msg2 = {
"v": "1", "v": "1",
@@ -213,7 +214,7 @@ def test_preset_update():
"""Test that editing an existing preset updates it.""" """Test that editing an existing preset updates it."""
print("\nTest 4: Preset update") print("\nTest 4: Preset update")
settings = Settings() settings = Settings()
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -256,7 +257,7 @@ def test_select():
print("\nTest 5: Preset selection") print("\nTest 5: Preset selection")
settings = Settings() settings = Settings()
settings["name"] = "device1" settings["name"] = "device1"
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -291,7 +292,7 @@ def test_full_message():
print("\nTest 6: Full message (presets + select)") print("\nTest 6: Full message (presets + select)")
settings = Settings() settings = Settings()
settings["name"] = "test_device" settings["name"] = "test_device"
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -331,7 +332,7 @@ def test_switch_presets():
print("\nTest 7: Switch between presets") print("\nTest 7: Switch between presets")
settings = Settings() settings = Settings()
settings["name"] = "switch_device" settings["name"] = "switch_device"
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -427,7 +428,7 @@ def test_beat_functionality():
print("\nTest 8: Beat functionality") print("\nTest 8: Beat functionality")
settings = Settings() settings = Settings()
settings["name"] = "beat_device" settings["name"] = "beat_device"
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -551,7 +552,7 @@ def test_select_with_step():
print("\nTest 9: Select with step value") print("\nTest 9: Select with step value")
settings = Settings() settings = Settings()
settings["name"] = "step_device" settings["name"] = "step_device"
patterns = Patterns(settings["led_pin"], settings["num_leds"]) patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow() mock_espnow = MockESPNow()
wdt = get_wdt() wdt = get_wdt()
@@ -602,7 +603,7 @@ def test_select_with_step():
print(" ✓ Step preserved when selecting same preset without step (tick advances it)") print(" ✓ Step preserved when selecting same preset without step (tick advances it)")
# Select different preset with step # Select different preset with step
patterns.edit("other_preset", {"pattern": "rainbow", "auto": False}) patterns.edit("other_preset", {"p": "rainbow", "a": False})
mock_espnow.clear() mock_espnow.clear()
msg4 = { msg4 = {
"v": "1", "v": "1",
@@ -621,6 +622,45 @@ def test_select_with_step():
print(" ✓ Step set correctly when switching presets") print(" ✓ Step set correctly when switching presets")
def test_preset_save_load():
"""Test saving and loading presets to/from JSON."""
print("\nTest 10: Preset save/load")
settings = Settings()
patterns = Presets(settings["led_pin"], settings["num_leds"])
patterns.edit("saved_preset", {
"p": "blink",
"d": 150,
"b": 200,
"c": [(1, 2, 3), (4, 5, 6)],
"a": False,
"n1": 1,
"n2": 2,
"n3": 3,
"n4": 4,
"n5": 5,
"n6": 6,
})
assert patterns.save(), "Save should return True"
reloaded = Presets(settings["led_pin"], settings["num_leds"])
assert reloaded.load(), "Load should return True"
preset = reloaded.presets.get("saved_preset")
assert preset is not None, "Preset should be loaded"
assert preset.p == "blink", "Pattern should be blink"
assert preset.d == 150, "Delay should be 150"
assert preset.b == 200, "Brightness should be 200"
assert preset.c == [(1, 2, 3), (4, 5, 6)], "Colors should be restored as tuples"
assert preset.a is False, "Auto should be False"
assert (preset.n1, preset.n2, preset.n3, preset.n4, preset.n5, preset.n6) == (1, 2, 3, 4, 5, 6), "n1-n6 should match"
try:
os.remove("presets.json")
except OSError:
pass
print(" ✓ Preset save/load works correctly")
def main(): def main():
"""Run all tests.""" """Run all tests."""
print("=" * 60) print("=" * 60)
@@ -637,6 +677,7 @@ def main():
test_switch_presets() test_switch_presets()
test_beat_functionality() test_beat_functionality()
test_select_with_step() test_select_with_step()
test_preset_save_load()
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("All tests passed! ✓") print("All tests passed! ✓")