Compare commits
3 Commits
b45823c479
...
52a5f0f8c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 52a5f0f8c4 | |||
| 646b988cdd | |||
| 615431d6c5 |
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": {}
|
||||
}
|
||||
2
esp32/lib/microdot/__init__.py
Normal file
2
esp32/lib/microdot/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from microdot.microdot import Microdot, Request, Response, abort, redirect, \
|
||||
send_file # noqa: F401
|
||||
8
esp32/lib/microdot/helpers.py
Normal file
8
esp32/lib/microdot/helpers.py
Normal file
@@ -0,0 +1,8 @@
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError: # pragma: no cover
|
||||
# MicroPython does not currently implement functools.wraps
|
||||
def wraps(wrapped):
|
||||
def _(wrapper):
|
||||
return wrapper
|
||||
return _
|
||||
1450
esp32/lib/microdot/microdot.py
Normal file
1450
esp32/lib/microdot/microdot.py
Normal file
File diff suppressed because it is too large
Load Diff
70
esp32/lib/microdot/utemplate.py
Normal file
70
esp32/lib/microdot/utemplate.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from utemplate import recompile
|
||||
|
||||
_loader = None
|
||||
|
||||
|
||||
class Template:
|
||||
"""A template object.
|
||||
|
||||
:param template: The filename of the template to render, relative to the
|
||||
configured template directory.
|
||||
"""
|
||||
@classmethod
|
||||
def initialize(cls, template_dir='templates',
|
||||
loader_class=recompile.Loader):
|
||||
"""Initialize the templating subsystem.
|
||||
|
||||
:param template_dir: the directory where templates are stored. This
|
||||
argument is optional. The default is to load
|
||||
templates from a *templates* subdirectory.
|
||||
:param loader_class: the ``utemplate.Loader`` class to use when loading
|
||||
templates. This argument is optional. The default
|
||||
is the ``recompile.Loader`` class, which
|
||||
automatically recompiles templates when they
|
||||
change.
|
||||
"""
|
||||
global _loader
|
||||
_loader = loader_class(None, template_dir)
|
||||
|
||||
def __init__(self, template):
|
||||
if _loader is None: # pragma: no cover
|
||||
self.initialize()
|
||||
#: The name of the template
|
||||
self.name = template
|
||||
self.template = _loader.load(template)
|
||||
|
||||
def generate(self, *args, **kwargs):
|
||||
"""Return a generator that renders the template in chunks, with the
|
||||
given arguments."""
|
||||
return self.template(*args, **kwargs)
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
"""Render the template with the given arguments and return it as a
|
||||
string."""
|
||||
return ''.join(self.generate(*args, **kwargs))
|
||||
|
||||
def generate_async(self, *args, **kwargs):
|
||||
"""Return an asynchronous generator that renders the template in
|
||||
chunks, using the given arguments."""
|
||||
class sync_to_async_iter():
|
||||
def __init__(self, iter):
|
||||
self.iter = iter
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
try:
|
||||
return next(self.iter)
|
||||
except StopIteration:
|
||||
raise StopAsyncIteration
|
||||
|
||||
return sync_to_async_iter(self.generate(*args, **kwargs))
|
||||
|
||||
async def render_async(self, *args, **kwargs):
|
||||
"""Render the template with the given arguments asynchronously and
|
||||
return it as a string."""
|
||||
response = ''
|
||||
async for chunk in self.generate_async(*args, **kwargs):
|
||||
response += chunk
|
||||
return response
|
||||
231
esp32/lib/microdot/websocket.py
Normal file
231
esp32/lib/microdot/websocket.py
Normal file
@@ -0,0 +1,231 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
from microdot import Request, Response
|
||||
from microdot.microdot import MUTED_SOCKET_ERRORS, print_exception
|
||||
from microdot.helpers import wraps
|
||||
|
||||
|
||||
class WebSocketError(Exception):
|
||||
"""Exception raised when an error occurs in a WebSocket connection."""
|
||||
pass
|
||||
|
||||
|
||||
class WebSocket:
|
||||
"""A WebSocket connection object.
|
||||
|
||||
An instance of this class is sent to handler functions to manage the
|
||||
WebSocket connection.
|
||||
"""
|
||||
CONT = 0
|
||||
TEXT = 1
|
||||
BINARY = 2
|
||||
CLOSE = 8
|
||||
PING = 9
|
||||
PONG = 10
|
||||
|
||||
#: Specify the maximum message size that can be received when calling the
|
||||
#: ``receive()`` method. Messages with payloads that are larger than this
|
||||
#: size will be rejected and the connection closed. Set to 0 to disable
|
||||
#: the size check (be aware of potential security issues if you do this),
|
||||
#: or to -1 to use the value set in
|
||||
#: ``Request.max_body_length``. The default is -1.
|
||||
#:
|
||||
#: Example::
|
||||
#:
|
||||
#: WebSocket.max_message_length = 4 * 1024 # up to 4KB messages
|
||||
max_message_length = -1
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
self.closed = False
|
||||
|
||||
async def handshake(self):
|
||||
response = self._handshake_response()
|
||||
await self.request.sock[1].awrite(
|
||||
b'HTTP/1.1 101 Switching Protocols\r\n')
|
||||
await self.request.sock[1].awrite(b'Upgrade: websocket\r\n')
|
||||
await self.request.sock[1].awrite(b'Connection: Upgrade\r\n')
|
||||
await self.request.sock[1].awrite(
|
||||
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
|
||||
|
||||
async def receive(self):
|
||||
"""Receive a message from the client."""
|
||||
while True:
|
||||
opcode, payload = await self._read_frame()
|
||||
send_opcode, data = self._process_websocket_frame(opcode, payload)
|
||||
if send_opcode: # pragma: no cover
|
||||
await self.send(data, send_opcode)
|
||||
elif data: # pragma: no branch
|
||||
return data
|
||||
|
||||
async def send(self, data, opcode=None):
|
||||
"""Send a message to the client.
|
||||
|
||||
:param data: the data to send, given as a string or bytes.
|
||||
:param opcode: a custom frame opcode to use. If not given, the opcode
|
||||
is ``TEXT`` or ``BINARY`` depending on the type of the
|
||||
data.
|
||||
"""
|
||||
frame = self._encode_websocket_frame(
|
||||
opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
|
||||
data)
|
||||
await self.request.sock[1].awrite(frame)
|
||||
|
||||
async def close(self):
|
||||
"""Close the websocket connection."""
|
||||
if not self.closed: # pragma: no cover
|
||||
self.closed = True
|
||||
await self.send(b'', self.CLOSE)
|
||||
|
||||
def _handshake_response(self):
|
||||
connection = False
|
||||
upgrade = False
|
||||
websocket_key = None
|
||||
for header, value in self.request.headers.items():
|
||||
h = header.lower()
|
||||
if h == 'connection':
|
||||
connection = True
|
||||
if 'upgrade' not in value.lower():
|
||||
return self.request.app.abort(400)
|
||||
elif h == 'upgrade':
|
||||
upgrade = True
|
||||
if not value.lower() == 'websocket':
|
||||
return self.request.app.abort(400)
|
||||
elif h == 'sec-websocket-key':
|
||||
websocket_key = value
|
||||
if not connection or not upgrade or not websocket_key:
|
||||
return self.request.app.abort(400)
|
||||
d = hashlib.sha1(websocket_key.encode())
|
||||
d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
||||
return binascii.b2a_base64(d.digest())[:-1]
|
||||
|
||||
@classmethod
|
||||
def _parse_frame_header(cls, header):
|
||||
fin = header[0] & 0x80
|
||||
opcode = header[0] & 0x0f
|
||||
if fin == 0 or opcode == cls.CONT: # pragma: no cover
|
||||
raise WebSocketError('Continuation frames not supported')
|
||||
has_mask = header[1] & 0x80
|
||||
length = header[1] & 0x7f
|
||||
if length == 126:
|
||||
length = -2
|
||||
elif length == 127:
|
||||
length = -8
|
||||
return fin, opcode, has_mask, length
|
||||
|
||||
def _process_websocket_frame(self, opcode, payload):
|
||||
if opcode == self.TEXT:
|
||||
payload = payload.decode()
|
||||
elif opcode == self.BINARY:
|
||||
pass
|
||||
elif opcode == self.CLOSE:
|
||||
raise WebSocketError('Websocket connection closed')
|
||||
elif opcode == self.PING:
|
||||
return self.PONG, payload
|
||||
elif opcode == self.PONG: # pragma: no branch
|
||||
return None, None
|
||||
return None, payload
|
||||
|
||||
@classmethod
|
||||
def _encode_websocket_frame(cls, opcode, payload):
|
||||
frame = bytearray()
|
||||
frame.append(0x80 | opcode)
|
||||
if opcode == cls.TEXT:
|
||||
payload = payload.encode()
|
||||
if len(payload) < 126:
|
||||
frame.append(len(payload))
|
||||
elif len(payload) < (1 << 16):
|
||||
frame.append(126)
|
||||
frame.extend(len(payload).to_bytes(2, 'big'))
|
||||
else:
|
||||
frame.append(127)
|
||||
frame.extend(len(payload).to_bytes(8, 'big'))
|
||||
frame.extend(payload)
|
||||
return frame
|
||||
|
||||
async def _read_frame(self):
|
||||
header = await self.request.sock[0].read(2)
|
||||
if len(header) != 2: # pragma: no cover
|
||||
raise WebSocketError('Websocket connection closed')
|
||||
fin, opcode, has_mask, length = self._parse_frame_header(header)
|
||||
if length == -2:
|
||||
length = await self.request.sock[0].read(2)
|
||||
length = int.from_bytes(length, 'big')
|
||||
elif length == -8:
|
||||
length = await self.request.sock[0].read(8)
|
||||
length = int.from_bytes(length, 'big')
|
||||
max_allowed_length = Request.max_body_length \
|
||||
if self.max_message_length == -1 else self.max_message_length
|
||||
if length > max_allowed_length:
|
||||
raise WebSocketError('Message too large')
|
||||
if has_mask: # pragma: no cover
|
||||
mask = await self.request.sock[0].read(4)
|
||||
payload = await self.request.sock[0].read(length)
|
||||
if has_mask: # pragma: no cover
|
||||
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
|
||||
return opcode, payload
|
||||
|
||||
|
||||
async def websocket_upgrade(request):
|
||||
"""Upgrade a request handler to a websocket connection.
|
||||
|
||||
This function can be called directly inside a route function to process a
|
||||
WebSocket upgrade handshake, for example after the user's credentials are
|
||||
verified. The function returns the websocket object::
|
||||
|
||||
@app.route('/echo')
|
||||
async def echo(request):
|
||||
if not authenticate_user(request):
|
||||
abort(401)
|
||||
ws = await websocket_upgrade(request)
|
||||
while True:
|
||||
message = await ws.receive()
|
||||
await ws.send(message)
|
||||
"""
|
||||
ws = WebSocket(request)
|
||||
await ws.handshake()
|
||||
|
||||
@request.after_request
|
||||
async def after_request(request, response):
|
||||
return Response.already_handled
|
||||
|
||||
return ws
|
||||
|
||||
|
||||
def websocket_wrapper(f, upgrade_function):
|
||||
@wraps(f)
|
||||
async def wrapper(request, *args, **kwargs):
|
||||
ws = await upgrade_function(request)
|
||||
try:
|
||||
await f(request, ws, *args, **kwargs)
|
||||
except OSError as exc:
|
||||
if exc.errno not in MUTED_SOCKET_ERRORS: # pragma: no cover
|
||||
raise
|
||||
except WebSocketError:
|
||||
pass
|
||||
except Exception as exc:
|
||||
print_exception(exc)
|
||||
finally: # pragma: no cover
|
||||
try:
|
||||
await ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
return Response.already_handled
|
||||
return wrapper
|
||||
|
||||
|
||||
def with_websocket(f):
|
||||
"""Decorator to make a route a WebSocket endpoint.
|
||||
|
||||
This decorator is used to define a route that accepts websocket
|
||||
connections. The route then receives a websocket object as a second
|
||||
argument that it can use to send and receive messages::
|
||||
|
||||
@app.route('/echo')
|
||||
@with_websocket
|
||||
async def echo(request, ws):
|
||||
while True:
|
||||
message = await ws.receive()
|
||||
await ws.send(message)
|
||||
"""
|
||||
return websocket_wrapper(f, websocket_upgrade)
|
||||
0
esp32/lib/utemplate/__init__.py
Normal file
0
esp32/lib/utemplate/__init__.py
Normal file
14
esp32/lib/utemplate/compiled.py
Normal file
14
esp32/lib/utemplate/compiled.py
Normal file
@@ -0,0 +1,14 @@
|
||||
class Loader:
|
||||
|
||||
def __init__(self, pkg, dir):
|
||||
if dir == ".":
|
||||
dir = ""
|
||||
else:
|
||||
dir = dir.replace("/", ".") + "."
|
||||
if pkg and pkg != "__main__":
|
||||
dir = pkg + "." + dir
|
||||
self.p = dir
|
||||
|
||||
def load(self, name):
|
||||
name = name.replace(".", "_")
|
||||
return __import__(self.p + name, None, None, (name,)).render
|
||||
21
esp32/lib/utemplate/recompile.py
Normal file
21
esp32/lib/utemplate/recompile.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# (c) 2014-2020 Paul Sokolovsky. MIT license.
|
||||
try:
|
||||
from uos import stat, remove
|
||||
except:
|
||||
from os import stat, remove
|
||||
from . import source
|
||||
|
||||
|
||||
class Loader(source.Loader):
|
||||
|
||||
def load(self, name):
|
||||
o_path = self.pkg_path + self.compiled_path(name)
|
||||
i_path = self.pkg_path + self.dir + "/" + name
|
||||
try:
|
||||
o_stat = stat(o_path)
|
||||
i_stat = stat(i_path)
|
||||
if i_stat[8] > o_stat[8]:
|
||||
# input file is newer, remove output to force recompile
|
||||
remove(o_path)
|
||||
finally:
|
||||
return super().load(name)
|
||||
188
esp32/lib/utemplate/source.py
Normal file
188
esp32/lib/utemplate/source.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# (c) 2014-2019 Paul Sokolovsky. MIT license.
|
||||
from . import compiled
|
||||
|
||||
|
||||
class Compiler:
|
||||
|
||||
START_CHAR = "{"
|
||||
STMNT = "%"
|
||||
STMNT_END = "%}"
|
||||
EXPR = "{"
|
||||
EXPR_END = "}}"
|
||||
|
||||
def __init__(self, file_in, file_out, indent=0, seq=0, loader=None):
|
||||
self.file_in = file_in
|
||||
self.file_out = file_out
|
||||
self.loader = loader
|
||||
self.seq = seq
|
||||
self._indent = indent
|
||||
self.stack = []
|
||||
self.in_literal = False
|
||||
self.flushed_header = False
|
||||
self.args = "*a, **d"
|
||||
|
||||
def indent(self, adjust=0):
|
||||
if not self.flushed_header:
|
||||
self.flushed_header = True
|
||||
self.indent()
|
||||
self.file_out.write("def render%s(%s):\n" % (str(self.seq) if self.seq else "", self.args))
|
||||
self.stack.append("def")
|
||||
self.file_out.write(" " * (len(self.stack) + self._indent + adjust))
|
||||
|
||||
def literal(self, s):
|
||||
if not s:
|
||||
return
|
||||
if not self.in_literal:
|
||||
self.indent()
|
||||
self.file_out.write('yield """')
|
||||
self.in_literal = True
|
||||
self.file_out.write(s.replace('"', '\\"'))
|
||||
|
||||
def close_literal(self):
|
||||
if self.in_literal:
|
||||
self.file_out.write('"""\n')
|
||||
self.in_literal = False
|
||||
|
||||
def render_expr(self, e):
|
||||
self.indent()
|
||||
self.file_out.write('yield str(' + e + ')\n')
|
||||
|
||||
def parse_statement(self, stmt):
|
||||
tokens = stmt.split(None, 1)
|
||||
if tokens[0] == "args":
|
||||
if len(tokens) > 1:
|
||||
self.args = tokens[1]
|
||||
else:
|
||||
self.args = ""
|
||||
elif tokens[0] == "set":
|
||||
self.indent()
|
||||
self.file_out.write(stmt[3:].strip() + "\n")
|
||||
elif tokens[0] == "include":
|
||||
if not self.flushed_header:
|
||||
# If there was no other output, we still need a header now
|
||||
self.indent()
|
||||
tokens = tokens[1].split(None, 1)
|
||||
args = ""
|
||||
if len(tokens) > 1:
|
||||
args = tokens[1]
|
||||
if tokens[0][0] == "{":
|
||||
self.indent()
|
||||
# "1" as fromlist param is uPy hack
|
||||
self.file_out.write('_ = __import__(%s.replace(".", "_"), None, None, 1)\n' % tokens[0][2:-2])
|
||||
self.indent()
|
||||
self.file_out.write("yield from _.render(%s)\n" % args)
|
||||
return
|
||||
|
||||
with self.loader.input_open(tokens[0][1:-1]) as inc:
|
||||
self.seq += 1
|
||||
c = Compiler(inc, self.file_out, len(self.stack) + self._indent, self.seq)
|
||||
inc_id = self.seq
|
||||
self.seq = c.compile()
|
||||
self.indent()
|
||||
self.file_out.write("yield from render%d(%s)\n" % (inc_id, args))
|
||||
elif len(tokens) > 1:
|
||||
if tokens[0] == "elif":
|
||||
assert self.stack[-1] == "if"
|
||||
self.indent(-1)
|
||||
self.file_out.write(stmt + ":\n")
|
||||
else:
|
||||
self.indent()
|
||||
self.file_out.write(stmt + ":\n")
|
||||
self.stack.append(tokens[0])
|
||||
else:
|
||||
if stmt.startswith("end"):
|
||||
assert self.stack[-1] == stmt[3:]
|
||||
self.stack.pop(-1)
|
||||
elif stmt == "else":
|
||||
assert self.stack[-1] == "if"
|
||||
self.indent(-1)
|
||||
self.file_out.write("else:\n")
|
||||
else:
|
||||
assert False
|
||||
|
||||
def parse_line(self, l):
|
||||
while l:
|
||||
start = l.find(self.START_CHAR)
|
||||
if start == -1:
|
||||
self.literal(l)
|
||||
return
|
||||
self.literal(l[:start])
|
||||
self.close_literal()
|
||||
sel = l[start + 1]
|
||||
#print("*%s=%s=" % (sel, EXPR))
|
||||
if sel == self.STMNT:
|
||||
end = l.find(self.STMNT_END)
|
||||
assert end > 0
|
||||
stmt = l[start + len(self.START_CHAR + self.STMNT):end].strip()
|
||||
self.parse_statement(stmt)
|
||||
end += len(self.STMNT_END)
|
||||
l = l[end:]
|
||||
if not self.in_literal and l == "\n":
|
||||
break
|
||||
elif sel == self.EXPR:
|
||||
# print("EXPR")
|
||||
end = l.find(self.EXPR_END)
|
||||
assert end > 0
|
||||
expr = l[start + len(self.START_CHAR + self.EXPR):end].strip()
|
||||
self.render_expr(expr)
|
||||
end += len(self.EXPR_END)
|
||||
l = l[end:]
|
||||
else:
|
||||
self.literal(l[start])
|
||||
l = l[start + 1:]
|
||||
|
||||
def header(self):
|
||||
self.file_out.write("# Autogenerated file\n")
|
||||
|
||||
def compile(self):
|
||||
self.header()
|
||||
for l in self.file_in:
|
||||
self.parse_line(l)
|
||||
self.close_literal()
|
||||
return self.seq
|
||||
|
||||
|
||||
class Loader(compiled.Loader):
|
||||
|
||||
def __init__(self, pkg, dir):
|
||||
super().__init__(pkg, dir)
|
||||
self.dir = dir
|
||||
if pkg == "__main__":
|
||||
# if pkg isn't really a package, don't bother to use it
|
||||
# it means we're running from "filesystem directory", not
|
||||
# from a package.
|
||||
pkg = None
|
||||
|
||||
self.pkg_path = ""
|
||||
if pkg:
|
||||
p = __import__(pkg)
|
||||
if isinstance(p.__path__, str):
|
||||
# uPy
|
||||
self.pkg_path = p.__path__
|
||||
else:
|
||||
# CPy
|
||||
self.pkg_path = p.__path__[0]
|
||||
self.pkg_path += "/"
|
||||
|
||||
def input_open(self, template):
|
||||
path = self.pkg_path + self.dir + "/" + template
|
||||
return open(path)
|
||||
|
||||
def compiled_path(self, template):
|
||||
return self.dir + "/" + template.replace(".", "_") + ".py"
|
||||
|
||||
def load(self, name):
|
||||
try:
|
||||
return super().load(name)
|
||||
except (OSError, ImportError):
|
||||
pass
|
||||
|
||||
compiled_path = self.pkg_path + self.compiled_path(name)
|
||||
|
||||
f_in = self.input_open(name)
|
||||
f_out = open(compiled_path, "w")
|
||||
c = Compiler(f_in, f_out, loader=self)
|
||||
c.compile()
|
||||
f_in.close()
|
||||
f_out.close()
|
||||
return super().load(name)
|
||||
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())
|
||||
58
esp32/src/buttons.json
Normal file
58
esp32/src/buttons.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"buttons": [
|
||||
{"id": "start", "preset": "off"},
|
||||
{"id": "grab", "preset": "grab"},
|
||||
{"id": "spin1", "preset": "spin1"},
|
||||
{"id": "lift", "preset": "lift"},
|
||||
{"id": "flare", "preset": "flare"},
|
||||
{"id": "hook", "preset": "hook"},
|
||||
{"id": "roll1", "preset": "roll1"},
|
||||
{"id": "invertsplit", "preset": "invertsplit"},
|
||||
{"id": "pose1", "preset": "pose1"},
|
||||
{"id": "pose1", "preset": "pose2"},
|
||||
{"id": "roll2", "preset": "roll2"},
|
||||
{"id": "backbalance1", "preset": "backbalance1"},
|
||||
{"id": "beat1", "preset": "beat1"},
|
||||
{"id": "pose3", "preset": "pose3"},
|
||||
{"id": "roll3", "preset": "roll3"},
|
||||
{"id": "crouch", "preset": "crouch"},
|
||||
{"id": "pose4", "preset": "pose4"},
|
||||
{"id": "roll4", "preset": "roll4"},
|
||||
{"id": "backbendsplit", "preset": "backbendsplit"},
|
||||
{"id": "backbalance2", "preset": "backbalance2"},
|
||||
{"id": "backbalance3", "preset": "backbalance3"},
|
||||
{"id": "beat2", "preset": "beat2"},
|
||||
{"id": "straddle", "preset": "straddle"},
|
||||
{"id": "beat3", "preset": "beat3"},
|
||||
{"id": "frontbalance1", "preset": "frontbalance1"},
|
||||
{"id": "pose5", "preset": "pose5"},
|
||||
{"id": "pose6", "preset": "pose6"},
|
||||
{"id": "elbowhang", "preset": "elbowhang"},
|
||||
{"id": "elbowhangspin", "preset": "elbowhangspin"},
|
||||
{"id": "spin2", "preset": "spin2"},
|
||||
{"id": "dismount", "preset": "dismount"},
|
||||
{"id": "spin3", "preset": "spin3"},
|
||||
{"id": "fluff", "preset": "fluff"},
|
||||
{"id": "spin4", "preset": "spin4"},
|
||||
{"id": "flare2", "preset": "flare2"},
|
||||
{"id": "elbowhang", "preset": "elbowhang"},
|
||||
{"id": "elbowhangsplit2", "preset": "elbowhangsplit2"},
|
||||
{"id": "invert", "preset": "invert"},
|
||||
{"id": "roll5", "preset": "roll5"},
|
||||
{"id": "backbend", "preset": "backbend"},
|
||||
{"id": "pose7", "preset": "pose7"},
|
||||
{"id": "roll6", "preset": "roll6"},
|
||||
{"id": "seat", "preset": "seat"},
|
||||
{"id": "kneehang", "preset": "kneehang"},
|
||||
{"id": "legswoop", "preset": "legswoop"},
|
||||
{"id": "split", "preset": "split"},
|
||||
{"id": "foothang", "preset": "foothang"},
|
||||
{"id": "end", "preset": "end"}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,34 +1,109 @@
|
||||
"""
|
||||
XIAO ESP32-C6: ESPNOW -> UART passthrough to Pico.
|
||||
Receives messages via ESPNOW, forwards them unchanged to UART (GPIO17).
|
||||
UART at 921600 baud. LED on GPIO15 blinks on activity.
|
||||
"""
|
||||
import network
|
||||
import espnow
|
||||
import machine
|
||||
import time
|
||||
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
|
||||
|
||||
# UART: TX on GPIO17 -> Pico RX, max baud for throughput
|
||||
UART_BAUD = 921600
|
||||
uart = machine.UART(1, baudrate=UART_BAUD, tx=17)
|
||||
led = machine.Pin(15, machine.Pin.OUT)
|
||||
# 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 []
|
||||
|
||||
# WLAN must be active for ESPNOW (no need to connect)
|
||||
sta = network.WLAN(network.WLAN.IF_STA)
|
||||
sta.active(True)
|
||||
sta.disconnect()
|
||||
|
||||
e = espnow.ESPNow()
|
||||
e.active(True)
|
||||
# No peers needed to receive; add_peer() only for send()
|
||||
def _save_buttons(buttons):
|
||||
try:
|
||||
with open("buttons.json", "w") as f:
|
||||
json.dump({"buttons": buttons}, f)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# Recv timeout 0 = non-blocking
|
||||
print("ESP32: ESPNOW -> UART passthrough, %d baud" % UART_BAUD)
|
||||
while True:
|
||||
mac, msg = e.irecv(0)
|
||||
if msg:
|
||||
uart.write(msg)
|
||||
led.value(1)
|
||||
else:
|
||||
led.value(0)
|
||||
time.sleep_ms(1)
|
||||
|
||||
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 */
|
||||
145
esp32/src/templates/index.html
Normal file
145
esp32/src/templates/index.html
Normal file
@@ -0,0 +1,145 @@
|
||||
{% 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="point">point</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>
|
||||
@@ -1,70 +1,70 @@
|
||||
|
||||
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, 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)
|
||||
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 = 293, 2, 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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, 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)
|
||||
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 = 293, 2, 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
148
pico/src/main.py
148
pico/src/main.py
@@ -1,99 +1,63 @@
|
||||
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
|
||||
from machine import UART, Pin
|
||||
import json
|
||||
from presets import Presets
|
||||
import gc
|
||||
|
||||
# --- Rainbow pattern (outside ws2812): pregen double buffer, show via head offset ---
|
||||
uart = UART(0, baudrate=921600, rx=Pin(1, Pin.IN))
|
||||
|
||||
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))
|
||||
presets = Presets()
|
||||
presets.load()
|
||||
|
||||
print(presets.presets.keys())
|
||||
|
||||
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)
|
||||
presets.select("off")
|
||||
|
||||
#print memory usage
|
||||
print(f"Memory usage: {gc.mem_free()/1024} kB free")
|
||||
|
||||
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, 270),
|
||||
(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=0.2) # 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
|
||||
|
||||
i = 0
|
||||
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)
|
||||
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")
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import asyncio
|
||||
import aioespnow
|
||||
import json
|
||||
|
||||
async def p2p(settings, patterns):
|
||||
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
|
||||
e.active(True)
|
||||
async for mac, msg in e:
|
||||
try:
|
||||
data = json.loads(msg)
|
||||
except:
|
||||
print(f"Failed to load espnow data {msg}")
|
||||
continue
|
||||
|
||||
if "names" not in data or settings.get("name") in data.get("names", []):
|
||||
await settings.set_settings(data.get("settings", {}), patterns, data.get("save", False))
|
||||
@@ -4,3 +4,32 @@ from .pulse import Pulse
|
||||
from .transition import Transition
|
||||
from .chase import Chase
|
||||
from .circle import Circle
|
||||
from .roll import Roll
|
||||
from .calibration import Calibration
|
||||
from .test import Test
|
||||
from .grab import Grab
|
||||
from .spin import Spin
|
||||
from .lift import Lift
|
||||
from .flare import Flare
|
||||
from .hook import Hook
|
||||
from .invertsplit import Invertsplit
|
||||
from .pose import Pose
|
||||
from .backbalance import Backbalance
|
||||
from .beat import Beat
|
||||
from .crouch import Crouch
|
||||
from .backbendsplit import Backbendsplit
|
||||
from .straddle import Straddle
|
||||
from .frontbalance import Frontbalance
|
||||
from .elbowhang import Elbowhang
|
||||
from .elbowhangspin import Elbowhangspin
|
||||
from .dismount import Dismount
|
||||
from .fluff import Fluff
|
||||
from .elbowhangsplit import Elbowhangsplit
|
||||
from .invert import Invert
|
||||
from .backbend import Backbend
|
||||
from .seat import Seat
|
||||
from .kneehang import Kneehang
|
||||
from .legswoop import Legswoop
|
||||
from .split import Split
|
||||
from .foothang import Foothang
|
||||
from .point import Point
|
||||
|
||||
9
pico/src/patterns/backbalance.py
Normal file
9
pico/src/patterns/backbalance.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbalance:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/backbend.py
Normal file
9
pico/src/patterns/backbend.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbend:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/backbendsplit.py
Normal file
9
pico/src/patterns/backbendsplit.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbendsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/beat.py
Normal file
9
pico/src/patterns/beat.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Beat:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
38
pico/src/patterns/calibration.py
Normal file
38
pico/src/patterns/calibration.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Calibration: strips 2 and 6 only. First 10 green, then alternating 10 blue / 10 red. 10% brightness."""
|
||||
|
||||
BRIGHTNESS = 0.10
|
||||
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)
|
||||
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()
|
||||
9
pico/src/patterns/crouch.py
Normal file
9
pico/src/patterns/crouch.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Crouch:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/dismount.py
Normal file
9
pico/src/patterns/dismount.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Dismount:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/elbowhang.py
Normal file
9
pico/src/patterns/elbowhang.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/elbowhangspin.py
Normal file
9
pico/src/patterns/elbowhangspin.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhangspin:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/elbowhangsplit.py
Normal file
9
pico/src/patterns/elbowhangsplit.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhangsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
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
|
||||
9
pico/src/patterns/fluff.py
Normal file
9
pico/src/patterns/fluff.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Fluff:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/foothang.py
Normal file
9
pico/src/patterns/foothang.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Foothang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/frontbalance.py
Normal file
9
pico/src/patterns/frontbalance.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Frontbalance:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
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
|
||||
9
pico/src/patterns/invert.py
Normal file
9
pico/src/patterns/invert.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Invert:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/invertsplit.py
Normal file
9
pico/src/patterns/invertsplit.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Invertsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/kneehang.py
Normal file
9
pico/src/patterns/kneehang.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Kneehang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/legswoop.py
Normal file
9
pico/src/patterns/legswoop.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Legswoop:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
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
|
||||
68
pico/src/patterns/point.py
Normal file
68
pico/src/patterns/point.py
Normal file
@@ -0,0 +1,68 @@
|
||||
class Point:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""
|
||||
Point pattern: color bands defined by n ranges.
|
||||
|
||||
- n1–n2: LEDs with color1 (c[0])
|
||||
- n3–n4: LEDs with color2 (c[1])
|
||||
- n5–n6: LEDs with color3 (c[2])
|
||||
- n7–n8: LEDs with color4 (c[3])
|
||||
|
||||
All indices are along the logical ring (driver.n), inclusive ranges.
|
||||
"""
|
||||
num_leds = self.driver.num_leds
|
||||
|
||||
# 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):
|
||||
a = int(a)
|
||||
b = int(b)
|
||||
if a > b:
|
||||
a, b = b, a
|
||||
if b < 0 or a >= num_leds:
|
||||
return None
|
||||
a = max(0, a)
|
||||
b = min(num_leds - 1, b)
|
||||
if a > b:
|
||||
return None
|
||||
return a, b
|
||||
|
||||
ranges = []
|
||||
r1 = norm_range(getattr(preset, "n1", 0), getattr(preset, "n2", -1))
|
||||
if r1:
|
||||
ranges.append((r1[0], r1[1], c1))
|
||||
r2 = norm_range(getattr(preset, "n3", 0), getattr(preset, "n4", -1))
|
||||
if r2:
|
||||
ranges.append((r2[0], r2[1], c2))
|
||||
r3 = norm_range(getattr(preset, "n5", 0), getattr(preset, "n6", -1))
|
||||
if r3:
|
||||
ranges.append((r3[0], r3[1], c3))
|
||||
r4 = norm_range(getattr(preset, "n7", 0), getattr(preset, "n8", -1))
|
||||
if r4:
|
||||
ranges.append((r4[0], r4[1], c4))
|
||||
|
||||
# Static draw: last range wins on overlaps
|
||||
for i in range(num_leds):
|
||||
color = (0, 0, 0)
|
||||
for start, end, c in ranges:
|
||||
if start <= i <= end:
|
||||
color = c
|
||||
self.driver.n[i] = color
|
||||
self.driver.n.write()
|
||||
|
||||
while True:
|
||||
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
|
||||
@@ -1,51 +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
|
||||
|
||||
def _wheel(self, pos):
|
||||
if pos < 85:
|
||||
return (pos * 3, 255 - pos * 3, 0)
|
||||
elif pos < 170:
|
||||
pos -= 85
|
||||
return (255 - pos * 3, 0, pos * 3)
|
||||
else:
|
||||
pos -= 170
|
||||
return (0, pos * 3, 255 - pos * 3)
|
||||
self._buffers_cache = {}
|
||||
|
||||
def run(self, preset):
|
||||
step = self.driver.step % 256
|
||||
step_amount = max(1, int(preset.n1)) # n1 controls step increment
|
||||
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
|
||||
|
||||
# If auto is False, run a single step and then stop
|
||||
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:
|
||||
for i in range(self.driver.num_leds):
|
||||
rc_index = (i * 256 // self.driver.num_leds) + step
|
||||
self.driver.n[i] = self.driver.apply_brightness(self._wheel(rc_index & 255), preset.b)
|
||||
self.driver.n.write()
|
||||
# Increment step by n1 for next manual call
|
||||
self.driver.step = (step + step_amount) % 256
|
||||
# Allow tick() to advance the generator once
|
||||
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()
|
||||
sleep_ms = max(1, int(preset.d)) # Get delay from preset
|
||||
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
|
||||
for i in range(self.driver.num_leds):
|
||||
rc_index = (i * 256 // self.driver.num_leds) + step
|
||||
self.driver.n[i] = self.driver.apply_brightness(
|
||||
self._wheel(rc_index & 255),
|
||||
preset.b,
|
||||
)
|
||||
self.driver.n.write()
|
||||
step = (step + step_amount) % 256
|
||||
self.driver.step = step
|
||||
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 once per tick so other logic can run
|
||||
yield
|
||||
|
||||
154
pico/src/patterns/roll.py
Normal file
154
pico/src/patterns/roll.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Roll:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Roll: moving band with gradient from color1 to color2 over the strips.
|
||||
|
||||
- n1: offset from start of strip (effective start = start + n1)
|
||||
- n2: offset from end of strip (effective end = end - n2, inclusive)
|
||||
- n3: number of full rotations before stopping (0 = infinite)
|
||||
- n4: direction (0 = clockwise, 1 = anti-clockwise)
|
||||
- c[0]: color1 at the head strip
|
||||
- c[1]: color2 at the tail strip
|
||||
"""
|
||||
colors = preset.c
|
||||
color1_raw = colors[0] if colors else (255, 255, 255)
|
||||
color2_raw = colors[1] if len(colors) > 1 else (0, 0, 0)
|
||||
color1 = self.driver.apply_brightness(color1_raw, preset.b)
|
||||
color2 = self.driver.apply_brightness(color2_raw, preset.b)
|
||||
|
||||
n_segments = self.driver.n.num_strips if hasattr(self.driver.n, "num_strips") else 1
|
||||
# Margins from the start and end of each strip
|
||||
start_margin = max(0, int(getattr(preset, "n1", 0)))
|
||||
end_margin = max(0, int(getattr(preset, "n2", 0)))
|
||||
|
||||
# Debug info to see why roll might be black
|
||||
try:
|
||||
print(
|
||||
"ROLL preset",
|
||||
"p=", getattr(preset, "p", None),
|
||||
"b=", getattr(preset, "b", None),
|
||||
"colors_raw=", color1_raw, color2_raw,
|
||||
"colors_bright=", color1, color2,
|
||||
)
|
||||
print(
|
||||
"ROLL n1..n4",
|
||||
getattr(preset, "n1", None),
|
||||
getattr(preset, "n2", None),
|
||||
getattr(preset, "n3", None),
|
||||
getattr(preset, "n4", None),
|
||||
"n_segments=", n_segments,
|
||||
"start_margin=", start_margin,
|
||||
"end_margin=", end_margin,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# n3: number of rotations (0 = infinite)
|
||||
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
|
||||
delay_ms = max(1, int(preset.d) or 1)
|
||||
last_update = utime.ticks_ms()
|
||||
rotations_done = 0
|
||||
|
||||
def scale_color(c, f):
|
||||
return tuple(int(x * f) for x in c)
|
||||
|
||||
def lerp_color(c1, c2, t):
|
||||
"""Linear gradient between two colors."""
|
||||
if t <= 0:
|
||||
return c1
|
||||
if t >= 1:
|
||||
return c2
|
||||
return (
|
||||
int(c1[0] + (c2[0] - c1[0]) * t),
|
||||
int(c1[1] + (c2[1] - c1[1]) * t),
|
||||
int(c1[2] + (c2[2] - c1[2]) * t),
|
||||
)
|
||||
|
||||
def draw(head):
|
||||
# Remember head strip for flare
|
||||
try:
|
||||
self.driver.last_roll_head = head
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
strips_list = self.driver.strips
|
||||
|
||||
for strip_idx, strip in enumerate(strips_list):
|
||||
if strip_idx < 0 or strip_idx >= n_segments:
|
||||
continue
|
||||
|
||||
# Distance from head along direction, 0..n_segments-1
|
||||
if clockwise:
|
||||
dist = (head - strip_idx) % n_segments
|
||||
else:
|
||||
dist = (strip_idx - head) % n_segments
|
||||
|
||||
# Color gradient from color1 at the head strip to color2 at the tail strip
|
||||
if n_segments > 1:
|
||||
t = dist / (n_segments - 1)
|
||||
else:
|
||||
t = 0.0
|
||||
c_strip = lerp_color(color1, color2, t)
|
||||
|
||||
n = strip.num_leds
|
||||
# Effective segment per strip:
|
||||
# start = 0 + start_margin
|
||||
# end = (n - 1) - end_margin (inclusive)
|
||||
width = n - start_margin - end_margin
|
||||
if width <= 0:
|
||||
# If margins are too large, fall back to full strip
|
||||
seg_s = 0
|
||||
seg_e = n
|
||||
else:
|
||||
seg_s = max(0, min(n, start_margin))
|
||||
seg_e = min(n, n - end_margin)
|
||||
|
||||
# Debug for first strip/head to see segment
|
||||
try:
|
||||
if strip_idx == 0 and head == 0:
|
||||
print("ROLL seg strip0 n=", n, "seg_s=", seg_s, "seg_e=", seg_e)
|
||||
except Exception:
|
||||
pass
|
||||
for i in range(n):
|
||||
if seg_s <= i < seg_e:
|
||||
strip.set(i, c_strip)
|
||||
else:
|
||||
strip.set(i, (0, 0, 0))
|
||||
strip.show()
|
||||
|
||||
if not preset.a:
|
||||
head = step % n_segments if n_segments > 0 else 0
|
||||
draw(head)
|
||||
self.driver.step = step + 1
|
||||
yield
|
||||
return
|
||||
|
||||
while True:
|
||||
current_time = utime.ticks_ms()
|
||||
if utime.ticks_diff(current_time, last_update) >= delay_ms:
|
||||
head = step % n_segments if n_segments > 0 else 0
|
||||
if not clockwise and n_segments > 0:
|
||||
head = (n_segments - 1 - head)
|
||||
|
||||
draw(head)
|
||||
step += 1
|
||||
|
||||
if max_rotations > 0 and n_segments > 0 and (step % n_segments) == 0:
|
||||
rotations_done += 1
|
||||
if rotations_done >= max_rotations:
|
||||
self.driver.step = step
|
||||
last_update = current_time
|
||||
return
|
||||
|
||||
self.driver.step = step
|
||||
last_update = current_time
|
||||
yield
|
||||
9
pico/src/patterns/seat.py
Normal file
9
pico/src/patterns/seat.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Seat:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
98
pico/src/patterns/spin.py
Normal file
98
pico/src/patterns/spin.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Spin: continues from Grab — segment (10 each side of center) moves slowly up to the top. Preset color, n1 = rate."""
|
||||
|
||||
import utime
|
||||
|
||||
SPAN = 10 # 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
|
||||
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: inside (strip 1, idx 0) moves slower, outside (strip 5, idx 4) faster
|
||||
step = max(1, rate // 2) if idx == 0 else 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
|
||||
9
pico/src/patterns/split.py
Normal file
9
pico/src/patterns/split.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Split:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/straddle.py
Normal file
9
pico/src/patterns/straddle.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Straddle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
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()
|
||||
@@ -12,6 +12,8 @@ class Preset:
|
||||
self.n4 = 0
|
||||
self.n5 = 0
|
||||
self.n6 = 0
|
||||
self.n7 = 0
|
||||
self.n8 = 0
|
||||
|
||||
# Override defaults with provided data
|
||||
self.edit(data)
|
||||
@@ -76,4 +78,6 @@ class Preset:
|
||||
"n4": self.n4,
|
||||
"n5": self.n5,
|
||||
"n6": self.n6,
|
||||
"n7": self.n7,
|
||||
"n8": self.n8,
|
||||
}
|
||||
|
||||
618
pico/src/presets.json
Normal file
618
pico/src/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
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,92 @@
|
||||
from machine import Pin
|
||||
from ws2812 import WS2812B
|
||||
from preset import Preset
|
||||
from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle
|
||||
from patterns import (
|
||||
Blink, Rainbow, Pulse, Transition, Chase, Circle, Roll, Calibration, Test,
|
||||
Grab, Spin, Lift, Flare, Hook, Invertsplit, Pose,
|
||||
Backbalance, Beat, Crouch, Backbendsplit, Straddle,
|
||||
Frontbalance, Elbowhang, Elbowhangspin, Dismount, Fluff,
|
||||
Elbowhangsplit, Invert, Backbend, Seat, Kneehang,
|
||||
Legswoop, Split, Foothang, Point,
|
||||
)
|
||||
import json
|
||||
|
||||
# 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 StripRing:
|
||||
"""Treat multiple WS2812B strips as one logical ring. Equal weight per strip (scale by strip length)."""
|
||||
|
||||
def __init__(self, strips):
|
||||
self.strips = strips
|
||||
self._cumul = [0]
|
||||
for s in strips:
|
||||
self._cumul.append(self._cumul[-1] + s.num_leds)
|
||||
self.num_leds = self._cumul[-1]
|
||||
self.num_strips = len(strips)
|
||||
|
||||
def _strip_and_local(self, i):
|
||||
if i < 0 or i >= self.num_leds:
|
||||
raise IndexError(i)
|
||||
for s in range(len(self.strips)):
|
||||
if i < self._cumul[s + 1]:
|
||||
return s, i - self._cumul[s]
|
||||
return len(self.strips) - 1, i - self._cumul[-2]
|
||||
|
||||
def __setitem__(self, i, color):
|
||||
s, local = self._strip_and_local(i)
|
||||
self.strips[s].set(local, color)
|
||||
|
||||
def fill(self, color):
|
||||
for s in self.strips:
|
||||
s.fill(color)
|
||||
|
||||
def write(self):
|
||||
for s in self.strips:
|
||||
s.show()
|
||||
|
||||
def position(self, i):
|
||||
"""Normalized position 0..1 with equal span per strip (longer strips get same angular span)."""
|
||||
s, local = self._strip_and_local(i)
|
||||
strip_len = self.strips[s].num_leds
|
||||
frac = (local / strip_len) if strip_len else 0
|
||||
return (s + frac) / self.num_strips
|
||||
|
||||
def segment(self, i):
|
||||
"""Segment index 0..num_strips-1 (strip index) so segments align with physical strips."""
|
||||
s, _ = self._strip_and_local(i)
|
||||
return s
|
||||
|
||||
|
||||
class Presets:
|
||||
def __init__(self, pin, num_leds, state_machine=0):
|
||||
def __init__(self):
|
||||
|
||||
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
|
||||
# Single logical strip over all 8 strips for patterns (n[i], .fill(), .write())
|
||||
self.n = StripRing(self.strips)
|
||||
self.num_leds = self.n.num_leds
|
||||
# WS2812B with brightness=1.0 so Presets.apply_brightness() does all scaling (NeoPixel drop-in)
|
||||
num_leds = int(num_leds)
|
||||
if isinstance(pin, Pin):
|
||||
self.n = WS2812B(pin, num_leds) # NeoPixel-style (Pin, n)
|
||||
else:
|
||||
self.n = WS2812B(num_leds, int(pin), state_machine, brightness=1.0)
|
||||
self.num_leds = num_leds
|
||||
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
|
||||
|
||||
@@ -32,8 +104,79 @@ class Presets:
|
||||
"transition": Transition(self).run,
|
||||
"chase": Chase(self).run,
|
||||
"circle": Circle(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,
|
||||
"invertsplit": Invertsplit(self).run,
|
||||
"pose": Pose(self).run,
|
||||
"backbalance": Backbalance(self).run,
|
||||
"beat": Beat(self).run,
|
||||
"crouch": Crouch(self).run,
|
||||
"backbendsplit": Backbendsplit(self).run,
|
||||
"straddle": Straddle(self).run,
|
||||
"frontbalance": Frontbalance(self).run,
|
||||
"elbowhang": Elbowhang(self).run,
|
||||
"elbowhangspin": Elbowhangspin(self).run,
|
||||
"dismount": Dismount(self).run,
|
||||
"fluff": Fluff(self).run,
|
||||
"elbowhangsplit": Elbowhangsplit(self).run,
|
||||
"invert": Invert(self).run,
|
||||
"backbend": Backbend(self).run,
|
||||
"seat": Seat(self).run,
|
||||
"kneehang": Kneehang(self).run,
|
||||
"legswoop": Legswoop(self).run,
|
||||
"split": Split(self).run,
|
||||
"foothang": Foothang(self).run,
|
||||
"point": Point(self).run,
|
||||
}
|
||||
|
||||
# --- 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 strip_index_to_angle(self, strip_idx, index):
|
||||
"""Map an LED index on a given strip to a normalized angle 0..1.
|
||||
|
||||
This accounts for different strip lengths so that the same angle value
|
||||
corresponds to the same physical angle on all concentric strips.
|
||||
"""
|
||||
n = self.strip_length(strip_idx)
|
||||
if n <= 0:
|
||||
return 0.0
|
||||
index = int(index) % n
|
||||
return index / float(n)
|
||||
|
||||
def strip_angle_to_index(self, strip_idx, angle):
|
||||
"""Map a normalized angle 0..1 to an LED index on a given strip.
|
||||
|
||||
Use this when you want patterns to align by angle instead of raw index,
|
||||
despite strips having different circumferences.
|
||||
"""
|
||||
n = self.strip_length(strip_idx)
|
||||
if n <= 0:
|
||||
return 0
|
||||
# Normalize angle into [0,1)
|
||||
angle = float(angle)
|
||||
angle = angle - int(angle)
|
||||
if angle < 0.0:
|
||||
angle += 1.0
|
||||
return int(angle * n) % n
|
||||
|
||||
def remap_strip_index(self, src_strip_idx, dst_strip_idx, src_index):
|
||||
"""Remap an index from one strip to another so they align by angle."""
|
||||
angle = self.strip_index_to_angle(src_strip_idx, src_index)
|
||||
return self.strip_angle_to_index(dst_strip_idx, angle)
|
||||
|
||||
|
||||
def save(self):
|
||||
"""Save the presets to a file."""
|
||||
with open("presets.json", "w") as f:
|
||||
@@ -87,19 +230,26 @@ class Presets:
|
||||
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]
|
||||
if preset.p in self.patterns:
|
||||
# Set step value if explicitly provided
|
||||
if step is not None:
|
||||
self.step = step
|
||||
elif preset.p == "off" or self.selected != preset_name:
|
||||
self.step = 0
|
||||
self.generator = self.patterns[preset.p](preset)
|
||||
self.selected = preset_name # Store the preset name, not the object
|
||||
return True
|
||||
# If preset doesn't exist or pattern not found, default to "off"
|
||||
return False
|
||||
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 update_num_leds(self, pin, num_leds):
|
||||
num_leds = int(num_leds)
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import json
|
||||
import ubinascii
|
||||
import machine
|
||||
|
||||
class Settings(dict):
|
||||
SETTINGS_FILE = "/settings.json"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.load() # Load settings from file during initialization
|
||||
self.color_order = self.get_color_order(self["color_order"])
|
||||
|
||||
def _default_name(self):
|
||||
"""Device name: use unique_id on Pico (no WiFi); use AP MAC on ESP32."""
|
||||
try:
|
||||
import network
|
||||
mac = network.WLAN(network.AP_IF).config("mac")
|
||||
return "led-%s" % ubinascii.hexlify(mac).decode()
|
||||
except Exception:
|
||||
return "led-%s" % ubinascii.hexlify(machine.unique_id()).decode()
|
||||
|
||||
def set_defaults(self):
|
||||
self["led_pin"] = 10
|
||||
self["num_leds"] = 50
|
||||
self["color_order"] = "rgb"
|
||||
self["name"] = self._default_name()
|
||||
self["debug"] = False
|
||||
self["startup_preset"] = None
|
||||
self["brightness"] = 255
|
||||
|
||||
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()
|
||||
self.save()
|
||||
|
||||
|
||||
def get_color_order(self, color_order):
|
||||
"""Convert color order string to tuple of hex string indices."""
|
||||
color_orders = {
|
||||
"rgb": (1, 3, 5),
|
||||
"rbg": (1, 5, 3),
|
||||
"grb": (3, 1, 5),
|
||||
"gbr": (3, 5, 1),
|
||||
"brg": (5, 1, 3),
|
||||
"bgr": (5, 3, 1)
|
||||
}
|
||||
return color_orders.get(color_order.lower(), (1, 3, 5)) # Default to RGB
|
||||
|
||||
def get_rgb_channel_order(self, color_order=None):
|
||||
"""Convert color order string to RGB channel indices for reordering tuples.
|
||||
Returns tuple of channel indices: (r_channel, g_channel, b_channel)
|
||||
Example: 'grb' -> (1, 0, 2) means (G, R, B)"""
|
||||
if color_order is None:
|
||||
color_order = self.get("color_order", "rgb")
|
||||
color_order = color_order.lower()
|
||||
# Map hex string positions to RGB channel indices
|
||||
# Position 1 (R in hex) -> channel 0, Position 3 (G) -> channel 1, Position 5 (B) -> channel 2
|
||||
hex_to_channel = {1: 0, 3: 1, 5: 2}
|
||||
hex_indices = self.get_color_order(color_order)
|
||||
return tuple(hex_to_channel[pos] for pos in hex_indices)
|
||||
|
||||
# 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()
|
||||
78
pico/test/chase.py
Normal file
78
pico/test/chase.py
Normal file
@@ -0,0 +1,78 @@
|
||||
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: trail length (0 = single LED), color (R,G,B)
|
||||
TRAIL_LEN = 8
|
||||
CHASE_COLOR = (0, 255, 100) # cyan-green
|
||||
|
||||
|
||||
def make_chase_double(num_leds, cumulative_leds, total_ring_leds, color, trail_len=0):
|
||||
"""Pregenerate strip double buffer: when head shows index b first, that pixel is at
|
||||
distance (2*cumulative_leds - b) % total_ring_leds from chase head. GRB order."""
|
||||
n = 2 * num_leds
|
||||
buf = bytearray(n * 3)
|
||||
for b in range(n):
|
||||
dist = (2 * cumulative_leds - b) % total_ring_leds
|
||||
if dist == 0:
|
||||
r, grn, b_ = color[0], color[1], color[2]
|
||||
elif trail_len and 0 < dist <= trail_len:
|
||||
fade = 1.0 - (dist / (trail_len + 1))
|
||||
r = int(color[0] * fade)
|
||||
grn = int(color[1] * fade)
|
||||
b_ = int(color[2] * fade)
|
||||
else:
|
||||
r = grn = b_ = 0
|
||||
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, CHASE_COLOR, TRAIL_LEN)
|
||||
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 + 1) % total_ring_leds
|
||||
time.sleep_ms(20)
|
||||
@@ -28,13 +28,13 @@ def hue_to_rgb(hue):
|
||||
return (int(r * 255), int(g * 255), int(b * 255))
|
||||
|
||||
|
||||
def make_rainbow_ring(total_leds, brightness=1.0):
|
||||
"""Build one rainbow over the whole ring: 2 full hue cycles over total_leds (GRB).
|
||||
Returns (double_buf, ring_len_bytes). All strips sample from this so phase is continuous."""
|
||||
n = 2 * total_leds
|
||||
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 % total_leds) / total_leds) * 360 * 2
|
||||
hue = (i / n) * 360 * 2
|
||||
r, g, b = hue_to_rgb(hue)
|
||||
g = int(g * brightness) & 0xFF
|
||||
r = int(r * brightness) & 0xFF
|
||||
@@ -43,48 +43,27 @@ def make_rainbow_ring(total_leds, brightness=1.0):
|
||||
double_buf[o] = g
|
||||
double_buf[o + 1] = r
|
||||
double_buf[o + 2] = b
|
||||
ring_len_bytes = total_leds * 3
|
||||
return (double_buf, ring_len_bytes)
|
||||
strip_len = num_leds * 3
|
||||
return (double_buf, strip_len)
|
||||
|
||||
|
||||
def make_strip_rainbow(num_leds, cumulative_leds, total_ring_leds, brightness=1.0):
|
||||
"""Per-strip double buffer: pixel j has hue at global position (cumulative_leds + j) % total_ring_leds.
|
||||
Use same head for all strips: head = rainbow_head % (2*num_leds*3)."""
|
||||
n = 2 * num_leds
|
||||
buf = bytearray(n * 3)
|
||||
for j in range(n):
|
||||
global_pos = (cumulative_leds + j) % total_ring_leds
|
||||
hue = (global_pos / total_ring_leds) * 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 = j * 3
|
||||
buf[o] = g
|
||||
buf[o + 1] = r
|
||||
buf[o + 2] = b
|
||||
strip_len_bytes = num_leds * 3
|
||||
return (buf, strip_len_bytes)
|
||||
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)
|
||||
|
||||
|
||||
def show_rainbow_segment(strip, buf, strip_len_bytes, head):
|
||||
"""DMA reads strip's segment from buf at head."""
|
||||
strip.show(buf, head)
|
||||
|
||||
|
||||
# --- Strips + one global ring rainbow (all strips in phase) ---
|
||||
# Each strip can have a different length; one rainbow spans total_ring_leds so hue is continuous.
|
||||
|
||||
# (pin, num_leds) per strip — lengths differ per segment
|
||||
# --- 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 = (
|
||||
(2, 291),
|
||||
(7, 291),
|
||||
(3, 290),
|
||||
(4, 283),
|
||||
(7, 278),
|
||||
(0, 275),
|
||||
(6, 283),
|
||||
(28, 278),
|
||||
(29, 283),
|
||||
(6, 290),
|
||||
(29, 275),
|
||||
(4, 278),
|
||||
(0, 283),
|
||||
(2, 290),
|
||||
)
|
||||
|
||||
strips = []
|
||||
@@ -102,24 +81,19 @@ for ws in strips[:-1]:
|
||||
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
||||
bytes_per_cycle = total_ring_leds * 3
|
||||
|
||||
# Per-strip rainbow buffers: each strip's segment of the ring (same phase, no shared-buffer DMA)
|
||||
# One rainbow double buffer per strip (length = 2 * num_leds for that strip)
|
||||
now = time.ticks_ms()
|
||||
rainbow_data = [
|
||||
make_strip_rainbow(ws.num_leds, cumulative_leds[i], total_ring_leds, ws.brightness)
|
||||
for i, ws in enumerate(strips)
|
||||
]
|
||||
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, (buf, strip_len_bytes)) in enumerate(zip(strips, rainbow_data)):
|
||||
# Same head for all: each strip's buffer is already offset by cumulative_leds[i]
|
||||
double_len_bytes = 2 * strip.num_leds * 3
|
||||
head = rainbow_head % double_len_bytes
|
||||
show_rainbow_segment(strip, buf, strip_len_bytes, head)
|
||||
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)
|
||||
|
||||
81
pico/test/roll_strips.py
Normal file
81
pico/test/roll_strips.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import math
|
||||
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
|
||||
|
||||
# --- Roll: N buffers (length = max strip), gradient full -> off; sequence through strips ---
|
||||
N_BUFFERS = 32 # more buffers = smoother transition
|
||||
|
||||
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
|
||||
|
||||
num_strips = len(strips)
|
||||
max_leds = max(ws.num_leds for ws in strips)
|
||||
# Color when "on" (R, G, B); GRB order in buffer
|
||||
ROLL_COLOR = (0, 255, 120) # cyan-green
|
||||
|
||||
|
||||
def make_gradient_buffers(n_buffers, max_leds, color):
|
||||
"""Create n_buffers buffers, each max_leds long. Buffer 0 = full brightness, last = off.
|
||||
Gradient is logarithmic (perceptually smoother: more steps near full, fewer near off). GRB order."""
|
||||
out = []
|
||||
for j in range(n_buffers):
|
||||
# log gradient: scale = 255 * log(1 + (n - 1 - j)) / log(n) so 255 at j=0, 0 at j=n-1
|
||||
if n_buffers <= 1:
|
||||
scale = 255
|
||||
elif j >= n_buffers - 1:
|
||||
scale = 0
|
||||
else:
|
||||
# 1 + (n_buffers - 1 - j) runs from n_buffers down to 1
|
||||
scale = int(255 * math.log(1 + (n_buffers - 1 - j)) / math.log(n_buffers))
|
||||
scale = min(255, scale)
|
||||
buf = bytearray(max_leds * 3)
|
||||
r = (color[0] * scale) // 255
|
||||
g = (color[1] * scale) // 255
|
||||
b = (color[2] * scale) // 255
|
||||
for i in range(max_leds):
|
||||
o = i * 3
|
||||
buf[o] = g & 0xFF
|
||||
buf[o + 1] = r & 0xFF
|
||||
buf[o + 2] = b & 0xFF
|
||||
out.append(buf)
|
||||
return out
|
||||
|
||||
|
||||
# N buffers: first full, last off, gradient between
|
||||
buffers = make_gradient_buffers(N_BUFFERS, max_leds, ROLL_COLOR)
|
||||
|
||||
step = 0
|
||||
delay_ms = 50
|
||||
# Deadline-based loop: no extra pause at rotation wrap, smooth continuous roll
|
||||
next_ms = time.ticks_ms()
|
||||
|
||||
while True:
|
||||
for i, strip in enumerate(strips):
|
||||
buf_index = (step + i) % N_BUFFERS
|
||||
strip.show(buffers[buf_index], 0)
|
||||
step += 1 # unbounded; wrap only in index so no hitch at cycle end
|
||||
next_ms += delay_ms
|
||||
# Sleep until next frame time (handles drift, no pause at wrap)
|
||||
while time.ticks_diff(next_ms, time.ticks_ms()) > 0:
|
||||
time.sleep_ms(1)
|
||||
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.")
|
||||
Reference in New Issue
Block a user