Compare commits
15 Commits
6b5ae78591
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 292c5bde01 | |||
| a0687cff57 | |||
| 5f457b3ae7 | |||
| 3e58f4e97e | |||
| 47c19eecf1 | |||
| 47c17dba36 | |||
| e75723e2e7 | |||
| 52a5f0f8c4 | |||
| 646b988cdd | |||
| 615431d6c5 | |||
| b45823c479 | |||
| 0c73d56ab5 | |||
| 86b28a1b9c | |||
| 2869ec9300 | |||
| c062110e35 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
settings.json
|
||||
__pycache__/
|
||||
194
DRY_RUN_COMMITS.md
Normal file
194
DRY_RUN_COMMITS.md
Normal 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 doesn’t 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
15
Pipfile
Normal 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
617
Pipfile.lock
generated
Normal 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": {}
|
||||
}
|
||||
28
README.md
28
README.md
@@ -1,2 +1,30 @@
|
||||
# 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**, right‑click the device, delete `boot.py` and `main.py` on the device, then **Tools → Reset**.
|
||||
|
||||
If the board doesn’t 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.
|
||||
|
||||
20
box.scad
20
box.scad
@@ -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]);
|
||||
|
||||
};
|
||||
108
dev.py
108
dev.py
@@ -1,33 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import serial
|
||||
import sys
|
||||
|
||||
print(sys.argv)
|
||||
|
||||
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)
|
||||
import serial
|
||||
|
||||
|
||||
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
12
esp32/src/boot.py
Normal 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
109
esp32/src/main.py
Normal 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
48
esp32/src/squence.txt
Normal 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
617
esp32/src/static/main.js
Normal 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
256
esp32/src/static/styles.css
Normal 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 */
|
||||
147
esp32/src/templates/index.html
Normal file
147
esp32/src/templates/index.html
Normal 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 (0–255)" />
|
||||
</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
102
main.py
Normal 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()
|
||||
@@ -93,8 +93,8 @@ class PIO_DMA_Transfer():
|
||||
self.dma_chan.CTRL_TRIG.INCR_WRITE = 0
|
||||
self.dma_chan.CTRL_TRIG.INCR_READ = 1
|
||||
|
||||
def start_transfer(self, buffer):
|
||||
self.dma_chan.READ_ADDR_REG = uctypes.addressof(buffer)
|
||||
def start_transfer(self, buffer, offset=0):
|
||||
self.dma_chan.READ_ADDR_REG = uctypes.addressof(buffer) + offset
|
||||
self.dma_chan.CTRL_TRIG.EN = 1
|
||||
|
||||
def transfer_count(self):
|
||||
|
||||
@@ -28,9 +28,13 @@ class WS2812B:
|
||||
self.brightness = brightness
|
||||
self.invert = invert
|
||||
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):
|
||||
if array is None:
|
||||
array = self.ar
|
||||
self.pio_dma.start_transfer(array, offset)
|
||||
|
||||
def set(self, i, color):
|
||||
self.ar[i*3] = int(color[1]*self.brightness)
|
||||
|
||||
63
pico/src/main.py
Normal file
63
pico/src/main.py
Normal 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")
|
||||
|
||||
19
pico/src/patterns/__init__.py
Normal file
19
pico/src/patterns/__init__.py
Normal 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
|
||||
33
pico/src/patterns/blink.py
Normal file
33
pico/src/patterns/blink.py
Normal 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
|
||||
39
pico/src/patterns/calibration.py
Normal file
39
pico/src/patterns/calibration.py
Normal 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
166
pico/src/patterns/chase.py
Normal 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
|
||||
96
pico/src/patterns/circle.py
Normal file
96
pico/src/patterns/circle.py
Normal 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
|
||||
84
pico/src/patterns/double_circle.py
Normal file
84
pico/src/patterns/double_circle.py
Normal 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
|
||||
|
||||
63
pico/src/patterns/flare.py
Normal file
63
pico/src/patterns/flare.py
Normal 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
26
pico/src/patterns/grab.py
Normal 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
31
pico/src/patterns/hook.py
Normal 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
116
pico/src/patterns/lift.py
Normal 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
75
pico/src/patterns/pose.py
Normal 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):
|
||||
|
||||
- n1–n2: color c[0]
|
||||
- n3–n4: color c[1]
|
||||
- n5–n6: color c[2]
|
||||
- n7–n8: 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
|
||||
64
pico/src/patterns/pulse.py
Normal file
64
pico/src/patterns/pulse.py
Normal 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
|
||||
97
pico/src/patterns/rainbow.py
Normal file
97
pico/src/patterns/rainbow.py
Normal 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
111
pico/src/patterns/roll.py
Normal 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
|
||||
56
pico/src/patterns/scale_test.py
Normal file
56
pico/src/patterns/scale_test.py
Normal 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
|
||||
|
||||
18
pico/src/patterns/segments.py
Normal file
18
pico/src/patterns/segments.py
Normal 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()
|
||||
|
||||
136
pico/src/patterns/segments_transition.py
Normal file
136
pico/src/patterns/segments_transition.py
Normal 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 n1–n8 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
101
pico/src/patterns/spin.py
Normal 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
28
pico/src/patterns/test.py
Normal 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()
|
||||
57
pico/src/patterns/transition.py
Normal file
57
pico/src/patterns/transition.py
Normal 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
83
pico/src/preset.py
Normal 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
245
pico/src/presets.py
Normal 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 (0–255), 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
53
pico/src/utils.py
Normal 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
79
pico/test/chase.py
Normal 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)
|
||||
@@ -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
|
||||
strips.append(ws)
|
||||
sm += 1
|
||||
ws.fill((255,0,0))
|
||||
ws.fill((8,0,0))
|
||||
ws.show()
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
190
pico/test/patterns/auto_manual.py
Normal file
190
pico/test/patterns/auto_manual.py
Normal 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()
|
||||
35
pico/test/patterns/blink.py
Normal file
35
pico/test/patterns/blink.py
Normal 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
161
pico/test/patterns/chase.py
Normal 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()
|
||||
|
||||
113
pico/test/patterns/circle.py
Normal file
113
pico/test/patterns/circle.py
Normal 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
30
pico/test/patterns/off.py
Normal 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
47
pico/test/patterns/on.py
Normal 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()
|
||||
|
||||
|
||||
92
pico/test/patterns/pulse.py
Normal file
92
pico/test/patterns/pulse.py
Normal 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()
|
||||
|
||||
|
||||
151
pico/test/patterns/rainbow.py
Normal file
151
pico/test/patterns/rainbow.py
Normal 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()
|
||||
|
||||
81
pico/test/patterns/transition.py
Normal file
81
pico/test/patterns/transition.py
Normal 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
99
pico/test/rainbow.py
Normal 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
114
pico/test/roll.py
Normal 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
6
pico/test/test_all_on.py
Normal 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()
|
||||
71
pico/test/test_all_on_presets.py
Normal file
71
pico/test/test_all_on_presets.py
Normal 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()
|
||||
|
||||
184
pico/test/test_chase_via_presets.py
Normal file
184
pico/test/test_chase_via_presets.py
Normal 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()
|
||||
|
||||
157
pico/test/test_double_circle.py
Normal file
157
pico/test/test_double_circle.py
Normal 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()
|
||||
|
||||
694
pico/test/test_espnow_receive.py
Normal file
694
pico/test/test_espnow_receive.py
Normal 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
52
pico/test/test_fill_n.py
Normal 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()
|
||||
|
||||
264
pico/test/test_multi_patterns.py
Normal file
264
pico/test/test_multi_patterns.py
Normal 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
100
pico/test/test_scale.py
Normal 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
130
pico/test/test_segments.py
Normal 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
45
pico/test/test_serial.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Serial loopback test – single file, runs on Pico and ESP32.
|
||||
Wire TX to RX (Pico: GP0–GP1, ESP32: 17–18), 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)
|
||||
32
pico/test/test_serial_receive.py
Normal file
32
pico/test/test_serial_receive.py
Normal 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.")
|
||||
23
pico/test/test_serial_send.py
Normal file
23
pico/test/test_serial_send.py
Normal 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
128
pico/test/test_spin.py
Normal 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
618
presets.json
Normal 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
|
||||
}
|
||||
}
|
||||
19
src/boot.py
19
src/boot.py
@@ -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)
|
||||
113
src/dma.py
113
src/dma.py
@@ -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))
|
||||
34
src/main.py
34
src/main.py
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
"""
|
||||
101
src/web.py
101
src/web.py
@@ -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
|
||||
46
src/wifi.py
46
src/wifi.py
@@ -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())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user