Compare commits

...

15 Commits

Author SHA1 Message Date
292c5bde01 Rework roll pattern to use gradient palette and add preset-based tests
Made-with: Cursor
2026-03-06 01:39:40 +13:00
a0687cff57 Switch chase to double-buffered ring chase
Made-with: Cursor
2026-03-06 01:00:25 +13:00
5f457b3ae7 Refine calibration, chase, and spin patterns and add spin test
Increase calibration brightness, update chase to use the new logical ring abstraction, and make spin start from a cleared frame with symmetric arm speed, alongside a dedicated on-device spin test script.

Made-with: Cursor
2026-03-05 23:41:25 +13:00
3e58f4e97e Add segments and double_circle patterns with shared presets
Introduce double_circle and segments-based patterns on the Pico, refactor the Presets engine to expose a logical ring over all strips, and migrate presets/test code from the old point pattern to segments while switching to a top-level presets.json.

Made-with: Cursor
2026-03-05 23:41:13 +13:00
47c19eecf1 Clean up ESP32 buttons and UART tests
Remove unused buttons.json and legacy UART UART test scripts, and update the web UI preset dropdown for new Pico patterns.

Made-with: Cursor
2026-03-05 23:40:28 +13:00
47c17dba36 Add mpremote tests for scaling, roll, and point
Made-with: Cursor
2026-03-05 20:25:57 +13:00
e75723e2e7 Refactor presets scaling and clean up patterns
Made-with: Cursor
2026-03-05 20:25:28 +13:00
52a5f0f8c4 Add Pico presets engine, patterns, and tests.
Wire the Pico to UART-driven preset selection, add pattern modules and presets data, remove old p2p/settings code, and update tests and LED driver.

Made-with: Cursor
2026-03-03 19:28:11 +13:00
646b988cdd Add ESP32 web control and UART bridge.
Replace ESPNOW passthrough with a Microdot-based web UI and WebSocket-to-UART bridge for preset selection.

Made-with: Cursor
2026-03-03 19:28:08 +13:00
615431d6c5 Add Pipenv environment and dry-run notes.
Document commit strategy and add Pipenv config for local development.

Made-with: Cursor
2026-03-03 19:28:06 +13:00
b45823c479 Update STRIP_CONFIG 2026-02-28 08:58:55 +13:00
0c73d56ab5 Import led-driver app: pico/ and esp32/ layout
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 18:14:17 +13:00
86b28a1b9c Restructure: remove old flat src/lib layout
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 18:14:01 +13:00
2869ec9300 Allow seperate buffer with offset 2026-02-19 17:53:57 +13:00
c062110e35 Add rainbow test 2026-02-19 17:52:29 +13:00
90 changed files with 7861 additions and 1004 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
settings.json settings.json
__pycache__/

194
DRY_RUN_COMMITS.md Normal file
View File

@@ -0,0 +1,194 @@
# Dry run: two-commit plan
## Current state
- **Staged:** 9 deleted files under old `src/` (boot, dma, main, patterns, settings, static/main.css, web, wifi, ws2812).
- **Tracked elsewhere:** `pico/lib/`, `pico/test/leds.py`, `pico/test/rainbow.py` (already in repo).
- **Unstaged / untracked:** Rest of restructure (more deletions, new `pico/src/`, `esp32/`, `main.py`, etc.).
---
## Commit 1: Folder restructure
**Goal:** Remove old flat layout; no new code from led-driver yet.
### 1. Unstage current changes (so we can stage only restructure)
```bash
git restore --staged .
```
### 2. Stage only restructure changes
**Deletions (remove old layout):**
```bash
# Old top-level src/ (already partially staged; include all)
git add -u src/
# Old lib (microdot, utemplate)
git add -u lib/microdot/
git add -u lib/utemplate/
# Old box + test
git rm box.scad box.stl test.py 2>/dev/null || true
git add -u box.scad box.stl test.py
```
Or in one go (stage all deletions under src/, lib/, and the files):
```bash
git restore --staged .
git add -u src/ lib/ box.scad box.stl test.py
```
**Optional for restructure:** If you want commit 1 to also introduce the new tree shape (empty or minimal), you could add only the new directories with a single placeholder each—but Git doesnt track empty dirs, so the “restructure” commit is usually just the removals.
**Files that would be in commit 1 (deletions only):**
| Path | Change |
|------|--------|
| `src/boot.py` | deleted |
| `src/dma.py` | deleted |
| `src/main.py` | deleted |
| `src/patterns.py` | deleted |
| `src/settings.py` | deleted |
| `src/static/main.css` | deleted |
| `src/static/main.js` | deleted |
| `src/templates/index.html` | deleted |
| `src/templates/index_html.py` | deleted |
| `src/web.py` | deleted |
| `src/wifi.py` | deleted |
| `src/ws2812.py` | deleted |
| `lib/microdot/__init__.py` | deleted |
| `lib/microdot/helpers.py` | deleted |
| `lib/microdot/microdot.py` | deleted |
| `lib/microdot/utemplate.py` | deleted |
| `lib/microdot/websocket.py` | deleted |
| `lib/utemplate/__init__.py` | deleted |
| `lib/utemplate/compiled.py` | deleted |
| `lib/utemplate/recompile.py` | deleted |
| `lib/utemplate/source.py` | deleted |
| `box.scad` | deleted |
| `box.stl` | deleted |
| `test.py` | deleted |
**Suggested commit message:**
```
Restructure: remove old flat src/lib layout
- Remove src/, lib/microdot, lib/utemplate, box.*, test.py
- Pico/ESP32 layout and led-driver code in follow-up commit
```
### 3. (Dry run) Show what would be committed
```bash
git status
git diff --cached --stat
```
---
## Commit 2: Import code from led-driver
**Goal:** Add pico/ and esp32/ app code and top-level entrypoint (from led-driver).
### 1. Stage new and modified files
```bash
# New layout: pico app and tests
git add pico/src/
git add pico/test/patterns/
git add pico/test/test_espnow_receive.py
git add -u pico/test/leds.py
git add -u pico/test/rainbow.py
# ESP32 app and tests
git add esp32/
# Top-level entrypoint
git add main.py
# Tooling / docs (if you want them in this commit)
git add dev.py README.md
# Optional
git add Pipfile Pipfile.lock
```
### 2. Files that would be in commit 2
**New files (led-driver / app):**
| Path |
|------|
| `main.py` |
| `pico/src/main.py` |
| `pico/src/p2p.py` |
| `pico/src/preset.py` |
| `pico/src/presets.py` |
| `pico/src/settings.py` |
| `pico/src/utils.py` |
| `pico/src/patterns/__init__.py` |
| `pico/src/patterns/blink.py` |
| `pico/src/patterns/chase.py` |
| `pico/src/patterns/circle.py` |
| `pico/src/patterns/pulse.py` |
| `pico/src/patterns/rainbow.py` |
| `pico/src/patterns/transition.py` |
| `esp32/src/main.py` |
| `esp32/test/test_uart_send_json.py` |
| `esp32/test/test_uart_tx.py` |
| `pico/test/patterns/auto_manual.py` |
| `pico/test/patterns/blink.py` |
| … (other pico/test/patterns/*) |
**Modified:**
| Path |
|------|
| `pico/test/leds.py` |
| `pico/test/rainbow.py` |
| `dev.py` |
| `README.md` (optional) |
**Optional (tooling):**
| Path |
|------|
| `Pipfile` |
| `Pipfile.lock` |
**Suggested commit message:**
```
Import led-driver app: pico/ and esp32/ layout
- pico/src: main, presets, settings, patterns (UART JSON)
- esp32/src: main (UART sender)
- main.py top-level entrypoint
- pico/test: pattern tests, test_espnow_receive
- dev.py: deploy by device (pico | esp32)
```
### 3. (Dry run) Show what would be committed
```bash
git status
git diff --cached --stat
```
---
## Summary
| Step | Action | Commit message |
|------|--------|----------------|
| 1 | `git restore --staged .` then stage only `src/` `lib/` `box.scad` `box.stl` `test.py` deletions | Restructure: remove old flat src/lib layout |
| 2 | Commit | (above) |
| 3 | Stage `pico/src/`, `pico/test/` changes, `esp32/`, `main.py`, optionally `dev.py` `README.md` `Pipfile*` | Import led-driver app: pico/ and esp32/ layout |
| 4 | Commit | (above) |
No commits are made in this dry run; run the `git add` and `git status` / `git diff --cached --stat` commands to confirm before running `git commit`.

15
Pipfile Normal file
View File

@@ -0,0 +1,15 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
esptool = "*"
mpremote = "*"
pyserial = "*"
requests = "*"
[dev-packages]
[requires]
python_version = "3.13"

617
Pipfile.lock generated Normal file
View File

@@ -0,0 +1,617 @@
{
"_meta": {
"hash": {
"sha256": "eb62a51e8d40130b1e758dc972ead3111fe880e1638e2b7e6037a44b4831dbeb"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.13"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"bitarray": {
"hashes": [
"sha256:004d518fa410e6da43386d20e07b576a41eb417ac67abf9f30fa75e125697199",
"sha256:014df8a9430276862392ac5d471697de042367996c49f32d0008585d2c60755a",
"sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e",
"sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3",
"sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e",
"sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4",
"sha256:0f8069a807a3e6e3c361ce302ece4bf1c3b49962c1726d1d56587e8f48682861",
"sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5",
"sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521",
"sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d",
"sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55",
"sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9",
"sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce",
"sha256:1a926fa554870642607fd10e66ee25b75fdd9a7ca4bbffa93d424e4ae2bf734a",
"sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9",
"sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e",
"sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b",
"sha256:239578587b9c29469ab61149dda40a2fe714a6a4eca0f8ff9ea9439ec4b7bc30",
"sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6",
"sha256:26714898eb0d847aac8af94c4441c9cb50387847d0fe6b9fc4217c086cd68b80",
"sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11",
"sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f",
"sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25",
"sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77",
"sha256:2fe8c54b15a9cd4f93bc2aaceab354ec65af93370aa1496ba2f9c537a4855ee0",
"sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125",
"sha256:31a4ad2b730128e273f1c22300da3e3631f125703e4fee0ac44d385abfb15671",
"sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de",
"sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860",
"sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe",
"sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d",
"sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc",
"sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df",
"sha256:46cf239856b87fe1c86dfbb3d459d840a8b1649e7922b1e0bfb6b6464692644a",
"sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8",
"sha256:4902f4ecd5fcb6a5f482d7b0ae1c16c21f26fc5279b3b6127363d13ad8e7a9d9",
"sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe",
"sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607",
"sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf",
"sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee",
"sha256:5338a313f998e1be7267191b7caaae82563b4a2b42b393561055412a34042caa",
"sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954",
"sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a",
"sha256:58a01ea34057463f7a98a4d6ff40160f65f945e924fec08a5b39e327e372875d",
"sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428",
"sha256:5c5a8a83df95e51f7a7c2b083eaea134cbed39fc42c6aeb2e764ddb7ccccd43e",
"sha256:5f2fb10518f6b365f5b720e43a529c3b2324ca02932f609631a44edb347d8d54",
"sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5",
"sha256:6d70fa9c6d2e955bde8cd327ffc11f2cc34bc21944e5571a46ca501e7eadef24",
"sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f",
"sha256:720963fee259291a88348ae9735d9deb5d334e84a016244f61c89f5a49aa400a",
"sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b",
"sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70",
"sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7",
"sha256:7f14d6b303e55bd7d19b28309ef8014370e84a3806c5e452e078e7df7344d97a",
"sha256:7f65bd5d4cdb396295b6aa07f84ca659ac65c5c68b53956a6d95219e304b0ada",
"sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541",
"sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b",
"sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4",
"sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2",
"sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd",
"sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646",
"sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89",
"sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa",
"sha256:94652da1a4ca7cfb69c15dd6986b205e0bd9c63a05029c3b48b4201085f527bd",
"sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1",
"sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb",
"sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220",
"sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c",
"sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310",
"sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2",
"sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e",
"sha256:a358277122456666a8b2a0b9aa04f1b89d34e8aa41d08a6557d693e6abb6667c",
"sha256:a60da2f9efbed355edb35a1fb6829148676786c829fad708bb6bb47211b3593a",
"sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a",
"sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594",
"sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8",
"sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52",
"sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20",
"sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8",
"sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6",
"sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9",
"sha256:cbba763d99de0255a3e4938f25a8579930ac8aa089233cb2fb2ed7d04d4aff02",
"sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425",
"sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d",
"sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2",
"sha256:d2dbe8a3baf2d842e342e8acb06ae3844765d38df67687c144cdeb71f1bcb5d7",
"sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4",
"sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096",
"sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d",
"sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149",
"sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b",
"sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35",
"sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773",
"sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d",
"sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6",
"sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741",
"sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f",
"sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8",
"sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f",
"sha256:f8d3417db5e14a6789073b21ae44439a755289477901901bae378a57b905e148",
"sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8",
"sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97",
"sha256:ff1863f037dad765ef5963efc2e37d399ac023e192a6f2bb394e2377d023cefe"
],
"version": "==3.8.0"
},
"bitstring": {
"hashes": [
"sha256:69d1587f0ac18dc7d93fc7e80d5f447161a33e57027e726dc18a0a8bacf1711a",
"sha256:a08bc09d3857216d4c0f412a1611056f1cc2b64fd254fb1e8a0afba7cfa1a95a"
],
"markers": "python_version >= '3.8'",
"version": "==4.3.1"
},
"certifi": {
"hashes": [
"sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c",
"sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"
],
"markers": "python_version >= '3.7'",
"version": "==2026.1.4"
},
"cffi": {
"hashes": [
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
"sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
"sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
"sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
"sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
"sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
"sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
"sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
"sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
"sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
"sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
"sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
"sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
"sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
"sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
"sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
"sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
"sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
"sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
"sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
"sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
"sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
"sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
"sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
"sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
"sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
"sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
"sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
"sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
"sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
"sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
"sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
"sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
"sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
"sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
"sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
"sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
"sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
"sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
"sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
"sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
"sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
"sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
"sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
"sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
"sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
"sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
"sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
"sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
"sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
"sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
"sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
"sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
"sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
"sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
"sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
"sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
"sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
"sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
"sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
"sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
"sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
"sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
"sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
"sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
"sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
"sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
],
"markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'",
"version": "==2.0.0"
},
"charset-normalizer": {
"hashes": [
"sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad",
"sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93",
"sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394",
"sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89",
"sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc",
"sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86",
"sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63",
"sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d",
"sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f",
"sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8",
"sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0",
"sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505",
"sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161",
"sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af",
"sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152",
"sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318",
"sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72",
"sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4",
"sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e",
"sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3",
"sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576",
"sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c",
"sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1",
"sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8",
"sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1",
"sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2",
"sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44",
"sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26",
"sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88",
"sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016",
"sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede",
"sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf",
"sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a",
"sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc",
"sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0",
"sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84",
"sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db",
"sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1",
"sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7",
"sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed",
"sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8",
"sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133",
"sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e",
"sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef",
"sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14",
"sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2",
"sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0",
"sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d",
"sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828",
"sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f",
"sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf",
"sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6",
"sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328",
"sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090",
"sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa",
"sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381",
"sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c",
"sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb",
"sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc",
"sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a",
"sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec",
"sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc",
"sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac",
"sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e",
"sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313",
"sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569",
"sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3",
"sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d",
"sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525",
"sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894",
"sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3",
"sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9",
"sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a",
"sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9",
"sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14",
"sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25",
"sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50",
"sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf",
"sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1",
"sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3",
"sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac",
"sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e",
"sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815",
"sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c",
"sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6",
"sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6",
"sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e",
"sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4",
"sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84",
"sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69",
"sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15",
"sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191",
"sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0",
"sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897",
"sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd",
"sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2",
"sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794",
"sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d",
"sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074",
"sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3",
"sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224",
"sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838",
"sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a",
"sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d",
"sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d",
"sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f",
"sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8",
"sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490",
"sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966",
"sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9",
"sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3",
"sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e",
"sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"
],
"markers": "python_version >= '3.7'",
"version": "==3.4.4"
},
"click": {
"hashes": [
"sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a",
"sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"
],
"markers": "python_version >= '3.10'",
"version": "==8.3.1"
},
"cryptography": {
"hashes": [
"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.5"
},
"esptool": {
"hashes": [
"sha256:2ea9bcd7eb263d380a4fe0170856a10e4c65e3f38c757ebdc73584c8dd8322da"
],
"index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==5.1.0"
},
"idna": {
"hashes": [
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
],
"markers": "python_version >= '3.8'",
"version": "==3.11"
},
"intelhex": {
"hashes": [
"sha256:87cc5225657524ec6361354be928adfd56bcf2a3dcc646c40f8f094c39c07db4",
"sha256:892b7361a719f4945237da8ccf754e9513db32f5628852785aea108dcd250093"
],
"version": "==2.3.0"
},
"markdown-it-py": {
"hashes": [
"sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147",
"sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"
],
"markers": "python_version >= '3.10'",
"version": "==4.0.0"
},
"mdurl": {
"hashes": [
"sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
"sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
],
"markers": "python_version >= '3.7'",
"version": "==0.1.2"
},
"mpremote": {
"hashes": [
"sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4",
"sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5"
],
"index": "pypi",
"markers": "python_version >= '3.4'",
"version": "==1.27.0"
},
"platformdirs": {
"hashes": [
"sha256:1ed8db354e344c5bb6039cd727f096af975194b508e37177719d562b2b540ee6",
"sha256:fd1a5f8599c85d49b9ac7d6e450bc2f1aaf4a23f1fe86d09952fe20ad365cf36"
],
"markers": "python_version >= '3.10'",
"version": "==4.7.0"
},
"pycparser": {
"hashes": [
"sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29",
"sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"
],
"markers": "implementation_name != 'PyPy'",
"version": "==3.0"
},
"pygments": {
"hashes": [
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
],
"markers": "python_version >= '3.8'",
"version": "==2.19.2"
},
"pyserial": {
"hashes": [
"sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb",
"sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"
],
"index": "pypi",
"version": "==3.5"
},
"pyyaml": {
"hashes": [
"sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c",
"sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a",
"sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3",
"sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956",
"sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6",
"sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c",
"sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65",
"sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a",
"sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0",
"sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b",
"sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1",
"sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6",
"sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7",
"sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e",
"sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007",
"sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310",
"sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4",
"sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9",
"sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295",
"sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea",
"sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0",
"sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e",
"sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac",
"sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9",
"sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7",
"sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35",
"sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb",
"sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b",
"sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69",
"sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5",
"sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b",
"sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c",
"sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369",
"sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd",
"sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824",
"sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198",
"sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065",
"sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c",
"sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c",
"sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764",
"sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196",
"sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b",
"sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00",
"sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac",
"sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8",
"sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e",
"sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28",
"sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3",
"sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5",
"sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4",
"sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b",
"sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf",
"sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5",
"sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702",
"sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8",
"sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788",
"sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da",
"sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d",
"sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc",
"sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c",
"sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba",
"sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f",
"sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917",
"sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5",
"sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26",
"sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f",
"sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b",
"sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be",
"sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c",
"sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3",
"sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6",
"sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926",
"sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"
],
"markers": "python_version >= '3.8'",
"version": "==6.0.3"
},
"reedsolo": {
"hashes": [
"sha256:2b6a3e402a1ee3e1eea3f932f81e6c0b7bbc615588074dca1dbbcdeb055002bd",
"sha256:c1359f02742751afe0f1c0de9f0772cc113835aa2855d2db420ea24393c87732"
],
"version": "==1.7.0"
},
"requests": {
"hashes": [
"sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6",
"sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==2.32.5"
},
"rich": {
"hashes": [
"sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69",
"sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"
],
"markers": "python_full_version >= '3.8.0'",
"version": "==14.3.2"
},
"rich-click": {
"hashes": [
"sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc",
"sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b"
],
"markers": "python_version >= '3.8'",
"version": "==1.9.7"
},
"urllib3": {
"hashes": [
"sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed",
"sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"
],
"markers": "python_version >= '3.9'",
"version": "==2.6.3"
}
},
"develop": {}
}

View File

@@ -1,2 +1,30 @@
# led-bar # led-bar
## Recovery: when the board is stuck and you have to nuke the flash
**Option A clear startup files (no reflash)**
If the board runs but REPL/Thonny is blocked by `main.py` or `boot.py`, remove them so the next boot drops straight to REPL. From your PC (with the Pico connected via USB):
```bash
# Remove startup files (paths are on the device; try without colons if one form fails)
mpremote fs rm boot.py
mpremote fs rm main.py
mpremote reset
```
If that fails, try one of these:
```bash
mpremote rm boot.py
mpremote rm main.py
```
Or in **Thonny**: Stop the running program (Ctrl+C or Stop button), then **View → Files**, rightclick the device, delete `boot.py` and `main.py` on the device, then **Tools → Reset**.
If the board doesnt respond to serial at all, use Option B.
**Option B full flash erase (Pico 2)**
1. Unplug the Pico 2.
2. Hold **BOOTSEL**, plug USB in, then release BOOTSEL.
3. It should mount as a drive. Delete any existing UF2 if you want a clean state.
4. Copy the MicroPython UF2 for Pico 2 (RP2350) onto the drive. The board will reboot with a fresh install and empty filesystem.

View File

@@ -1,20 +0,0 @@
difference() {
cube([30,25,20]);
//hoop
translate([15,12.5,-905]){
rotate([90,0,0])
rotate_extrude($fn=300)
translate([900,0,0])
circle(d=25, $fn=100);
};
//pico
translate([3.25,3.5,0])
cube([23.5,18,12]);
//pico usb port
translate([26.75,8,6.5])
cube([3.25,9,5.5]);
//wifi
translate([2.5,5,12])
cube([25,15,5]);
};

BIN
box.stl

Binary file not shown.

108
dev.py
View File

@@ -1,33 +1,91 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import subprocess import subprocess
import serial
import sys import sys
print(sys.argv) import serial
port = sys.argv[1]
cmd = sys.argv[1]
for cmd in sys.argv[1:]:
print(cmd)
match cmd:
case "src":
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":" ], cwd="src")
case "lib":
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":" ])
case "ls":
subprocess.call(["mpremote", "connect", port, "fs", "ls", ":" ])
case "reset":
with serial.Serial(port, baudrate=115200) as ser:
ser.write(b'\x03\x03\x04')
case "follow":
with serial.Serial(port, baudrate=115200) as ser:
while True:
if ser.in_waiting > 0: # Check if there is data in the buffer
data = ser.readline().decode('utf-8').strip() # Read and decode the data
print(data)
def usage() -> None:
print("Usage:")
print(" dev.py <port> <device> [src] [lib] [reset] [follow]")
print(" e.g. dev.py /dev/ttyUSB0 pico src lib")
print(" e.g. dev.py /dev/ttyUSB0 esp32 src reset follow")
print(" device: pico | esp32. If no src/lib given, deploys both.")
def main() -> None:
if len(sys.argv) < 3:
usage()
return
port = sys.argv[1]
device = sys.argv[2].lower()
actions = [a.lower() for a in sys.argv[3:]]
if port.startswith("/") or (len(port) >= 3 and port.upper().startswith("COM")):
pass
else:
print("First argument must be serial port (e.g. /dev/ttyUSB0 or COM3).")
usage()
return
if device not in ("pico", "esp32"):
print("Device must be pico or esp32.")
usage()
return
if not actions:
actions = ["src", "lib"]
src_dir = f"{device}/src"
lib_dir = f"{device}/lib"
for a in actions:
print(a)
match a:
case "src":
if os.path.isdir(src_dir):
# Ensure remote directories exist before copying files
created_dirs: set[str] = set()
for dirpath, _, filenames in os.walk(src_dir):
for name in filenames:
path = os.path.join(dirpath, name)
rel = os.path.relpath(path, src_dir).replace(os.sep, "/")
remote_dir = ""
if "/" in rel:
remote_dir = rel.rsplit("/", 1)[0]
if remote_dir and remote_dir not in created_dirs:
subprocess.call(
["mpremote", "connect", port, "fs", "mkdir", ":" + remote_dir],
)
created_dirs.add(remote_dir)
subprocess.call(
["mpremote", "connect", port, "fs", "cp", path, ":" + rel],
)
else:
print(" (no src dir)")
case "lib":
if os.path.isdir(lib_dir):
subprocess.call(
["mpremote", "connect", port, "fs", "cp", "-r", lib_dir, ":"],
)
else:
print(" (no lib dir)")
case "reset":
with serial.Serial(port, baudrate=115200) as ser:
ser.write(b"\x03\x03\x04")
case "follow":
with serial.Serial(port, baudrate=115200) as ser:
while True:
if ser.in_waiting > 0:
data = ser.readline().decode("utf-8").strip()
print(data)
case _:
print("Unknown action:", a)
usage()
if __name__ == "__main__":
main()

12
esp32/src/boot.py Normal file
View File

@@ -0,0 +1,12 @@
#create an accesstpoit called led-hoop with password hoop-1234
#enable password protection
import network
ap_if = network.WLAN(network.AP_IF)
ap_mac = ap_if.config('mac')
ap_if.active(True)
ap_if.config(essid="led-hoop", password="hoop-1234")
ap_if.active(False)
ap_if.active(True)
print(ap_if.ifconfig())

109
esp32/src/main.py Normal file
View File

@@ -0,0 +1,109 @@
from microdot import Microdot, send_file, Response
from microdot.utemplate import Template
from microdot.websocket import with_websocket
import json
from machine import Pin, UART, WDT
import asyncio
# Load button config: {"buttons": [{"id": "...", "preset": "..."}, ...]}
def _load_buttons():
try:
with open("buttons.json") as f:
raw = json.load(f)
return raw.get("buttons", [])
except (OSError, KeyError, ValueError):
return []
def _save_buttons(buttons):
try:
with open("buttons.json", "w") as f:
json.dump({"buttons": buttons}, f)
return True
except OSError:
return False
BUTTONS = _load_buttons()
uart = UART(1, baudrate=921600, tx=Pin(16, Pin.OUT))
app = Microdot()
Response.default_content_type = 'text/html'
# Device id used in select payload (e.g. Pico name)
DEVICE_ID = "1"
# All connected WebSocket clients (for broadcasting button updates)
_ws_clients = set()
@app.route('/')
async def index_handler(request):
return Template('/index.html').render(buttons=BUTTONS, device_id=DEVICE_ID)
@app.route("/api/buttons", methods=["GET"])
async def api_get_buttons(request):
return {"buttons": BUTTONS}
@app.route("/api/buttons", methods=["POST"])
async def api_save_buttons(request):
global BUTTONS
try:
data = request.json or {}
buttons = data.get("buttons", [])
if not isinstance(buttons, list):
return {"ok": False, "error": "buttons must be a list"}, 400
if _save_buttons(buttons):
BUTTONS = buttons
return {"ok": True}
return {"ok": False, "error": "save failed"}, 500
except Exception as e:
return {"ok": False, "error": str(e)}, 500
@app.route("/static/<path:path>")
async def static_handler(request, path):
if '..' in path:
# Directory traversal is not allowed
return 'Not found', 404
return send_file('static/' + path)
@app.route("/ws")
@with_websocket
async def ws(request, ws):
_ws_clients.add(ws)
print("WebSocket connection established")
try:
while True:
data = await ws.receive()
if data:
# Forward WebSocket message to UART (line-delimited for Pico)
payload = data if isinstance(data, bytes) else data.encode("utf-8")
uart.write(payload + b"\n")
print(data)
# Broadcast to all other clients so their UIs stay in sync
for other in list(_ws_clients):
if other is not ws and not other.closed:
try:
await other.send(data)
except Exception:
pass
else:
break
finally:
_ws_clients.discard(ws)
print("WebSocket connection closed")
async def main():
server = asyncio.create_task(app.start_server("0.0.0.0", 80))
await server
if __name__ == "__main__":
asyncio.run(main())

48
esp32/src/squence.txt Normal file
View File

@@ -0,0 +1,48 @@
start
grab
spin1
lift
flare
hook
roll1
invertsplit
pose1
pose1
roll2
backbalance1
beat1
pose3
roll3
crouch
pose4
roll4
backbendsplit
backbalance2
backbalance3
beat2
straddle
beat3
frontbalance1
pose5
pose6
elbowhang
elbowhangspin
spin2
dismount
spin3
fluff
spin4
flare2
elbowhang
elbowhangsplit2
invert
roll5
backbend
pose7
roll6
seat
kneehang
legswoop
split
foothang
end

617
esp32/src/static/main.js Normal file
View File

@@ -0,0 +1,617 @@
var deviceId = document.body.getAttribute('data-device-id') || '1';
var ws = null;
var currentEditIndex = -1; // -1 means "new button"
function getButtonsFromDom() {
var btns = document.querySelectorAll('#buttonsContainer .btn');
return Array.prototype.map.call(btns, function (el) {
var obj = {
id: el.getAttribute('data-id') || el.textContent.trim(),
preset: el.getAttribute('data-preset') || ''
};
var p = el.getAttribute('data-p');
if (p) obj.p = p;
var d = el.getAttribute('data-d');
if (d !== null && d !== '') obj.d = parseInt(d, 10) || 0;
var b = el.getAttribute('data-b');
if (b !== null && b !== '') obj.b = parseInt(b, 10) || 0;
var c = el.getAttribute('data-c');
if (c) {
try {
var parsed = JSON.parse(c);
if (Array.isArray(parsed)) obj.c = parsed;
} catch (e) {}
}
for (var i = 1; i <= 8; i++) {
var key = 'n' + i;
var v = el.getAttribute('data-' + key);
if (v !== null && v !== '') {
obj[key] = parseInt(v, 10) || 0;
}
}
return obj;
});
}
function renderButtons(buttons) {
var container = document.getElementById('buttonsContainer');
container.innerHTML = '';
buttons.forEach(function (btn, idx) {
var el = document.createElement('button');
el.className = 'btn';
el.type = 'button';
el.setAttribute('data-preset', btn.preset);
el.setAttribute('data-id', btn.id);
el.setAttribute('data-index', String(idx));
// Optional preset config stored per button
if (btn.p !== undefined) el.setAttribute('data-p', btn.p);
if (btn.d !== undefined) el.setAttribute('data-d', String(btn.d));
if (btn.b !== undefined) el.setAttribute('data-b', String(btn.b));
if (btn.c !== undefined) {
try {
el.setAttribute('data-c', JSON.stringify(btn.c));
} catch (e) {}
}
for (var i = 1; i <= 8; i++) {
var key = 'n' + i;
if (btn[key] !== undefined) {
el.setAttribute('data-' + key, String(btn[key]));
}
}
el.draggable = true;
el.textContent = btn.id;
container.appendChild(el);
});
attachButtonListeners();
}
function attachButtonListeners() {
var container = document.getElementById('buttonsContainer');
if (!container) return;
var btns = container.querySelectorAll('.btn');
for (var i = 0; i < btns.length; i++) {
var el = btns[i];
el.setAttribute('data-index', String(i));
el.onclick = function(ev) {
if (longPressTriggered) {
ev.preventDefault();
return;
}
var btn = ev.currentTarget;
sendSelect(btn.getAttribute('data-preset'), btn);
};
el.oncontextmenu = function(ev) {
ev.preventDefault();
showButtonContextMenu(ev, ev.currentTarget);
};
(function(buttonEl) {
var startX, startY;
el.ontouchstart = function(ev) {
if (ev.touches.length !== 1) return;
longPressTriggered = false;
startX = ev.touches[0].clientX;
startY = ev.touches[0].clientY;
longPressTimer = setTimeout(function() {
longPressTimer = null;
longPressTriggered = true;
showButtonContextMenu({ clientX: startX, clientY: startY }, buttonEl);
}, 500);
};
el.ontouchmove = function(ev) {
if (longPressTimer && ev.touches.length === 1) {
var dx = ev.touches[0].clientX - startX;
var dy = ev.touches[0].clientY - startY;
if (dx * dx + dy * dy > 100) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
}
};
el.ontouchend = el.ontouchcancel = function() {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
setTimeout(function() { longPressTriggered = false; }, 400);
};
})(el);
el.ondragstart = function(ev) {
ev.dataTransfer.setData('text/plain', ev.currentTarget.getAttribute('data-index'));
ev.dataTransfer.effectAllowed = 'move';
ev.currentTarget.classList.add('dragging');
};
el.ondragend = function(ev) {
ev.currentTarget.classList.remove('dragging');
};
el.ondragover = function(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = 'move';
var target = ev.currentTarget;
if (target.classList.contains('dragging')) return;
target.classList.add('drop-target');
};
el.ondragleave = function(ev) {
ev.currentTarget.classList.remove('drop-target');
};
el.ondrop = function(ev) {
ev.preventDefault();
ev.currentTarget.classList.remove('drop-target');
var fromIdx = parseInt(ev.dataTransfer.getData('text/plain'), 10);
var toIdx = parseInt(ev.currentTarget.getAttribute('data-index'), 10);
if (fromIdx === toIdx) return;
var buttons = getButtonsFromDom();
var item = buttons.splice(fromIdx, 1)[0];
buttons.splice(toIdx, 0, item);
renderButtons(buttons);
saveButtons(buttons);
};
}
}
// Button editor (per-button preset config)
function saveButtons(buttons, callback) {
fetch('/api/buttons', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ buttons: buttons })
}).then(function(r) { return r.json(); }).then(function(data) {
if (callback) callback(data);
else showToast(data.ok ? 'Saved' : (data.error || 'Save failed'));
}).catch(function() {
if (callback) callback({ ok: false });
else showToast('Save failed');
});
}
function saveCurrentButtons() {
var buttons = getButtonsFromDom();
saveButtons(buttons, function(data) {
showToast(data.ok ? 'Saved' : (data.error || 'Save failed'));
});
}
var toastTimer = null;
function showToast(message) {
var el = document.getElementById('toast');
if (!el) {
el = document.createElement('div');
el.id = 'toast';
el.className = 'toast';
document.body.appendChild(el);
}
el.textContent = message;
el.classList.add('toast-visible');
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function() {
el.classList.remove('toast-visible');
toastTimer = null;
}, 2000);
}
function addButton(button) {
var buttons = getButtonsFromDom();
buttons.push(button);
renderButtons(buttons);
saveButtons(buttons);
}
var contextMenuEl = null;
var longPressTimer = null;
var longPressTriggered = false;
function openNewButtonEditor() {
currentEditIndex = -1;
var title = document.getElementById('buttonEditorTitle');
if (title) title.textContent = 'New button';
fillButtonEditorFields({
id: '',
preset: '',
p: 'off',
d: 0,
b: 0,
c: [[0, 0, 0]]
});
openButtonEditor();
}
function openExistingButtonEditor(index) {
currentEditIndex = index;
var buttons = getButtonsFromDom();
var btn = buttons[index];
var title = document.getElementById('buttonEditorTitle');
if (title) title.textContent = 'Edit button';
fillButtonEditorFields(btn);
openButtonEditor();
}
function openButtonEditor() {
var editor = document.getElementById('buttonEditor');
if (!editor) return;
editor.classList.add('open');
document.body.classList.add('button-editor-open');
}
function closeButtonEditor() {
var editor = document.getElementById('buttonEditor');
if (!editor) return;
editor.classList.remove('open');
document.body.classList.remove('button-editor-open');
}
function fillButtonEditorFields(btn) {
document.getElementById('be-label').value = btn.id || '';
document.getElementById('be-preset').value = btn.preset || btn.id || '';
var pattern = btn.p || btn.preset || 'off';
var patternSelect = document.getElementById('be-pattern');
if (patternSelect) {
patternSelect.value = pattern;
}
document.getElementById('be-delay').value = btn.d != null ? String(btn.d) : '';
document.getElementById('be-brightness').value = btn.b != null ? String(btn.b) : '';
var colors = btn.c;
var colorsStr = '';
if (Array.isArray(colors) && colors.length) {
colorsStr = colors.map(function (rgb) {
return (rgb[0] || 0) + ',' + (rgb[1] || 0) + ',' + (rgb[2] || 0);
}).join('; ');
}
document.getElementById('be-colors').value = colorsStr;
for (var i = 1; i <= 8; i++) {
var key = 'n' + i;
var el = document.getElementById('be-' + key);
if (el) el.value = btn[key] != null ? String(btn[key]) : '';
}
}
function buildButtonFromEditor() {
function toInt(val, fallback) {
var n = parseInt(val, 10);
return isNaN(n) ? fallback : n;
}
var label = (document.getElementById('be-label').value || '').trim();
var presetName = (document.getElementById('be-preset').value || '').trim() || label || 'preset';
var patternEl = document.getElementById('be-pattern');
var pattern = patternEl ? (patternEl.value || '').trim() : 'off';
var delayVal = document.getElementById('be-delay').value;
var brightVal = document.getElementById('be-brightness').value;
var colorsRaw = (document.getElementById('be-colors').value || '').trim();
var d = toInt(delayVal, 0);
var b = toInt(brightVal, 0);
var colors = [];
if (colorsRaw) {
colorsRaw.split(';').forEach(function (chunk) {
var parts = chunk.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
if (parts.length === 3) {
var r = toInt(parts[0], 0);
var g = toInt(parts[1], 0);
var bl = toInt(parts[2], 0);
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
bl = Math.max(0, Math.min(255, bl));
colors.push([r, g, bl]);
}
});
}
if (!colors.length) colors = [[0, 0, 0]];
var btn = {
id: label || presetName,
preset: presetName,
p: pattern,
d: d,
b: b,
c: colors
};
for (var i = 1; i <= 8; i++) {
var key = 'n' + i;
var el = document.getElementById('be-' + key);
if (!el) continue;
var v = el.value;
if (v !== '') {
btn[key] = toInt(v, 0);
}
}
return btn;
}
function saveButtonFromEditor() {
var btn = buildButtonFromEditor();
var buttons = getButtonsFromDom();
if (currentEditIndex >= 0 && currentEditIndex < buttons.length) {
buttons[currentEditIndex] = btn;
} else {
buttons.push(btn);
}
renderButtons(buttons);
saveButtons(buttons);
// Also send this preset to the Pico and save it there
if (ws && ws.readyState === WebSocket.OPEN && btn.preset) {
var presetData = {
p: btn.p,
d: btn.d,
b: btn.b,
c: btn.c
};
for (var i = 1; i <= 8; i++) {
var key = 'n' + i;
if (btn[key] !== undefined) {
presetData[key] = btn[key];
}
}
ws.send(JSON.stringify({
preset_edit: {
name: btn.preset,
data: presetData
}
}));
ws.send(JSON.stringify({ preset_save: true }));
}
closeButtonEditor();
}
function showButtonContextMenu(evOrCoords, buttonEl) {
hideContextMenu();
var x = evOrCoords.clientX != null ? evOrCoords.clientX : evOrCoords.x;
var y = evOrCoords.clientY != null ? evOrCoords.clientY : evOrCoords.y;
var buttons = getButtonsFromDom();
var idx = parseInt(buttonEl.getAttribute('data-index'), 10);
var btn = buttons[idx];
contextMenuEl = document.createElement('div');
contextMenuEl.className = 'context-menu';
contextMenuEl.style.left = x + 'px';
contextMenuEl.style.top = y + 'px';
contextMenuEl.innerHTML = '<button type="button" class="context-menu-item" data-action="edit">Edit</button><button type="button" class="context-menu-item" data-action="delete">Delete</button>';
document.body.appendChild(contextMenuEl);
document.addEventListener('click', hideContextMenuOnce);
contextMenuEl.querySelector('[data-action="edit"]').onclick = function() {
hideContextMenu();
openExistingButtonEditor(idx);
};
contextMenuEl.querySelector('[data-action="delete"]').onclick = function() {
hideContextMenu();
buttons.splice(idx, 1);
renderButtons(buttons);
saveButtons(buttons);
};
}
function hideContextMenu() {
if (contextMenuEl && contextMenuEl.parentNode) contextMenuEl.parentNode.removeChild(contextMenuEl);
contextMenuEl = null;
document.removeEventListener('click', hideContextMenuOnce);
}
function hideContextMenuOnce() {
hideContextMenu();
}
function connect() {
var proto = location.protocol === "https:" ? "wss:" : "ws:";
ws = new WebSocket(proto + "//" + location.host + "/ws");
ws.onclose = function() { setTimeout(connect, 2000); };
ws.onmessage = function(ev) {
try {
var msg = JSON.parse(ev.data);
if (msg && msg.select != null) {
var preset = typeof msg.select === 'string' ? msg.select : (Array.isArray(msg.select) ? msg.select[1] : null);
if (preset != null) {
document.querySelectorAll('.buttons .btn').forEach(function(b) { b.classList.remove('selected'); });
if (preset !== 'off') {
var el = document.querySelector('.buttons .btn[data-preset="' + preset + '"]');
if (el) {
el.classList.add('selected');
scrollSelectedIntoView();
updateSelectedObserver();
}
}
}
}
} catch (e) {}
};
}
function sendSelect(preset, el) {
var msg = JSON.stringify({ select: preset });
if (ws && ws.readyState === WebSocket.OPEN) ws.send(msg);
document.querySelectorAll('.buttons .btn').forEach(function(b) { b.classList.remove('selected'); });
if (el) el.classList.add('selected');
updateSelectedObserver();
}
function togglePresetEditor() {
var editor = document.getElementById('presetEditor');
if (!editor) return;
var isOpen = editor.classList.contains('open');
if (isOpen) {
editor.classList.remove('open');
document.body.classList.remove('preset-editor-open');
} else {
editor.classList.add('open');
document.body.classList.add('preset-editor-open');
}
}
function buildPresetPayloadFromForm() {
var name = (document.getElementById('pe-name').value || '').trim();
var patternEl = document.getElementById('pe-pattern');
var pattern = patternEl ? (patternEl.value || '').trim() : 'off';
var delayVal = document.getElementById('pe-delay').value;
var brightVal = document.getElementById('pe-brightness').value;
var colorsRaw = (document.getElementById('pe-colors').value || '').trim();
function toInt(val, fallback) {
var n = parseInt(val, 10);
return isNaN(n) ? fallback : n;
}
var d = toInt(delayVal, 0);
var b = toInt(brightVal, 0);
var colors = [];
if (colorsRaw) {
colorsRaw.split(';').forEach(function (chunk) {
var parts = chunk.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
if (parts.length === 3) {
var r = toInt(parts[0], 0);
var g = toInt(parts[1], 0);
var bl = toInt(parts[2], 0);
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
bl = Math.max(0, Math.min(255, bl));
colors.push([r, g, bl]);
}
});
}
if (!colors.length) colors = [[0, 0, 0]];
var data = { p: pattern, d: d, b: b, c: colors };
['n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8'].forEach(function (key) {
var el = document.getElementById('pe-' + key);
if (!el) return;
var v = el.value;
if (v !== '') {
data[key] = toInt(v, 0);
}
});
return { name: name, data: data };
}
function sendPresetToPico() {
var payload = buildPresetPayloadFromForm();
if (!payload.name) {
showToast('Preset name is required');
return;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ preset_edit: payload }));
ensurePresetInList(payload.name);
showToast('Preset sent (not yet saved)');
} else {
showToast('Not connected');
}
}
function savePresetsOnPico() {
var payload = buildPresetPayloadFromForm();
if (!payload.name) {
showToast('Preset name is required');
return;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ preset_edit: payload }));
ws.send(JSON.stringify({ preset_save: true }));
ensurePresetInList(payload.name);
showToast('Preset saved to Pico');
} else {
showToast('Not connected');
}
}
function deletePresetOnPico() {
var name = (document.getElementById('pe-name').value || '').trim();
if (!name) {
showToast('Preset name is required');
return;
}
var msg = { preset_delete: name };
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
removePresetFromList(name);
ws.send(JSON.stringify(msg));
showToast('Delete command sent');
} else {
showToast('Not connected');
}
}
function toggleMenu() {
var menu = document.getElementById('menu');
menu.classList.toggle('open');
}
function closeMenu() {
document.getElementById('menu').classList.remove('open');
}
document.addEventListener('click', function(ev) {
var menu = document.getElementById('menu');
var menuBtn = document.querySelector('.menu-btn');
if (menu.classList.contains('open') && menuBtn && !menu.contains(ev.target) && !menuBtn.contains(ev.target)) {
closeMenu();
}
});
function scrollSelectedIntoView() {
var el = document.querySelector('.buttons .btn.selected');
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
}
var scrollObserver = null;
var observedSelectedEl = null;
function setupScrollObserver() {
var container = document.getElementById('buttonsContainer');
if (!container || scrollObserver) return;
scrollObserver = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.intersectionRatio < 0.5 && entry.target.classList.contains('selected')) {
scrollSelectedIntoView();
}
});
},
{ root: container, rootMargin: '0px', threshold: [0, 0.25, 0.5, 0.75, 1] }
);
}
function updateSelectedObserver() {
var el = document.querySelector('.buttons .btn.selected');
if (el === observedSelectedEl) return;
if (scrollObserver) {
if (observedSelectedEl) scrollObserver.unobserve(observedSelectedEl);
observedSelectedEl = el;
if (el) {
setupScrollObserver();
scrollObserver.observe(el);
}
}
}
function nextPreset() {
var btns = document.querySelectorAll('.buttons .btn');
if (btns.length === 0) return;
var idx = -1;
for (var i = 0; i < btns.length; i++) {
if (btns[i].classList.contains('selected')) { idx = i; break; }
}
idx = (idx + 1) % btns.length;
var nextEl = btns[idx];
sendSelect(nextEl.getAttribute('data-preset'), nextEl);
scrollSelectedIntoView();
}
setupScrollObserver();
// Re-render buttons from server config (including per-button presets) once loaded.
fetch('/api/buttons')
.then(function (r) { return r.json(); })
.then(function (data) {
var buttons = Array.isArray(data.buttons) ? data.buttons : getButtonsFromDom();
renderButtons(buttons);
})
.catch(function () {
// Fallback: use buttons rendered by the template
renderButtons(getButtonsFromDom());
});
updateSelectedObserver();
connect();

256
esp32/src/static/styles.css Normal file
View File

@@ -0,0 +1,256 @@
body { font-family: sans-serif; margin: 0; padding: 0 0 6.5rem 0; }
body.button-editor-open {
overflow: hidden;
}
.header { position: relative; }
.menu-btn {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
cursor: pointer;
border: 1px solid #555;
border-radius: 0;
background: #333;
color: #fff;
text-align: left;
}
.menu {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 10;
flex-direction: column;
border: 1px solid #555;
border-top: none;
background: #2a2a2a;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.menu.open { display: flex; }
.menu-item {
padding: 0.75rem 1rem;
font-size: 1rem;
cursor: pointer;
border: none;
border-bottom: 1px solid #444;
background: transparent;
color: #fff;
text-align: left;
}
.menu-item:last-child { border-bottom: none; }
.menu-item:hover { background: #444; }
.menu-item.off { background: #522; }
.buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
max-height: calc(100vh - 10rem);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
@media (min-width: 768px) {
.buttons {
grid-template-columns: repeat(6, 1fr);
}
}
.btn {
padding: 0.5rem 0.25rem;
border: 1px solid #555;
border-radius: 0;
background: #444;
color: #fff;
font-size: 1rem;
cursor: pointer;
min-height: 4.15rem;
}
.btn.selected {
background: #2a7;
border-color: #3b8;
}
.btn.dragging { opacity: 0.5; }
.btn.drop-target { outline: 2px solid #3b8; outline-offset: -2px; }
.context-menu {
position: fixed;
z-index: 100;
background: #2a2a2a;
border: 1px solid #555;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
min-width: 8rem;
}
.context-menu-item {
display: block;
width: 100%;
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: #fff;
font-size: 1rem;
text-align: left;
cursor: pointer;
}
.context-menu-item:hover { background: #444; }
.toast {
position: fixed;
bottom: 5rem;
left: 50%;
transform: translateX(-50%) translateY(2rem);
padding: 0.5rem 1rem;
background: #333;
color: #fff;
border-radius: 4px;
font-size: 0.9rem;
z-index: 1000;
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
pointer-events: none;
}
.toast.toast-visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.next-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
padding: 2rem 1rem;
padding-bottom: max(2rem, env(safe-area-inset-bottom));
font-size: 1.25rem;
cursor: pointer;
border: 2px solid #555;
border-radius: 0;
background: #333;
color: #fff;
}
.preset-editor {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.65);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
padding: 1rem;
}
.preset-editor.open {
display: flex;
}
.preset-editor-inner {
width: 100%;
max-width: 28rem;
max-height: calc(100vh - 4rem);
overflow-y: auto;
background: #222;
border: 1px solid #555;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.6);
border-radius: 6px;
padding: 1rem 1.25rem 0.75rem;
box-sizing: border-box;
}
.preset-editor-title {
margin: 0 0 0.75rem;
font-size: 1.25rem;
color: #fff;
}
.preset-editor-field {
display: flex;
flex-direction: column;
margin-bottom: 0.5rem;
gap: 0.25rem;
font-size: 0.9rem;
color: #ddd;
}
.preset-editor-field span {
opacity: 0.85;
}
.preset-editor-field input {
padding: 0.35rem 0.5rem;
border-radius: 4px;
border: 1px solid #555;
background: #111;
color: #fff;
font-size: 0.95rem;
}
.preset-editor-row {
display: flex;
gap: 0.5rem;
}
.preset-editor-field.small {
flex: 0 0 48%;
}
.preset-editor-field.small input {
padding: 0.25rem 0.4rem;
font-size: 0.85rem;
}
.preset-editor-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.75rem 0 0.5rem;
}
.preset-editor-btn {
flex: 1;
padding: 0.5rem 0.75rem;
border-radius: 4px;
border: 1px solid #555;
background: #333;
color: #fff;
font-size: 0.95rem;
cursor: pointer;
}
.preset-editor-btn.primary {
background: #2a7;
border-color: #3b8;
}
.preset-editor-btn.danger {
background: #722;
border-color: #a33;
}
.preset-editor-close {
width: 100%;
margin-top: 0.25rem;
padding: 0.4rem 0.75rem;
border-radius: 4px;
border: 1px solid #555;
background: #111;
color: #ccc;
font-size: 0.9rem;
cursor: pointer;
}
/* preset-list removed with old preset editor */

View File

@@ -0,0 +1,147 @@
{% args buttons, device_id %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Led Hoop</title>
<link rel="stylesheet" href="static/styles.css" />
</head>
<body data-device-id="{{ device_id }}">
<div class="header">
<button class="menu-btn" type="button" onclick="toggleMenu()" aria-label="Menu">☰ Menu</button>
<div class="menu" id="menu">
<button class="menu-item off" type="button" onclick="sendSelect('off', null); closeMenu();">Off</button>
<button class="menu-item" type="button" onclick="sendSelect('test', null); closeMenu();">Test</button>
<button class="menu-item" type="button" onclick="sendSelect('calibration', null); closeMenu();">Calibration</button>
<button class="menu-item" type="button" onclick="closeMenu(); openNewButtonEditor();">Add button</button>
<button class="menu-item" type="button" onclick="closeMenu(); saveCurrentButtons();">Save</button>
</div>
</div>
<div class="buttons" id="buttonsContainer">
{% for btn in buttons %}
<button class="btn" type="button" data-preset="{{ btn['preset'] }}" data-id="{{ btn['id'] }}"
draggable="true">{{ btn['id'] }}</button>
{% endfor %}
</div>
<div class="preset-editor" id="buttonEditor">
<div class="preset-editor-inner">
<h2 class="preset-editor-title" id="buttonEditorTitle">Button</h2>
<label class="preset-editor-field">
<span>Button label</span>
<input id="be-label" type="text" placeholder="e.g. grab" />
</label>
<label class="preset-editor-field">
<span>Preset name</span>
<input id="be-preset" type="text" placeholder="e.g. grab" />
</label>
<label class="preset-editor-field">
<span>Pattern (p)</span>
<select id="be-pattern">
<option value="spin">spin</option>
<option value="roll">roll</option>
<option value="grab">grab</option>
<option value="lift">lift</option>
<option value="flare">flare</option>
<option value="hook">hook</option>
<option value="invertsplit">invertsplit</option>
<option value="pose">pose</option>
<option value="backbalance">backbalance</option>
<option value="beat">beat</option>
<option value="crouch">crouch</option>
<option value="backbendsplit">backbendsplit</option>
<option value="straddle">straddle</option>
<option value="frontbalance">frontbalance</option>
<option value="elbowhang">elbowhang</option>
<option value="elbowhangspin">elbowhangspin</option>
<option value="dismount">dismount</option>
<option value="fluff">fluff</option>
<option value="elbowhangsplit">elbowhangsplit</option>
<option value="invert">invert</option>
<option value="backbend">backbend</option>
<option value="seat">seat</option>
<option value="kneehang">kneehang</option>
<option value="legswoop">legswoop</option>
<option value="split">split</option>
<option value="foothang">foothang</option>
<option value="segments">segments</option>
<option value="segments_transition">segments_transition</option>
<option value="double_circle">double_circle</option>
<option value="off">off</option>
<option value="on">on</option>
<option value="blink">blink</option>
<option value="rainbow">rainbow</option>
<option value="pulse">pulse</option>
<option value="transition">transition</option>
<option value="chase">chase</option>
<option value="circle">circle</option>
<option value="calibration">calibration</option>
<option value="test">test</option>
</select>
</label>
<div class="preset-editor-row">
<label class="preset-editor-field">
<span>Delay (d)</span>
<input id="be-delay" type="number" inputmode="numeric" />
</label>
<label class="preset-editor-field">
<span>Brightness (b)</span>
<input id="be-brightness" type="number" inputmode="numeric" min="0" max="255" />
</label>
</div>
<label class="preset-editor-field">
<span>Colors (c)</span>
<input id="be-colors" type="text" placeholder="r,g,b; r,g,b (0255)" />
</label>
<div class="preset-editor-row">
<label class="preset-editor-field small">
<span>n1</span>
<input id="be-n1" type="number" inputmode="numeric" />
</label>
<label class="preset-editor-field small">
<span>n2</span>
<input id="be-n2" type="number" inputmode="numeric" />
</label>
</div>
<div class="preset-editor-row">
<label class="preset-editor-field small">
<span>n3</span>
<input id="be-n3" type="number" inputmode="numeric" />
</label>
<label class="preset-editor-field small">
<span>n4</span>
<input id="be-n4" type="number" inputmode="numeric" />
</label>
</div>
<div class="preset-editor-row">
<label class="preset-editor-field small">
<span>n5</span>
<input id="be-n5" type="number" inputmode="numeric" />
</label>
<label class="preset-editor-field small">
<span>n6</span>
<input id="be-n6" type="number" inputmode="numeric" />
</label>
</div>
<div class="preset-editor-row">
<label class="preset-editor-field small">
<span>n7</span>
<input id="be-n7" type="number" inputmode="numeric" />
</label>
<label class="preset-editor-field small">
<span>n8</span>
<input id="be-n8" type="number" inputmode="numeric" />
</label>
</div>
<div class="preset-editor-actions">
<button type="button" class="preset-editor-btn primary" onclick="saveButtonFromEditor()">Save</button>
<button type="button" class="preset-editor-btn" onclick="closeButtonEditor()">Cancel</button>
</div>
</div>
</div>
<button class="next-btn" type="button" onclick="nextPreset()">Next</button>
<script src="static/main.js"></script>
</body>
</html>

102
main.py Normal file
View File

@@ -0,0 +1,102 @@
# """
# Pico: receive led-driver JSON from UART (one message per line). Runs Presets + patterns.
# UART RX on D7 (GPIO1). Non-blocking so presets.tick() runs every loop.
# """
# from settings import Settings
# from machine import UART, Pin
# import utime
# from presets import Presets
# from utils import convert_and_reorder_colors
# import json
# # UART (Pico XIAO: D7 = GPIO1)
# UART_RX_PIN = 1
# UART_BAUD = 115200
# UART_ID = 0
# settings = Settings()
# print(settings)
# 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("Selected startup preset:", startup_preset)
# last_brightness_save = 0
# # Non-blocking UART
# uart = UART(UART_ID, baudrate=UART_BAUD, rx=Pin(UART_RX_PIN), rxbuf=512, timeout=0)
# uart_buf = bytearray()
# print("UART RX on pin %s, %s baud (one JSON object per line)" % (UART_RX_PIN, UART_BAUD))
# def process_message(data):
# """Handle one JSON message (led-driver protocol: v, b, presets, select, default, save)."""
# if data.get("v") != "1":
# return
# global last_brightness_save
# 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:
# for id, preset_data in data["presets"].items():
# if "c" in preset_data:
# preset_data["c"] = convert_and_reorder_colors(preset_data["c"], settings)
# presets.edit(id, preset_data)
# print("Edited preset", id, preset_data.get("name", ""))
# if settings.get("name") in data.get("select", {}):
# select_list = data["select"][settings.get("name")]
# 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)
# if "default" in data:
# settings["startup_preset"] = data["default"]
# print("Set startup preset to", data["default"])
# settings.save()
# if "save" in data:
# presets.save()
# while True:
# presets.tick()
# n = uart.any()
# if n:
# data_in = uart.read(n)
# if data_in:
# for b in data_in:
# if b in (0x0A, 0x0D): # LF or CR
# if uart_buf:
# try:
# msg = uart_buf.decode("utf-8").strip()
# if msg:
# data = json.loads(msg)
# process_message(data)
# except (ValueError, UnicodeError):
# pass
# uart_buf = bytearray()
# else:
# if len(uart_buf) < 1024:
# uart_buf.append(b)
# utime.sleep_ms(1)
from neopixel import NeoPixel
from machine import Pin
pins = ((2,270), (3,271), (4,272), (0,273), (7,274), (6,275), (29,276), (28,277))
for pin, num_leds in pins:
print(pin, num_leds)
np = NeoPixel(Pin(pin), num_leds)
np.fill((8, 0, 0))
np.write()

View File

@@ -93,8 +93,8 @@ class PIO_DMA_Transfer():
self.dma_chan.CTRL_TRIG.INCR_WRITE = 0 self.dma_chan.CTRL_TRIG.INCR_WRITE = 0
self.dma_chan.CTRL_TRIG.INCR_READ = 1 self.dma_chan.CTRL_TRIG.INCR_READ = 1
def start_transfer(self, buffer): def start_transfer(self, buffer, offset=0):
self.dma_chan.READ_ADDR_REG = uctypes.addressof(buffer) self.dma_chan.READ_ADDR_REG = uctypes.addressof(buffer) + offset
self.dma_chan.CTRL_TRIG.EN = 1 self.dma_chan.CTRL_TRIG.EN = 1
def transfer_count(self): def transfer_count(self):

View File

@@ -1,68 +1,72 @@
import array, time import array, time
from machine import Pin from machine import Pin
import rp2 import rp2
from time import sleep from time import sleep
import dma import dma
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=8) @rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=8)
def ws2812(): def ws2812():
T1 = 2 T1 = 2
T2 = 5 T2 = 5
T3 = 3 T3 = 3
wrap_target() wrap_target()
label("bitloop") label("bitloop")
out(x, 1) .side(0) [T3 - 1] out(x, 1) .side(0) [T3 - 1]
jmp(not_x, "do_zero") .side(1) [T1 - 1] jmp(not_x, "do_zero") .side(1) [T1 - 1]
jmp("bitloop") .side(1) [T2 - 1] jmp("bitloop") .side(1) [T2 - 1]
label("do_zero") label("do_zero")
nop() .side(0) [T2 - 1] nop() .side(0) [T2 - 1]
wrap() wrap()
class WS2812B: class WS2812B:
def __init__(self, num_leds, pin, state_machine, brightness=0.1, invert=False): def __init__(self, num_leds, pin, state_machine, brightness=0.1, invert=False):
self.sm = rp2.StateMachine(state_machine, ws2812, freq=8_000_000, sideset_base=Pin(pin)) self.sm = rp2.StateMachine(state_machine, ws2812, freq=8_000_000, sideset_base=Pin(pin))
self.sm.active(1) self.sm.active(1)
self.ar = bytearray(num_leds*3) self.ar = bytearray(num_leds*3)
self.num_leds = num_leds self.num_leds = num_leds
self.brightness = brightness self.brightness = brightness
self.invert = invert self.invert = invert
self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3) self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3)
self.dma.start_transfer(self.ar)
def show(self):
self.pio_dma.start_transfer(self.ar)
def show(self, array=None, offset=0):
def set(self, i, color): if array is None:
self.ar[i*3] = int(color[1]*self.brightness) array = self.ar
self.ar[i*3+1] = int(color[0]*self.brightness) self.pio_dma.start_transfer(array, offset)
self.ar[i*3+2] = int(color[2]*self.brightness)
def set(self, i, color):
def fill(self, color): self.ar[i*3] = int(color[1]*self.brightness)
for i in range(self.num_leds): self.ar[i*3+1] = int(color[0]*self.brightness)
self.set(i, color) self.ar[i*3+2] = int(color[2]*self.brightness)
def busy(self): def fill(self, color):
return self.pio_dma.busy() for i in range(self.num_leds):
self.set(i, color)
BLACK = (0, 0, 0)
RED = (255, 0, 0) def busy(self):
YELLOW = (255, 150, 0) return self.pio_dma.busy()
GREEN = (0, 255, 0)
CYAN = (0, 255, 255) BLACK = (0, 0, 0)
BLUE = (0, 0, 255) RED = (255, 0, 0)
PURPLE = (180, 0, 255) YELLOW = (255, 150, 0)
WHITE = (255, 255, 255) GREEN = (0, 255, 0)
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE) CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
if __name__ == "__main__": PURPLE = (180, 0, 255)
num_leds, pin, sm, brightness = 293, 2, 0, 0.1 WHITE = (255, 255, 255)
ws0 = WS2812B(num_leds, pin, sm, brightness) COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)
while True:
for color in ws0.COLORS: if __name__ == "__main__":
ws0.fill(color) num_leds, pin, sm, brightness = 293, 2, 0, 0.1
ws0.show() ws0 = WS2812B(num_leds, pin, sm, brightness)
time.sleep(1) while True:
for color in ws0.COLORS:
ws0.fill(color)
ws0.show()
time.sleep(1)

63
pico/src/main.py Normal file
View File

@@ -0,0 +1,63 @@
from machine import UART, Pin
import json
from presets import Presets
import gc
uart = UART(0, baudrate=921600, rx=Pin(1, Pin.IN))
presets = Presets()
presets.load()
print(presets.presets.keys())
presets.select("off")
#print memory usage
print(f"Memory usage: {gc.mem_free()/1024} kB free")
i = 0
while True:
presets.tick()
if uart.any():
data = uart.readline()
try:
data = json.loads(data)
except:
# Ignore malformed JSON lines
continue
# Select a preset by name (existing behaviour)
preset_name = data.get("select")
if preset_name is not None:
presets.select(preset_name)
presets.tick()
# Create or update a preset:
# {"preset_edit": {"name": "<name>", "data": {<preset_dict>}}}
edit_payload = data.get("preset_edit")
if isinstance(edit_payload, dict):
name = edit_payload.get("name")
preset_data = edit_payload.get("data") or {}
if isinstance(name, str) and isinstance(preset_data, dict):
# Log the incoming preset payload for debugging
print("PRESET_EDIT", name, preset_data)
presets.edit(name, preset_data)
# Delete a preset:
# {"preset_delete": "<name>"}
delete_name = data.get("preset_delete")
if isinstance(delete_name, str):
print("PRESET_DELETE", delete_name)
presets.delete(delete_name)
# Persist all presets to flash:
# {"preset_save": true}
if data.get("preset_save"):
print("PRESET_SAVE")
presets.save()
print(data)
gc.collect()
#print used and free memory
print(f"Memory usage: {gc.mem_alloc()/1024} kB used, {gc.mem_free()/1024} kB free")

View File

@@ -0,0 +1,19 @@
from .blink import Blink
from .rainbow import Rainbow
from .pulse import Pulse
from .transition import Transition
from .chase import Chase
from .circle import Circle
from .double_circle import DoubleCircle
from .roll import Roll
from .calibration import Calibration
from .test import Test
from .scale_test import ScaleTest
from .grab import Grab
from .spin import Spin
from .lift import Lift
from .flare import Flare
from .hook import Hook
from .pose import Pose
from .segments import Segments
from .segments_transition import SegmentsTransition

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

View File

@@ -0,0 +1,39 @@
"""Calibration: strips 2 and 6 only. First 10 green, then alternating 10 blue / 10 red. 10% brightness."""
BRIGHTNESS = 1
BLOCK = 10
STRIPS_ON = (2, 6) # 0-based: 3rd and 7th strip only
GREEN = (0, 255, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
def _scale(color, factor):
return tuple(int(c * factor) for c in color)
class Calibration:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
strips = self.driver.strips
green = _scale(GREEN, BRIGHTNESS)
red = _scale(RED, BRIGHTNESS)
blue = _scale(BLUE, BRIGHTNESS)
blue = (0,0,0)
on_set = set(STRIPS_ON)
for strip_idx, strip in enumerate(strips):
n = strip.num_leds
if strip_idx not in on_set:
strip.fill((0, 0, 0))
strip.show()
continue
for i in range(n):
if i < BLOCK:
strip.set(i, green)
else:
block = (i - BLOCK) // BLOCK
strip.set(i, blue if block % 2 == 0 else red)
strip.show()

166
pico/src/patterns/chase.py Normal file
View File

@@ -0,0 +1,166 @@
import utime
def _make_chase_double(num_leds, cumulative_leds, total_ring_leds, color0, color1, n1, n2):
"""Pregenerate strip double buffer with repeating segments:
color0 for n1 pixels, then color1 for n2 pixels, around the full ring. GRB order."""
n = 2 * num_leds
buf = bytearray(n * 3)
pattern_len = n1 + n2
for b in range(n):
# Position of this pixel along the logical ring
pos = (2 * cumulative_leds - b) % total_ring_leds
seg_pos = pos % pattern_len
if seg_pos < n1:
r, g, b_ = color0
else:
r, g, b_ = color1
o = b * 3
buf[o] = g
buf[o + 1] = r
buf[o + 2] = b_
strip_len_bytes = num_leds * 3
return buf, strip_len_bytes
def _ensure_chase_buffers(driver, color0, color1, n1, n2, cache):
"""Build or refresh per-strip double buffers for the chase pattern."""
strips = driver.strips
key = (
color0,
color1,
int(n1),
int(n2),
tuple(s.num_leds for s in strips),
)
if cache.get("key") == key and cache.get("data") is not None:
return cache["data"], cache["cumulative_leds"], cache["total_ring_leds"]
if not strips:
cache["key"] = key
cache["data"] = []
cache["cumulative_leds"] = []
cache["total_ring_leds"] = 0
return cache["data"], cache["cumulative_leds"], cache["total_ring_leds"]
cumulative_leds = [0]
for s in strips[:-1]:
cumulative_leds.append(cumulative_leds[-1] + s.num_leds)
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
chase_data = []
for idx, s in enumerate(strips):
buf, strip_len_bytes = _make_chase_double(
s.num_leds,
cumulative_leds[idx],
total_ring_leds,
color0,
color1,
n1,
n2,
)
chase_data.append((buf, strip_len_bytes))
cache["key"] = key
cache["data"] = chase_data
cache["cumulative_leds"] = cumulative_leds
cache["total_ring_leds"] = total_ring_leds
return chase_data, cumulative_leds, total_ring_leds
class Chase:
def __init__(self, driver):
self.driver = driver
self._buffers_cache = {}
def run(self, preset):
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating around the full ring.
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)."""
colors = preset.c or []
if not colors:
return
# If only one color provided, use it for both colors
if len(colors) < 2:
base0 = colors[0]
base1 = colors[0]
else:
base0 = colors[0]
base1 = colors[1]
# Apply preset/global brightness
color0 = self.driver.apply_brightness(base0, preset.b)
color1 = self.driver.apply_brightness(base1, preset.b)
n1 = max(1, int(getattr(preset, "n1", 1)) or 1) # LEDs of color 0
n2 = max(1, int(getattr(preset, "n2", 1)) or 1) # LEDs of color 1
n3 = int(getattr(preset, "n3", 0) or 0) # step on even steps
n4 = int(getattr(preset, "n4", 0) or 0) # step on odd steps
if n3 == 0 and n4 == 0:
# Nothing to move; default to simple forward motion
n3 = 1
n4 = 1
chase_data, cumulative_leds, total_ring_leds = _ensure_chase_buffers(
self.driver, color0, color1, n1, n2, self._buffers_cache
)
strips = self.driver.strips
def show_frame(chase_pos):
for i, (strip, (buf, strip_len_bytes)) in enumerate(zip(strips, chase_data)):
# head in [0, strip_len_bytes) so DMA read head..head+strip_len_bytes stays in double buffer
head = ((chase_pos + cumulative_leds[i]) * 3) % strip_len_bytes
strip.show(buf, head)
# Helper to compute head position from current step_count
def head_from_step(step_count):
if step_count % 2 == 0:
# Even steps: (step_count//2) pairs of (n3+n4) plus one extra n3
pos = (step_count // 2) * (n3 + n4) + n3
else:
# Odd steps: ((step_count+1)//2) pairs of (n3+n4)
pos = ((step_count + 1) // 2) * (n3 + n4)
if total_ring_leds <= 0:
return 0
pos %= total_ring_leds
if pos < 0:
pos += total_ring_leds
return pos
# Single-step mode: render one frame, then stop
if not preset.a:
step_count = int(self.driver.step)
chase_pos = head_from_step(step_count)
show_frame(chase_pos)
# Advance step for next trigger
self.driver.step = step_count + 1
yield
return
# Auto mode: continuous loop driven by delay d
transition_duration = max(10, int(getattr(preset, "d", 50)) or 10)
last_update = utime.ticks_ms() - transition_duration
while True:
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= transition_duration:
# Rebuild buffers if geometry/colors changed
chase_data, cumulative_leds, total_ring_leds = _ensure_chase_buffers(
self.driver, color0, color1, n1, n2, self._buffers_cache
)
step_count = int(self.driver.step)
chase_pos = head_from_step(step_count)
show_frame(chase_pos)
# Advance step for next frame
self.driver.step = step_count + 1
last_update = current_time
# Yield once per tick so other logic can run
yield

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

View File

@@ -0,0 +1,84 @@
class DoubleCircle:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""
DoubleCircle: symmetric band around a center index on the logical ring.
- n1: center index on the logical ring (0-based, on reference strip 0)
- n2: radius of the band (max distance from center)
- n3: direction mode
0 → LEDs start ALL OFF and turn ON n4 LEDs at a time outward from n1 toward n1±n2
1 → LEDs start ALL ON within radius n2 and turn OFF n4 LEDs at a time inward toward n1
- n4: step size in LEDs per update
- c[0]: base color used for the band
"""
num_leds = self.driver.num_leds
if num_leds <= 0:
while True:
yield
colors = preset.c or []
base1 = colors[0] if len(colors) >= 1 else (255, 255, 255)
off = (0, 0, 0)
# Apply preset/global brightness
color_on = self.driver.apply_brightness(base1, preset.b)
color_off = off
# Center index and radius from preset; clamp center to ring length
center = int(getattr(preset, "n1", 0)) % num_leds
radius = max(1, int(getattr(preset, "n2", 0)) or 1)
mode = int(getattr(preset, "n3", 0) or 0) # 0 = grow band outward, 1 = shrink band inward
step_size = max(1, int(getattr(preset, "n4", 1)) or 1)
num_strips = len(self.driver.strips)
# Current "front" of the band, as a distance from center
# mode 0: grow band outward (0 → radius)
# mode 1: shrink band inward (radius → 0)
if mode == 0:
current = 0
else:
current = radius
while True:
# Draw current frame based on current radius
for i in range(num_leds):
# Shortest circular distance from i to center
forward = (i - center) % num_leds
backward = (center - i) % num_leds
dist = forward if forward < backward else backward
if dist > radius:
c = color_off
else:
if mode == 0:
# Grow outward: lit if within current radius
c = color_on if dist <= current else color_off
else:
# Shrink inward: lit if within current radius (band contracts toward center)
c = color_on if dist <= current else color_off
for strip_idx in range(num_strips):
self.driver.set(strip_idx, i, c)
self.driver.show_all()
# Update current radius for next frame
if mode == 0:
if current >= radius:
# Finished growing; hold final frame
while True:
yield
current = min(radius, current + step_size)
else:
if current <= 0:
# Finished shrinking; hold final frame
while True:
yield
current = max(0, current - step_size)
yield

View File

@@ -0,0 +1,63 @@
import utime
class Flare:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""
Flare: on the strip used by the first roll head,
make the strip fade up to brightness over the delay time.
- c[0]: color1 for the first n1 LEDs
- c[1]: color2 for the rest of the strip
- n1: number of LEDs from the start of the strip that use color1
- d: fade-in duration in ms (time to reach full preset brightness b)
"""
strips = self.driver.strips
# Which strip to flare: last roll head, clamped to valid range
strip_idx = getattr(self.driver, "last_roll_head", 0)
if strip_idx < 0 or strip_idx >= len(strips):
strip_idx = 0
strip = strips[strip_idx]
n = strip.num_leds
colors = preset.c
base_c1 = colors[0] if len(colors) > 0 else (255, 255, 255)
base_c2 = colors[1] if len(colors) > 1 else (0, 0, 0)
count_c1 = max(0, min(int(preset.n1), n))
fade_ms = max(1, int(preset.d) or 1)
target_b = int(preset.b) if hasattr(preset, "b") else 255
start_time = utime.ticks_ms()
done = False
while True:
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, start_time)
if not done:
if elapsed >= fade_ms:
factor = 1.0
done = True
else:
factor = elapsed / fade_ms if fade_ms > 0 else 1.0
else:
factor = 1.0
# Effective per-preset brightness scaled over time
current_b = int(target_b * factor)
# Apply global + local brightness to both colors
c1 = self.driver.apply_brightness(base_c1, current_b)
c2 = self.driver.apply_brightness(base_c2, current_b)
for i in range(n):
strip.set(i, c1 if i < count_c1 else c2)
strip.show()
yield

26
pico/src/patterns/grab.py Normal file
View File

@@ -0,0 +1,26 @@
"""Grab: from center of each strip, 10 LEDs each side (21 total) in purple."""
SPAN = 10 # LEDs on each side of center
PURPLE = (180, 0, 255)
class Grab:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
strips = self.driver.strips
for strip_idx, strip in enumerate(strips):
n = strip.num_leds
mid = self.driver.strip_midpoints[strip_idx]
strip.fill((0, 0, 0))
start = max(0, mid - SPAN)
end = min(n, mid + SPAN + 1)
for i in range(start, end):
strip.set(i, preset.c[0])
for strip in strips:
strip.show()
while True:
yield

31
pico/src/patterns/hook.py Normal file
View File

@@ -0,0 +1,31 @@
"""Hook: light strips n1 to n2 with a segment span long, offset from dead center."""
class Hook:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
strips = self.driver.strips
midpoints = self.driver.strip_midpoints
n1 = max(0, int(preset.n1))
n2 = max(n1, int(preset.n2))
span = max(0, int(preset.n3))
offset = int(preset.n4) # positive = toward one end
color = preset.c[0] if preset.c else (0, 0, 0)
for strip_idx, strip in enumerate(strips):
strip.fill((0, 0, 0))
if n1 <= strip_idx <= n2:
mid = midpoints[strip_idx]
n = strip.num_leds
center = mid + offset
start = max(0, center - span)
end = min(n, center + span + 1)
for i in range(start, end):
strip.set(i, color)
for strip in strips:
strip.show()
while True:
yield

116
pico/src/patterns/lift.py Normal file
View File

@@ -0,0 +1,116 @@
"""Lift: opposite of Spin — arms contract from the ends toward the center. Preset color, n1 = rate."""
import utime
SPAN = 10 # LEDs on each side of center (match Grab)
LUT_SIZE = 256
class Lift:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
strips = self.driver.strips
active_indices = (0, 4)
c0 = preset.c[0] if preset.c else (0, 0, 0)
c1 = preset.c[1] if len(preset.c) > 1 else c0
lut = []
for k in range(LUT_SIZE):
t = k / (LUT_SIZE - 1) if LUT_SIZE > 1 else 1
r = int(c0[0] + (c1[0] - c0[0]) * t)
g = int(c0[1] + (c1[1] - c0[1]) * t)
b = int(c0[2] + (c1[2] - c0[2]) * t)
lut.append((r, g, b))
midpoints = self.driver.strip_midpoints
rate = max(1, int(preset.n1) or 1)
delay_ms = max(1, int(preset.d) or 1)
margin = max(0, int(preset.n2) or 0)
left = {}
right = {}
for idx in active_indices:
if 0 <= idx < len(strips):
strip = strips[idx]
n = strip.num_leds
mid = midpoints[idx]
left[idx] = margin
right[idx] = n - margin
last_update = utime.ticks_ms()
while True:
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) < delay_ms:
yield
continue
last_update = now
for idx in active_indices:
if idx < 0 or idx >= len(strips):
continue
strip = strips[idx]
n = strip.num_leds
mid = midpoints[idx]
step = max(1, rate // 2) if idx == 0 else rate
new_left = min(mid - SPAN, left[idx] + step)
new_right = max(mid + SPAN + 1, right[idx] - step)
left_len = max(0, (mid - SPAN) - new_left)
right_len = max(0, new_right - (mid + SPAN + 1))
bright = strip.brightness
ar = strip.ar
# Clear arm regions to black so contracted pixels turn off
for i in range(margin, mid - SPAN):
if 0 <= i < n:
base = i * 3
ar[base] = ar[base + 1] = ar[base + 2] = 0
for i in range(mid + SPAN + 1, n - margin):
if 0 <= i < n:
base = i * 3
ar[base] = ar[base + 1] = ar[base + 2] = 0
for j, i in enumerate(range(new_left, mid - SPAN)):
if 0 <= i < n:
t = 1 - j / (left_len - 1) if left_len > 1 else 0
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
r, g, b = lut[lut_idx]
base = i * 3
ar[base] = int(g * bright)
ar[base + 1] = int(r * bright)
ar[base + 2] = int(b * bright)
for j, i in enumerate(range(mid + SPAN + 1, new_right)):
if 0 <= i < n:
t = j / (right_len - 1) if right_len > 1 else 0
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
r, g, b = lut[lut_idx]
base = i * 3
ar[base] = int(g * bright)
ar[base + 1] = int(r * bright)
ar[base + 2] = int(b * bright)
left[idx] = new_left
right[idx] = new_right
strip.show()
# Check if all arms have contracted to center - run once, then hold
all_done = True
for idx in active_indices:
if idx < 0 or idx >= len(strips):
continue
mid = midpoints[idx]
if left[idx] < mid - SPAN or right[idx] > mid + SPAN + 1:
all_done = False
break
if all_done:
while True:
yield
return
yield

75
pico/src/patterns/pose.py Normal file
View File

@@ -0,0 +1,75 @@
class Pose:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""
Pose pattern: simple static bands that turn the hoop on
within the specified n ranges, across ALL strips.
Uses the preset's n values as inclusive ranges over the
logical ring (driver.n):
- n1n2: color c[0]
- n3n4: color c[1]
- n5n6: color c[2]
- n7n8: color c[3]
"""
# Base colors (up to 4), missing ones default to black
colors = list(preset.c) if getattr(preset, "c", None) else []
while len(colors) < 4:
colors.append((0, 0, 0))
# Apply preset/global brightness once per color
c1 = self.driver.apply_brightness(colors[0], preset.b)
c2 = self.driver.apply_brightness(colors[1], preset.b)
c3 = self.driver.apply_brightness(colors[2], preset.b)
c4 = self.driver.apply_brightness(colors[3], preset.b)
# Helper to normalize and clamp a range
def norm_range(a, b, max_len):
a = int(a)
b = int(b)
if a > b:
a, b = b, a
if b < 0 or a >= max_len:
return None
a = max(0, a)
b = min(max_len - 1, b)
if a > b:
return None
return a, b
# For Pose, apply the same ranges on EVERY strip:
# each color band is repeated across all strips.
for strip in self.driver.strips:
strip_len = strip.num_leds
ranges = []
r1 = norm_range(getattr(preset, "n1", 0), getattr(preset, "n2", -1), strip_len)
if r1:
ranges.append((r1[0], r1[1], c1))
r2 = norm_range(getattr(preset, "n3", 0), getattr(preset, "n4", -1), strip_len)
if r2:
ranges.append((r2[0], r2[1], c2))
r3 = norm_range(getattr(preset, "n5", 0), getattr(preset, "n6", -1), strip_len)
if r3:
ranges.append((r3[0], r3[1], c3))
r4 = norm_range(getattr(preset, "n7", 0), getattr(preset, "n8", -1), strip_len)
if r4:
ranges.append((r4[0], r4[1], c4))
# Static draw on this strip: last range wins on overlaps
for i in range(strip_len):
color = (0, 0, 0)
for start, end, c in ranges:
if start <= i <= end:
color = c
strip.set(i, color)
# Flush all strips
for strip in self.driver.strips:
strip.show()
while True:
yield

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

View File

@@ -0,0 +1,97 @@
import utime
def _hue_to_rgb(hue):
"""Hue 0..360 -> (r, g, b). Simple HSV with S=V=1."""
h = hue % 360
x = 1 - abs((h / 60) % 2 - 1)
if h < 60:
r, g, b = 1, x, 0
elif h < 120:
r, g, b = x, 1, 0
elif h < 180:
r, g, b = 0, 1, x
elif h < 240:
r, g, b = 0, x, 1
elif h < 300:
r, g, b = x, 0, 1
else:
r, g, b = 1, 0, x
return (int(r * 255), int(g * 255), int(b * 255))
def _make_rainbow_double(num_leds, brightness=1.0):
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len_bytes).
DMA reads double_buf[head:head+strip_len] with no copy."""
n = 2 * num_leds
double_buf = bytearray(n * 3)
for i in range(n):
hue = (i / n) * 360 * 2
r, g, b = _hue_to_rgb(hue)
double_buf[i * 3] = int(g * brightness) & 0xFF
double_buf[i * 3 + 1] = int(r * brightness) & 0xFF
double_buf[i * 3 + 2] = int(b * brightness) & 0xFF
strip_len_bytes = num_leds * 3
return (double_buf, strip_len_bytes)
def _ensure_buffers(driver, preset, buffers_cache):
"""Build or refresh per-strip double buffers with current brightness. Returns (rainbow_data, cumulative_bytes)."""
effective = (preset.b * driver.b) / (255 * 255)
key = (preset.b, driver.b)
if buffers_cache.get("key") == key and buffers_cache.get("data"):
return buffers_cache["data"], buffers_cache["cumulative_bytes"]
strips = driver.strips
rainbow_data = [_make_rainbow_double(s.num_leds, effective) for s in strips]
cumulative_bytes = [0]
for s in strips:
cumulative_bytes.append(cumulative_bytes[-1] + s.num_leds * 3)
buffers_cache["key"] = key
buffers_cache["data"] = rainbow_data
buffers_cache["cumulative_bytes"] = cumulative_bytes
return rainbow_data, cumulative_bytes
class Rainbow:
def __init__(self, driver):
self.driver = driver
self._buffers_cache = {}
def run(self, preset):
step_amount = max(1, int(preset.n1)) # n1 = bytes to advance per frame (speed)
total_ring_bytes = self.driver.num_leds * 3
# Phase in bytes; driver.step kept in 0..255 for compatibility
phase = (self.driver.step * total_ring_bytes) // 256
rainbow_data, cumulative_bytes = _ensure_buffers(
self.driver, preset, self._buffers_cache
)
strips = self.driver.strips
def show_frame(phase):
for i, (strip, (double_buf, strip_len_bytes)) in enumerate(zip(strips, rainbow_data)):
head = (phase + cumulative_bytes[i]) % strip_len_bytes
strip.show(double_buf, head)
self.driver.step = (phase * 256) // total_ring_bytes
# Single step then stop
if not preset.a:
show_frame(phase)
phase = (phase + step_amount) % total_ring_bytes
self.driver.step = (phase * 256) // total_ring_bytes
yield
return
last_update = utime.ticks_ms()
sleep_ms = max(1, int(preset.d))
while True:
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
rainbow_data, cumulative_bytes = _ensure_buffers(
self.driver, preset, self._buffers_cache
)
show_frame(phase)
phase = (phase + step_amount) % total_ring_bytes
last_update = current_time
yield

111
pico/src/patterns/roll.py Normal file
View File

@@ -0,0 +1,111 @@
import utime
import math
class Roll:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""Roll: all strips show a shared color gradient palette, cycling out of phase.
- All strips participate; each frame shows N discrete colors
(one per strip), from color1 to color2.
- Over time, each strip cycles through all colors, out of phase with the
others, creating a smooth rolling band around the hoop.
- n4: direction (0 = clockwise, 1 = anti-clockwise)
- c[0]: head color (full intensity)
- c[1]: tail color (usually darker or off)
"""
colors = preset.c
base1 = colors[0] if colors else (255, 255, 255)
base2 = colors[1] if len(colors) > 1 else (0, 0, 0)
color1 = self.driver.apply_brightness(base1, preset.b)
color2 = self.driver.apply_brightness(base2, preset.b)
n_segments = self.driver.n.num_strips if hasattr(self.driver.n, "num_strips") else 1
# n3: number of full rotations before stopping (0 = continuous)
max_rotations = int(getattr(preset, "n3", 0) or 0)
# n4: direction (0=cw, 1=ccw); default clockwise if missing
clockwise = int(getattr(preset, "n4", 0)) == 0
step = self.driver.step
# Precompute one shared buffer per brightness level (one per strip),
# using the longest strip length so any strip can DMA from it safely.
strips_list = self.driver.strips
if not strips_list or n_segments <= 0:
while True:
yield
max_leds = max(s.num_leds for s in strips_list)
# Build N discrete color buffers forming a gradient from color1 to color2.
buffers = []
for j in range(n_segments):
if n_segments > 1:
t = j / (n_segments - 1)
else:
t = 0.0
# Linear interpolation between color1 and color2
r = int(color1[0] + (color2[0] - color1[0]) * t)
g = int(color1[1] + (color2[1] - color1[1]) * t)
b = int(color1[2] + (color2[2] - color1[2]) * t)
buf = bytearray(max_leds * 3)
for i in range(max_leds):
o = i * 3
buf[o] = g & 0xFF
buf[o + 1] = r & 0xFF
buf[o + 2] = b & 0xFF
buffers.append(buf)
def draw(step_index):
# Each strip picks a buffer index offset by its strip index so that:
# - all brightness levels are visible simultaneously (one per strip)
# - over time, each strip cycles through all brightness levels
try:
self.driver.last_roll_head = step_index % n_segments
except AttributeError:
pass
for strip_idx, strip in enumerate(strips_list):
if strip_idx < 0 or strip_idx >= n_segments:
continue
if clockwise:
buf_index = (step_index + strip_idx) % n_segments
else:
buf_index = (step_index - strip_idx) % n_segments
buf = buffers[buf_index]
# Show the shared buffer; WS2812B will read num_leds*3 bytes.
strip.show(buf, 0)
if not preset.a:
draw(step)
self.driver.step = step + 1
yield
return
# Auto mode: advance based on preset.d (ms) for smooth, controllable speed
delay_ms = max(10, int(getattr(preset, "d", 60)) or 10)
last_update = utime.ticks_ms() - delay_ms
rotations_done = 0
while True:
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= delay_ms:
draw(step)
step += 1
self.driver.step = step
last_update = now
# Count full rotations if requested: one rotation per n_segments steps
if max_rotations > 0 and n_segments > 0 and (step % n_segments) == 0:
rotations_done += 1
if rotations_done >= max_rotations:
# Hold the final frame and stop advancing; keep yielding so
# the generator stays alive without changing the LEDs.
while True:
yield
yield

View File

@@ -0,0 +1,56 @@
import utime
RED = (255, 0, 0)
class ScaleTest:
"""
Animated test for the scale() helper.
A single red pixel moves along the reference strip (strip 0). For each other
strip, the position is mapped using:
n2 = scale(l1, l2, n1)
so that all lit pixels stay aligned by proportional position along the strips.
"""
def __init__(self, driver):
self.driver = driver
def run(self, preset):
strips = self.driver.strips
if not strips:
return
src_strip_idx = 0
l1 = self.driver.strip_length(src_strip_idx)
if l1 <= 0:
return
step = self.driver.step
delay_ms = max(1, int(getattr(preset, "d", 30) or 30))
last_update = utime.ticks_ms()
color = self.driver.apply_brightness(RED, getattr(preset, "b", 255))
while True:
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= delay_ms:
n1 = step % l1
# Clear all strips
for strip in strips:
strip.fill((0, 0, 0))
# Light mapped position on each strip using Presets.set/show
for dst_strip_idx, _ in enumerate(strips):
self.driver.set(dst_strip_idx, n1, color)
self.driver.show(dst_strip_idx)
step += 1
self.driver.step = step
last_update = now
yield

View File

@@ -0,0 +1,18 @@
class Segments:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
# Apply preset/global brightness once per color
ranges = [
(preset.n1, preset.n2),
(preset.n3, preset.n4),
(preset.n5, preset.n6),
(preset.n7, preset.n8),
]
for n, color in enumerate(preset.c):
self.driver.fill_n(color, ranges[n][0], ranges[n][1])
self.driver.show_all()

View File

@@ -0,0 +1,136 @@
import utime
class SegmentsTransition:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""
SegmentsTransition: fade from whatever is currently on the strips
to a new static Segments layout defined by n1n8 and c[0..3].
- Uses the existing strip buffers as the starting state.
- Target state matches the Segments pattern: up to 4 colored bands
along the logical reference strip, mapped to all physical strips.
- Transition duration is taken from preset.d (ms), minimum 50ms.
"""
strips = self.driver.strips
if not strips:
while True:
yield
# Snapshot starting GRB buffers (already scaled by per-strip brightness)
start_bufs = [bytes(strip.ar) for strip in strips]
# Prepare target buffers (same length as each strip's ar)
target_bufs = [bytearray(len(strip.ar)) for strip in strips]
# Base colors (up to 4), missing ones default to black
colors = list(preset.c) if getattr(preset, "c", None) else []
while len(colors) < 4:
colors.append((0, 0, 0))
# Apply preset/global brightness once per color
bright_colors = [
self.driver.apply_brightness(colors[0], preset.b),
self.driver.apply_brightness(colors[1], preset.b),
self.driver.apply_brightness(colors[2], preset.b),
self.driver.apply_brightness(colors[3], preset.b),
]
# Logical reference length for all strips (from scale_map[0])
ref_len = len(self.driver.scale_map[0]) if self.driver.scale_map else 0
if ref_len <= 0:
# Fallback: nothing to do, just hold current state
while True:
yield
# Helper to clamp and normalize a logical range [a, b] (inclusive) over ref_len.
# Returns (start, end_exclusive) suitable for range(start, end_exclusive).
def norm_range(a, b):
a = int(a)
b = int(b)
if a > b:
a, b = b, a
if b < 0 or a >= ref_len:
return None
a = max(0, a)
b = min(ref_len - 1, b)
if a > b:
return None
return a, b + 1
raw_ranges = [
(getattr(preset, "n1", 0), getattr(preset, "n2", -1), bright_colors[0]),
(getattr(preset, "n3", 0), getattr(preset, "n4", -1), bright_colors[1]),
(getattr(preset, "n5", 0), getattr(preset, "n6", -1), bright_colors[2]),
(getattr(preset, "n7", 0), getattr(preset, "n8", -1), bright_colors[3]),
]
# Build target buffers using the same logical indexing idea as Segments
for strip_idx, strip in enumerate(strips):
bright = strip.brightness
scale_map = self.driver.scale_map[strip_idx]
buf = target_bufs[strip_idx]
n_leds = strip.num_leds
# Start from black everywhere
for i in range(len(buf)):
buf[i] = 0
# Apply each logical range to this strip
for a, b, color in raw_ranges:
rng = norm_range(a, b)
if not rng:
continue
start, end = rng
r, g, bl = color
for logical_idx in range(start, end):
if logical_idx < 0 or logical_idx >= len(scale_map):
continue
phys_idx = scale_map[logical_idx]
if phys_idx < 0 or phys_idx >= n_leds:
continue
base = phys_idx * 3
if base + 2 >= len(buf):
continue
buf[base] = int(g * bright)
buf[base + 1] = int(r * bright)
buf[base + 2] = int(bl * bright)
# Duration in ms for the whole transition (slower by default)
# If preset.d is provided, use it; otherwise default to a slow 3000ms fade.
raw_d = int(getattr(preset, "d", 3000) or 3000)
duration = max(1000, raw_d) # enforce at least 1s for a clearly visible transition
start_time = utime.ticks_ms()
while True:
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, start_time)
if elapsed >= duration:
# Final frame: commit target buffers and hold, then update all strips together
for strip, target in zip(strips, target_bufs):
ar = strip.ar
for i in range(len(ar)):
ar[i] = target[i]
self.driver.show_all()
while True:
yield
# Interpolation factor in [0,1]
factor = elapsed / duration
inv = 1.0 - factor
# Blend from start to target in GRB space per byte
for idx, strip in enumerate(strips):
start_buf = start_bufs[idx]
target_buf = target_bufs[idx]
ar = strip.ar
for i in range(len(ar)):
ar[i] = int(start_buf[i] * inv + target_buf[i] * factor)
self.driver.show_all()
yield

101
pico/src/patterns/spin.py Normal file
View File

@@ -0,0 +1,101 @@
"""Spin: continues from Grab — segment (10 each side of center) moves slowly up to the top. Preset color, n1 = rate."""
import utime
SPAN = 0 # LEDs on each side of center (match Grab)
LUT_SIZE = 256 # gradient lookup table entries
class Spin:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
strips = self.driver.strips
self.driver.fill((0, 0, 0))
self.driver.show_all()
active_indices = (0, 4)
c0 = preset.c[0]
c1 = preset.c[1]
# Precompute gradient LUT: t in [0,1] maps to (r,g,b)
lut = []
for k in range(LUT_SIZE):
t = k / (LUT_SIZE - 1) if LUT_SIZE > 1 else 1
r = int(c0[0] + (c1[0] - c0[0]) * t)
g = int(c0[1] + (c1[1] - c0[1]) * t)
b = int(c0[2] + (c1[2] - c0[2]) * t)
lut.append((r, g, b))
# For each active strip we expand from just outside the grab center
# left: from (mid - SPAN) down to 0
# right: from (mid + SPAN) up to end
midpoints = self.driver.strip_midpoints
rate = max(1, int(preset.n1) or 1)
delay_ms = max(1, int(preset.d) or 1)
margin = max(0, int(preset.n2) or 0)
# Track current extents of each arm
left = {}
right = {}
for idx in active_indices:
if 0 <= idx < len(strips):
mid = midpoints[idx]
left[idx] = mid - SPAN # inner edge of left arm
right[idx] = mid + SPAN + 1 # inner edge of right arm
last_update = utime.ticks_ms()
while True:
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) < delay_ms:
yield
continue
last_update = now
for idx in active_indices:
if idx < 0 or idx >= len(strips):
continue
strip = strips[idx]
n = strip.num_leds
mid = midpoints[idx]
# Expand arms at the same rate on both sides
step = max(1, rate)
new_left = max(margin, left[idx] - step)
new_right = min(n - margin, right[idx] + step)
# Left arm: c1 at outer, c0 at inner. Right arm: c0 at inner, c1 at outer.
left_len = max(0, (mid - SPAN) - new_left)
right_len = max(0, new_right - (mid + SPAN + 1))
bright = strip.brightness
ar = strip.ar
for j, i in enumerate(range(new_left, mid - SPAN)):
if 0 <= i < n:
t = 1 - j / (left_len - 1) if left_len > 1 else 0
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
r, g, b = lut[lut_idx]
base = i * 3
ar[base] = int(g * bright)
ar[base + 1] = int(r * bright)
ar[base + 2] = int(b * bright)
for j, i in enumerate(range(mid + SPAN + 1, new_right)):
if 0 <= i < n:
t = j / (right_len - 1) if right_len > 1 else 0
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
r, g, b = lut[lut_idx]
base = i * 3
ar[base] = int(g * bright)
ar[base + 1] = int(r * bright)
ar[base + 2] = int(b * bright)
left[idx] = new_left
right[idx] = new_right
# Show only on this strip
strip.show()
yield

28
pico/src/patterns/test.py Normal file
View File

@@ -0,0 +1,28 @@
"""Test pattern: strip i has (i+1) LEDs on at the start (indices 0..i) plus the midpoint LED on. 50% red."""
BRIGHTNESS = 0.50
RED = (255, 0, 0)
def _scale(color, factor):
return tuple(int(c * factor) for c in color)
class Test:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
strips = self.driver.strips
red = _scale(RED, BRIGHTNESS)
for strip_idx, strip in enumerate(strips):
n = strip.num_leds
mid = self.driver.strip_midpoints[strip_idx] # from STRIP_CONFIG
strip.fill((0, 0, 0))
# First (strip_idx + 1) LEDs on: indices 0..strip_idx
for i in range(min(strip_idx + 1, n)):
strip.set(i, red)
# Midpoint LED on
strip.set(mid, red)
strip.show()

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

83
pico/src/preset.py Normal file
View File

@@ -0,0 +1,83 @@
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
self.n7 = 0
self.n8 = 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,
"n7": self.n7,
"n8": self.n8,
}

245
pico/src/presets.py Normal file
View File

@@ -0,0 +1,245 @@
from machine import Pin
from ws2812 import WS2812B
from preset import Preset
from patterns import (
Blink,
Rainbow,
Pulse,
Transition,
Chase,
Circle,
DoubleCircle,
Roll,
Calibration,
Test,
Grab,
Spin,
Lift,
Flare,
Hook,
Pose,
Segments,
SegmentsTransition,
)
import json
class _LogicalRing:
"""
Lightweight logical ring over all strips.
Used by patterns that expect driver.n (e.g. Circle, Roll legacy API).
"""
def __init__(self, driver):
self._driver = driver
self.num_strips = len(driver.strips)
def __len__(self):
return self._driver.num_leds
def fill(self, color):
# Apply color to all logical positions across all strips
for i in range(self._driver.num_leds):
for strip_idx in range(self.num_strips):
self._driver.set(strip_idx, i, color)
def __setitem__(self, index, color):
if index < 0 or index >= self._driver.num_leds:
return
for strip_idx in range(self.num_strips):
self._driver.set(strip_idx, index, color)
def write(self):
self._driver.show_all()
# Order: strips[0]=physical 1 … strips[7]=physical 8. (pin, num_leds, midpoint_index).
STRIP_CONFIG = (
(6, 291, 291 // 2), # 1
(29, 290, 290 // 2-1), # 2
(3, 283, 283 // 2), # 3
(28, 278, 278 // 2-1), # 4
(2, 278, 275 // 2), # 5 (bottom of hoop)
(0, 283, 278 // 2-1), # 6
(4, 290, 283 // 2), # 7
(7, 291, 290 // 2-1), # 8
)
class Presets:
def __init__(self):
self.scale_map = []
self.strips = []
self.strip_midpoints = [] # midpoint LED index per strip (from STRIP_CONFIG)
state_machine = 0
for entry in STRIP_CONFIG:
pin, num_leds = entry[0], entry[1]
mid = entry[2] if len(entry) >= 3 else num_leds // 2
self.strip_midpoints.append(mid)
self.strips.append(WS2812B(num_leds, pin, state_machine, brightness=1.0))
state_machine += 1
self.scale_map.append(self.create_scale_map(num_leds))
# Single logical strip using strip 0 as reference for patterns (n[i], .fill(), .write())
# WS2812B with brightness=1.0 so Presets.apply_brightness() does all scaling (NeoPixel drop-in)
# Reference logical length for patterns that use driver.num_leds (Rainbow/Chase/Circle, etc.)
self.num_leds = self.strips[0].num_leds if self.strips else 0
# Legacy logical ring interface for patterns expecting driver.n
self.n = _LogicalRing(self)
self.step = 0
# Remember which strip was last used as the roll head (for flare, etc.)
self.last_roll_head = 0
# Global brightness (0255), controlled via UART/JSON {"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,
"double_circle": DoubleCircle(self).run,
"roll": Roll(self).run,
"calibration": Calibration(self).run,
"test": Test(self).run,
"grab": Grab(self).run,
"spin": Spin(self).run,
"lift": Lift(self).run,
"flare": Flare(self).run,
"hook": Hook(self).run,
"pose": Pose(self).run,
"segments": Segments(self).run,
"segments_transition": SegmentsTransition(self).run,
"point": Segments(self).run, # backwards-compatible alias
}
# --- Strip geometry utilities -------------------------------------------------
def strip_length(self, strip_idx):
"""Return number of LEDs for a physical strip index."""
if 0 <= strip_idx < len(self.strips):
return self.strips[strip_idx].num_leds
return 0
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 is None:
return False
print(f"Selecting preset: {preset_name}")
preset = None
pattern_key = preset_name
if preset_name in self.presets:
preset = self.presets[preset_name]
pattern_key = preset.p
if pattern_key not in self.patterns:
return False
# Run by pattern name (works for saved presets and built-ins like calibration, off, test)
if preset is None:
preset = Preset({"p": pattern_key}) if pattern_key != "off" else None
if step is not None:
self.step = step
elif pattern_key == "off" or self.selected != preset_name:
self.step = 0
self.generator = self.patterns[pattern_key](preset)
self.selected = preset_name
return True
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 off(self, preset=None):
self.fill((0, 0, 0))
self.show_all()
def on(self, preset):
colors = preset.c
color = colors[0] if colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.b))
self.show_all()
def fill(self, color):
for strip in self.strips:
strip.fill(color)
def fill_n(self, color, n1, n2):
for i in range(n1, n2):
for strip_idx in range(8):
self.set(strip_idx, i, color)
def set(self, strip, index, color):
if index >= self.strips[0].num_leds:
return False
self.strips[strip].set(self.scale_map[strip][index], color)
return True
def create_scale_map(self, num_leds):
ref_len = STRIP_CONFIG[0][1]
return [int(i * num_leds / ref_len) for i in range(ref_len)]
def show_all(self):
for strip in self.strips:
strip.show()

53
pico/src/utils.py Normal file
View File

@@ -0,0 +1,53 @@
def convert_and_reorder_colors(colors, settings_or_color_order):
"""Convert hex color strings to RGB tuples and reorder based on device color order.
Args:
colors: List of colors, either hex strings like "#FF0000" or RGB tuples like (255, 0, 0)
settings_or_color_order: Either a Settings object or a color_order string (e.g., "rgb", "grb")
Returns:
List of RGB tuples reordered according to device color order
"""
# Get channel order from settings or color_order string
if hasattr(settings_or_color_order, 'get_rgb_channel_order'):
# It's a Settings object
channel_order = settings_or_color_order.get_rgb_channel_order()
elif isinstance(settings_or_color_order, str):
# It's a color_order string, convert to channel order
color_order = settings_or_color_order.lower()
color_orders = {
"rgb": (1, 3, 5),
"rbg": (1, 5, 3),
"grb": (3, 1, 5),
"gbr": (3, 5, 1),
"brg": (5, 1, 3),
"bgr": (5, 3, 1)
}
hex_indices = color_orders.get(color_order, (1, 3, 5))
# Map hex string positions to RGB channel indices
hex_to_channel = {1: 0, 3: 1, 5: 2}
channel_order = tuple(hex_to_channel[pos] for pos in hex_indices)
else:
# Assume it's already a channel order tuple
channel_order = 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)
# 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)
return converted_colors

79
pico/test/chase.py Normal file
View File

@@ -0,0 +1,79 @@
import sys
if "lib" not in sys.path:
sys.path.insert(0, "lib")
if "../lib" not in sys.path:
sys.path.insert(0, "../lib")
from ws2812 import WS2812B
import time
# --- Chase test: pregenerated double buffer per strip, show via head offset (same as rainbow) ---
# (pin, num_leds) per strip — same config as rainbow
STRIP_CONFIG = (
(2, 291),
(3, 290),
(4, 283),
(7, 278),
(0, 275),
(28, 278),
(29, 283),
(6, 290),
)
strips = []
sm = 0
for pin, num_leds in STRIP_CONFIG:
print(pin, num_leds)
ws = WS2812B(num_leds, pin, sm, brightness=1.0)
strips.append(ws)
sm += 1
cumulative_leds = [0]
for ws in strips[:-1]:
cumulative_leds.append(cumulative_leds[-1] + ws.num_leds)
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
# Chase: color1 n1 long, then color2 n2 long, stepping n3 pixels
COLOR1 = (255, 0, 0) # red
COLOR2 = (0, 0, 255) # blue
N1 = 24 # length of color1 segment
N2 = 24 # length of color2 segment
STEP = 1 # step size in pixels per frame
def make_chase_double(num_leds, cumulative_leds, total_ring_leds, color1, color2, n1, n2):
"""Pregenerate strip double buffer with repeating segments:
color1 for n1 pixels, then color2 for n2 pixels, around the full ring. GRB order."""
n = 2 * num_leds
buf = bytearray(n * 3)
pattern_len = n1 + n2
for b in range(n):
# Position of this pixel along the logical ring
pos = (2 * cumulative_leds - b) % total_ring_leds
seg_pos = pos % pattern_len
if seg_pos < n1:
r, grn, b_ = color1[0], color1[1], color1[2]
else:
r, grn, b_ = color2[0], color2[1], color2[2]
o = b * 3
buf[o] = grn
buf[o + 1] = r
buf[o + 2] = b_
return buf
# Pregenerate one double buffer per strip
chase_buffers = [
make_chase_double(ws.num_leds, cumulative_leds[i], total_ring_leds, COLOR1, COLOR2, N1, N2)
for i, ws in enumerate(strips)
]
chase_pos = 0
while True:
for i, strip in enumerate(strips):
# head in [0, strip_len) so DMA read head..head+num_leds*3 stays in double buffer (same as rainbow)
strip_len = strip.num_leds * 3
head = (chase_pos + cumulative_leds[i]) * 3 % strip_len
strip.show(chase_buffers[i], head)
chase_pos = (chase_pos + STEP) % total_ring_leds
time.sleep_ms(40)

View File

@@ -24,7 +24,7 @@ for pin, num_leds in pins:
ws = WS2812B(num_leds, pin, sm, brightness=1.0) # 1.0 so fill() is visible ws = WS2812B(num_leds, pin, sm, brightness=1.0) # 1.0 so fill() is visible
strips.append(ws) strips.append(ws)
sm += 1 sm += 1
ws.fill((255,0,0)) ws.fill((8,0,0))
ws.show() ws.show()
time.sleep(1) time.sleep(1)

View File

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

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
# Create blink preset (use short-key fields: p=pattern, b=brightness, d=delay, c=colors)
p.edit("test_blink", {
"p": "blink",
"b": 64,
"d": 200,
"c": [(255, 0, 0), (0, 0, 255)],
})
p.select("test_blink")
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 1500:
wdt.feed()
p.tick()
utime.sleep_ms(10)
if __name__ == "__main__":
main()

161
pico/test/patterns/chase.py Normal file
View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def run_for(p, wdt, ms):
"""Helper: run current pattern for given ms using tick()."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
# 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": "chase",
"b": 255,
"d": 200,
"n1": 5,
"n2": 5,
"n3": 1,
"n4": 1,
"c": [(255, 0, 0), (0, 255, 0)],
})
p.select("chase1")
run_for(p, wdt, 3000)
# Test 2: Forward and backward (n3=2, n4=-1)
print("Test 2: Forward and backward (n3=2, n4=-1)")
p.edit("chase2", {
"p": "chase",
"n1": 3,
"n2": 3,
"n3": 2,
"n4": -1,
"d": 150,
"c": [(0, 0, 255), (255, 255, 0)],
})
p.select("chase2")
run_for(p, wdt, 3000)
# Test 3: Large segments (n1=10, n2=5)
print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)")
p.edit("chase3", {
"p": "chase",
"n1": 10,
"n2": 5,
"n3": 3,
"n4": 3,
"d": 200,
"c": [(255, 128, 0), (128, 0, 255)],
})
p.select("chase3")
run_for(p, wdt, 3000)
# Test 4: Fast movement (n3=5, n4=5)
print("Test 4: Fast movement (n3=5, n4=5)")
p.edit("chase4", {
"p": "chase",
"n1": 4,
"n2": 4,
"n3": 5,
"n4": 5,
"d": 100,
"c": [(255, 0, 255), (0, 255, 255)],
})
p.select("chase4")
run_for(p, wdt, 2000)
# Test 5: Backward movement (n3=-2, n4=-2)
print("Test 5: Backward movement (n3=-2, n4=-2)")
p.edit("chase5", {
"p": "chase",
"n1": 6,
"n2": 4,
"n3": -2,
"n4": -2,
"d": 200,
"c": [(255, 255, 255), (0, 0, 0)],
})
p.select("chase5")
run_for(p, wdt, 3000)
# Test 6: Alternating forward/backward (n3=3, n4=-2)
print("Test 6: Alternating forward/backward (n3=3, n4=-2)")
p.edit("chase6", {
"p": "chase",
"n1": 5,
"n2": 5,
"n3": 3,
"n4": -2,
"d": 250,
"c": [(255, 0, 0), (0, 255, 0)],
})
p.select("chase6")
run_for(p, wdt, 4000)
# Test 7: Manual mode - advance one step per beat
print("Test 7: Manual mode chase (auto=False, n3=2, n4=1)")
p.edit("chase_manual", {
"p": "chase",
"n1": 4,
"n2": 4,
"n3": 2,
"n4": 1,
"d": 200,
"c": [(255, 255, 0), (0, 255, 255)],
"a": False,
})
p.step = 0 # Reset step counter
print(" Advancing pattern with 10 beats (select + tick)...")
for i in range(10):
p.select("chase_manual") # Simulate beat - restarts generator
p.tick() # Advance one step
utime.sleep_ms(500) # Pause to see the pattern
wdt.feed()
print(f" Beat {i+1}: step={p.step}")
# Test 8: Verify step increments correctly in manual mode
print("Test 8: Verify step increments (auto=False)")
p.edit("chase_manual2", {
"p": "chase",
"n1": 3,
"n2": 3,
"n3": 1,
"n4": 1,
"a": False,
})
p.step = 0
initial_step = p.step
p.select("chase_manual2")
p.tick()
final_step = p.step
print(f" Step updated from {initial_step} to {final_step} (expected: 1)")
if final_step == 1:
print(" ✓ Step increment working correctly")
else:
print(f" ✗ Step increment mismatch! Expected 1, got {final_step}")
# Cleanup
print("Test complete, turning off")
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def run_for(p, wdt, ms):
"""Helper: run current pattern for given ms using tick()."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
# Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)
print("Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)")
p.edit("circle1", {
"p": "circle",
"b": 255,
"n1": 50, # Head moves 50 LEDs/second
"n2": 100, # Max length 100 LEDs
"n3": 200, # Tail moves 200 LEDs/second
"n4": 0, # Min length 0 LEDs
"c": [(255, 0, 0)], # Red
})
p.select("circle1")
run_for(p, wdt, 5000)
# Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)
print("Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)")
p.edit("circle2", {
"p": "circle",
"n1": 20,
"n2": 50,
"n3": 100,
"n4": 0,
"c": [(0, 255, 0)], # Green
})
p.select("circle2")
run_for(p, wdt, 5000)
# Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)
print("Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)")
p.edit("circle3", {
"p": "circle",
"n1": 100,
"n2": 30,
"n3": 20,
"n4": 0,
"c": [(0, 0, 255)], # Blue
})
p.select("circle3")
run_for(p, wdt, 5000)
# Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)
print("Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)")
p.edit("circle4", {
"p": "circle",
"n1": 50,
"n2": 40,
"n3": 100,
"n4": 10,
"c": [(255, 255, 0)], # Yellow
})
p.select("circle4")
run_for(p, wdt, 5000)
# Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)
print("Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)")
p.edit("circle5", {
"p": "circle",
"n1": 200,
"n2": 20,
"n3": 200,
"n4": 0,
"c": [(255, 0, 255)], # Magenta
})
p.select("circle5")
run_for(p, wdt, 3000)
# Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)
print("Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)")
p.edit("circle6", {
"p": "circle",
"n1": 10,
"n2": 25,
"n3": 10,
"n4": 0,
"c": [(0, 255, 255)], # Cyan
})
p.select("circle6")
run_for(p, wdt, 5000)
# Cleanup
print("Test complete, turning off")
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

30
pico/test/patterns/off.py Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
# Create an "off" preset (use short-key field `p` for pattern)
p.edit("test_off", {"p": "off"})
p.select("test_off")
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 200:
wdt.feed()
p.tick()
utime.sleep_ms(10)
if __name__ == "__main__":
main()

47
pico/test/patterns/on.py Normal file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
# 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": "on",
"b": 64,
"d": 120,
"c": [(255, 0, 0), (0, 0, 255)],
})
p.edit("test_off", {"p": "off"})
# ON phase
p.select("test_on")
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 800:
wdt.feed()
p.tick()
utime.sleep_ms(10)
# OFF phase
p.select("test_off")
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 100:
wdt.feed()
p.tick()
utime.sleep_ms(10)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def run_for(p, wdt, ms):
"""Helper: run current pattern for given ms using tick()."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
# Test 1: Simple single-color pulse
print("Test 1: Single-color pulse (attack=500, hold=500, decay=500, delay=500)")
p.edit("pulse1", {
"p": "pulse",
"b": 255,
"c": [(255, 0, 0)],
"n1": 500, # attack ms
"n2": 500, # hold ms
"n3": 500, # decay ms
"d": 500, # delay ms between pulses
"a": True,
})
p.select("pulse1")
run_for(p, wdt, 5000)
# Test 2: Faster pulse
print("Test 2: Fast pulse (attack=100, hold=100, decay=100, delay=100)")
p.edit("pulse2", {
"p": "pulse",
"n1": 100,
"n2": 100,
"n3": 100,
"d": 100,
"c": [(0, 255, 0)],
})
p.select("pulse2")
run_for(p, wdt, 4000)
# Test 3: Multi-color pulse cycle
print("Test 3: Multi-color pulse (red -> green -> blue)")
p.edit("pulse3", {
"p": "pulse",
"n1": 300,
"n2": 300,
"n3": 300,
"d": 200,
"c": [(255, 0, 0), (0, 255, 0), (0, 0, 255)],
"a": True,
})
p.select("pulse3")
run_for(p, wdt, 6000)
# Test 4: One-shot pulse (auto=False)
print("Test 4: Single pulse, auto=False")
p.edit("pulse4", {
"p": "pulse",
"n1": 400,
"n2": 0,
"n3": 400,
"d": 0,
"c": [(255, 255, 255)],
"a": False,
})
p.select("pulse4")
# Run long enough to allow one full pulse cycle
run_for(p, wdt, 1500)
# Cleanup
print("Test complete, turning off")
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 200)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def run_for(p, wdt, ms):
"""Helper: run current pattern for given ms using tick()."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
# Test 1: Basic rainbow with auto=True (continuous)
print("Test 1: Basic rainbow (auto=True, n1=1)")
p.edit("rainbow1", {
"p": "rainbow",
"b": 255,
"d": 100,
"n1": 1,
"a": True,
})
p.select("rainbow1")
run_for(p, wdt, 3000)
# Test 2: Fast rainbow
print("Test 2: Fast rainbow (low delay, n1=1)")
p.edit("rainbow2", {
"p": "rainbow",
"d": 50,
"n1": 1,
"a": True,
})
p.select("rainbow2")
run_for(p, wdt, 2000)
# Test 3: Slow rainbow
print("Test 3: Slow rainbow (high delay, n1=1)")
p.edit("rainbow3", {
"p": "rainbow",
"d": 500,
"n1": 1,
"a": True,
})
p.select("rainbow3")
run_for(p, wdt, 3000)
# Test 4: Low brightness rainbow
print("Test 4: Low brightness rainbow (n1=1)")
p.edit("rainbow4", {
"p": "rainbow",
"b": 64,
"d": 100,
"n1": 1,
"a": True,
})
p.select("rainbow4")
run_for(p, wdt, 2000)
# Test 5: Single-step rainbow (auto=False)
print("Test 5: Single-step rainbow (auto=False, n1=1)")
p.edit("rainbow5", {
"p": "rainbow",
"b": 255,
"d": 100,
"n1": 1,
"a": False,
})
p.step = 0
for i in range(10):
p.select("rainbow5")
# One tick advances the generator one frame when auto=False
p.tick()
utime.sleep_ms(100)
wdt.feed()
# Test 6: Verify step updates correctly
print("Test 6: Verify step updates (auto=False, n1=1)")
p.edit("rainbow6", {
"p": "rainbow",
"n1": 1,
"a": False,
})
initial_step = p.step
p.select("rainbow6")
p.tick()
final_step = p.step
print(f"Step updated from {initial_step} to {final_step} (expected increment: 1)")
# Test 7: Fast step increment (n1=5)
print("Test 7: Fast rainbow (n1=5, auto=True)")
p.edit("rainbow7", {
"p": "rainbow",
"b": 255,
"d": 100,
"n1": 5,
"a": True,
})
p.select("rainbow7")
run_for(p, wdt, 2000)
# Test 8: Very fast step increment (n1=10)
print("Test 8: Very fast rainbow (n1=10, auto=True)")
p.edit("rainbow8", {
"p": "rainbow",
"n1": 10,
"a": True,
})
p.select("rainbow8")
run_for(p, wdt, 2000)
# Test 9: Verify n1 controls step increment (auto=False)
print("Test 9: Verify n1 step increment (auto=False, n1=5)")
p.edit("rainbow9", {
"p": "rainbow",
"n1": 5,
"a": False,
})
p.step = 0
initial_step = p.step
p.select("rainbow9")
p.tick()
final_step = p.step
expected_step = (initial_step + 5) % 256
print(f"Step updated from {initial_step} to {final_step} (expected: {expected_step})")
if final_step == expected_step:
print("✓ n1 step increment working correctly")
else:
print(f"✗ Step increment mismatch! Expected {expected_step}, got {final_step}")
# Cleanup
print("Test complete, turning off")
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def run_for(p, wdt, ms):
"""Helper: run current pattern for given ms using tick()."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
pin = s.get("led_pin", 10)
num = s.get("num_leds", 30)
p = Presets(pin=pin, num_leds=num)
wdt = WDT(timeout=10000)
# Test 1: Simple two-color transition
print("Test 1: Two-color transition (red <-> blue, delay=1000)")
p.edit("transition1", {
"p": "transition",
"b": 255,
"d": 1000, # transition duration
"c": [(255, 0, 0), (0, 0, 255)],
"a": True,
})
p.select("transition1")
run_for(p, wdt, 6000)
# Test 2: Multi-color transition
print("Test 2: Multi-color transition (red -> green -> blue -> white)")
p.edit("transition2", {
"p": "transition",
"d": 800,
"c": [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)],
"a": True,
})
p.select("transition2")
run_for(p, wdt, 8000)
# Test 3: One-shot transition (auto=False)
print("Test 3: One-shot transition (auto=False)")
p.edit("transition3", {
"p": "transition",
"d": 1000,
"c": [(255, 0, 0), (0, 255, 0)],
"a": False,
})
p.select("transition3")
# Run long enough for a single transition step
run_for(p, wdt, 2000)
# Test 4: Single-color behavior (should just stay on)
print("Test 4: Single-color transition (should hold color)")
p.edit("transition4", {
"p": "transition",
"c": [(0, 0, 255)],
"d": 500,
"a": True,
})
p.select("transition4")
run_for(p, wdt, 3000)
# Cleanup
print("Test complete, turning off")
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 200)
if __name__ == "__main__":
main()

99
pico/test/rainbow.py Normal file
View File

@@ -0,0 +1,99 @@
import sys
# So "from ws2812 import WS2812B" finds pico/lib when run from device / or test/
if "lib" not in sys.path:
sys.path.insert(0, "lib")
if "../lib" not in sys.path:
sys.path.insert(0, "../lib")
from ws2812 import WS2812B
import time
# --- Rainbow pattern (outside ws2812): pregen double buffer, show via head offset ---
def hue_to_rgb(hue):
"""Hue 0..360 -> (r, g, b). Simple HSV with S=V=1."""
h = hue % 360
x = 1 - abs((h / 60) % 2 - 1)
if h < 60:
r, g, b = 1, x, 0
elif h < 120:
r, g, b = x, 1, 0
elif h < 180:
r, g, b = 0, 1, x
elif h < 240:
r, g, b = 0, x, 1
elif h < 300:
r, g, b = x, 0, 1
else:
r, g, b = 1, 0, x
return (int(r * 255), int(g * 255), int(b * 255))
def make_rainbow_double(num_leds, brightness=1.0):
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len).
head must be in 0..strip_len-1 so DMA reads double_buf[head:head+strip_len] with no copy."""
n = 2 * num_leds
double_buf = bytearray(n * 3)
for i in range(n):
hue = (i / n) * 360 * 2
r, g, b = hue_to_rgb(hue)
g = int(g * brightness) & 0xFF
r = int(r * brightness) & 0xFF
b = int(b * brightness) & 0xFF
o = i * 3
double_buf[o] = g
double_buf[o + 1] = r
double_buf[o + 2] = b
strip_len = num_leds * 3
return (double_buf, strip_len)
def show_rainbow(strip, double_buf, strip_len, head):
"""DMA reads directly from double_buf at head; no copy. head in 0..strip_len-1."""
strip.show(double_buf, head)
# --- Strips + rainbow buffers per strip ---
# Each strip can have a different length; buffers and phase are per-strip.
# Strip config must match pico/src/main.py pins.
STRIP_CONFIG = (
(7, 291),
(3, 290),
(6, 283),
(28, 278),
(29, 275),
(4, 278),
(0, 283),
(2, 290),
)
strips = []
sm = 0
for pin, num_leds in STRIP_CONFIG:
print(pin, num_leds)
ws = WS2812B(num_leds, pin, sm, brightness=1.0) # 1.0 so fill() is visible
strips.append(ws)
sm += 1
# Cumulative LED count before each strip; total ring size
cumulative_leds = [0]
for ws in strips[:-1]:
cumulative_leds.append(cumulative_leds[-1] + ws.num_leds)
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
bytes_per_cycle = total_ring_leds * 3
# One rainbow double buffer per strip (length = 2 * num_leds for that strip)
now = time.ticks_ms()
rainbow_data = [make_rainbow_double(ws.num_leds, ws.brightness) for ws in strips]
# Global phase in bytes; each strip: head = (phase + cumulative_leds[i]*3) % strip_len[i]
print(time.ticks_diff(time.ticks_ms(), now), "ms")
rainbow_head = 0
step = 3
while True:
now = time.ticks_ms()
for i, (strip, (double_buf, strip_len)) in enumerate(zip(strips, rainbow_data)):
head = (rainbow_head + cumulative_leds[i] * 3) % strip_len
show_rainbow(strip, double_buf, strip_len, head)
rainbow_head = (rainbow_head + step) % bytes_per_cycle
#print(time.ticks_diff(time.ticks_ms(), now), "ms")
time.sleep_ms(10)

114
pico/test/roll.py Normal file
View File

@@ -0,0 +1,114 @@
"""
On-device visual test for the Roll pattern via Presets.
This exercises src/patterns/roll.py (gradient from color1 to color2 across strips),
not the low-level WS2812 driver.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/roll.py :
mpremote connect <device> run roll.py
"""
import utime
from presets import Presets, Preset
def make_roll_preset(name, color1, color2, delay_ms=60, brightness=255, direction=0):
"""
Helper to build a Preset for the 'roll' pattern.
- color1: head color (full intensity)
- color2: tail color (end of gradient)
- direction: 0 = clockwise, 1 = anti-clockwise
"""
data = {
"p": "roll",
"c": [color1, color2],
"b": brightness,
"d": delay_ms,
"n4": direction,
"a": True, # animated
}
return name, Preset(data)
def run_for(presets, duration_ms):
"""Tick the current pattern for duration_ms."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
presets.tick()
utime.sleep_ms(10)
def main():
presets = Presets()
presets.load()
num_leds = presets.strip_length(0)
if num_leds <= 0:
print("No strips; aborting roll test.")
return
print("Starting roll pattern gradient tests via Presets...")
# A few different roll presets to compare:
roll_presets = []
# 1. White → off, clockwise (50% brightness, faster)
roll_presets.append(
make_roll_preset(
"roll_white_off_cw",
color1=(255, 255, 255),
color2=(0, 0, 0),
delay_ms=120,
brightness=128,
direction=0,
)
)
# 2. Warm white → cool blue, clockwise (50% brightness, faster)
roll_presets.append(
make_roll_preset(
"roll_warm_cool_cw",
color1=(255, 200, 100),
color2=(0, 0, 255),
delay_ms=130,
brightness=128,
direction=0,
)
)
# 3. Red → green, counter-clockwise (50% brightness, faster)
roll_presets.append(
make_roll_preset(
"roll_red_green_ccw",
color1=(255, 0, 0),
color2=(0, 255, 0),
delay_ms=110,
brightness=128,
direction=1,
)
)
# Register presets and run them one after another
for name, preset_obj in roll_presets:
presets.presets[name] = preset_obj
for name, _preset in roll_presets:
print("Running roll preset:", name)
presets.select(name)
run_for(presets, duration_ms=8000)
print("Roll pattern Presets test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

6
pico/test/test_all_on.py Normal file
View File

@@ -0,0 +1,6 @@
from neopixel import NeoPixel
from machine import Pin
p = NeoPixel(Pin(6), 291)
p.fill((255, 255, 255))
p.write()

View File

@@ -0,0 +1,71 @@
"""
On-device test that turns all LEDs on via Presets and verifies strip 0 (pin 6).
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_all_on_presets.py :
mpremote connect <device> run test_all_on_presets.py
"""
from presets import Presets, Preset
def verify_strip0_on(presets, expected_color):
"""Check that every LED on strip 0 matches expected_color."""
if not presets.strips:
print("No strips; skipping strip-0 on test.")
return
strip = presets.strips[0]
r_exp, g_exp, b_exp = expected_color
for i in range(strip.num_leds):
o = i * 3
g = strip.ar[o]
r = strip.ar[o + 1]
b = strip.ar[o + 2]
if (r, g, b) != (r_exp, g_exp, b_exp):
raise AssertionError(
"Strip 0 LED %d: got (%d,%d,%d), expected (%d,%d,%d)"
% (i, r, g, b, r_exp, g_exp, b_exp)
)
def main():
presets = Presets()
if not presets.strips:
print("No strips; skipping all-on-presets test.")
return
# Full-brightness white via the built-in 'on' pattern.
base_color = (255, 255, 255)
brightness = 255
data = {
"p": "on",
"c": [base_color],
"b": brightness,
}
name = "test_all_on_presets"
preset = Preset(data)
presets.presets[name] = preset
presets.select(name)
presets.tick()
# Compute the color actually written by the pattern after brightness scaling.
expected_color = presets.apply_brightness(base_color, brightness)
verify_strip0_on(presets, expected_color)
print("test_all_on_presets: OK")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,184 @@
"""
On-device test that exercises the Chase pattern via Presets.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_chase_via_presets.py :
mpremote connect <device> run test_chase_via_presets.py
"""
import utime
from presets import Presets, Preset
def snapshot_strip_colors(presets, strip_idx=0, max_leds=32):
"""Return a list of (r,g,b) tuples for the first max_leds of the given strip."""
strip = presets.strips[strip_idx]
num = min(strip.num_leds, max_leds)
out = []
for i in range(num):
o = i * 3
g = strip.ar[o]
r = strip.ar[o + 1]
b = strip.ar[o + 2]
out.append((r, g, b))
return out
def expected_chase_color(i, num_leds, step_count, color0, color1, n1, n2, n3, n4):
"""Mirror the position logic from patterns/chase.py for a single logical LED."""
segment_length = n1 + n2
if step_count % 2 == 0:
position = (step_count // 2) * (n3 + n4) + n3
else:
position = ((step_count + 1) // 2) * (n3 + n4)
max_pos = num_leds + segment_length
position = position % max_pos
if position < 0:
position += max_pos
relative_pos = (i - position) % segment_length
if relative_pos < 0:
relative_pos = (relative_pos + segment_length) % segment_length
return color0 if relative_pos < n1 else color1
def test_chase_single_step_via_presets():
presets = Presets()
num_leds = presets.num_leds
if num_leds <= 0:
print("No strips; skipping chase test.")
return
# Simple alternating colors with known lengths.
base_color0 = (10, 0, 0)
base_color1 = (0, 0, 20)
# Use full brightness so apply_brightness is identity.
brightness = 255
n1 = 2
n2 = 3
# Same step size on even/odd for easier reasoning.
n3 = 1
n4 = 1
data = {
"p": "chase",
"c": [base_color0, base_color1],
"b": brightness,
"d": 0,
"a": False, # single-step mode
"n1": n1,
"n2": n2,
"n3": n3,
"n4": n4,
}
name = "test_chase_pattern"
preset = Preset(data)
presets.presets[name] = preset
# Select and run one tick; this should render exactly one chase frame for step 0.
presets.select(name, step=0)
presets.tick()
# Colors after brightness scaling (driver.apply_brightness is used in the pattern).
color0 = presets.apply_brightness(base_color0, brightness)
color1 = presets.apply_brightness(base_color1, brightness)
# Snapshot first few LEDs of strip 0 and compare against expected pattern for step 0.
colors = snapshot_strip_colors(presets, strip_idx=0, max_leds=16)
step_count = 0
for i, actual in enumerate(colors):
expected = expected_chase_color(
i, num_leds, step_count, color0, color1, n1, n2, n3, n4
)
assert (
actual == expected
), "LED %d: got %r, expected %r" % (i, actual, expected)
print("test_chase_single_step_via_presets: OK")
def test_chase_multiple_steps_via_presets():
"""Render several steps and verify pattern advances correctly."""
presets = Presets()
num_leds = presets.num_leds
if num_leds <= 0:
print("No strips; skipping chase multi-step test.")
return
base_color0 = (10, 0, 0)
base_color1 = (0, 0, 20)
brightness = 255
n1 = 2
n2 = 3
n3 = 1
n4 = 1
data = {
"p": "chase",
"c": [base_color0, base_color1],
"b": brightness,
"d": 0,
"a": False,
"n1": n1,
"n2": n2,
"n3": n3,
"n4": n4,
}
name = "test_chase_pattern_multi"
preset = Preset(data)
presets.presets[name] = preset
color0 = presets.apply_brightness(base_color0, brightness)
color1 = presets.apply_brightness(base_color1, brightness)
# In non-auto mode (a=False), the Chase pattern advances one step per
# invocation of the generator, and Presets is expected to call select()
# again for each beat. Emulate that here by re-selecting with an
# explicit step value for each frame we want to test.
for step_count in range(4):
presets.select(name, step=step_count)
presets.tick()
colors = snapshot_strip_colors(presets, strip_idx=0, max_leds=16)
for i, actual in enumerate(colors):
expected = expected_chase_color(
i, num_leds, step_count, color0, color1, n1, n2, n3, n4
)
assert (
actual == expected
), "step %d, LED %d: got %r, expected %r" % (
step_count,
i,
actual,
expected,
)
print("test_chase_multiple_steps_via_presets: OK")
def main():
test_chase_single_step_via_presets()
test_chase_multiple_steps_via_presets()
# Give a brief pause so message is visible if run interactively.
utime.sleep_ms(100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,157 @@
"""
On-device test for the double_circle pattern using mpremote.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_double_circle.py :
mpremote connect <device> run test_double_circle.py
This script:
- Instantiates Presets
- Creates a few in-memory 'double_circle' presets with different centers, widths, and colors
- Selects each one so you can visually confirm the symmetric bands and color gradients
"""
from presets import Presets, Preset
def make_double_circle_preset(
name, center, half_width, colors, direction=0, step_size=1, brightness=255
):
"""
Helper to build a Preset for the 'double_circle' pattern.
center: logical index (0-based, on reference strip 0)
half_width: number of LEDs each side of center
colors: [color1, color2] where each color is (r,g,b)
"""
cs = list(colors)[:2]
while len(cs) < 2:
cs.append((0, 0, 0))
data = {
"p": "double_circle",
"c": cs,
"b": brightness,
"n1": center,
"n2": half_width,
"n3": direction,
"n4": step_size,
}
return name, Preset(data)
def show_and_wait(presets, name, preset_obj, wait_ms):
"""Select a static double_circle preset and hold it for wait_ms."""
presets.presets[name] = preset_obj
presets.select(name)
# DoubleCircle draws immediately in run(), then just yields; one tick is enough.
presets.tick()
import utime
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < wait_ms:
presets.tick()
def main():
presets = Presets()
presets.load()
num_leds = presets.strip_length(0)
if num_leds <= 0:
print("No strips; aborting double_circle test.")
return
print("Starting double_circle pattern test...")
quarter = num_leds // 4
half = num_leds // 2
dc_presets = []
# 1. Center at top (0), moderate width, color1 at center (n3=0)
dc_presets.append(
make_double_circle_preset(
"dc_top_red_to_blue",
center=0,
half_width=quarter,
colors=[(255, 0, 0), (0, 0, 255)],
direction=0,
)
)
# 2. Center at bottom (half), narrow band, color1 at endpoints (n3=1)
dc_presets.append(
make_double_circle_preset(
"dc_bottom_green_to_purple",
center=half,
half_width=quarter // 2,
colors=[(0, 255, 0), (128, 0, 128)],
direction=1,
)
)
# 3. Center at quarter, wide band, both directions for comparison
dc_presets.append(
make_double_circle_preset(
"dc_quarter_white_to_cyan_inward",
center=quarter,
half_width=half,
colors=[(255, 255, 255), (0, 255, 255)],
direction=0,
)
)
dc_presets.append(
make_double_circle_preset(
"dc_quarter_white_to_cyan_outward",
center=quarter,
half_width=half,
colors=[(255, 255, 255), (0, 255, 255)],
direction=1,
)
)
# 4. Explicit test: n1 = 50, n2 = 40 (half of 80) inward
dc_presets.append(
make_double_circle_preset(
"dc_n1_50_n2_40_inward",
center=50,
half_width=40,
colors=[(255, 100, 0), (0, 0, 0)],
direction=0,
)
)
# 5. Explicit test: n1 = num_leds//2, n2 = num_leds//4 outward, stepping as fast as possible
center_half = num_leds // 2
radius_quarter = max(1, num_leds // 4)
dc_presets.append(
make_double_circle_preset(
"dc_n1_half_n2_quarter_outward",
center=center_half,
half_width=radius_quarter,
colors=[(0, 150, 255), (0, 0, 0)],
direction=1,
step_size=radius_quarter, # jump to full radius in one step
)
)
# Show each for ~4 seconds
for name, preset_obj in dc_presets:
print("Showing double_circle preset:", name)
show_and_wait(presets, name, preset_obj, wait_ms=4000)
print("Double_circle pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,694 @@
#!/usr/bin/env python3
"""Test ESPNow receive functionality - runs on MicroPython device."""
import json
import os
import utime
from settings import Settings
from presets import Presets
from utils import convert_and_reorder_colors
class MockESPNow:
"""Mock ESPNow for testing that can send messages."""
def __init__(self):
self.messages = []
self.active_state = False
def active(self, state):
self.active_state = state
def any(self):
"""Return True if there are messages."""
return len(self.messages) > 0
def recv(self):
"""Receive a message (removes it from queue)."""
if self.messages:
return self.messages.pop(0)
return None, None
def send_message(self, host, msg_data):
"""Send a message by adding it to the queue (testing helper)."""
if isinstance(msg_data, dict):
msg = json.dumps(msg_data)
else:
msg = msg_data
self.messages.append((host, msg))
def clear(self):
"""Clear all messages (testing helper)."""
self.messages = []
from machine import WDT
def get_wdt():
"""Get a real WDT instance for tests."""
return WDT(timeout=10000) # 10 second timeout for tests
def run_main_loop_iterations(espnow, patterns, settings, wdt, max_iterations=10):
"""Run main loop iterations until no messages or max reached."""
iterations = 0
results = []
while iterations < max_iterations:
wdt.feed()
patterns.tick()
if espnow.any():
host, msg = espnow.recv()
data = json.loads(msg)
if data.get("v") != "1":
results.append(("version_rejected", data))
continue
if "presets" in data:
for name, preset_data in data["presets"].items():
# Convert hex color strings to RGB tuples and reorder based on device color order
if "colors" in preset_data:
preset_data["colors"] = convert_and_reorder_colors(preset_data["colors"], settings)
patterns.edit(name, preset_data)
results.append(("presets_processed", list(data["presets"].keys())))
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
if patterns.select(preset_name, step=step):
results.append(("selected", preset_name))
iterations += 1
# Stop if no more messages
if not espnow.any():
break
return results
def test_version_check():
"""Test that messages with wrong version are rejected."""
print("Test 1: Version check")
settings = Settings()
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
# Send message with wrong version
mock_espnow.send_message(b"\xaa\xaa\xaa\xaa\xaa\xaa", {"v": "2", "presets": {"test": {"pattern": "on"}}})
results = run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert len([r for r in results if r[0] == "version_rejected"]) > 0, "Should reject wrong version"
assert "test" not in patterns.presets, "Preset should not be created"
print(" ✓ Version check passed")
# Send message with correct version
mock_espnow.clear()
mock_espnow.send_message(b"\xaa\xaa\xaa\xaa\xaa\xaa", {"v": "1", "presets": {"test": {"pattern": "on"}}})
results = run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert len([r for r in results if r[0] == "presets_processed"]) > 0, "Should process correct version"
assert "test" in patterns.presets, "Preset should be created"
print(" ✓ Correct version accepted")
def test_preset_creation():
"""Test preset creation from ESPNow messages."""
print("\nTest 2: Preset creation")
settings = Settings()
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
msg = {
"v": "1",
"presets": {
"test_blink": {
"pattern": "blink",
"colors": ["#FF0000", "#00FF00"],
"delay": 200,
"brightness": 128
},
"test_rainbow": {
"pattern": "rainbow",
"delay": 100,
"n1": 2
}
}
}
mock_espnow.send_message(b"\xbb\xbb\xbb\xbb\xbb\xbb", msg)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert "test_blink" in patterns.presets, "test_blink preset should exist"
assert "test_rainbow" in patterns.presets, "test_rainbow preset should exist"
# Check preset values
blink_preset = patterns.presets["test_blink"]
assert blink_preset.pattern == "blink", "Pattern should be blink"
assert blink_preset.delay == 200, "Delay should be 200"
assert blink_preset.brightness == 128, "Brightness should be 128"
rainbow_preset = patterns.presets["test_rainbow"]
assert rainbow_preset.pattern == "rainbow", "Pattern should be rainbow"
assert rainbow_preset.n1 == 2, "n1 should be 2"
print(" ✓ Presets created correctly")
def test_color_conversion():
"""Test hex color string conversion and reordering."""
print("\nTest 3: Color conversion")
settings = Settings()
settings["color_order"] = "rgb" # Default RGB order
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
msg = {
"v": "1",
"presets": {
"test_colors": {
"pattern": "on",
"colors": ["#FF0000", "#00FF00", "#0000FF"] # Red, Green, Blue
}
}
}
mock_espnow.send_message(b"\xcc\xcc\xcc\xcc\xcc\xcc", msg)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
preset = patterns.presets["test_colors"]
assert len(preset.colors) == 3, "Should have 3 colors"
assert preset.colors[0] == (255, 0, 0), "First color should be red (255,0,0)"
assert preset.colors[1] == (0, 255, 0), "Second color should be green (0,255,0)"
assert preset.colors[2] == (0, 0, 255), "Third color should be blue (0,0,255)"
print(" ✓ Colors converted correctly (RGB order)")
# Test GRB order
settings["color_order"] = "grb"
patterns2 = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow2 = MockESPNow()
msg2 = {
"v": "1",
"presets": {
"test_grb": {
"pattern": "on",
"colors": ["#FF0000"] # Red in RGB, should become (0, 255, 0) in GRB
}
}
}
mock_espnow2.send_message(b"\xdd\xdd\xdd\xdd\xdd\xdd", msg2)
wdt2 = get_wdt()
run_main_loop_iterations(mock_espnow2, patterns2, settings, wdt2)
preset2 = patterns2.presets["test_grb"]
assert preset2.colors[0] == (0, 255, 0), "GRB: Red should become green (0,255,0)"
print(" ✓ Colors reordered correctly (GRB order)")
def test_preset_update():
"""Test that editing an existing preset updates it."""
print("\nTest 4: Preset update")
settings = Settings()
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
# Create initial preset
msg1 = {
"v": "1",
"presets": {
"test_update": {
"pattern": "blink",
"delay": 100,
"brightness": 64
}
}
}
mock_espnow.send_message(b"\xee\xee\xee\xee\xee\xee", msg1)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.presets["test_update"].delay == 100, "Initial delay should be 100"
# Update preset
mock_espnow.clear()
msg2 = {
"v": "1",
"presets": {
"test_update": {
"pattern": "blink",
"delay": 200,
"brightness": 128
}
}
}
mock_espnow.send_message(b"\xff\xff\xff\xff\xff\xff", msg2)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.presets["test_update"].delay == 200, "Updated delay should be 200"
assert patterns.presets["test_update"].brightness == 128, "Updated brightness should be 128"
print(" ✓ Preset updated correctly")
def test_select():
"""Test preset selection."""
print("\nTest 5: Preset selection")
settings = Settings()
settings["name"] = "device1"
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
# Create presets
msg1 = {
"v": "1",
"presets": {
"preset1": {"pattern": "on", "colors": [(255, 0, 0)]},
"preset2": {"pattern": "rainbow", "delay": 50}
}
}
mock_espnow.send_message(b"\x11\x11\x11\x11\x11\x11", msg1)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
# Select preset
mock_espnow.clear()
msg2 = {
"v": "1",
"select": {
"device1": ["preset1"],
"device2": ["preset2"]
}
}
mock_espnow.send_message(b"\x22\x22\x22\x22\x22\x22", msg2)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.selected == "preset1", "Should select preset1"
print(" ✓ Preset selected correctly")
def test_full_message():
"""Test a full message with presets and select."""
print("\nTest 6: Full message (presets + select)")
settings = Settings()
settings["name"] = "test_device"
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
msg = {
"v": "1",
"presets": {
"my_preset": {
"pattern": "pulse",
"colors": ["#FF0000", "#00FF00"],
"delay": 150,
"n1": 500,
"n2": 200,
"n3": 500
}
},
"select": {
"test_device": ["my_preset"],
"other_device": ["other_preset"]
}
}
mock_espnow.send_message(b"\x44\x44\x44\x44\x44\x44", msg)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert "my_preset" in patterns.presets, "Preset should be created"
assert patterns.selected == "my_preset", "Preset should be selected"
preset = patterns.presets["my_preset"]
assert preset.pattern == "pulse", "Pattern should be pulse"
assert preset.delay == 150, "Delay should be 150"
assert preset.n1 == 500, "n1 should be 500"
print(" ✓ Full message processed correctly")
def test_switch_presets():
"""Test switching between different presets."""
print("\nTest 7: Switch between presets")
settings = Settings()
settings["name"] = "switch_device"
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
# Create multiple presets
msg1 = {
"v": "1",
"presets": {
"preset_blink": {"pattern": "blink", "delay": 200, "colors": [(255, 0, 0)]},
"preset_rainbow": {"pattern": "rainbow", "delay": 100, "n1": 2},
"preset_pulse": {"pattern": "pulse", "delay": 150, "n1": 500, "n2": 200, "n3": 500}
}
}
mock_espnow.send_message(b"\x55\x55\x55\x55\x55\x55", msg1)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
# Select and run first preset for 2 seconds
mock_espnow.clear()
msg2 = {
"v": "1",
"select": {
"switch_device": ["preset_blink"]
}
}
mock_espnow.send_message(b"\x66\x66\x66\x66\x66\x66", msg2)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.selected == "preset_blink", "Should select preset_blink"
print(" ✓ Selected preset_blink, running for 2 seconds...")
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
patterns.tick()
utime.sleep_ms(10)
# Switch to second preset and run for 2 seconds
mock_espnow.clear()
msg3 = {
"v": "1",
"select": {
"switch_device": ["preset_rainbow"]
}
}
mock_espnow.send_message(b"\x77\x77\x77\x77\x77\x77", msg3)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.selected == "preset_rainbow", "Should switch to preset_rainbow"
print(" ✓ Switched to preset_rainbow, running for 2 seconds...")
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
patterns.tick()
utime.sleep_ms(10)
# Switch to third preset and run for 2 seconds
mock_espnow.clear()
msg4 = {
"v": "1",
"select": {
"switch_device": ["preset_pulse"]
}
}
mock_espnow.send_message(b"\x88\x88\x88\x88\x88\x88", msg4)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.selected == "preset_pulse", "Should switch to preset_pulse"
print(" ✓ Switched to preset_pulse, running for 2 seconds...")
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
patterns.tick()
utime.sleep_ms(10)
# Switch back to first preset and run for 2 seconds
mock_espnow.clear()
msg5 = {
"v": "1",
"select": {
"switch_device": ["preset_blink"]
}
}
mock_espnow.send_message(b"\x99\x99\x99\x99\x99\x99", msg5)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.selected == "preset_blink", "Should switch back to preset_blink"
print(" ✓ Switched back to preset_blink, running for 2 seconds...")
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
wdt.feed()
patterns.tick()
utime.sleep_ms(10)
print(" ✓ Preset switching works correctly")
def test_beat_functionality():
"""Test beat functionality - calling select() again with same preset restarts pattern."""
print("\nTest 8: Beat functionality")
settings = Settings()
settings["name"] = "beat_device"
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
# Create presets with manual mode
msg1 = {
"v": "1",
"presets": {
"beat_rainbow": {"pattern": "rainbow", "delay": 100, "n1": 1, "auto": False},
"beat_chase": {"pattern": "chase", "delay": 200, "n1": 4, "n2": 4, "n3": 2, "n4": 1, "auto": False},
"beat_pulse": {"pattern": "pulse", "delay": 150, "n1": 300, "n2": 100, "n3": 300, "auto": False}
}
}
mock_espnow.send_message(b"\xaa\xaa\xaa\xaa\xaa\xaa", msg1)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
# Test 1: Beat with rainbow (manual mode) - should advance one step per beat
print(" Test 8.1: Beat with rainbow (manual mode)")
patterns.step = 0
mock_espnow.clear()
msg2 = {
"v": "1",
"select": {
"beat_device": ["beat_rainbow"]
}
}
mock_espnow.send_message(b"\xbb\xbb\xbb\xbb\xbb\xbb", msg2)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.selected == "beat_rainbow", "Should select beat_rainbow"
initial_step = patterns.step
# First beat - advance one step
mock_espnow.clear()
mock_espnow.send_message(b"\xcc\xcc\xcc\xcc\xcc\xcc", msg2) # Same select message = beat
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
# tick() is already called in run_main_loop_iterations, so step should be incremented
assert patterns.step == (initial_step + 1) % 256, f"Step should increment from {initial_step} to {(initial_step + 1) % 256}, got {patterns.step}"
# Second beat - advance another step
mock_espnow.clear()
mock_espnow.send_message(b"\xdd\xdd\xdd\xdd\xdd\xdd", msg2) # Beat again
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
assert patterns.step == (initial_step + 2) % 256, f"Step should increment to {(initial_step + 2) % 256}, got {patterns.step}"
print(" ✓ Rainbow beat advances one step per beat")
# Test 2: Beat with chase (manual mode) - should advance one step per beat
print(" Test 8.2: Beat with chase (manual mode)")
patterns.step = 0
mock_espnow.clear()
msg3 = {
"v": "1",
"select": {
"beat_device": ["beat_chase"]
}
}
mock_espnow.send_message(b"\xee\xee\xee\xee\xee\xee", msg3)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.selected == "beat_chase", "Should select beat_chase"
initial_step = patterns.step
# First beat
mock_espnow.clear()
mock_espnow.send_message(b"\xff\xff\xff\xff\xff\xff", msg3) # Beat
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
# tick() is already called in run_main_loop_iterations
assert patterns.step == initial_step + 1, f"Chase step should increment from {initial_step} to {initial_step + 1}, got {patterns.step}"
# Second beat
mock_espnow.clear()
mock_espnow.send_message(b"\x11\x11\x11\x11\x11\x11", msg3) # Beat again
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
assert patterns.step == initial_step + 2, f"Chase step should increment to {initial_step + 2}, got {patterns.step}"
print(" ✓ Chase beat advances one step per beat")
# Test 3: Beat with pulse (manual mode) - should restart full cycle
print(" Test 8.3: Beat with pulse (manual mode)")
mock_espnow.clear()
msg4 = {
"v": "1",
"select": {
"beat_device": ["beat_pulse"]
}
}
mock_espnow.send_message(b"\x22\x22\x22\x22\x22\x22", msg4)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.selected == "beat_pulse", "Should select beat_pulse"
assert patterns.generator is not None, "Generator should be active"
# First beat - should restart generator
initial_generator = patterns.generator
mock_espnow.clear()
mock_espnow.send_message(b"\x33\x33\x33\x33\x33\x33", msg4) # Beat
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
assert patterns.generator is not None, "Generator should still be active after beat"
assert patterns.generator != initial_generator, "Generator should be restarted (new instance)"
print(" ✓ Pulse beat restarts generator for full cycle")
# Test 4: Multiple beats in sequence
print(" Test 8.4: Multiple beats in sequence")
patterns.step = 0
mock_espnow.clear()
mock_espnow.send_message(b"\x44\x44\x44\x44\x44\x44", msg2) # Select rainbow
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
# Send 5 beats
for i in range(5):
mock_espnow.clear()
mock_espnow.send_message(b"\x55\x55\x55\x55\x55\x55", msg2) # Beat
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
# tick() is already called in run_main_loop_iterations
wdt.feed()
utime.sleep_ms(50)
assert patterns.step == 5, f"After 5 beats, step should be 5, got {patterns.step}"
print(" ✓ Multiple beats work correctly")
print(" ✓ Beat functionality works correctly")
def test_select_with_step():
"""Test selecting a preset with an explicit step value."""
print("\nTest 9: Select with step value")
settings = Settings()
settings["name"] = "step_device"
patterns = Presets(settings["led_pin"], settings["num_leds"])
mock_espnow = MockESPNow()
wdt = get_wdt()
# Create preset
msg1 = {
"v": "1",
"presets": {
"step_preset": {"pattern": "rainbow", "delay": 100, "n1": 1, "auto": False}
}
}
mock_espnow.send_message(b"\xaa\xaa\xaa\xaa\xaa\xaa", msg1)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
# Select with explicit step value
mock_espnow.clear()
msg2 = {
"v": "1",
"select": {
"step_device": ["step_preset", 10]
}
}
mock_espnow.send_message(b"\xbb\xbb\xbb\xbb\xbb\xbb", msg2)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=2)
# Ensure tick() is called after select() to advance the step
patterns.tick()
assert patterns.selected == "step_preset", "Should select step_preset"
# Step is set to 10, then tick() advances it, so it should be 11
assert patterns.step == 11, f"Step should be set to 10 then advanced to 11 by tick(), got {patterns.step}"
print(" ✓ Step value set correctly")
# Select without step (should use default behavior)
mock_espnow.clear()
msg3 = {
"v": "1",
"select": {
"step_device": ["step_preset"]
}
}
mock_espnow.send_message(b"\xcc\xcc\xcc\xcc\xcc\xcc", msg3)
initial_step = patterns.step # Should be 11
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=2)
# Ensure tick() is called after select() to advance the step
patterns.tick()
# Since it's the same preset, step should not be reset, but tick() will advance it
# So step should be initial_step + 1 (one tick call)
assert patterns.step == initial_step + 1, f"Step should advance from {initial_step} to {initial_step + 1} (not reset), got {patterns.step}"
print(" ✓ Step preserved when selecting same preset without step (tick advances it)")
# Select different preset with step
patterns.edit("other_preset", {"p": "rainbow", "a": False})
mock_espnow.clear()
msg4 = {
"v": "1",
"select": {
"step_device": ["other_preset", 5]
}
}
mock_espnow.send_message(b"\xdd\xdd\xdd\xdd\xdd\xdd", msg4)
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=2)
# Ensure tick() is called after select() to advance the step
patterns.tick()
assert patterns.selected == "other_preset", "Should select other_preset"
# Step is set to 5, then tick() advances it, so it should be 6
assert patterns.step == 6, f"Step should be set to 5 then advanced to 6 by tick(), got {patterns.step}"
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():
"""Run all tests."""
print("=" * 60)
print("ESPNow Receive Functionality Tests")
print("=" * 60)
try:
test_version_check()
test_preset_creation()
test_color_conversion()
test_preset_update()
test_select()
test_full_message()
test_switch_presets()
test_beat_functionality()
test_select_with_step()
test_preset_save_load()
print("\n" + "=" * 60)
print("All tests passed! ✓")
print("=" * 60)
except AssertionError as e:
print("\n✗ Test failed:", e)
raise
except Exception as e:
print("\n✗ Unexpected error:", e)
raise
if __name__ == "__main__":
main()

52
pico/test/test_fill_n.py Normal file
View File

@@ -0,0 +1,52 @@
"""
On-device test for Presets.fill_n() using mpremote.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_fill_n.py :
mpremote connect <device> run test_fill_n.py
This script:
- Instantiates Presets
- Calls fill_n() with a simple range
- Lets you visually confirm that all strips show the same proportional segment
and that equal-length strip pairs have identical lit indices.
"""
from presets import Presets
def main():
presets = Presets()
presets.load()
# Choose a simple test range on the reference strip (strip 0).
ref_len = presets.strip_length(0)
if ref_len <= 0:
print("No strips or invalid length; aborting fill_n test.")
return
# Use a central segment so it's easy to see.
start = ref_len // 4
end = 3 * ref_len // 4
print("Running fill_n test from", start, "to", end, "on reference strip 0.")
color = (0, 50, 0) # dim green
# First, clear everything
for strip in presets.strips:
strip.fill((0, 0, 0))
strip.show()
# Apply fill_n, which will use scale() internally.
presets.fill_n(color, start, end)
print("fill_n test applied; visually inspect strips.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,264 @@
"""
On-device test that exercises Segments and multiple SegmentsTransition presets via Presets.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_multi_patterns.py :
mpremote connect <device> run test_multi_patterns.py
"""
import utime
from presets import Presets, Preset
def run_for(presets, duration_ms):
"""Tick the current pattern for duration_ms."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
presets.tick()
utime.sleep_ms(10)
def make_segments_preset(name, colors, n_values, brightness=255):
"""
Helper to build a Preset for the 'segments' pattern.
colors: list of up to 4 (r,g,b) tuples
n_values: list/tuple of 8 ints [n1..n8]
"""
cs = list(colors)[:4]
while len(cs) < 4:
cs.append((0, 0, 0))
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
data = {
"p": "segments",
"c": cs,
"b": brightness,
"n1": n1,
"n2": n2,
"n3": n3,
"n4": n4,
"n5": n5,
"n6": n6,
"n7": n7,
"n8": n8,
}
return name, Preset(data)
def make_segments_transition_preset(name, colors, n_values, duration_ms=1000, brightness=255):
"""
Helper to build a Preset for the 'segments_transition' pattern.
Starts from whatever is currently displayed and fades to the
new segments layout over duration_ms.
"""
cs = list(colors)[:4]
while len(cs) < 4:
cs.append((0, 0, 0))
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
data = {
"p": "segments_transition",
"c": cs,
"b": brightness,
"d": duration_ms,
"n1": n1,
"n2": n2,
"n3": n3,
"n4": n4,
"n5": n5,
"n6": n6,
"n7": n7,
"n8": n8,
}
return name, Preset(data)
def main():
presets = Presets()
presets.load()
num_leds = presets.strip_length(0)
if num_leds <= 0:
print("No strips; aborting multi-pattern test.")
return
print("Starting multi-pattern test with Presets...")
quarter = num_leds // 4
half = num_leds // 2
# 1. Static segments: simple R/G/B bands
name_static, preset_static = make_segments_preset(
"mp_segments_static",
colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)],
n_values=[
0,
quarter - 1, # red
quarter,
2 * quarter - 1, # green
2 * quarter,
3 * quarter - 1, # blue
0,
-1,
],
)
# 2a. Segments transition: fade from previous buffer to new colors (slow)
name_trans1, preset_trans1 = make_segments_transition_preset(
"mp_segments_transition_1",
colors=[(255, 255, 255), (255, 0, 255), (0, 255, 255)],
n_values=[
0,
half - 1, # white on first half
half,
num_leds - 1, # magenta on second half
0,
-1, # cyan unused in this example
0,
-1,
],
duration_ms=3000,
)
# 2b. Segments transition: fade between two different segment layouts
name_trans2, preset_trans2 = make_segments_transition_preset(
"mp_segments_transition_2",
colors=[(255, 0, 0), (0, 0, 255)],
n_values=[
0,
quarter - 1, # red first quarter
quarter,
2 * quarter - 1, # blue second quarter
0,
-1,
0,
-1,
],
duration_ms=4000,
)
# 2c. Segments transition: thin moving band (center quarter only)
band_start = quarter // 2
band_end = band_start + quarter
name_trans3, preset_trans3 = make_segments_transition_preset(
"mp_segments_transition_3",
colors=[(0, 255, 0)],
n_values=[
band_start,
band_end - 1, # green band in the middle
0,
-1,
0,
-1,
0,
-1,
],
duration_ms=5000,
)
# 2d. Segments transition: full-ring warm white fade
name_trans4, preset_trans4 = make_segments_transition_preset(
"mp_segments_transition_4",
colors=[(255, 200, 100)],
n_values=[
0,
num_leds - 1, # entire strip
0,
-1,
0,
-1,
0,
-1,
],
duration_ms=6000,
)
# 2e. Segments transition: alternating warm/cool halves
name_trans5, preset_trans5 = make_segments_transition_preset(
"mp_segments_transition_5",
colors=[(255, 180, 100), (100, 180, 255)],
n_values=[
0,
half - 1, # warm first half
half,
num_leds - 1, # cool second half
0,
-1,
0,
-1,
],
duration_ms=5000,
)
# 2f. Segments transition: narrow red band near start
narrow_start = num_leds // 16
narrow_end = narrow_start + max(4, num_leds // 32)
name_trans6, preset_trans6 = make_segments_transition_preset(
"mp_segments_transition_6",
colors=[(255, 0, 0)],
n_values=[
narrow_start,
narrow_end - 1,
0,
-1,
0,
-1,
0,
-1,
],
duration_ms=4000,
)
# Register presets in Presets and run them in sequence
presets.presets[name_static] = preset_static
presets.presets[name_trans1] = preset_trans1
presets.presets[name_trans2] = preset_trans2
presets.presets[name_trans3] = preset_trans3
presets.presets[name_trans4] = preset_trans4
presets.presets[name_trans5] = preset_trans5
presets.presets[name_trans6] = preset_trans6
print("Showing static segments...")
presets.select(name_static)
presets.tick() # draw once
run_for(presets, 3000)
print("Running segments transition 1 (fading to new half/half layout)...")
presets.select(name_trans1)
run_for(presets, 3500)
print("Running segments transition 2 (fading to quarter-band layout)...")
presets.select(name_trans2)
run_for(presets, 4500)
print("Running segments transition 3 (fading to center green band)...")
presets.select(name_trans3)
run_for(presets, 5500)
print("Running segments transition 4 (fading to full warm white ring)...")
presets.select(name_trans4)
run_for(presets, 6500)
print("Running segments transition 5 (fading to warm/cool halves)...")
presets.select(name_trans5)
run_for(presets, 5500)
print("Running segments transition 6 (fading to narrow red band)...")
presets.select(name_trans6)
run_for(presets, 4500)
print("Multi-pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

100
pico/test/test_scale.py Normal file
View File

@@ -0,0 +1,100 @@
"""
Test the Presets.scale() helper on-device with mpremote.
Usage (from project root):
mpremote connect <device> cp pico/src/*.py : &&
mpremote connect <device> cp pico/src/patterns/*.py :patterns &&
mpremote connect <device> cp pico/lib/*.py : &&
mpremote connect <device> cp tests/test_scale.py : &&
mpremote connect <device> run test_scale.py
This script:
- Creates a minimal Presets instance
- Runs a few numeric test cases for scale()
- Optionally displays a short visual check on the LEDs
"""
from presets import Presets
def numeric_tests(presets):
"""
Numeric sanity checks for scale() using the actual strip config.
We treat strip 0 as the reference and print the mapped indices for
a few positions on each other strip.
"""
print("Numeric scale() tests (from strip 0):")
ref_len = presets.strip_length(0)
if ref_len <= 0:
print(" strip 0 length <= 0; skipping numeric tests.")
return
test_positions = [0, ref_len // 2, ref_len - 1]
for pos in test_positions:
print(" pos on strip 0:", pos)
for dst_idx in range(len(presets.strips)):
dst_len = presets.strip_length(dst_idx)
if dst_len <= 0:
continue
n2 = presets.scale(dst_idx, pos)
print(" -> strip", dst_idx, "len", dst_len, "pos", n2)
def visual_test(presets):
"""
Simple visual test:
- Use strip 0 as reference
- Move a pixel along strip 0
- Map position to all other strips with scale()
"""
import utime
strips = presets.strips
if not strips:
print("No strips available for visual test.")
return
src_strip_idx = 0
l1 = presets.strip_length(src_strip_idx)
if l1 <= 0:
print("strip_length(0) <= 0; aborting visual test.")
return
color = (50, 0, 0) # dim red so it doesn't blind you
# Run once across the full length of the reference strip,
# jumping 10 LEDs at a time.
step_size = 10
steps = (l1 + step_size - 1) // step_size
print("Starting visual scale() test with 10-LED jumps:", steps, "steps...")
for step in range(steps):
n1 = (step * step_size) % l1
# Clear all strips
for strip in strips:
strip.fill((0, 0, 0))
# Light mapped position on each strip using Presets.set/show
for dst_strip_idx, _ in enumerate(strips):
presets.set(dst_strip_idx, n1, color)
presets.show(dst_strip_idx)
print("Visual test finished.")
def main():
presets = Presets()
presets.load()
numeric_tests(presets)
# Comment this in/out depending on whether you want the LEDs to run:
try:
visual_test(presets)
except Exception as e:
print("Visual test error:", e)
if __name__ == "__main__":
main()

130
pico/test/test_segments.py Normal file
View File

@@ -0,0 +1,130 @@
"""
On-device test for the Segments pattern using mpremote.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_segments.py :
mpremote connect <device> run test_segments.py
This script:
- Instantiates Presets
- Creates a few in-memory 'point' (Segments) presets with different ranges/colors
- Selects each one so you can visually confirm the segments
"""
from presets import Presets, Preset
def make_segments_preset(name, colors, n_values, brightness=255):
"""
Helper to build a Preset for the 'segments' pattern (key 'point').
colors: list of up to 4 (r,g,b) tuples
n_values: list/tuple of 8 ints [n1..n8]
"""
# Pad or trim colors to 4 entries
cs = list(colors)[:4]
while len(cs) < 4:
cs.append((0, 0, 0))
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
data = {
"p": "segments", # pattern key for Segments
"c": cs,
"b": brightness,
"n1": n1,
"n2": n2,
"n3": n3,
"n4": n4,
"n5": n5,
"n6": n6,
"n7": n7,
"n8": n8,
# 'a' is not used by segments; it's static
}
return name, Preset(data)
def show_and_wait(presets, name, preset_obj, wait_ms):
"""Select a static segments preset and hold it for wait_ms."""
presets.presets[name] = preset_obj
presets.select(name)
# Segments draws immediately in run(), then just yields; one tick is enough.
presets.tick()
import utime
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < wait_ms:
# Keep ticking in case other logic ever depends on it
presets.tick()
def main():
presets = Presets()
presets.load()
num_leds = presets.strip_length(0)
if num_leds <= 0:
print("No strips; aborting segments test.")
return
print("Starting segments pattern test...")
quarter = num_leds // 4
half = num_leds // 2
segments_presets = []
# 1. Single band: first quarter, red
segments_presets.append(
make_segments_preset(
"segments_red_q1",
colors=[(255, 0, 0)],
n_values=[0, quarter - 1, 0, -1, 0, -1, 0, -1],
)
)
# 2. Two bands: red first half, green second half
segments_presets.append(
make_segments_preset(
"segments_red_green_halves",
colors=[(255, 0, 0), (0, 255, 0)],
n_values=[0, half - 1, half, num_leds - 1, 0, -1, 0, -1],
)
)
# 3. Three bands: R, G, B quarters
segments_presets.append(
make_segments_preset(
"segments_rgb_quarters",
colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)],
n_values=[
0,
quarter - 1, # red
quarter,
2 * quarter - 1, # green
2 * quarter,
3 * quarter - 1, # blue
0,
-1,
],
)
)
# Show each for ~4 seconds
for name, preset_obj in segments_presets:
print("Showing segments preset:", name)
show_and_wait(presets, name, preset_obj, wait_ms=4000)
print("Segments pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

45
pico/test/test_serial.py Normal file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
Serial loopback test single file, runs on Pico and ESP32.
Wire TX to RX (Pico: GP0GP1, ESP32: 1718), then:
mpremote run pico/test/test_serial.py
For ESP32→Pico: run test_serial_send.py on ESP32, test_serial_receive.py on Pico; wire ESP32 TX (17) to Pico RX (1).
"""
import time
import sys
from machine import UART, Pin
if "esp32" in sys.platform:
UART_ID, TX_PIN, RX_PIN, BAUD = 1, 17, 18, 115200
else:
UART_ID, TX_PIN, RX_PIN, BAUD = 0, 0, 1, 115200
READ_TIMEOUT_MS = 100
LINE_TERM = b"\n"
print("UART loopback: %s UART%d TX=%s RX=%s %d baud" % (sys.platform, UART_ID, TX_PIN, RX_PIN, BAUD))
uart = UART(UART_ID, baudrate=BAUD, tx=Pin(TX_PIN, Pin.OUT), rx=Pin(RX_PIN, Pin.IN))
uart.read()
to_send = [b"hello", b"123", b"{\"v\":\"1\"}"]
errors = []
for msg in to_send:
uart.write(msg + LINE_TERM)
time.sleep_ms(20)
buf = bytearray()
deadline = time.ticks_add(time.ticks_ms(), READ_TIMEOUT_MS)
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
n = uart.any()
if n:
buf.extend(uart.read(n))
if LINE_TERM in buf:
break
time.sleep_ms(2)
got = bytes(buf).strip()
if got != msg:
errors.append((msg, got))
uart.deinit()
if errors:
print("FAIL loopback:", errors)
else:
print("PASS loopback: sent and received", to_send)

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""
Serial receive test single file. Run on Pico (RX side).
Wire: ESP32 TX (GPIO17) → Pico RX (GPIO1); GND ↔ GND. Run send test on ESP32.
mpremote run pico/test/test_serial_receive.py
"""
import time
import sys
from machine import UART, Pin
if "esp32" in sys.platform:
UART_ID, TX_PIN, RX_PIN, BAUD = 1, 17, 18, 115200
else:
UART_ID, TX_PIN, RX_PIN, BAUD = 0, 0, 1, 115200
print("UART receive: %s UART%d TX=%s RX=%s %d baud (10 s)" % (sys.platform, UART_ID, TX_PIN, RX_PIN, BAUD))
uart = UART(UART_ID, baudrate=BAUD, tx=Pin(TX_PIN, Pin.OUT), rx=Pin(RX_PIN, Pin.IN))
buf = bytearray()
deadline = time.ticks_add(time.ticks_ms(), 10000)
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
n = uart.any()
if n:
buf.extend(uart.read(n))
while b"\n" in buf:
idx = buf.index(b"\n")
line = bytes(buf[:idx]).strip()
buf = buf[idx + 1:]
if line:
print("rx:", line.decode("utf-8", "replace"))
time.sleep_ms(10)
uart.deinit()
print("Receive test done.")

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""
Serial send test single file. Run on ESP32 (TX side).
Wire: ESP32 TX (GPIO17) → Pico RX (GPIO1); GND ↔ GND. Run receive test on Pico.
mpremote run pico/test/test_serial_send.py
"""
import time
import sys
from machine import UART, Pin
if "esp32" in sys.platform:
UART_ID, TX_PIN, BAUD = 1, 17, 115200
else:
UART_ID, TX_PIN, BAUD = 0, 0, 115200
print("UART send: %s UART%d TX=%s %d baud" % (sys.platform, UART_ID, TX_PIN, BAUD))
uart = UART(UART_ID, baudrate=BAUD, tx=Pin(TX_PIN, Pin.OUT))
for line in [b"serial send test 1", b"serial send test 2", b"{\"v\":\"1\",\"b\":128}"]:
uart.write(line + b"\n")
print("sent:", line.decode("utf-8"))
time.sleep_ms(50)
uart.deinit()
print("Send test done.")

128
pico/test/test_spin.py Normal file
View File

@@ -0,0 +1,128 @@
"""
On-device test for the Spin pattern using mpremote and Presets.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp presets.json :
mpremote connect <device> cp test/test_spin.py :
mpremote connect <device> run test_spin.py
This script:
- Instantiates Presets
- Creates a few in-memory spin presets with different parameters
- Runs each one for a short time so you can visually compare behaviour
"""
import utime
from presets import Presets, Preset
def make_spin_preset(
name,
color_inner,
color_outer,
rate=4,
delay_ms=30,
margin=0,
brightness=255,
):
"""Helper to build a Preset dict for the spin pattern."""
data = {
"p": "spin",
"c": [color_inner, color_outer],
"b": brightness,
"d": delay_ms,
"n1": rate, # expansion step per tick
"n2": margin, # margin from strip ends
"a": True,
}
return name, Preset(data)
def run_preset(presets, name, preset_obj, duration_ms):
"""Run a given spin preset for duration_ms using the existing tick loop."""
# Start each preset from a blank frame so both sides are balanced.
presets.select("off")
presets.tick()
presets.presets[name] = preset_obj
presets.select(name)
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
presets.tick()
utime.sleep_ms(10)
def main():
presets = Presets()
presets.load()
# Ensure we start from a blank frame.
presets.select("off")
presets.tick()
print("Starting spin pattern test...")
# Use strip 0 length to derive a reasonable margin.
ref_len = presets.strip_length(0)
margin_small = ref_len // 16 if ref_len > 0 else 0
margin_large = ref_len // 8 if ref_len > 0 else 0
spin_presets = []
# 1. Slow spin, warm white to orange, small margin
spin_presets.append(
make_spin_preset(
"spin_slow_warm",
color_inner=(255, 200, 120),
color_outer=(255, 100, 0),
rate=2,
delay_ms=40,
margin=margin_small,
brightness=255,
)
)
# 2. Medium spin, cyan to magenta, larger margin
spin_presets.append(
make_spin_preset(
"spin_medium_cyan_magenta",
color_inner=(0, 255, 180),
color_outer=(255, 0, 180),
rate=4,
delay_ms=30,
margin=margin_large,
brightness=255,
)
)
# 3. Fast spin, white to off (fade outwards), no margin
spin_presets.append(
make_spin_preset(
"spin_fast_white",
color_inner=(255, 255, 255),
color_outer=(0, 0, 0),
rate=6,
delay_ms=20,
margin=0,
brightness=255,
)
)
# Run each spin preset for about 6 seconds
for name, preset_obj in spin_presets:
print("Running spin preset:", name)
run_preset(presets, name, preset_obj, duration_ms=6000)
print("Spin pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

618
presets.json Normal file
View File

@@ -0,0 +1,618 @@
{
"start": {
"p": "off",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"grab": {
"p": "grab",
"d": 0,
"b": 0,
"c": [
[64,0,255]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"spin1": {
"p": "spin",
"d": 0,
"b": 100,
"c": [
[64,0,255],
[255,105,180]
],
"n1": 1,
"n2": 20,
"n3": 0,
"n4": 0
},
"lift": {
"p": "lift",
"d": 0,
"b": 100,
"c": [
[64,0,255],
[255,105,180]
],
"n1": 1,
"n2": 20,
"n3": 0,
"n4": 0
},
"flare": {
"p": "flare",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"hook": {
"p": "hook",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 7,
"n3": 15,
"n4": 15
},
"roll1": {
"p": "roll",
"d": 200,
"b": 100,
"c": [
[64,0,255],
[20,20,40]
],
"n1": 50,
"n2": 160,
"n3": 1,
"n4": 0
},
"invertsplit": {
"p": "invertsplit",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"pose1": {
"p": "point",
"d": 0,
"b": 220,
"c": [
[255,0,0],
[0,255,0],
[0,0,255],
[255,255,255]
],
"n1": 100,
"n2": 150,
"n3": 650,
"n4": 700,
"n5": 1200,
"n6": 1250,
"n7": 1750,
"n8": 1800
},
"pose2": {
"p": "point",
"d": 0,
"b": 220,
"c": [
[255,105,180],
[64,0,255],
[255,165,0],
[0,255,255]
],
"n1": 150,
"n2": 200,
"n3": 700,
"n4": 750,
"n5": 1250,
"n6": 1300,
"n7": 1800,
"n8": 1850
},
"roll2": {
"p": "roll",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"backbalance1": {
"p": "backbalance",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"beat1": {
"p": "beat",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"pose3": {
"p": "point",
"d": 0,
"b": 220,
"c": [
[255,255,0],
[255,0,255],
[0,255,255],
[255,255,255]
],
"n1": 200,
"n2": 250,
"n3": 750,
"n4": 800,
"n5": 1300,
"n6": 1350,
"n7": 1850,
"n8": 1900
},
"roll3": {
"p": "roll",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"crouch": {
"p": "crouch",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"pose4": {
"p": "point",
"d": 0,
"b": 220,
"c": [
[64,0,255],
[255,105,180],
[255,255,255],
[255,140,0]
],
"n1": 250,
"n2": 300,
"n3": 800,
"n4": 850,
"n5": 1350,
"n6": 1400,
"n7": 1900,
"n8": 1950
},
"roll4": {
"p": "roll",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"backbendsplit": {
"p": "backbendsplit",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"backbalance2": {
"p": "backbalance",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"backbalance3": {
"p": "backbalance",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"beat2": {
"p": "beat",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"straddle": {
"p": "straddle",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"beat3": {
"p": "beat",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"frontbalance1": {
"p": "frontbalance",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"pose5": {
"p": "point",
"d": 0,
"b": 220,
"c": [
[255,0,127],
[0,127,255],
[127,255,0],
[255,255,255]
],
"n1": 300,
"n2": 350,
"n3": 850,
"n4": 900,
"n5": 1400,
"n6": 1450,
"n7": 1950,
"n8": 2000
},
"pose6": {
"p": "point",
"d": 0,
"b": 220,
"c": [
[255,80,0],
[0,200,120],
[80,0,255],
[255,255,255]
],
"n1": 350,
"n2": 400,
"n3": 900,
"n4": 950,
"n5": 1450,
"n6": 1500,
"n7": 2000,
"n8": 2050
},
"elbowhang": {
"p": "elbowhang",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"elbowhangspin": {
"p": "elbowhangspin",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"spin2": {
"p": "spin",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"dismount": {
"p": "dismount",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"spin3": {
"p": "spin",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"fluff": {
"p": "fluff",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"spin4": {
"p": "spin",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"flare2": {
"p": "flare",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"elbowhangsplit2": {
"p": "elbowhangsplit",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"invert": {
"p": "invert",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"roll5": {
"p": "roll",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"backbend": {
"p": "backbend",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"pose7": {
"p": "point",
"d": 0,
"b": 220,
"c": [
[255,0,0],
[255,165,0],
[255,255,0],
[255,255,255]
],
"n1": 400,
"n2": 450,
"n3": 950,
"n4": 1000,
"n5": 1500,
"n6": 1550,
"n7": 2050,
"n8": 2100
},
"roll6": {
"p": "roll",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"seat": {
"p": "seat",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"kneehang": {
"p": "kneehang",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"legswoop": {
"p": "legswoop",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"split": {
"p": "split",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"foothang": {
"p": "foothang",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
},
"end": {
"p": "off",
"d": 0,
"b": 0,
"c": [
[0,0,0]
],
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0
}
}

View File

@@ -1,19 +0,0 @@
import wifi
import time
from settings import Settings
print(wifi.ap('qwerty'))
settings = Settings()
ssid = settings.get('wifi', {}).get('ssid', None)
password = settings.get('wifi', {}).get('password', None)
ip = settings.get('wifi', {}).get('ip', None)
gateway = settings.get('wifi', {}).get('gateway', None)
# for i in range(10):
# config = wifi.connect(ssid, password, ip, gateway)
# if config:
# print(config)
# break
# time.sleep(0.1)

View File

@@ -1,113 +0,0 @@
# DMA driver for Raspberry Pi Pico 2 (RP2350) only.
from machine import Pin
from rp2 import PIO, StateMachine, asm_pio
import array
import uctypes
from uctypes import BF_POS, BF_LEN, UINT32, BFUINT32, struct
PIO0_BASE = 0x50200000
PIO1_BASE = 0x50300000
PIO2_BASE = 0x50400000
DMA_BASE = 0x50000000
DMA_CHAN_WIDTH = 0x40
DMA_CHAN_COUNT = 12
DMA_SIZE_BYTE = 0x0
DMA_SIZE_HALFWORD = 0x1
DMA_SIZE_WORD = 0x2
# RP2350 DMA CTRL_TRIG bit positions
DMA_CTRL_TRIG_FIELDS = {
"AHB_ERROR": 31<<BF_POS | 1<<BF_LEN | BFUINT32,
"READ_ERROR": 30<<BF_POS | 1<<BF_LEN | BFUINT32,
"WRITE_ERROR": 29<<BF_POS | 1<<BF_LEN | BFUINT32,
"BUSY": 26<<BF_POS | 1<<BF_LEN | BFUINT32,
"SNIFF_EN": 25<<BF_POS | 1<<BF_LEN | BFUINT32,
"BSWAP": 24<<BF_POS | 1<<BF_LEN | BFUINT32,
"IRQ_QUIET": 23<<BF_POS | 1<<BF_LEN | BFUINT32,
"TREQ_SEL": 17<<BF_POS | 6<<BF_LEN | BFUINT32,
"CHAIN_TO": 13<<BF_POS | 4<<BF_LEN | BFUINT32,
"RING_SEL": 12<<BF_POS | 1<<BF_LEN | BFUINT32,
"RING_SIZE": 8<<BF_POS | 4<<BF_LEN | BFUINT32,
"INCR_WRITE": 6<<BF_POS | 1<<BF_LEN | BFUINT32,
"INCR_READ": 4<<BF_POS | 1<<BF_LEN | BFUINT32,
"DATA_SIZE": 2<<BF_POS | 2<<BF_LEN | BFUINT32,
"HIGH_PRIORITY":1<<BF_POS | 1<<BF_LEN | BFUINT32,
"EN": 0<<BF_POS | 1<<BF_LEN | BFUINT32
}
DMA_CHAN_REGS = {
"READ_ADDR_REG": 0x00|UINT32,
"WRITE_ADDR_REG": 0x04|UINT32,
"TRANS_COUNT_REG": 0x08|UINT32,
"CTRL_TRIG_REG": 0x0c|UINT32,
"CTRL_TRIG": (0x0c, DMA_CTRL_TRIG_FIELDS)
}
DMA_REGS = {
"INTR": 0x400|UINT32,
"INTE0": 0x404|UINT32,
"INTF0": 0x408|UINT32,
"INTS0": 0x40c|UINT32,
"INTE1": 0x414|UINT32,
"INTF1": 0x418|UINT32,
"INTS1": 0x41c|UINT32,
"TIMER0": 0x420|UINT32,
"TIMER1": 0x424|UINT32,
"TIMER2": 0x428|UINT32,
"TIMER3": 0x42c|UINT32,
"MULTI_CHAN_TRIGGER": 0x430|UINT32,
"SNIFF_CTRL": 0x434|UINT32,
"SNIFF_DATA": 0x438|UINT32,
"FIFO_LEVELS": 0x440|UINT32,
"CHAN_ABORT": 0x444|UINT32
}
DMA_CHANS = [struct(DMA_BASE + n*DMA_CHAN_WIDTH, DMA_CHAN_REGS) for n in range(DMA_CHAN_COUNT)]
DMA_DEVICE = struct(DMA_BASE, DMA_REGS)
PIO_TX_FIFO_OFFSET = 0x10
# RP2350 DREQ: PIO0_TX=0-3, PIO0_RX=4-7, PIO1_TX=8-11, PIO1_RX=12-15, PIO2_TX=16-19
def _pio_base_and_treq(sm_num):
"""Return (PIO_BASE, TREQ_SEL) for state machine 0..11."""
if sm_num < 4:
return (PIO0_BASE, sm_num)
if sm_num < 8:
return (PIO1_BASE, sm_num + 4)
if sm_num < 12:
return (PIO2_BASE, sm_num + 8)
raise ValueError("state machine index out of range")
class PIO_DMA_Transfer():
def __init__(self, dma_channel, sm_num, block_size, transfer_count):
self.dma_chan = DMA_CHANS[dma_channel]
self.channel_number = dma_channel
pio_base, treq_sel = _pio_base_and_treq(sm_num)
sm_offset = (sm_num % 4) * 4
self.dma_chan.WRITE_ADDR_REG = pio_base + PIO_TX_FIFO_OFFSET + sm_offset
self.dma_chan.CTRL_TRIG.TREQ_SEL = treq_sel
if block_size == 8:
self.dma_chan.CTRL_TRIG.DATA_SIZE = DMA_SIZE_BYTE
elif block_size == 16:
self.dma_chan.CTRL_TRIG.DATA_SIZE = DMA_SIZE_HALFWORD
elif block_size == 32:
self.dma_chan.CTRL_TRIG.DATA_SIZE = DMA_SIZE_WORD
self.dma_chan.TRANS_COUNT_REG = transfer_count
self.dma_chan.CTRL_TRIG.INCR_WRITE = 0
self.dma_chan.CTRL_TRIG.INCR_READ = 1
self.dma_chan.CTRL_TRIG.CHAIN_TO = dma_channel
def start_transfer(self, buffer):
self.dma_chan.READ_ADDR_REG = uctypes.addressof(buffer)
self.dma_chan.CTRL_TRIG.EN = 1
def transfer_count(self):
return self.dma_chan.TRANS_COUNT_REG
def busy(self):
return bool(self.dma_chan.CTRL_TRIG_REG & (1 << 26))

View File

@@ -1,34 +0,0 @@
from time import sleep
from neopixel import NeoPixel
from machine import UART, Pin, PWM, ADC
import _thread
import network
import espnow
from patterns import Patterns
adc = ADC(2, atten=ADC.ATTN_11DB)
sta = network.WLAN(network.WLAN.IF_STA) # Or network.WLAN.IF_AP
sta.active(True)
e = espnow.ESPNow()
e.active(True)
#e.add_peer(broadcast)
p = Patterns()
_thread.start_new_thread(p.scan_single_led, ((255,0,0),0))
while True:
value = adc.read_uv()*2
if value < 3_500_000:
p.run = False
p.off()
print(f"Voltage {value}")
sleep(1)
i = 0

View File

@@ -1,95 +0,0 @@
from machine import Pin
from neopixel import NeoPixel
import utime
import random
class Patterns:
def __init__(self):
self.pin_data = (21, 277) # Example: Pin 21, 277 LEDs
self.strip = NeoPixel(Pin(self.pin_data[0]), self.pin_data[1])
self.run = False
self.strip.fill((0,0,0))
self.strip.write()
print(f"Initialized single strip on Pin {self.pin_data[0]} with {self.pin_data[1]} LEDs.")
def scan_single_led(self, color=(255, 255, 255), delay_ms=0):
"""
Scans a single LED along the length of the strip, turning it on and then off
as it moves. Optimized for speed by batching writes.
Args:
color (tuple): The (R, G, B) color of the scanning LED.
delay_ms (int): Optional extra delay in milliseconds between each LED position.
Set to 0 for fastest possible without *extra* delay.
"""
self.run = True
num_pixels = len(self.strip)
last_pixel_index = num_pixels - 1
# Turn off all pixels initially for a clean start if not already off
self.strip.fill((0, 0, 0))
# No write here yet, as the first pixel will be set immediately
while self.run:
# --- Scan Forward ---
for i in range(num_pixels):
if not self.run:
break
# Turn on the current pixel
self.strip[i] = color
# Turn off the previous pixel if not the first one
if i > 0:
self.strip[i - 1] = (0, 0, 0)
# If it's the first pixel, ensure the last one from previous cycle is off (if applicable)
elif i == 0 and num_pixels > 1: # Only relevant if scanning backwards too
self.strip[last_pixel_index] = (0,0,0)
self.strip.write() # Write changes to the strip
if delay_ms > 0:
utime.sleep_ms(delay_ms)
# Ensure the last pixel of the forward scan is turned off
if self.run and num_pixels > 0:
self.strip[last_pixel_index] = (0, 0, 0)
self.strip.write() # Write this final change
# --- Scan Backward (optional, remove this loop if you only want forward) ---
for i in range(num_pixels - 1, -1, -1): # From last_pixel_index down to 0
if not self.run:
break
# Turn on the current pixel
self.strip[i] = color
# Turn off the next pixel (which was the previous one in reverse scan)
if i < last_pixel_index:
self.strip[i + 1] = (0, 0, 0)
# If it's the last pixel of the reverse scan, ensure the first one from previous cycle is off (if applicable)
elif i == last_pixel_index and num_pixels > 1: # Only relevant if scanning forward too
self.strip[0] = (0,0,0)
self.strip.write() # Write changes to the strip
if delay_ms > 0:
utime.sleep_ms(delay_ms)
# Ensure the first pixel of the backward scan is turned off
if self.run and num_pixels > 0:
self.strip[0] = (0, 0, 0)
self.strip.write() # Write this final change
def off(self):
print("Turning off LEDs.")
self.run = False
self.strip.fill((0,0,0))
self.strip.write()
utime.sleep_ms(50)
# Example Usage (for MicroPython on actual hardware):
# (Same as before, just removed from the main block for brevity)

View File

@@ -1,53 +0,0 @@
import json
class Settings(dict):
SETTINGS_FILE = "/settings.json"
def __init__(self):
super().__init__()
self.load() # Load settings from file during initialization
def set_defaults(self):
self["num_leds"] = 50
self["selected_pattern"] = "blink"
self["color1"] = "#000f00"
self["color2"] = "#0f0000"
self["delay"] = 100
self["brightness"] = 100
self["wifi"] = {"ssid": "", "password": ""}
def save(self):
try:
j = json.dumps(self)
with open(self.SETTINGS_FILE, 'w') as file:
file.write(j)
print("Settings saved successfully.")
except Exception as e:
print(f"Error saving settings: {e}")
def load(self):
try:
with open(self.SETTINGS_FILE, 'r') as file:
loaded_settings = json.load(file)
self.update(loaded_settings)
print("Settings loaded successfully.")
except Exception as e:
print(f"Error loading settings")
self.set_defaults()
# Example usage
def main():
settings = Settings()
print(f"Number of LEDs: {settings['num_leds']}")
settings['num_leds'] = 100
print(f"Updated number of LEDs: {settings['num_leds']}")
settings.save()
# Create a new Settings object to test loading
new_settings = Settings()
print(f"Loaded number of LEDs: {new_settings['num_leds']}")
print(settings)
# Run the example
if __name__ == "__main__":
main()

View File

@@ -1,75 +0,0 @@
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1 {
text-align: center;
}
form {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="submit"], input[type="range"], input[type="color"] {
width: 100%;
margin-bottom: 10px;
box-sizing: border-box;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 25px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
transition: opacity .2s;
}
input[type="range"]:hover {
opacity: 1;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
background: #4CAF50;
cursor: pointer;
border-radius: 50%;
}
input[type="range"]::-moz-range-thumb {
width: 25px;
height: 25px;
background: #4CAF50;
cursor: pointer;
border-radius: 50%;
}
#pattern_buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
#pattern_buttons button {
flex: 1 0 calc(33.333% - 10px);
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
#pattern_buttons button:hover {
background-color: #45a049;
}
@media (max-width: 480px) {
#pattern_buttons button {
flex: 1 0 calc(50% - 10px);
}
}

View File

@@ -1,147 +0,0 @@
let delayTimeout;
let brightnessTimeout;
let colorTimeout;
let color2Timeout;
async function post(path, data) {
console.log(`POST to ${path}`, data);
try {
const response = await fetch(path, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data) // Convert data to JSON string
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error('Error during POST request:', error);
}
}
async function get(path) {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json(); // Assuming you are expecting JSON response
} catch (error) {
console.error('Error during GET request:', error);
}
}
async function updateColor(event) {
event.preventDefault();
clearTimeout(colorTimeout);
colorTimeout = setTimeout(async function() {
const color = document.getElementById('color').value;
await post("/color", { color }); // Send as JSON
}, 500);
}
async function updateColor2(event) {
event.preventDefault();
clearTimeout(color2Timeout);
color2Timeout = setTimeout(async function() {
const color = document.getElementById('color2').value;
await post("/color2", { color }); // Send as JSON
}, 500);
}
async function updatePattern(pattern) {
event.preventDefault();
await post("/pattern", { pattern }); // Send as JSON
}
async function updateBrightness(event) {
event.preventDefault();
clearTimeout(brightnessTimeout);
brightnessTimeout = setTimeout(async function() {
const brightness = document.getElementById('brightness').value;
await post('/brightness', { brightness }); // Send as JSON
}, 500);
}
async function updateDelay(event) {
event.preventDefault();
clearTimeout(delayTimeout);
delayTimeout = setTimeout(async function() {
const delay = document.getElementById('delay').value;
await post('/delay', { delay }); // Send as JSON
}, 500);
}
async function updateNumLeds(event) {
event.preventDefault();
const numLeds = document.getElementById('num_leds').value;
await post('/num_leds', { num_leds: numLeds }); // Send as JSON
}
async function updateWifi(event) {
event.preventDefault();
const ssid = document.getElementById('ssid').value;
const password = document.getElementById('password').value;
const ip = document.getElementById('ip').value;
const gateway = document.getElementById('gateway').value;
const wifiSettings = { ssid, password, ip, gateway }; // Create JSON object
console.log(wifiSettings);
const response = await post('/wifi_settings', wifiSettings); // Send as JSON
if (response === 500) {
alert("Failed to connect to Wi-Fi");
}
}
function createPatternButtons(patterns) {
const container = document.getElementById('pattern_buttons');
container.innerHTML = ''; // Clear previous buttons
patterns.forEach(pattern => {
const button = document.createElement('button');
button.type = 'button'; // Use 'button' instead of 'submit'
button.textContent = pattern;
button.value = pattern;
button.addEventListener('click', async function(event) {
event.preventDefault();
await updatePattern(pattern);
});
container.appendChild(button);
});
}
document.addEventListener('DOMContentLoaded', async function() {
document.getElementById('color').addEventListener('input', updateColor);
document.getElementById('color2').addEventListener('input', updateColor2);
document.getElementById('delay').addEventListener('input', updateDelay);
document.getElementById('brightness').addEventListener('input', updateBrightness);
document.getElementById('num_leds_form').addEventListener('submit', updateNumLeds);
document.getElementById('wifi_form').addEventListener('submit', updateWifi);
document.getElementById('delay').addEventListener('touchend', updateDelay);
document.getElementById('brightness').addEventListener('touchend', updateBrightness);
document.querySelectorAll(".pattern_button").forEach(button => {
console.log(button.value);
button.addEventListener('click', async event => {
event.preventDefault();
await updatePattern(button.value);
});
});
});
// Function to toggle the display of the settings menu
function selectSettings() {
const settingsMenu = document.getElementById('settings_menu');
controls = document.getElementById('controls');
settingsMenu.style.display = 'block';
controls.style.display = 'none';
}
function selectControls() {
const settingsMenu = document.getElementById('settings_menu');
controls = document.getElementById('controls');
settingsMenu.style.display = 'none';
controls.style.display = 'block';
}

View File

@@ -1,41 +0,0 @@
{% args settings, patterns %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Control</title>
<script src="static/main.js"></script>
<link rel="stylesheet" href="static/main.css">
</head>
<body>
<h1>Control LEDs</h1>
<button onclick="selectControls()">Controls</button>
<button onclick="selectSettings()">Settings</button>
<!-- Main LED Controls -->
<div id="controls">
<div id="pattern_buttons">
{% for p in patterns %}
<button class="pattern_button" value="{{p}}">{{p}}</button>
{% endfor %}
<!-- Pattern buttons will be inserted here -->
</div>
<form id="delay_form" method="post" action="/delay">
<label for="delay">Delay:</label>
<input type="range" id="delay" name="delay" min="1" max="1000" value="{{settings['delay']}}" step="10">
</form>
<form id="brightness_form" method="post" action="/brightness">
<label for="brightness">Brightness:</label>
<input type="range" id="brightness" name="brightness" min="0" max="100" value="{{settings['brightness']}}" step="1">
</form>
<form id="color_form" method="post" action="/color">
<input type="color" id="color" name="color" value="{{settings['color1']}}">
</form>
<form id="color2_form" method="post" action="/color2">
<input type="color" id="color2" name="color2" value="{{settings['color2']}}">
</form>
</div>
</body>
</html>

View File

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

View File

@@ -1,101 +0,0 @@
from microdot import Microdot, send_file, Response
from microdot.utemplate import Template
from microdot.websocket import with_websocket
import json
import wifi
def web(settings, patterns, patterns2):
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
async def index(request):
return Template('/index.html').render(settings=settings, patterns=patterns.patterns.keys())
@app.route("/static/<path:path>")
def static(request, path):
if '..' in path:
# Directory traversal is not allowed
return 'Not found', 404
return send_file('static/' + path)
@app.post("/pattern")
def pattern(request):
try:
data = json.loads(request.body.decode('utf-8'))
pattern = data["pattern"]
if patterns.select(pattern):
patterns2.select(pattern)
settings["selected_pattern"] = pattern
settings.save()
return "OK", 200
else:
return "Bad request", 400
except (KeyError, json.JSONDecodeError):
return "Bad request", 400
@app.post("/delay")
def delay(request):
try:
data = json.loads(request.body.decode('utf-8'))
delay = int(data["delay"])
patterns.set_delay(delay)
patterns2.set_delay(delay)
settings["delay"] = delay
settings.save()
return "OK", 200
except (ValueError, KeyError, json.JSONDecodeError):
return "Bad request", 400
@app.post("/brightness")
def brightness(request):
try:
data = json.loads(request.body.decode('utf-8'))
brightness = int(data["brightness"])
patterns.set_brightness(brightness)
patterns2.set_brightness(brightness)
settings["brightness"] = brightness
settings.save()
return "OK", 200
except (ValueError, KeyError, json.JSONDecodeError):
return "Bad request", 400
@app.post("/color")
def color(request):
try:
data = json.loads(request.body.decode('utf-8'))
color = data["color"]
patterns.set_color1(tuple(int(color[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
patterns2.set_color1(tuple(int(color[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
settings["color1"] = color
settings.save()
return "OK", 200
except (KeyError, json.JSONDecodeError, ValueError):
return "Bad request", 400
@app.post("/color2")
def color2(request):
try:
data = json.loads(request.body.decode('utf-8'))
color = data["color2"]
patterns.set_color2(tuple(int(color[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
patterns2.set_color2(tuple(int(color[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
settings["color2"] = color
settings.save()
return "OK", 200
except (KeyError, json.JSONDecodeError, ValueError):
return "Bad request", 400
@app.route("/external")
@with_websocket
async def ws(request, ws):
patterns.select("external")
while True:
data = await ws.receive()
print(data)
for i in range(min(patterns.num_leds, int(len(data)/3))):
patterns.set(i, (data[i*3], data[i*3+1], data[i*3+2]))
patterns.write()
return app

View File

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

View File

@@ -1,68 +0,0 @@
import array, time
from machine import Pin
import rp2
from time import sleep
import dma
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=8)
def ws2812():
T1 = 2
T2 = 5
T3 = 3
wrap_target()
label("bitloop")
out(x, 1) .side(0) [T3 - 1]
jmp(not_x, "do_zero") .side(1) [T1 - 1]
jmp("bitloop") .side(1) [T2 - 1]
label("do_zero")
nop() .side(0) [T2 - 1]
wrap()
class WS2812B:
def __init__(self, num_leds, pin, state_machine, brightness=0.1, invert=False):
self.sm = rp2.StateMachine(state_machine, ws2812, freq=8_000_000, sideset_base=Pin(pin))
self.sm.active(1)
self.ar = bytearray(num_leds*3)
self.num_leds = num_leds
self.brightness = brightness
self.invert = invert
self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3)
def show(self):
self.pio_dma.start_transfer(self.ar)
def set(self, i, color):
self.ar[i*3] = int(color[1]*self.brightness)
self.ar[i*3+1] = int(color[0]*self.brightness)
self.ar[i*3+2] = int(color[2]*self.brightness)
def fill(self, color):
for i in range(self.num_leds):
self.set(i, color)
def busy(self):
return self.pio_dma.busy()
BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)
if __name__ == "__main__":
num_leds, pin, sm, brightness = 10, 0, 0, 1
ws0 = WS2812B(num_leds, pin, sm, brightness)
while True:
for color in ws0.COLORS:
ws0.fill(color)
ws0.show()
time.sleep(1)

View File

@@ -1 +0,0 @@
print("Hello")