Compare commits

..

7 Commits

Author SHA1 Message Date
fb53f900fb refactor(driver): simplify and harden espnow message handlers 2026-03-22 02:53:23 +13:00
044dd815dc refactor(driver): harden preset parsing and refresh tooling 2026-03-22 02:00:13 +13:00
f3bcc89320 test(driver): cover default targets and color alias handling 2026-03-22 01:52:15 +13:00
4b74f3ef02 fix(driver): gate targeted default and normalize preset colours 2026-03-22 01:47:14 +13:00
8403f36a1f fix(presets): normalize loaded colours before pattern math 2026-03-22 00:36:53 +13:00
4c7646b2fe Adjust defaults and preset handling
- Switch startup_preset to default key
- Add built-in on/off presets and tweak device defaults

Made-with: Cursor
2026-03-14 02:41:07 +13:00
1616471859 Change startup_preset to default 2026-03-10 22:48:19 +13:00
14 changed files with 614 additions and 174 deletions

View File

@@ -11,6 +11,7 @@ watchfiles = "*"
fastapi = "*"
uvicorn = "*"
flask = "*"
serial = "*"
[dev-packages]

215
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "921fc0268aaeb27ac977902942dd25f0f84ea35bcbbee0412a4d7c801652eb67"
"sha256": "5d970f8c0ea9e8ffa98cf0ea5f791161589a97d953d2629da026d01fa7a8bce7"
},
"pipfile-spec": 6,
"requires": {
@@ -151,11 +151,11 @@
},
"bitstring": {
"hashes": [
"sha256:69d1587f0ac18dc7d93fc7e80d5f447161a33e57027e726dc18a0a8bacf1711a",
"sha256:a08bc09d3857216d4c0f412a1611056f1cc2b64fd254fb1e8a0afba7cfa1a95a"
"sha256:e682ac522bb63e041d16cbc9d0ca86a4f00194db16d0847c7efe066f836b2e37",
"sha256:feac49524fcf3ef27e6081e86f02b10d2adf6c3773bf22fbe0e7eea9534bc737"
],
"markers": "python_version >= '3.8'",
"version": "==4.3.1"
"version": "==4.4.0"
},
"blinker": {
"hashes": [
@@ -265,84 +265,89 @@
},
"cryptography": {
"hashes": [
"sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa",
"sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc",
"sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da",
"sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255",
"sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2",
"sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485",
"sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0",
"sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d",
"sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616",
"sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947",
"sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0",
"sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908",
"sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81",
"sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc",
"sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd",
"sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b",
"sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019",
"sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7",
"sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b",
"sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973",
"sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b",
"sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5",
"sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80",
"sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef",
"sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0",
"sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b",
"sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e",
"sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c",
"sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2",
"sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af",
"sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4",
"sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab",
"sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82",
"sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3",
"sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59",
"sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da",
"sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061",
"sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085",
"sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b",
"sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263",
"sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e",
"sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829",
"sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4",
"sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c",
"sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f",
"sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095",
"sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32",
"sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976",
"sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822"
"sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72",
"sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235",
"sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9",
"sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356",
"sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257",
"sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad",
"sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4",
"sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c",
"sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614",
"sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed",
"sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31",
"sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229",
"sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0",
"sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731",
"sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b",
"sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4",
"sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4",
"sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263",
"sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595",
"sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1",
"sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678",
"sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48",
"sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76",
"sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0",
"sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18",
"sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d",
"sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d",
"sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1",
"sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981",
"sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7",
"sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82",
"sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2",
"sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4",
"sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663",
"sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c",
"sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d",
"sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a",
"sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a",
"sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d",
"sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b",
"sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a",
"sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826",
"sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee",
"sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9",
"sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648",
"sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da",
"sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2",
"sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2",
"sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"
],
"markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'",
"version": "==46.0.4"
"version": "==46.0.5"
},
"esptool": {
"hashes": [
"sha256:2ea9bcd7eb263d380a4fe0170856a10e4c65e3f38c757ebdc73584c8dd8322da"
"sha256:9c355b7d6331cc92979cc710ae5c41f59830d1ea29ec24c467c6005a092c06d6"
],
"index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==5.1.0"
"version": "==5.2.0"
},
"fastapi": {
"hashes": [
"sha256:c8cdf7c2182c9a06bf9cfa3329819913c189dc86389b90d5709892053582db29",
"sha256:ed99383fd96063447597d5aa2a9ec3973be198e3b4fc10c55f15c62efdb21c60"
"sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e",
"sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==0.128.3"
"version": "==0.135.1"
},
"flask": {
"hashes": [
"sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87",
"sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"
"sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb",
"sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==3.1.2"
"version": "==3.1.3"
},
"future": {
"hashes": [
"sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216",
"sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.0.0"
},
"h11": {
"hashes": [
@@ -367,6 +372,14 @@
],
"version": "==2.3.0"
},
"iso8601": {
"hashes": [
"sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df",
"sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==2.1.0"
},
"itsdangerous": {
"hashes": [
"sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef",
@@ -500,16 +513,15 @@
"sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5"
],
"index": "pypi",
"markers": "python_version >= '3.4'",
"version": "==1.27.0"
},
"platformdirs": {
"hashes": [
"sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda",
"sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"
"sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934",
"sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"
],
"markers": "python_version >= '3.10'",
"version": "==4.5.1"
"version": "==4.9.4"
},
"pycparser": {
"hashes": [
@@ -758,11 +770,11 @@
},
"rich": {
"hashes": [
"sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69",
"sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"
"sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d",
"sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"
],
"markers": "python_full_version >= '3.8.0'",
"version": "==14.3.2"
"version": "==14.3.3"
},
"rich-click": {
"hashes": [
@@ -772,6 +784,14 @@
"markers": "python_version >= '3.8'",
"version": "==1.9.7"
},
"serial": {
"hashes": [
"sha256:542150a127ddbf5ed2acc3a6ac4ce807cbcdae3b197acf785bbda6565c94f848",
"sha256:e887f06e07e190e39174b694eee6724e3c48bd361be1d97964caef5d5b61c73b"
],
"index": "pypi",
"version": "==0.0.97"
},
"starlette": {
"hashes": [
"sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74",
@@ -780,6 +800,43 @@
"markers": "python_version >= '3.10'",
"version": "==0.52.1"
},
"tibs": {
"hashes": [
"sha256:01ea5258bdf942d21560dc07d532082cd04f07cfef65fedd58ae84f7d0d2562a",
"sha256:0a7ce857ef05c59dc61abadc31c4b9b1e3c62f9e5fb29217988c308936aea71e",
"sha256:130bc68ff500fc8185677df7a97350b5d5339e6ba7e325bc3031337f6424ede7",
"sha256:173dfbecb2309edd9771f453580c88cf251e775613461566b23dbd756b3d54cb",
"sha256:1906729038b85c3b4c040aa28a456d85bc976d0c5007177350eb73374ffa0fd0",
"sha256:1b56583db148e5094d781c3d746815dbcbb6378c6f813c8ce291efd4ab21da8b",
"sha256:1d5521cc6768bfa6282a0c591ba06b079ab91b5c7d5696925ad2abac59779a54",
"sha256:1f95d5db62960205a1e9eba73ce67dc14e7366ae080cd4e5b6f005ebd90faf02",
"sha256:29480bf03e3372a5f9cc59ea0541f76f8efd696d4f0d214715e94247c342a037",
"sha256:2a618de62004d9217d2d2ab0f7f9bbdd098c12642dc01f07b3fb00f0b5f3131a",
"sha256:42725200f1b02687ed6e6a1c01e0ec150dc829d21d901ffc74cc0ac4d821f57f",
"sha256:477608f9b87e24a22ab6d50b81da04a5cb59bfa49598ff7ec5165035a18fb392",
"sha256:4b7510235379368b7523f624d46e0680f3706e3a3965877a6583cdcb598b8bac",
"sha256:501728d096e10d9a165aa526743d47418a6bbfd7b084fa47ecb22be7641d3edb",
"sha256:63255749f937c5e6fedcc7d54e7bd359aef711017e6855f373b0510a14ee2215",
"sha256:6a9feed5931b881809a950eca0e01e757113e2383a2af06a3e6982f110c869e2",
"sha256:76746f01b3db9dbd802f5e615f11f68df7a29ecef521b082dca53f3fa7d0084f",
"sha256:77103a9f1af72ac4cf5006828d0fb21578d19ce55fd990e9a1c8e46fd549561f",
"sha256:7d6592ed93c6748acd39df484c1ee24d40ee247c2a20ca38ba03363506fd24f3",
"sha256:847709c108800ad6a45efaf9a040628278956938a4897f7427a2587013dc3b98",
"sha256:859f05315ffb307d3474c505d694f3a547f00730a024c982f5f60316a5505b3c",
"sha256:a61d36155f8ab8642e1b6744e13822f72050fc7ec4f86ec6965295afa04949e2",
"sha256:a883ca13a922a66b2c1326a9c188123a574741a72510a4bf52fd6f97db191e44",
"sha256:ac0aa2aae38f7325c91c261ce1d18f769c4c7033c98d6ea3ea5534585cf16452",
"sha256:ace018a057459e3dccd06a4aae1c5c8cd57e352b263dcef534ae39bf3e03b5cf",
"sha256:ad61df93b50f875b277ab736c5d37b6bce56f9abce489a22f4e02d9daa2966e3",
"sha256:b9535dc7b7484904a58b51bd8e64da7efbf1d8466ff7e84ed1d78f4ddc561c99",
"sha256:d4f3ff613d486650816bc5516760c0382a2cc0ca8aeddd8914d011bc3b81d9a2",
"sha256:e13b9c7ff2604b0146772025e1ac6f85c8c625bf6ac73736ff671eaf357dda41",
"sha256:f5eea45851c960628a2bd29847765d55e19a687c5374456ad2c8cf6410eb1efa",
"sha256:f70bd250769381c73110d6f24feaf8b6fcd44f680b3cb28a20ea06db3d04fb6f"
],
"markers": "python_version >= '3.8'",
"version": "==0.5.7"
},
"typing-extensions": {
"hashes": [
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
@@ -798,12 +855,11 @@
},
"uvicorn": {
"hashes": [
"sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea",
"sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee"
"sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359",
"sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775"
],
"index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==0.40.0"
"version": "==0.42.0"
},
"watchfiles": {
"hashes": [
@@ -918,16 +974,15 @@
"sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==1.1.1"
},
"werkzeug": {
"hashes": [
"sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc",
"sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"
"sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25",
"sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131"
],
"markers": "python_version >= '3.9'",
"version": "==3.1.5"
"version": "==3.1.6"
}
},
"develop": {}

0
deploy.sh Normal file
View File

27
dev.py
View File

@@ -3,11 +3,12 @@
import subprocess
import serial
import sys
from pathlib import Path
print(sys.argv)
# Extract port (first arg if it's not a command)
commands = ["src", "lib", "ls", "reset", "follow", "db"]
commands = ["src", "lib", "ls", "reset", "follow", "db", "test"]
port = None
if len(sys.argv) > 1 and sys.argv[1] not in commands:
port = sys.argv[1]
@@ -51,3 +52,27 @@ for cmd in sys.argv[1:]:
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "db", ":" ])
else:
print("Error: Port required for 'db' command")
case "test":
if port:
if "all" in sys.argv[1:]:
test_files = sorted(
str(path)
for path in Path("test").rglob("*.py")
if path.is_file()
)
failed = []
for test_file in test_files:
print(f"Running {test_file}")
code = subprocess.call(
["mpremote", "connect", port, "run", test_file]
)
if code != 0:
failed.append((test_file, code))
if failed:
print("Some tests failed:")
for test_file, code in failed:
print(f" {test_file} (exit {code})")
else:
subprocess.call(["mpremote", "connect", port, "run", "test/all.py"])
else:
print("Error: Port required for 'test' command")

View File

@@ -48,17 +48,17 @@ Presets define LED patterns with their configuration. Each preset has a name and
- **`pattern`** (required): Pattern type. Options:
- `"off"` - Turn off all LEDs
- `"on"` - Solid color
- `"on"` - Solid colour
- `"blink"` - Blinking pattern
- `"rainbow"` - Rainbow color cycle
- `"rainbow"` - Rainbow colour cycle
- `"pulse"` - Pulse/fade pattern
- `"transition"` - Color transition
- `"transition"` - Colour transition
- `"chase"` - Chasing pattern
- `"circle"` - Circle loading pattern
- **`colors`** (optional): Array of hex color strings (e.g., `"#FF0000"` for red). Default: `["#FFFFFF"]`
- Colors are automatically converted from hex to RGB and reordered based on device color order setting
- Supports multiple colors for patterns that use them
- **`colors`** (optional): Array of hex colour strings (e.g., `"#FF0000"` for red). Default: `["#FFFFFF"]`
- Colours are automatically converted from hex to RGB and reordered based on device colour order setting
- Supports multiple colours for patterns that use them
- **`delay`** (optional): Delay in milliseconds between pattern updates. Default: `100`
@@ -74,7 +74,7 @@ Presets define LED patterns with their configuration. Each preset has a name and
### Pattern-Specific Parameters
#### Rainbow
- **`n1`**: Step increment (how many color wheel positions to advance per update). Default: `1`
- **`n1`**: Step increment (how many colour wheel positions to advance per update). Default: `1`
#### Pulse
- **`n1`**: Attack time in milliseconds (fade in)
@@ -86,8 +86,8 @@ Presets define LED patterns with their configuration. Each preset has a name and
- **`delay`**: Transition duration in milliseconds
#### Chase
- **`n1`**: Number of LEDs with first color
- **`n2`**: Number of LEDs with second color
- **`n1`**: Number of LEDs with first colour
- **`n2`**: Number of LEDs with second colour
- **`n3`**: Movement amount on even steps (can be negative)
- **`n4`**: Movement amount on odd steps (can be negative)
@@ -235,7 +235,7 @@ All devices will start at step 10 and advance together on subsequent beats.
1. **Version Check**: Messages with `v != "1"` are rejected
2. **Preset Processing**: Presets are created or updated (upsert behavior)
3. **Color Conversion**: Hex colors are converted to RGB tuples and reordered based on device color order
3. **Colour Conversion**: Hex colours are converted to RGB tuples and reordered based on device colour order
4. **Selection**: Devices select their assigned preset, optionally with step value
## Best Practices
@@ -244,20 +244,20 @@ All devices will start at step 10 and advance together on subsequent beats.
2. **Use "off" for sync**: Select "off" pattern to synchronize devices before starting patterns
3. **Beats for manual mode**: Send select messages repeatedly with same preset name to advance manual patterns
4. **Step for precision**: Use step parameter when exact synchronization is required
5. **Color format**: Always use hex strings (`"#RRGGBB"`), conversion is automatic
5. **Colour format**: Always use hex strings (`"#RRGGBB"`), conversion is automatic
## Error Handling
- Invalid version: Message is ignored
- Missing preset: Selection fails, device keeps current preset
- Invalid pattern: Selection fails, device keeps current preset
- Missing colors: Pattern uses default white color
- Missing colours: Pattern uses default white colour
- Invalid step: Step value is used as-is (may cause unexpected behavior)
## Notes
- Colors are automatically converted from hex strings to RGB tuples
- Color order reordering happens automatically based on device settings
- Colours are automatically converted from hex strings to RGB tuples
- Colour order reordering happens automatically based on device settings
- Step counter wraps around (0-255 for rainbow, unbounded for others)
- Manual mode patterns stop after one step/cycle, waiting for next beat
- Auto mode patterns run continuously until changed

View File

@@ -11,16 +11,16 @@ settings = Settings()
print(settings)
presets = Presets(settings["led_pin"], settings["num_leds"])
presets.load()
presets.load(settings)
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}")
# Use the default preset name from settings (set via controller or defaults)
default_preset = settings.get("default", "")
if default_preset and default_preset in presets.presets:
presets.select(default_preset)
print(f"Selected startup preset: {default_preset}")
wdt = WDT(timeout=10000)
wdt.feed()
last_brightness_save = 0
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
@@ -30,44 +30,95 @@ e = ESPNow()
e.active(True)
def as_dict(value):
return value if isinstance(value, dict) else {}
def as_list(value):
return value if isinstance(value, list) else []
def receive_data(receiver):
"""Read one ESPNow message and decode JSON dict payload."""
try:
host, msg = receiver.recv()
data = json.loads(msg)
print(msg)
data = as_dict(data)
if data.get("v", "") != "1":
return None
return data
except (ValueError, TypeError):
return None
def apply_brightness(data):
"""Apply and persist global brightness from payload."""
try:
presets.b = max(0, min(255, int(data["b"])))
settings["brightness"] = presets.b
except (TypeError, ValueError):
pass
def apply_presets(data):
"""Create/update preset definitions from payload."""
presets_map = as_dict(data.get("presets"))
for id, preset_data in presets_map.items():
preset_data = as_dict(preset_data)
if not preset_data:
continue
color_key = "c" if "c" in preset_data else ("colors" if "colors" in preset_data else None)
if color_key is not None:
try:
preset_data[color_key] = convert_and_reorder_colors(
preset_data[color_key], settings
)
except (TypeError, ValueError, KeyError):
continue
presets.edit(id, preset_data)
print(f"Edited preset {id}: {preset_data.get('name', '')}")
def apply_select(data):
"""Select preset for this device when addressed."""
select_map = as_dict(data.get("select"))
device_name = settings["name"]
# Case-sensitive: select key must match device name exactly.
select_list = as_list(select_map.get(device_name))
if not select_list:
return
preset_name = select_list[0]
step = select_list[1] if len(select_list) > 1 else None
if isinstance(preset_name, str):
presets.select(preset_name, step=step)
def apply_default(data):
targets = as_list(data.get("targets"))
default_name = data.get("default", "")
if (
settings["name"] in targets
and isinstance(default_name, str)
and default_name in presets.presets
):
settings["default"] = default_name
while True:
wdt.feed()
presets.tick()
if e.any():
host, msg = e.recv()
data = json.loads(msg)
# Only handle messages with the expected version.
if data.get("v") != "1":
if (data := receive_data(e)) is None:
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
apply_brightness(data)
if "presets" in data:
for id, preset_data in data["presets"].items():
# Convert hex color strings to RGB tuples and reorder based on device color order
if "c" in preset_data:
preset_data["c"] = convert_and_reorder_colors(preset_data["c"], settings)
presets.edit(id, preset_data)
print(f"Edited preset {id}: {preset_data.get('name', '')}")
if settings.get("name") in data.get("select", {}):
select_list = data["select"][settings.get("name")]
# Select value is always a list: ["preset_name"] or ["preset_name", step]
if select_list:
preset_name = select_list[0]
step = select_list[1] if len(select_list) > 1 else None
presets.select(preset_name, step=step)
apply_presets(data)
if "select" in data:
apply_select(data)
if "default" in data:
settings["startup_preset"] = data["default"]
print(f"Set startup preset to: {data['default']}")
settings.save()
if "save" in data:
apply_default(data)
if "save" in data and ("presets" in data or "default" in data):
presets.save()

View File

@@ -1,16 +0,0 @@
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

@@ -19,8 +19,45 @@ class Preset:
def edit(self, data=None):
if not data:
return False
aliases = {
"pattern": "p",
"colors": "c",
"delay": "d",
"brightness": "b",
"auto": "a",
}
int_fields = {"d", "b", "n1", "n2", "n3", "n4", "n5", "n6"}
allowed_fields = {"p", "c", "d", "b", "a", "n1", "n2", "n3", "n4", "n5", "n6"}
for key, value in data.items():
setattr(self, key, value)
key = aliases.get(key, key)
if key not in allowed_fields:
continue
if key in int_fields:
try:
parsed = int(value)
if key == "b":
parsed = max(0, min(255, parsed))
elif key in ("d", "n1", "n2", "n3", "n4", "n5", "n6"):
parsed = max(0, parsed)
setattr(self, key, parsed)
except (TypeError, ValueError):
continue
elif key == "a":
if isinstance(value, bool):
self.a = value
elif isinstance(value, int):
self.a = bool(value)
elif isinstance(value, str):
lowered = value.lower()
if lowered in ("true", "1", "yes", "on"):
self.a = True
elif lowered in ("false", "0", "no", "off"):
self.a = False
elif key == "c":
if isinstance(value, (list, tuple)):
self.c = value
else:
setattr(self, key, value)
return True
@property

1
src/presets.json Normal file
View File

@@ -0,0 +1 @@
{"14": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 102, 0]], "b": 255, "n2": 1000, "n1": 2000, "p": "pulse", "n3": 2000, "d": 800}, "15": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 255, 0]], "b": 255, "n2": 0, "n1": 0, "p": "blink", "n3": 0, "d": 500}, "5": {"n5": 0, "n4": 1, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 0, 255]], "b": 255, "n2": 5, "n1": 5, "p": "chase", "n3": 1, "d": 200}, "4": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 255, 0], [0, 0, 255]], "b": 255, "n2": 0, "n1": 0, "p": "transition", "n3": 0, "d": 500}, "7": {"n5": 0, "n4": 5, "a": true, "n6": 0, "c": [[255, 165, 0], [128, 0, 128]], "b": 255, "n2": 10, "n1": 2, "p": "circle", "n3": 2, "d": 200}, "11": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0]], "b": 255, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "12": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[0, 0, 255]], "b": 255, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "6": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[0, 255, 0]], "b": 255, "n2": 500, "n1": 1000, "p": "pulse", "n3": 1000, "d": 500}, "3": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 255]], "b": 255, "n2": 0, "n1": 2, "p": "rainbow", "n3": 0, "d": 100}, "2": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 255]], "b": 0, "n2": 0, "n1": 0, "p": "off", "n3": 0, "d": 100}, "1": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 255]], "b": 255, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "10": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[230, 242, 255]], "b": 200, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "13": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 255]], "b": 255, "n2": 0, "n1": 1, "p": "rainbow", "n3": 0, "d": 150}, "9": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 245, 230]], "b": 200, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "8": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]], "b": 255, "n2": 0, "n1": 0, "p": "blink", "n3": 0, "d": 1000}}

View File

@@ -2,6 +2,7 @@ from machine import Pin
from neopixel import NeoPixel
from preset import Preset
from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle
from utils import convert_and_reorder_colors
import json
@@ -35,8 +36,13 @@ class Presets:
json.dump({name: preset.to_dict() for name, preset in self.presets.items()}, f)
return True
def load(self):
"""Load presets from a file."""
def load(self, settings=None):
"""Load presets from a file.
`settings` is used to convert hex strings in `c` to RGB tuples and apply
the device's colour order (same as ESPNow receive). If omitted, RGB order
is assumed.
"""
try:
with open("presets.json", "r") as f:
data = json.load(f)
@@ -46,10 +52,14 @@ class Presets:
self.save()
return True
order = settings if settings is not None else "rgb"
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"]]
color_key = "c" if "c" in preset_data else ("colors" if "colors" in preset_data else None)
if color_key is not None:
preset_data[color_key] = convert_and_reorder_colors(
preset_data[color_key], order
)
self.presets[name] = Preset(preset_data)
if self.presets:
print("Loaded presets:")
@@ -80,8 +90,18 @@ class Presets:
next(self.generator)
except StopIteration:
self.generator = None
except Exception as e:
print(f"Error in tick: {e}")
self.generator = None
def select(self, preset_name, step=None):
# Auto-create simple built-in presets for common names on first use
if preset_name not in self.presets and preset_name in ("on", "off"):
if preset_name == "on":
self.presets[preset_name] = Preset({"p": "on"})
else:
self.presets[preset_name] = Preset({"p": "off"})
if preset_name in self.presets:
preset = self.presets[preset_name]
if preset.p in self.patterns:
@@ -93,7 +113,7 @@ class Presets:
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"
# If preset doesn't exist or pattern not found, indicate failure
return False
def update_num_leds(self, pin, num_leds):

View File

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

View File

@@ -33,21 +33,26 @@ def convert_and_reorder_colors(colors, settings_or_color_order):
converted_colors = []
for color in colors:
# Convert "#RRGGBB" to (R, G, B)
if isinstance(color, str) and color.startswith("#"):
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
rgb = (r, g, b)
try:
# Convert "#RRGGBB" to (R, G, B)
if isinstance(color, str) and color.startswith("#") and len(color) == 7:
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
rgb = (r, g, b)
elif isinstance(color, (list, tuple)) and len(color) == 3:
# Already a tuple/list, just coerce and clamp.
rgb = tuple(max(0, min(255, int(x))) for x in color)
else:
# Unknown format: ignore safely.
continue
# Reorder based on device color order
reordered = (rgb[channel_order[0]], rgb[channel_order[1]], rgb[channel_order[2]])
converted_colors.append(reordered)
elif isinstance(color, (list, tuple)) and len(color) == 3:
# Already a tuple/list, just reorder
rgb = tuple(color)
reordered = (rgb[channel_order[0]], rgb[channel_order[1]], rgb[channel_order[2]])
converted_colors.append(reordered)
else:
# Keep as-is if not recognized format
converted_colors.append(color)
except (TypeError, ValueError, IndexError):
# Skip malformed color entries to avoid crashing pattern loops.
continue
if not converted_colors:
converted_colors.append((255, 255, 255))
return converted_colors

261
test/all.py Normal file
View File

@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""Self-contained led-driver test runner for MicroPython/mpremote."""
import json
import os
import utime
from machine import WDT
from settings import Settings
from presets import Presets
from utils import convert_and_reorder_colors
class _TestContext:
def __init__(self):
self.settings = Settings()
self.settings["name"] = self.settings.get("name", "test_device")
self.presets = Presets(self.settings["led_pin"], self.settings["num_leds"])
self.presets.b = self.settings.get("brightness", 255)
self.wdt = WDT(timeout=10000)
def tick_for_ms(self, duration_ms, sleep_ms=5):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
self.wdt.feed()
self.presets.tick()
utime.sleep_ms(sleep_ms)
def _process_message(ctx, payload):
"""Small test helper that mirrors the main message handling logic."""
try:
if isinstance(payload, (bytes, bytearray)):
data = json.loads(payload)
elif isinstance(payload, str):
data = json.loads(payload)
else:
data = payload
except (TypeError, ValueError):
return "invalid_json"
if not isinstance(data, dict):
return "invalid_shape"
if data.get("v") != "1":
return "wrong_version"
if "b" in data:
try:
ctx.presets.b = max(0, min(255, int(data["b"])))
except (TypeError, ValueError):
pass
if isinstance(data.get("presets"), dict):
for name, preset_data in data["presets"].items():
if not isinstance(preset_data, dict):
continue
color_key = "c" if "c" in preset_data else ("colors" if "colors" in preset_data else None)
if color_key is not None:
try:
preset_data[color_key] = convert_and_reorder_colors(
preset_data[color_key], ctx.settings
)
except (TypeError, ValueError):
continue
ctx.presets.edit(name, preset_data)
if isinstance(data.get("select"), dict) and ctx.settings.get("name") in data["select"]:
select_list = data["select"][ctx.settings.get("name")]
if isinstance(select_list, list) and select_list:
preset_name = select_list[0]
step = select_list[1] if len(select_list) > 1 else None
if isinstance(preset_name, str):
ctx.presets.select(preset_name, step=step)
if "default" in data:
default_name = data["default"]
this_device_name = ctx.settings.get("name")
this_device_name_norm = (
this_device_name.strip().lower()
if isinstance(this_device_name, str)
else None
)
should_apply_default = True
if "targets" in data:
should_apply_default = False
targets = data.get("targets")
if isinstance(targets, list) and this_device_name_norm:
normalized_targets = [
target.strip().lower()
for target in targets
if isinstance(target, str) and target.strip()
]
should_apply_default = this_device_name_norm in normalized_targets
if (
should_apply_default
and
isinstance(default_name, str)
and default_name
and default_name in ctx.presets.presets
):
ctx.settings["default"] = default_name
if "save" in data:
ctx.presets.save()
return "ok"
def test_invalid_messages_do_not_crash():
ctx = _TestContext()
cases = [
b"{not-json",
"[]",
json.dumps({"v": "2"}),
json.dumps({"v": "1", "presets": ["bad"]}),
json.dumps({"v": "1", "select": {"test_device": "not-list"}}),
json.dumps({"v": "1", "presets": {"x": {"c": ["#GG0000"]}}}),
]
for payload in cases:
_process_message(ctx, payload)
ctx.wdt.feed()
def test_preset_edit_sanitization():
ctx = _TestContext()
ctx.presets.edit(
"sanitize",
{
"pattern": "blink",
"delay": "120",
"brightness": "999",
"auto": "false",
"n1": "-5",
"n2": "7",
"unknown_field": "ignored",
},
)
p = ctx.presets.presets["sanitize"]
assert p.p == "blink"
assert p.d == 120
assert p.b == 255
assert p.a is False
assert p.n1 == 0
assert p.n2 == 7
assert not hasattr(p, "unknown_field")
def test_colour_conversion_and_transition():
ctx = _TestContext()
msg = {
"v": "1",
"presets": {
"fade": {
"p": "transition",
"c": ["#ff0000", "#00ff00"],
"d": 80,
"a": True,
}
},
"select": {ctx.settings["name"]: ["fade"]},
}
result = _process_message(ctx, msg)
assert result == "ok"
assert ctx.presets.selected == "fade"
# Smoke-run the generator to ensure math runs without type errors.
ctx.tick_for_ms(250)
def test_pattern_smoke():
ctx = _TestContext()
cases = {
"t_on": {"p": "on", "c": [(16, 8, 4)]},
"t_off": {"p": "off"},
"t_blink": {"p": "blink", "c": [(255, 0, 0)], "d": 20},
"t_rainbow": {"p": "rainbow", "d": 5, "n1": 2},
"t_pulse": {"p": "pulse", "c": [(255, 0, 0)], "n1": 20, "n2": 10, "n3": 20, "d": 10},
"t_transition": {"p": "transition", "c": [(255, 0, 0), (0, 0, 255)], "d": 30},
"t_chase": {"p": "chase", "c": [(255, 0, 0), (0, 0, 255)], "n1": 3, "n2": 2, "n3": 1, "n4": 1, "d": 20},
"t_circle": {"p": "circle", "c": [(255, 255, 0), (0, 0, 8)], "n1": 5, "n2": 10, "n3": 5, "n4": 2},
}
for name, data in cases.items():
ctx.presets.edit(name, data)
assert ctx.presets.select(name), "select failed: %s" % name
ctx.tick_for_ms(120)
def test_default_requires_existing_preset():
ctx = _TestContext()
_process_message(ctx, {"v": "1", "default": "missing"})
assert ctx.settings.get("default") != "missing"
ctx.presets.edit("exists", {"p": "on"})
_process_message(ctx, {"v": "1", "default": "exists"})
assert ctx.settings.get("default") == "exists"
def test_default_targets_gate_by_device_name():
ctx = _TestContext()
ctx.settings["name"] = "a"
ctx.presets.edit("targeted", {"p": "on"})
ctx.settings["default"] = "baseline"
_process_message(
ctx,
{"v": "1", "default": "targeted", "targets": ["11"]},
)
assert ctx.settings.get("default") == "baseline"
_process_message(
ctx,
{"v": "1", "default": "targeted", "targets": [" A "]},
)
assert ctx.settings.get("default") == "targeted"
def test_save_and_load_roundtrip():
ctx = _TestContext()
ctx.presets.edit(
"persist",
{"p": "blink", "c": [(1, 2, 3), (4, 5, 6)], "d": 77, "b": 123, "a": False},
)
assert ctx.presets.save()
reloaded = Presets(ctx.settings["led_pin"], ctx.settings["num_leds"])
assert reloaded.load(ctx.settings)
p = reloaded.presets.get("persist")
assert p is not None
assert p.p == "blink"
assert p.d == 77
assert p.b == 123
assert p.a is False
assert p.c == [(1, 2, 3), (4, 5, 6)]
try:
os.remove("presets.json")
except OSError:
pass
def run_all():
tests = [
test_invalid_messages_do_not_crash,
test_preset_edit_sanitization,
test_colour_conversion_and_transition,
test_pattern_smoke,
test_default_requires_existing_preset,
test_default_targets_gate_by_device_name,
test_save_and_load_roundtrip,
]
print("=" * 56)
print("led-driver self-contained tests")
print("=" * 56)
for test_func in tests:
print("Running %s ..." % test_func.__name__)
test_func()
print(" PASS")
print("-" * 56)
print("All tests passed")
if __name__ == "__main__":
run_all()

View File

@@ -644,7 +644,7 @@ def test_preset_save_load():
assert patterns.save(), "Save should return True"
reloaded = Presets(settings["led_pin"], settings["num_leds"])
assert reloaded.load(), "Load should return True"
assert reloaded.load(settings), "Load should return True"
preset = reloaded.presets.get("saved_preset")
assert preset is not None, "Preset should be loaded"