Compare commits
3 Commits
main
...
ff55e9ddb0
| Author | SHA1 | Date | |
|---|---|---|---|
| ff55e9ddb0 | |||
| ac2013769c | |||
| 6fc22fb4f4 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1 @@
|
|||||||
settings.json
|
settings.json
|
||||||
__pycache__/
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
4
Pipfile
4
Pipfile
@@ -6,10 +6,8 @@ name = "pypi"
|
|||||||
[packages]
|
[packages]
|
||||||
esptool = "*"
|
esptool = "*"
|
||||||
mpremote = "*"
|
mpremote = "*"
|
||||||
pyserial = "*"
|
|
||||||
requests = "*"
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.13"
|
python_version = "3.12"
|
||||||
|
|||||||
661
Pipfile.lock
generated
661
Pipfile.lock
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "eb62a51e8d40130b1e758dc972ead3111fe880e1638e2b7e6037a44b4831dbeb"
|
"sha256": "4cd5ca9be4f69340ea0ed740b1fd86d155de8e4b40efa538af17ae4e1b34334d"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
"python_version": "3.13"
|
"python_version": "3.12"
|
||||||
},
|
},
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
@@ -18,112 +18,142 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"bitarray": {
|
"bitarray": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:004d518fa410e6da43386d20e07b576a41eb417ac67abf9f30fa75e125697199",
|
"sha256:002b73bf4a9f7b3ecb02260bd4dd332a6ee4d7f74ee9779a1ef342a36244d0cf",
|
||||||
"sha256:014df8a9430276862392ac5d471697de042367996c49f32d0008585d2c60755a",
|
"sha256:01e3ba46c2dee6d47a4ab22561a01d8ee6772f681defc9fcb357097a055e48cf",
|
||||||
"sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e",
|
"sha256:03dc877ec286b7f2813185ea6bc5f1f5527fd859e61038d38768883b134e06b3",
|
||||||
"sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3",
|
"sha256:03eeab48f376c3cd988add2b75c20d2d084b6fcc9a164adb0dc390ef152255b4",
|
||||||
"sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e",
|
"sha256:05ee46a734b5110c5ac483815da4379f7622f4316362872ec7c0ed16db4b0148",
|
||||||
"sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4",
|
"sha256:0751596f60f33df66245b2dafa3f7fbe13cb7ac91dd14ead87d8c2eec57cb3ed",
|
||||||
"sha256:0f8069a807a3e6e3c361ce302ece4bf1c3b49962c1726d1d56587e8f48682861",
|
"sha256:08c114cf02a63e13ce6d70bc5b9e7bdcfa8d5db17cece207cfa085c4bc4a7a0c",
|
||||||
"sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5",
|
"sha256:0ed4a87eda16e2f95d536152c5acccae07841fbdda3b9a752f3dbf43e39f4d6b",
|
||||||
"sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521",
|
"sha256:101230b8074919970433ef79866570989157ade3421246d4c3afb7a994fdc614",
|
||||||
"sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d",
|
"sha256:11fcfdf272549a3d876f10d8422bcd5f675750aa746ce04ff04937ec3bb2329e",
|
||||||
"sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55",
|
"sha256:160f449bb91686f8fc9984200e78b8d793b79e382decf7eb1dc9948d7c21b36f",
|
||||||
"sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9",
|
"sha256:16426a843b1bc9c552a7c97d6d7555e69730c2de1e2f560503d3fc0e7f6d8005",
|
||||||
"sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce",
|
"sha256:1f1575cc0f66aa70a0bb5cb57c8d9d1b7d541d920455169c6266919bf804dc20",
|
||||||
"sha256:1a926fa554870642607fd10e66ee25b75fdd9a7ca4bbffa93d424e4ae2bf734a",
|
"sha256:1f7a8fc5085450635a539c47c9fce6d441b4a973686f88fc220aa20e3921fe55",
|
||||||
"sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9",
|
"sha256:1fb0a46ae4b8d244a3fb80c3055717baa3dec6be17938e6871042a8d5b4ce670",
|
||||||
"sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e",
|
"sha256:2965fd8ba31b04c42e4b696fad509dc5ab50663efca6eb06bb3b6d08587f3a09",
|
||||||
"sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b",
|
"sha256:2b524306104c1296f1e91d74ee4ccbeeea621f6a13e44addf0bb630a1839fd72",
|
||||||
"sha256:239578587b9c29469ab61149dda40a2fe714a6a4eca0f8ff9ea9439ec4b7bc30",
|
"sha256:2db04b165a57499fbcfe0eaa2f7752f118552bbcfab2163a43fef8d95f4ae745",
|
||||||
"sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6",
|
"sha256:3092f6bbf4a75b1e6f14a5b1030e27c435f341afeb23987115e45a25cc68ba91",
|
||||||
"sha256:26714898eb0d847aac8af94c4441c9cb50387847d0fe6b9fc4217c086cd68b80",
|
"sha256:30a2fc37698820cbf9b51d5f801219ef4bed828a04f3307072b8f983dc422a0e",
|
||||||
"sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11",
|
"sha256:3110b98c5dfb31dc1cf82d8b0c32e3fa6d6d0b268ff9f2a1599165770c1af80f",
|
||||||
"sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f",
|
"sha256:33f604bffd06b170637f8a48ddcf42074ed1e1980366ac46058e065ce04bfe2a",
|
||||||
"sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25",
|
"sha256:340c524c7c934b61d1985d805bffe7609180fb5d16ece6ce89b51aa535b936f2",
|
||||||
"sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77",
|
"sha256:37a6a8382864a1defb5b370b66a635e04358c7334054457bbbb8645610cd95b2",
|
||||||
"sha256:2fe8c54b15a9cd4f93bc2aaceab354ec65af93370aa1496ba2f9c537a4855ee0",
|
"sha256:3875578748b484638f6ea776f534e9088cfb15eee131aac051036cba40fd5d05",
|
||||||
"sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125",
|
"sha256:38b0261483c59bb39ae9300ad46bf0bbf431ab604266382d986a349c96171b36",
|
||||||
"sha256:31a4ad2b730128e273f1c22300da3e3631f125703e4fee0ac44d385abfb15671",
|
"sha256:3b9a2eb7d2e0e9c2f25256d2663c0a2a4798fe3110e3ddbbb1a7b71740b4de08",
|
||||||
"sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de",
|
"sha256:3bb3cf22c3c03ae698647e6766314149c9cf04aa2018d9f48d5efddc3ced2764",
|
||||||
"sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860",
|
"sha256:3db0648536f3e08afa7ceb928153c39913f98fd50a5c3adf92a4d0d4268f213e",
|
||||||
"sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe",
|
"sha256:3dc654da62b3a3027b7c922f7e9f4b27feaabd5d38b2a98ea98de5e8107c72f2",
|
||||||
"sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d",
|
"sha256:4079857566077f290d35e23ff0e8ba593069c139ae85b0d152b9fa476494f50a",
|
||||||
"sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc",
|
"sha256:44f468fb4857fff86c65bec5e2fb67067789e40dad69258e9bb78fc6a6df49e7",
|
||||||
"sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df",
|
"sha256:45660e2fabcdc1bab9699a468b312f47956300d41d6a2ea91c8f067572aaf38a",
|
||||||
"sha256:46cf239856b87fe1c86dfbb3d459d840a8b1649e7922b1e0bfb6b6464692644a",
|
"sha256:477b9456eb7d70f385dc8f097a1d66ee40771b62e47b3b3e33406dcfbc1c6a3b",
|
||||||
"sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8",
|
"sha256:481239cd0966f965c2b8fa78b88614be5f12a64e7773bb5feecc567d39bb2dd5",
|
||||||
"sha256:4902f4ecd5fcb6a5f482d7b0ae1c16c21f26fc5279b3b6127363d13ad8e7a9d9",
|
"sha256:4a83d247420b147d4b3cba0335e484365e117dc1cfe5ab35acd6a0817ad9244f",
|
||||||
"sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe",
|
"sha256:53d2abeabb91a822e9d76420c9b44980edd2d6b21767c7bb9cb2b1b4cf091049",
|
||||||
"sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607",
|
"sha256:55c31bc3d2c9e48741c812ee5ce4607c6f33e33f339831c214d923ffc7777d21",
|
||||||
"sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf",
|
"sha256:567d6891cb1ddbfd0051fcff3cb1bb86efc82ec818d9c5f98c37d59c1d23cc96",
|
||||||
"sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee",
|
"sha256:57b9df5d38ab49c13eaa9e0152fdfa8501fc23987f6dcf421b73484bfe573918",
|
||||||
"sha256:5338a313f998e1be7267191b7caaae82563b4a2b42b393561055412a34042caa",
|
"sha256:59ddb8a9f47ec807009c69e582d0de1c86c005f9f614557f4cebc7b8ac9b7d28",
|
||||||
"sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954",
|
"sha256:61b9f3cf3a55322baed8f0532b73bce77d688a01446c179392c4056ab74eb551",
|
||||||
"sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a",
|
"sha256:639389b023315596e0293f85999645f47ec3dc28c892e51242dde6176c91486b",
|
||||||
"sha256:58a01ea34057463f7a98a4d6ff40160f65f945e924fec08a5b39e327e372875d",
|
"sha256:64d1143e90299ba8c967324840912a63a903494b1870a52f6675bda53dc332f7",
|
||||||
"sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428",
|
"sha256:6542e1cfe060badd160cd383ad93a84871595c14bb05fb8129f963248affd946",
|
||||||
"sha256:5c5a8a83df95e51f7a7c2b083eaea134cbed39fc42c6aeb2e764ddb7ccccd43e",
|
"sha256:69687ef16d501c9217675af36fa3c68c009c03e184b07d22ba245e5c01d47e6b",
|
||||||
"sha256:5f2fb10518f6b365f5b720e43a529c3b2324ca02932f609631a44edb347d8d54",
|
"sha256:6f7e1cdf0abb11718e655bb258920453b1e89c2315e9019f60f0775704b12a8c",
|
||||||
"sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5",
|
"sha256:7378055c9f456c5bb034ac313d9a9028fc6597619a0b16584099adba5a589fdb",
|
||||||
"sha256:6d70fa9c6d2e955bde8cd327ffc11f2cc34bc21944e5571a46ca501e7eadef24",
|
"sha256:78103afbd0a94ac4c1f0b4014545fd149b968d5ea423aaa3b1f6e2c3fc19423e",
|
||||||
"sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f",
|
"sha256:79038bf1a7b13d243e51f4b6909c6997c2ba2bffc45bcae264704308a2d17198",
|
||||||
"sha256:720963fee259291a88348ae9735d9deb5d334e84a016244f61c89f5a49aa400a",
|
"sha256:795b1760418ab750826420ae24f06f392c08e21dc234f0a369a69cc00444f8ec",
|
||||||
"sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b",
|
"sha256:7998dfb1e9e0255fb8553abb019c3e7f558925de4edc8604243775ff9dd3898d",
|
||||||
"sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70",
|
"sha256:7afc740ad45ee0e0cef055765faf64789c2c183eb4aa3ecb8cecdb4b607396b3",
|
||||||
"sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7",
|
"sha256:7b4a41dc183d7d16750634f65566205990f94144755a39f33da44c0350c3e1a8",
|
||||||
"sha256:7f14d6b303e55bd7d19b28309ef8014370e84a3806c5e452e078e7df7344d97a",
|
"sha256:7f825ebedcad87a2825ddb6cf62f6d7d5b7a56ddaf7c93eef4b974e7ddc16408",
|
||||||
"sha256:7f65bd5d4cdb396295b6aa07f84ca659ac65c5c68b53956a6d95219e304b0ada",
|
"sha256:7f9f9bb2c5cc1f679605ebbeb72f46fc395d850b93fa7de7addd502a1dc66e99",
|
||||||
"sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541",
|
"sha256:7fdf059d4e3acec44f512ebe247718ae511fde632e2b06992022df8e637385a6",
|
||||||
"sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b",
|
"sha256:81e4648c09103bc18f488957c1e0863d2397bab6625c0e6771891f151ee0bd96",
|
||||||
"sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4",
|
"sha256:8489bff00a1f81ac0754355772e76775878c32a42f16f01d427c3645546761c4",
|
||||||
"sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2",
|
"sha256:851398428f5604c53371b72c5e0a28163274264ada4a08cd1eafe65fde1f68d0",
|
||||||
"sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd",
|
"sha256:87a29b8a4cc72af6118954592dcd4e49223420470ccc3f8091c255f6c7330bb1",
|
||||||
"sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646",
|
"sha256:8b8e07374d60040b24d1a158895d9758424db13be63d4b2fe1870e37f9dec009",
|
||||||
"sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89",
|
"sha256:8d4aa56782368269eb9402caf7378b2a5ada6f05eb9c7edc2362be258973fd7e",
|
||||||
"sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa",
|
"sha256:97c448a20aded59727261468873d9b11dfdcce5a6338a359135667d5e3f1d070",
|
||||||
"sha256:94652da1a4ca7cfb69c15dd6986b205e0bd9c63a05029c3b48b4201085f527bd",
|
"sha256:98373c273e01a5a7c17103ecb617de7c9980b7608351d58c72198e3525f0002e",
|
||||||
"sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1",
|
"sha256:98e4a17f55f3cbf6fe06cc79234269572f234467c8355b6758eb252073f78e6b",
|
||||||
"sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb",
|
"sha256:99124e39658b2f72d296819ec03418609dd4f1b275b00289c2f278a19da6f9c0",
|
||||||
"sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220",
|
"sha256:9ad0df7886cb9d6d2ff75e87d323108a0e32bdca5c9918071681864129ce8ea8",
|
||||||
"sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c",
|
"sha256:9bfdfe2e2af434d3f4e47250f693657334e34a7ec557cd703b129a814422b4b8",
|
||||||
"sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310",
|
"sha256:9faa4c6fcb19a31240ad389426699a99df481b6576f7286471e24efbf1b44dfc",
|
||||||
"sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2",
|
"sha256:a048e41e1cb0c1a37021269d02698e30d2a7cc9a0205dd3390e0807745b76dae",
|
||||||
"sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e",
|
"sha256:a05982bb49c73463cb0f0f4bed2d8da82631708a2c2d1926107ba99651b419ec",
|
||||||
"sha256:a358277122456666a8b2a0b9aa04f1b89d34e8aa41d08a6557d693e6abb6667c",
|
"sha256:a23b5f13f9b292004e94b0b13fead4dae79c7512db04dc817ff2c2478298e04a",
|
||||||
"sha256:a60da2f9efbed355edb35a1fb6829148676786c829fad708bb6bb47211b3593a",
|
"sha256:a393b0f881eff94440f72846a6f0f95b983594a0a50af81c41ed18107420d6a7",
|
||||||
"sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a",
|
"sha256:a3b6bd81c77d9925809b714980cd30b1831a86bd090316d37cab124d92af1daf",
|
||||||
"sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594",
|
"sha256:a43f4631ecb87bedc510568fef67db53f2a20c4a5953a9d1e07457e7b1d14911",
|
||||||
"sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8",
|
"sha256:a569c993942ac26c6c590639ed6712c6c9c3f0c8d287a067bf2a60eb615f3c6b",
|
||||||
"sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52",
|
"sha256:a5b89349f05431270d1ccc7321aaab91c42ff33f463868779e502438b7f0e668",
|
||||||
"sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20",
|
"sha256:ac39319e6322c2c093a660c02cea6bb3b1ae53d049b573d4781df8896e443e04",
|
||||||
"sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8",
|
"sha256:acc56700963f63307ac096689d4547e8061028a66bb78b90e42c5da2898898fb",
|
||||||
"sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6",
|
"sha256:b723f9d10f7d8259f010b87fa66e924bb4d67927d9dcff4526a755e9ee84fef4",
|
||||||
"sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9",
|
"sha256:b99a0347bc6131046c19e056a113daa34d7df99f1f45510161bc78bc8461a470",
|
||||||
"sha256:cbba763d99de0255a3e4938f25a8579930ac8aa089233cb2fb2ed7d04d4aff02",
|
"sha256:bc0880011b86f81c5353ce4abaeb2472d942ba2320985166a2a3dd4f783563a9",
|
||||||
"sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425",
|
"sha256:be2f40045432e8aa33d9fd5cb43c91b0c61d77d3d8810f88e84e2e46411c27a7",
|
||||||
"sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d",
|
"sha256:bebb17125373c499beea009cc5bced757bde52bcb3fa1d6335650e6c2d8111d7",
|
||||||
"sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2",
|
"sha256:befac6644c6f304a1b6a7948a04095682849c426cebcc44cb2459aa92d3e1735",
|
||||||
"sha256:d2dbe8a3baf2d842e342e8acb06ae3844765d38df67687c144cdeb71f1bcb5d7",
|
"sha256:c1f4880bcb6fb7a8e2ab89128032b3dcf59e1e877ff4493b11c8bf7c3a5b3df2",
|
||||||
"sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4",
|
"sha256:c3e014f7295b9327fa6f0b3e55a3fd485abac98be145b9597e0cdbb05c44ad07",
|
||||||
"sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096",
|
"sha256:c427dfcce13a8c814556dfe7c110b8ef61b8fab5fca0d856d4890856807321dc",
|
||||||
"sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d",
|
"sha256:c44cf0059633470c6bb415091def546adbeb5dcfa91cc3fcb1ac16593f14e52a",
|
||||||
"sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149",
|
"sha256:c4e04c12f507942f1ddf215cb3a08c244d24051cdd2ba571060166ce8a92be16",
|
||||||
"sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b",
|
"sha256:c65257899bb8faf6a111297b4ff0066324a6b901318582c0453a01422c3bcd5a",
|
||||||
"sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35",
|
"sha256:c6c48cf5a92244ef3df4161c8625ee1890bb3d931db9a9f3b699e61a037cd58a",
|
||||||
"sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773",
|
"sha256:c9bf2bf29854f165a47917b8782b6cf3a7d602971bf454806208d0cbb96f797a",
|
||||||
"sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d",
|
"sha256:ca4b6298c89b92d6b0a67dfc5f98d68ae92b08101d227263ef2033b9c9a03a72",
|
||||||
"sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6",
|
"sha256:cc76ad7453816318d794248fba4032967eaffd992d76e5d1af10ef9d46589770",
|
||||||
"sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741",
|
"sha256:cd7f6bfa2a36fb91b7dec9ddf905716f2ed0c3675d2b63c69b7530c9d211e715",
|
||||||
"sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f",
|
"sha256:d12c45da97b2f31d0233e15f8d68731cfa86264c9f04b2669b9fdf46aaf68e1f",
|
||||||
"sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8",
|
"sha256:d160173efdad8a57c22e422a034196df3d84753672c497aee2f94bd5b128f8dd",
|
||||||
"sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f",
|
"sha256:d2b1ed363a4ef5622dccbf7822f01b51195062c4f382b28c9bd125d046d0324c",
|
||||||
"sha256:f8d3417db5e14a6789073b21ae44439a755289477901901bae378a57b905e148",
|
"sha256:d30e7daaf228e3d69cdd8b02c0dd4199cec034c4b93c80109f56f4675a6db957",
|
||||||
"sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8",
|
"sha256:d3f38373d9b2629dedc559e647010541cc4ec4ad9bea560e2eb1017e6a00d9ef",
|
||||||
"sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97",
|
"sha256:d7e274ac1975e55ebfb8166cce27e13dc99120c1d6ce9e490d7a716b9be9abb5",
|
||||||
"sha256:ff1863f037dad765ef5963efc2e37d399ac023e192a6f2bb394e2377d023cefe"
|
"sha256:d877759842ff9eb16d9c2b8b497953a7d994d4b231c171515f0bf3a2ae185c0c",
|
||||||
|
"sha256:da3dfd2776226e15d3288a3a24c7975f9ee160ba198f2efa66bc28c5ba76d792",
|
||||||
|
"sha256:db0441e80773d747a1ed9edfb9f75e7acb68ce8627583bbb6f770b7ec49f0064",
|
||||||
|
"sha256:dbbaa147cf28b3e87738c624d390a3a9e2a5dfef4316f4c38b4ecaf3155a3eab",
|
||||||
|
"sha256:ddc646cec4899a137c134b13818469e4178a251d77f9f4b23229267e3da78cfb",
|
||||||
|
"sha256:df7cc9584614f495f474a5ded365cf72decbcee4efcdc888d2943f8a794c789e",
|
||||||
|
"sha256:dfde50ae55e075dcd5801e2c3ea0e749c849ed2cbbee991af0f97f1bdbadb2a6",
|
||||||
|
"sha256:e15e70a3cf5bb519e2448524d689c02ff6bcd4750587a517e2bffee06065bf27",
|
||||||
|
"sha256:e3572889fcb87e5ca94add412d8b365dbb7b59773a4362e52caa556e5fd98643",
|
||||||
|
"sha256:e39f5e85e1e3d7d84ac2217cd095b3678306c979e991532df47012880e02215d",
|
||||||
|
"sha256:e501bd27c795105aaba02b5212ecd1bb552ca2ee2ede53e5a8cb74deee0e2052",
|
||||||
|
"sha256:e62892645f6a214eefb58a42c3ed2501af2e40a797844e0e09ec1e400ce75f3d",
|
||||||
|
"sha256:e75eb1734046291c554d9addecca9a8785bdf5d53a64f525569f8549da863dde",
|
||||||
|
"sha256:e84cff8e8fe71903a6cf873fb3c8731df8bd7c1dac878e7a0fe19d8e2ef39aa9",
|
||||||
|
"sha256:ea60cf85b4e5a78b5a41eed3a65abc3839a50d915c6e0f6966cbcf81b85991bd",
|
||||||
|
"sha256:ec3fd30622180cbe2326d48c14a4ab7f98a504b104bdca7dda88b134adad6e31",
|
||||||
|
"sha256:eccc6829035c8b7b391a0aa124fade54932bb937dd1079f2740b9f1bde829226",
|
||||||
|
"sha256:eda67136343db96752e58ef36ac37116f36cba40961e79fd0e9bd858f5a09b38",
|
||||||
|
"sha256:ef5a99a8d1a5c47b4cf85925d1420fc4ee584c98be8efc548651447b3047242f",
|
||||||
|
"sha256:f0795e2be2aa8afd013635f30ffe599cc00f1bbaca2d1d19b6187b4d1c58fb44",
|
||||||
|
"sha256:f31d8c2168bf2a52e4539232392352832c2296e07e0e14b6e06a44da574099ba",
|
||||||
|
"sha256:f41a4b57cbc128a699e9d716a56c90c7fc76554e680fe2962f49cc4d8688b051",
|
||||||
|
"sha256:f583a1fb180a123c00064fab1a3bfb9d43e574b6474be1be3f6469e0331e3e2e",
|
||||||
|
"sha256:f7c531722e8c3901f6bb303db464cac98ab44ed422c0fd0c762baa4a8d49ffa1",
|
||||||
|
"sha256:f8ab90410b2ba5b8276657c66941bcaae556a38be8dd81630a7647e8735f0a20",
|
||||||
|
"sha256:fa05460dc4f57358680b977b4a254d331b24c8beb501319b998625fd6a22654b",
|
||||||
|
"sha256:fbe1ef622748d2edb3dd4fef933b934e90e479f9831dfe31bda3fdc16bf5287f",
|
||||||
|
"sha256:fdb7af369df317527d697c5bb37ab944bb9a17ea1a5e82e47d5c7c638f3ccdd6",
|
||||||
|
"sha256:fe1f1f4010244cb07f6a079854a12e1627e4fb9ea99d672f2ceccaf6653ca514",
|
||||||
|
"sha256:fe2493d3f49e314e573022ead4d8c845c9748979b7eb95e815429fe947c4bde2",
|
||||||
|
"sha256:ffd112646486a31ea5a45aa1eca0e2cd90b6a12f67e848e50349e324c24cc2e7"
|
||||||
],
|
],
|
||||||
"version": "==3.8.0"
|
"version": "==3.7.1"
|
||||||
},
|
},
|
||||||
"bitstring": {
|
"bitstring": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -133,14 +163,6 @@
|
|||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==4.3.1"
|
"version": "==4.3.1"
|
||||||
},
|
},
|
||||||
"certifi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c",
|
|
||||||
"sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"
|
|
||||||
],
|
|
||||||
"markers": "python_version >= '3.7'",
|
|
||||||
"version": "==2026.1.4"
|
|
||||||
},
|
|
||||||
"cffi": {
|
"cffi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
|
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
|
||||||
@@ -228,206 +250,67 @@
|
|||||||
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
|
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
|
||||||
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
|
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'",
|
"markers": "platform_python_implementation != 'PyPy'",
|
||||||
"version": "==2.0.0"
|
"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": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a",
|
"sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202",
|
||||||
"sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"
|
"sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.10'",
|
"markers": "python_version >= '3.10'",
|
||||||
"version": "==8.3.1"
|
"version": "==8.2.1"
|
||||||
},
|
},
|
||||||
"cryptography": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72",
|
"sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34",
|
||||||
"sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235",
|
"sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513",
|
||||||
"sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9",
|
"sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5",
|
||||||
"sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356",
|
"sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c",
|
||||||
"sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257",
|
"sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63",
|
||||||
"sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad",
|
"sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130",
|
||||||
"sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4",
|
"sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae",
|
||||||
"sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c",
|
"sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443",
|
||||||
"sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614",
|
"sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59",
|
||||||
"sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed",
|
"sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee",
|
||||||
"sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31",
|
"sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf",
|
||||||
"sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229",
|
"sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27",
|
||||||
"sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0",
|
"sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde",
|
||||||
"sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731",
|
"sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971",
|
||||||
"sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b",
|
"sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8",
|
||||||
"sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4",
|
"sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339",
|
||||||
"sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4",
|
"sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6",
|
||||||
"sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263",
|
"sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90",
|
||||||
"sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595",
|
"sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691",
|
||||||
"sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1",
|
"sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3",
|
||||||
"sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678",
|
"sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083",
|
||||||
"sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48",
|
"sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6",
|
||||||
"sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76",
|
"sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1",
|
||||||
"sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0",
|
"sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3",
|
||||||
"sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18",
|
"sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8",
|
||||||
"sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d",
|
"sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2",
|
||||||
"sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d",
|
"sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7",
|
||||||
"sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1",
|
"sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141",
|
||||||
"sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981",
|
"sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3",
|
||||||
"sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7",
|
"sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9",
|
||||||
"sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82",
|
"sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4",
|
||||||
"sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2",
|
"sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4",
|
||||||
"sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4",
|
"sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b",
|
||||||
"sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663",
|
"sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252",
|
||||||
"sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c",
|
"sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17",
|
||||||
"sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d",
|
"sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b",
|
||||||
"sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a",
|
"sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd"
|
||||||
"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'",
|
"markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'",
|
||||||
"version": "==46.0.5"
|
"version": "==45.0.7"
|
||||||
},
|
},
|
||||||
"esptool": {
|
"esptool": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2ea9bcd7eb263d380a4fe0170856a10e4c65e3f38c757ebdc73584c8dd8322da"
|
"sha256:05cc4732eb2a9a7766c9e3531f7943d76ff0ca06dc9cd308d1d3d0b72f74aac2"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.10'",
|
"markers": "python_version >= '3.10'",
|
||||||
"version": "==5.1.0"
|
"version": "==5.0.2"
|
||||||
},
|
|
||||||
"idna": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
|
|
||||||
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
|
|
||||||
],
|
|
||||||
"markers": "python_version >= '3.8'",
|
|
||||||
"version": "==3.11"
|
|
||||||
},
|
},
|
||||||
"intelhex": {
|
"intelhex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -454,28 +337,28 @@
|
|||||||
},
|
},
|
||||||
"mpremote": {
|
"mpremote": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4",
|
"sha256:7f347318fb6d3bb8f89401d399a05efba39b51c74f747cebe92d3c6a9a4ee0b4",
|
||||||
"sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5"
|
"sha256:daed9b795fdf98edb0c9c4f7f892bf66f075ec5e728bdcc4ab0915abf23d5d17"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.4'",
|
"markers": "python_version >= '3.4'",
|
||||||
"version": "==1.27.0"
|
"version": "==1.26.0"
|
||||||
},
|
},
|
||||||
"platformdirs": {
|
"platformdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1ed8db354e344c5bb6039cd727f096af975194b508e37177719d562b2b540ee6",
|
"sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85",
|
||||||
"sha256:fd1a5f8599c85d49b9ac7d6e450bc2f1aaf4a23f1fe86d09952fe20ad365cf36"
|
"sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.10'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==4.7.0"
|
"version": "==4.4.0"
|
||||||
},
|
},
|
||||||
"pycparser": {
|
"pycparser": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29",
|
"sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
|
||||||
"sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"
|
"sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
|
||||||
],
|
],
|
||||||
"markers": "implementation_name != 'PyPy'",
|
"markers": "implementation_name != 'PyPy'",
|
||||||
"version": "==3.0"
|
"version": "==2.23"
|
||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -490,87 +373,66 @@
|
|||||||
"sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb",
|
"sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb",
|
||||||
"sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"
|
"sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.5"
|
"version": "==3.5"
|
||||||
},
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c",
|
"sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff",
|
||||||
"sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a",
|
"sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48",
|
||||||
"sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3",
|
"sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086",
|
||||||
"sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956",
|
"sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e",
|
||||||
"sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6",
|
"sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133",
|
||||||
"sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c",
|
"sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5",
|
||||||
"sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65",
|
"sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484",
|
||||||
"sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a",
|
"sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee",
|
||||||
"sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0",
|
"sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5",
|
||||||
"sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b",
|
"sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68",
|
||||||
"sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1",
|
"sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a",
|
||||||
"sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6",
|
"sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf",
|
||||||
"sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7",
|
"sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99",
|
||||||
"sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e",
|
"sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8",
|
||||||
"sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007",
|
"sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85",
|
||||||
"sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310",
|
"sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19",
|
||||||
"sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4",
|
"sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc",
|
||||||
"sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9",
|
"sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a",
|
||||||
"sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295",
|
"sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1",
|
||||||
"sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea",
|
"sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317",
|
||||||
"sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0",
|
"sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c",
|
||||||
"sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e",
|
"sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631",
|
||||||
"sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac",
|
"sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d",
|
||||||
"sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9",
|
"sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652",
|
||||||
"sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7",
|
"sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5",
|
||||||
"sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35",
|
"sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e",
|
||||||
"sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb",
|
"sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b",
|
||||||
"sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b",
|
"sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8",
|
||||||
"sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69",
|
"sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476",
|
||||||
"sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5",
|
"sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706",
|
||||||
"sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b",
|
"sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563",
|
||||||
"sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c",
|
"sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237",
|
||||||
"sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369",
|
"sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b",
|
||||||
"sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd",
|
"sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083",
|
||||||
"sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824",
|
"sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180",
|
||||||
"sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198",
|
"sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425",
|
||||||
"sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065",
|
"sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e",
|
||||||
"sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c",
|
"sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f",
|
||||||
"sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c",
|
"sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725",
|
||||||
"sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764",
|
"sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183",
|
||||||
"sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196",
|
"sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab",
|
||||||
"sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b",
|
"sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774",
|
||||||
"sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00",
|
"sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725",
|
||||||
"sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac",
|
"sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e",
|
||||||
"sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8",
|
"sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5",
|
||||||
"sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e",
|
"sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d",
|
||||||
"sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28",
|
"sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290",
|
||||||
"sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3",
|
"sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44",
|
||||||
"sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5",
|
"sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed",
|
||||||
"sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4",
|
"sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4",
|
||||||
"sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b",
|
"sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba",
|
||||||
"sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf",
|
"sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12",
|
||||||
"sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5",
|
"sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"
|
||||||
"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'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==6.0.3"
|
"version": "==6.0.2"
|
||||||
},
|
},
|
||||||
"reedsolo": {
|
"reedsolo": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -579,38 +441,29 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.7.0"
|
"version": "==1.7.0"
|
||||||
},
|
},
|
||||||
"requests": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6",
|
|
||||||
"sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"markers": "python_version >= '3.9'",
|
|
||||||
"version": "==2.32.5"
|
|
||||||
},
|
|
||||||
"rich": {
|
"rich": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69",
|
"sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f",
|
||||||
"sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"
|
"sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.8.0'",
|
"markers": "python_full_version >= '3.8.0'",
|
||||||
"version": "==14.3.2"
|
"version": "==14.1.0"
|
||||||
},
|
},
|
||||||
"rich-click": {
|
"rich-click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc",
|
"sha256:c3fa81ed8a671a10de65a9e20abf642cfdac6fdb882db1ef465ee33919fbcfe2",
|
||||||
"sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b"
|
"sha256:fd98c0ab9ddc1cf9c0b7463f68daf28b4d0033a74214ceb02f761b3ff2af3136"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==1.9.7"
|
"version": "==1.8.9"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"typing-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed",
|
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
|
||||||
"sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"
|
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==2.6.3"
|
"version": "==4.15.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -1,30 +1,2 @@
|
|||||||
# led-bar
|
# led-bar
|
||||||
|
|
||||||
## Recovery: when the board is stuck and you have to nuke the flash
|
|
||||||
|
|
||||||
**Option A – clear startup files (no reflash)**
|
|
||||||
If the board runs but REPL/Thonny is blocked by `main.py` or `boot.py`, remove them so the next boot drops straight to REPL. From your PC (with the Pico connected via USB):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Remove startup files (paths are on the device; try without colons if one form fails)
|
|
||||||
mpremote fs rm boot.py
|
|
||||||
mpremote fs rm main.py
|
|
||||||
mpremote reset
|
|
||||||
```
|
|
||||||
|
|
||||||
If that fails, try one of these:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mpremote rm boot.py
|
|
||||||
mpremote rm main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Or in **Thonny**: Stop the running program (Ctrl+C or Stop button), then **View → Files**, right‑click the device, delete `boot.py` and `main.py` on the device, then **Tools → Reset**.
|
|
||||||
|
|
||||||
If the board doesn’t respond to serial at all, use Option B.
|
|
||||||
|
|
||||||
**Option B – full flash erase (Pico 2)**
|
|
||||||
1. Unplug the Pico 2.
|
|
||||||
2. Hold **BOOTSEL**, plug USB in, then release BOOTSEL.
|
|
||||||
3. It should mount as a drive. Delete any existing UF2 if you want a clean state.
|
|
||||||
4. Copy the MicroPython UF2 for Pico 2 (RP2350) onto the drive. The board will reboot with a fresh install and empty filesystem.
|
|
||||||
|
|||||||
20
box.scad
Normal file
20
box.scad
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
difference() {
|
||||||
|
cube([30,25,20]);
|
||||||
|
//hoop
|
||||||
|
translate([15,12.5,-905]){
|
||||||
|
rotate([90,0,0])
|
||||||
|
rotate_extrude($fn=300)
|
||||||
|
translate([900,0,0])
|
||||||
|
circle(d=25, $fn=100);
|
||||||
|
};
|
||||||
|
//pico
|
||||||
|
translate([3.25,3.5,0])
|
||||||
|
cube([23.5,18,12]);
|
||||||
|
//pico usb port
|
||||||
|
translate([26.75,8,6.5])
|
||||||
|
cube([3.25,9,5.5]);
|
||||||
|
//wifi
|
||||||
|
translate([2.5,5,12])
|
||||||
|
cube([25,15,5]);
|
||||||
|
|
||||||
|
};
|
||||||
88
dev.py
88
dev.py
@@ -1,91 +1,33 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import serial
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import serial
|
print(sys.argv)
|
||||||
|
|
||||||
|
port = sys.argv[1]
|
||||||
|
|
||||||
def usage() -> None:
|
cmd = sys.argv[1]
|
||||||
print("Usage:")
|
|
||||||
print(" dev.py <port> <device> [src] [lib] [reset] [follow]")
|
|
||||||
print(" e.g. dev.py /dev/ttyUSB0 pico src lib")
|
|
||||||
print(" e.g. dev.py /dev/ttyUSB0 esp32 src reset follow")
|
|
||||||
print(" device: pico | esp32. If no src/lib given, deploys both.")
|
|
||||||
|
|
||||||
|
for cmd in sys.argv[1:]:
|
||||||
def main() -> None:
|
print(cmd)
|
||||||
if len(sys.argv) < 3:
|
match cmd:
|
||||||
usage()
|
|
||||||
return
|
|
||||||
|
|
||||||
port = sys.argv[1]
|
|
||||||
device = sys.argv[2].lower()
|
|
||||||
actions = [a.lower() for a in sys.argv[3:]]
|
|
||||||
|
|
||||||
if port.startswith("/") or (len(port) >= 3 and port.upper().startswith("COM")):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
print("First argument must be serial port (e.g. /dev/ttyUSB0 or COM3).")
|
|
||||||
usage()
|
|
||||||
return
|
|
||||||
|
|
||||||
if device not in ("pico", "esp32"):
|
|
||||||
print("Device must be pico or esp32.")
|
|
||||||
usage()
|
|
||||||
return
|
|
||||||
|
|
||||||
if not actions:
|
|
||||||
actions = ["src", "lib"]
|
|
||||||
|
|
||||||
src_dir = f"{device}/src"
|
|
||||||
lib_dir = f"{device}/lib"
|
|
||||||
|
|
||||||
for a in actions:
|
|
||||||
print(a)
|
|
||||||
match a:
|
|
||||||
case "src":
|
case "src":
|
||||||
if os.path.isdir(src_dir):
|
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":" ], cwd="src")
|
||||||
# Ensure remote directories exist before copying files
|
|
||||||
created_dirs: set[str] = set()
|
|
||||||
for dirpath, _, filenames in os.walk(src_dir):
|
|
||||||
for name in filenames:
|
|
||||||
path = os.path.join(dirpath, name)
|
|
||||||
rel = os.path.relpath(path, src_dir).replace(os.sep, "/")
|
|
||||||
remote_dir = ""
|
|
||||||
if "/" in rel:
|
|
||||||
remote_dir = rel.rsplit("/", 1)[0]
|
|
||||||
if remote_dir and remote_dir not in created_dirs:
|
|
||||||
subprocess.call(
|
|
||||||
["mpremote", "connect", port, "fs", "mkdir", ":" + remote_dir],
|
|
||||||
)
|
|
||||||
created_dirs.add(remote_dir)
|
|
||||||
subprocess.call(
|
|
||||||
["mpremote", "connect", port, "fs", "cp", path, ":" + rel],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(" (no src dir)")
|
|
||||||
case "lib":
|
case "lib":
|
||||||
if os.path.isdir(lib_dir):
|
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":" ])
|
||||||
subprocess.call(
|
case "ls":
|
||||||
["mpremote", "connect", port, "fs", "cp", "-r", lib_dir, ":"],
|
subprocess.call(["mpremote", "connect", port, "fs", "ls", ":" ])
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(" (no lib dir)")
|
|
||||||
case "reset":
|
case "reset":
|
||||||
with serial.Serial(port, baudrate=115200) as ser:
|
with serial.Serial(port, baudrate=115200) as ser:
|
||||||
ser.write(b"\x03\x03\x04")
|
ser.write(b'\x03\x03\x04')
|
||||||
case "follow":
|
case "follow":
|
||||||
with serial.Serial(port, baudrate=115200) as ser:
|
with serial.Serial(port, baudrate=115200) as ser:
|
||||||
while True:
|
while True:
|
||||||
if ser.in_waiting > 0:
|
if ser.in_waiting > 0: # Check if there is data in the buffer
|
||||||
data = ser.readline().decode("utf-8").strip()
|
data = ser.readline().decode('utf-8').strip() # Read and decode the data
|
||||||
print(data)
|
print(data)
|
||||||
case _:
|
|
||||||
print("Unknown action:", a)
|
|
||||||
usage()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
#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())
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
from microdot import Microdot, send_file, Response
|
|
||||||
from microdot.utemplate import Template
|
|
||||||
from microdot.websocket import with_websocket
|
|
||||||
import json
|
|
||||||
from machine import Pin, UART, WDT
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# Load button config: {"buttons": [{"id": "...", "preset": "..."}, ...]}
|
|
||||||
def _load_buttons():
|
|
||||||
try:
|
|
||||||
with open("buttons.json") as f:
|
|
||||||
raw = json.load(f)
|
|
||||||
return raw.get("buttons", [])
|
|
||||||
except (OSError, KeyError, ValueError):
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def _save_buttons(buttons):
|
|
||||||
try:
|
|
||||||
with open("buttons.json", "w") as f:
|
|
||||||
json.dump({"buttons": buttons}, f)
|
|
||||||
return True
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
BUTTONS = _load_buttons()
|
|
||||||
|
|
||||||
uart = UART(1, baudrate=921600, tx=Pin(16, Pin.OUT))
|
|
||||||
|
|
||||||
app = Microdot()
|
|
||||||
Response.default_content_type = 'text/html'
|
|
||||||
|
|
||||||
# Device id used in select payload (e.g. Pico name)
|
|
||||||
DEVICE_ID = "1"
|
|
||||||
|
|
||||||
# All connected WebSocket clients (for broadcasting button updates)
|
|
||||||
_ws_clients = set()
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
async def index_handler(request):
|
|
||||||
return Template('/index.html').render(buttons=BUTTONS, device_id=DEVICE_ID)
|
|
||||||
|
|
||||||
@app.route("/api/buttons", methods=["GET"])
|
|
||||||
async def api_get_buttons(request):
|
|
||||||
return {"buttons": BUTTONS}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/buttons", methods=["POST"])
|
|
||||||
async def api_save_buttons(request):
|
|
||||||
global BUTTONS
|
|
||||||
try:
|
|
||||||
data = request.json or {}
|
|
||||||
buttons = data.get("buttons", [])
|
|
||||||
if not isinstance(buttons, list):
|
|
||||||
return {"ok": False, "error": "buttons must be a list"}, 400
|
|
||||||
if _save_buttons(buttons):
|
|
||||||
BUTTONS = buttons
|
|
||||||
return {"ok": True}
|
|
||||||
return {"ok": False, "error": "save failed"}, 500
|
|
||||||
except Exception as e:
|
|
||||||
return {"ok": False, "error": str(e)}, 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/<path:path>")
|
|
||||||
async def static_handler(request, path):
|
|
||||||
if '..' in path:
|
|
||||||
# Directory traversal is not allowed
|
|
||||||
return 'Not found', 404
|
|
||||||
return send_file('static/' + path)
|
|
||||||
|
|
||||||
@app.route("/ws")
|
|
||||||
@with_websocket
|
|
||||||
async def ws(request, ws):
|
|
||||||
_ws_clients.add(ws)
|
|
||||||
print("WebSocket connection established")
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
data = await ws.receive()
|
|
||||||
if data:
|
|
||||||
# Forward WebSocket message to UART (line-delimited for Pico)
|
|
||||||
payload = data if isinstance(data, bytes) else data.encode("utf-8")
|
|
||||||
uart.write(payload + b"\n")
|
|
||||||
print(data)
|
|
||||||
|
|
||||||
# Broadcast to all other clients so their UIs stay in sync
|
|
||||||
for other in list(_ws_clients):
|
|
||||||
if other is not ws and not other.closed:
|
|
||||||
try:
|
|
||||||
await other.send(data)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
_ws_clients.discard(ws)
|
|
||||||
print("WebSocket connection closed")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
server = asyncio.create_task(app.start_server("0.0.0.0", 80))
|
|
||||||
|
|
||||||
await server
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,617 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
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 */
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
{% args buttons, device_id %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Led Hoop</title>
|
|
||||||
<link rel="stylesheet" href="static/styles.css" />
|
|
||||||
</head>
|
|
||||||
<body data-device-id="{{ device_id }}">
|
|
||||||
<div class="header">
|
|
||||||
<button class="menu-btn" type="button" onclick="toggleMenu()" aria-label="Menu">☰ Menu</button>
|
|
||||||
<div class="menu" id="menu">
|
|
||||||
<button class="menu-item off" type="button" onclick="sendSelect('off', null); closeMenu();">Off</button>
|
|
||||||
<button class="menu-item" type="button" onclick="sendSelect('test', null); closeMenu();">Test</button>
|
|
||||||
<button class="menu-item" type="button" onclick="sendSelect('calibration', null); closeMenu();">Calibration</button>
|
|
||||||
<button class="menu-item" type="button" onclick="closeMenu(); openNewButtonEditor();">Add button</button>
|
|
||||||
<button class="menu-item" type="button" onclick="closeMenu(); saveCurrentButtons();">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="buttons" id="buttonsContainer">
|
|
||||||
{% for btn in buttons %}
|
|
||||||
<button class="btn" type="button" data-preset="{{ btn['preset'] }}" data-id="{{ btn['id'] }}"
|
|
||||||
draggable="true">{{ btn['id'] }}</button>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="preset-editor" id="buttonEditor">
|
|
||||||
<div class="preset-editor-inner">
|
|
||||||
<h2 class="preset-editor-title" id="buttonEditorTitle">Button</h2>
|
|
||||||
<label class="preset-editor-field">
|
|
||||||
<span>Button label</span>
|
|
||||||
<input id="be-label" type="text" placeholder="e.g. grab" />
|
|
||||||
</label>
|
|
||||||
<label class="preset-editor-field">
|
|
||||||
<span>Preset name</span>
|
|
||||||
<input id="be-preset" type="text" placeholder="e.g. grab" />
|
|
||||||
</label>
|
|
||||||
<label class="preset-editor-field">
|
|
||||||
<span>Pattern (p)</span>
|
|
||||||
<select id="be-pattern">
|
|
||||||
<option value="spin">spin</option>
|
|
||||||
<option value="roll">roll</option>
|
|
||||||
<option value="grab">grab</option>
|
|
||||||
<option value="lift">lift</option>
|
|
||||||
<option value="flare">flare</option>
|
|
||||||
<option value="hook">hook</option>
|
|
||||||
<option value="invertsplit">invertsplit</option>
|
|
||||||
<option value="pose">pose</option>
|
|
||||||
<option value="backbalance">backbalance</option>
|
|
||||||
<option value="beat">beat</option>
|
|
||||||
<option value="crouch">crouch</option>
|
|
||||||
<option value="backbendsplit">backbendsplit</option>
|
|
||||||
<option value="straddle">straddle</option>
|
|
||||||
<option value="frontbalance">frontbalance</option>
|
|
||||||
<option value="elbowhang">elbowhang</option>
|
|
||||||
<option value="elbowhangspin">elbowhangspin</option>
|
|
||||||
<option value="dismount">dismount</option>
|
|
||||||
<option value="fluff">fluff</option>
|
|
||||||
<option value="elbowhangsplit">elbowhangsplit</option>
|
|
||||||
<option value="invert">invert</option>
|
|
||||||
<option value="backbend">backbend</option>
|
|
||||||
<option value="seat">seat</option>
|
|
||||||
<option value="kneehang">kneehang</option>
|
|
||||||
<option value="legswoop">legswoop</option>
|
|
||||||
<option value="split">split</option>
|
|
||||||
<option value="foothang">foothang</option>
|
|
||||||
<option value="segments">segments</option>
|
|
||||||
<option value="segments_transition">segments_transition</option>
|
|
||||||
<option value="double_circle">double_circle</option>
|
|
||||||
<option value="off">off</option>
|
|
||||||
<option value="on">on</option>
|
|
||||||
<option value="blink">blink</option>
|
|
||||||
<option value="rainbow">rainbow</option>
|
|
||||||
<option value="pulse">pulse</option>
|
|
||||||
<option value="transition">transition</option>
|
|
||||||
<option value="chase">chase</option>
|
|
||||||
<option value="circle">circle</option>
|
|
||||||
<option value="calibration">calibration</option>
|
|
||||||
<option value="test">test</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<div class="preset-editor-row">
|
|
||||||
<label class="preset-editor-field">
|
|
||||||
<span>Delay (d)</span>
|
|
||||||
<input id="be-delay" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
<label class="preset-editor-field">
|
|
||||||
<span>Brightness (b)</span>
|
|
||||||
<input id="be-brightness" type="number" inputmode="numeric" min="0" max="255" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label class="preset-editor-field">
|
|
||||||
<span>Colors (c)</span>
|
|
||||||
<input id="be-colors" type="text" placeholder="r,g,b; r,g,b (0–255)" />
|
|
||||||
</label>
|
|
||||||
<div class="preset-editor-row">
|
|
||||||
<label class="preset-editor-field small">
|
|
||||||
<span>n1</span>
|
|
||||||
<input id="be-n1" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
<label class="preset-editor-field small">
|
|
||||||
<span>n2</span>
|
|
||||||
<input id="be-n2" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="preset-editor-row">
|
|
||||||
<label class="preset-editor-field small">
|
|
||||||
<span>n3</span>
|
|
||||||
<input id="be-n3" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
<label class="preset-editor-field small">
|
|
||||||
<span>n4</span>
|
|
||||||
<input id="be-n4" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="preset-editor-row">
|
|
||||||
<label class="preset-editor-field small">
|
|
||||||
<span>n5</span>
|
|
||||||
<input id="be-n5" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
<label class="preset-editor-field small">
|
|
||||||
<span>n6</span>
|
|
||||||
<input id="be-n6" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="preset-editor-row">
|
|
||||||
<label class="preset-editor-field small">
|
|
||||||
<span>n7</span>
|
|
||||||
<input id="be-n7" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
<label class="preset-editor-field small">
|
|
||||||
<span>n8</span>
|
|
||||||
<input id="be-n8" type="number" inputmode="numeric" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="preset-editor-actions">
|
|
||||||
<button type="button" class="preset-editor-btn primary" onclick="saveButtonFromEditor()">Save</button>
|
|
||||||
<button type="button" class="preset-editor-btn" onclick="closeButtonEditor()">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="next-btn" type="button" onclick="nextPreset()">Next</button>
|
|
||||||
<script src="static/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
102
main.py
102
main.py
@@ -1,102 +0,0 @@
|
|||||||
# """
|
|
||||||
# Pico: receive led-driver JSON from UART (one message per line). Runs Presets + patterns.
|
|
||||||
# UART RX on D7 (GPIO1). Non-blocking so presets.tick() runs every loop.
|
|
||||||
# """
|
|
||||||
# from settings import Settings
|
|
||||||
# from machine import UART, Pin
|
|
||||||
# import utime
|
|
||||||
# from presets import Presets
|
|
||||||
# from utils import convert_and_reorder_colors
|
|
||||||
# import json
|
|
||||||
|
|
||||||
# # UART (Pico XIAO: D7 = GPIO1)
|
|
||||||
# UART_RX_PIN = 1
|
|
||||||
# UART_BAUD = 115200
|
|
||||||
# UART_ID = 0
|
|
||||||
|
|
||||||
# settings = Settings()
|
|
||||||
# print(settings)
|
|
||||||
|
|
||||||
# presets = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
# presets.load()
|
|
||||||
# presets.b = settings.get("brightness", 255)
|
|
||||||
# startup_preset = settings.get("startup_preset")
|
|
||||||
# if startup_preset:
|
|
||||||
# presets.select(startup_preset)
|
|
||||||
# print("Selected startup preset:", startup_preset)
|
|
||||||
|
|
||||||
# last_brightness_save = 0
|
|
||||||
|
|
||||||
# # Non-blocking UART
|
|
||||||
# uart = UART(UART_ID, baudrate=UART_BAUD, rx=Pin(UART_RX_PIN), rxbuf=512, timeout=0)
|
|
||||||
# uart_buf = bytearray()
|
|
||||||
|
|
||||||
# print("UART RX on pin %s, %s baud (one JSON object per line)" % (UART_RX_PIN, UART_BAUD))
|
|
||||||
|
|
||||||
|
|
||||||
# def process_message(data):
|
|
||||||
# """Handle one JSON message (led-driver protocol: v, b, presets, select, default, save)."""
|
|
||||||
# if data.get("v") != "1":
|
|
||||||
# return
|
|
||||||
# global last_brightness_save
|
|
||||||
# if "b" in data:
|
|
||||||
# try:
|
|
||||||
# presets.b = max(0, min(255, int(data["b"])))
|
|
||||||
# settings["brightness"] = presets.b
|
|
||||||
# now = utime.ticks_ms()
|
|
||||||
# if utime.ticks_diff(now, last_brightness_save) >= 500:
|
|
||||||
# settings.save()
|
|
||||||
# last_brightness_save = now
|
|
||||||
# except (TypeError, ValueError):
|
|
||||||
# pass
|
|
||||||
# if "presets" in data:
|
|
||||||
# for id, preset_data in data["presets"].items():
|
|
||||||
# if "c" in preset_data:
|
|
||||||
# preset_data["c"] = convert_and_reorder_colors(preset_data["c"], settings)
|
|
||||||
# presets.edit(id, preset_data)
|
|
||||||
# print("Edited preset", id, preset_data.get("name", ""))
|
|
||||||
# if settings.get("name") in data.get("select", {}):
|
|
||||||
# select_list = data["select"][settings.get("name")]
|
|
||||||
# if select_list:
|
|
||||||
# preset_name = select_list[0]
|
|
||||||
# step = select_list[1] if len(select_list) > 1 else None
|
|
||||||
# presets.select(preset_name, step=step)
|
|
||||||
# if "default" in data:
|
|
||||||
# settings["startup_preset"] = data["default"]
|
|
||||||
# print("Set startup preset to", data["default"])
|
|
||||||
# settings.save()
|
|
||||||
# if "save" in data:
|
|
||||||
# presets.save()
|
|
||||||
|
|
||||||
|
|
||||||
# while True:
|
|
||||||
# presets.tick()
|
|
||||||
# n = uart.any()
|
|
||||||
# if n:
|
|
||||||
# data_in = uart.read(n)
|
|
||||||
# if data_in:
|
|
||||||
# for b in data_in:
|
|
||||||
# if b in (0x0A, 0x0D): # LF or CR
|
|
||||||
# if uart_buf:
|
|
||||||
# try:
|
|
||||||
# msg = uart_buf.decode("utf-8").strip()
|
|
||||||
# if msg:
|
|
||||||
# data = json.loads(msg)
|
|
||||||
# process_message(data)
|
|
||||||
# except (ValueError, UnicodeError):
|
|
||||||
# pass
|
|
||||||
# uart_buf = bytearray()
|
|
||||||
# else:
|
|
||||||
# if len(uart_buf) < 1024:
|
|
||||||
# uart_buf.append(b)
|
|
||||||
# utime.sleep_ms(1)
|
|
||||||
|
|
||||||
from neopixel import NeoPixel
|
|
||||||
from machine import Pin
|
|
||||||
|
|
||||||
pins = ((2,270), (3,271), (4,272), (0,273), (7,274), (6,275), (29,276), (28,277))
|
|
||||||
for pin, num_leds in pins:
|
|
||||||
print(pin, num_leds)
|
|
||||||
np = NeoPixel(Pin(pin), num_leds)
|
|
||||||
np.fill((8, 0, 0))
|
|
||||||
np.write()
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
from machine import UART, Pin
|
|
||||||
import json
|
|
||||||
from presets import Presets
|
|
||||||
import gc
|
|
||||||
|
|
||||||
uart = UART(0, baudrate=921600, rx=Pin(1, Pin.IN))
|
|
||||||
|
|
||||||
presets = Presets()
|
|
||||||
presets.load()
|
|
||||||
|
|
||||||
print(presets.presets.keys())
|
|
||||||
|
|
||||||
presets.select("off")
|
|
||||||
|
|
||||||
#print memory usage
|
|
||||||
print(f"Memory usage: {gc.mem_free()/1024} kB free")
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while True:
|
|
||||||
presets.tick()
|
|
||||||
if uart.any():
|
|
||||||
data = uart.readline()
|
|
||||||
try:
|
|
||||||
data = json.loads(data)
|
|
||||||
except:
|
|
||||||
# Ignore malformed JSON lines
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Select a preset by name (existing behaviour)
|
|
||||||
preset_name = data.get("select")
|
|
||||||
if preset_name is not None:
|
|
||||||
presets.select(preset_name)
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
# Create or update a preset:
|
|
||||||
# {"preset_edit": {"name": "<name>", "data": {<preset_dict>}}}
|
|
||||||
edit_payload = data.get("preset_edit")
|
|
||||||
if isinstance(edit_payload, dict):
|
|
||||||
name = edit_payload.get("name")
|
|
||||||
preset_data = edit_payload.get("data") or {}
|
|
||||||
if isinstance(name, str) and isinstance(preset_data, dict):
|
|
||||||
# Log the incoming preset payload for debugging
|
|
||||||
print("PRESET_EDIT", name, preset_data)
|
|
||||||
presets.edit(name, preset_data)
|
|
||||||
|
|
||||||
# Delete a preset:
|
|
||||||
# {"preset_delete": "<name>"}
|
|
||||||
delete_name = data.get("preset_delete")
|
|
||||||
if isinstance(delete_name, str):
|
|
||||||
print("PRESET_DELETE", delete_name)
|
|
||||||
presets.delete(delete_name)
|
|
||||||
|
|
||||||
# Persist all presets to flash:
|
|
||||||
# {"preset_save": true}
|
|
||||||
if data.get("preset_save"):
|
|
||||||
print("PRESET_SAVE")
|
|
||||||
presets.save()
|
|
||||||
|
|
||||||
print(data)
|
|
||||||
gc.collect()
|
|
||||||
#print used and free memory
|
|
||||||
print(f"Memory usage: {gc.mem_alloc()/1024} kB used, {gc.mem_free()/1024} kB free")
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from .blink import Blink
|
|
||||||
from .rainbow import Rainbow
|
|
||||||
from .pulse import Pulse
|
|
||||||
from .transition import Transition
|
|
||||||
from .chase import Chase
|
|
||||||
from .circle import Circle
|
|
||||||
from .double_circle import DoubleCircle
|
|
||||||
from .roll import Roll
|
|
||||||
from .calibration import Calibration
|
|
||||||
from .test import Test
|
|
||||||
from .scale_test import ScaleTest
|
|
||||||
from .grab import Grab
|
|
||||||
from .spin import Spin
|
|
||||||
from .lift import Lift
|
|
||||||
from .flare import Flare
|
|
||||||
from .hook import Hook
|
|
||||||
from .pose import Pose
|
|
||||||
from .segments import Segments
|
|
||||||
from .segments_transition import SegmentsTransition
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import utime
|
|
||||||
|
|
||||||
|
|
||||||
class Blink:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
"""Blink pattern: toggles LEDs on/off using preset delay, cycling through colors."""
|
|
||||||
# Use provided colors, or default to white if none
|
|
||||||
colors = preset.c if preset.c else [(255, 255, 255)]
|
|
||||||
color_index = 0
|
|
||||||
state = True # True = on, False = off
|
|
||||||
last_update = utime.ticks_ms()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
current_time = utime.ticks_ms()
|
|
||||||
# Re-read delay each loop so live updates to preset.d take effect
|
|
||||||
delay_ms = max(1, int(preset.d))
|
|
||||||
if utime.ticks_diff(current_time, last_update) >= delay_ms:
|
|
||||||
if state:
|
|
||||||
base_color = colors[color_index % len(colors)]
|
|
||||||
color = self.driver.apply_brightness(base_color, preset.b)
|
|
||||||
self.driver.fill(color)
|
|
||||||
# Advance to next color for the next "on" phase
|
|
||||||
color_index += 1
|
|
||||||
else:
|
|
||||||
# "Off" phase: turn all LEDs off
|
|
||||||
self.driver.fill((0, 0, 0))
|
|
||||||
state = not state
|
|
||||||
last_update = current_time
|
|
||||||
# Yield once per tick so other logic can run
|
|
||||||
yield
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
"""Calibration: strips 2 and 6 only. First 10 green, then alternating 10 blue / 10 red. 10% brightness."""
|
|
||||||
|
|
||||||
BRIGHTNESS = 1
|
|
||||||
BLOCK = 10
|
|
||||||
STRIPS_ON = (2, 6) # 0-based: 3rd and 7th strip only
|
|
||||||
|
|
||||||
GREEN = (0, 255, 0)
|
|
||||||
RED = (255, 0, 0)
|
|
||||||
BLUE = (0, 0, 255)
|
|
||||||
|
|
||||||
|
|
||||||
def _scale(color, factor):
|
|
||||||
return tuple(int(c * factor) for c in color)
|
|
||||||
|
|
||||||
|
|
||||||
class Calibration:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
strips = self.driver.strips
|
|
||||||
green = _scale(GREEN, BRIGHTNESS)
|
|
||||||
red = _scale(RED, BRIGHTNESS)
|
|
||||||
blue = _scale(BLUE, BRIGHTNESS)
|
|
||||||
blue = (0,0,0)
|
|
||||||
on_set = set(STRIPS_ON)
|
|
||||||
for strip_idx, strip in enumerate(strips):
|
|
||||||
n = strip.num_leds
|
|
||||||
if strip_idx not in on_set:
|
|
||||||
strip.fill((0, 0, 0))
|
|
||||||
strip.show()
|
|
||||||
continue
|
|
||||||
for i in range(n):
|
|
||||||
if i < BLOCK:
|
|
||||||
strip.set(i, green)
|
|
||||||
else:
|
|
||||||
block = (i - BLOCK) // BLOCK
|
|
||||||
strip.set(i, blue if block % 2 == 0 else red)
|
|
||||||
strip.show()
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import utime
|
|
||||||
|
|
||||||
|
|
||||||
def _make_chase_double(num_leds, cumulative_leds, total_ring_leds, color0, color1, n1, n2):
|
|
||||||
"""Pregenerate strip double buffer with repeating segments:
|
|
||||||
color0 for n1 pixels, then color1 for n2 pixels, around the full ring. GRB order."""
|
|
||||||
n = 2 * num_leds
|
|
||||||
buf = bytearray(n * 3)
|
|
||||||
pattern_len = n1 + n2
|
|
||||||
for b in range(n):
|
|
||||||
# Position of this pixel along the logical ring
|
|
||||||
pos = (2 * cumulative_leds - b) % total_ring_leds
|
|
||||||
seg_pos = pos % pattern_len
|
|
||||||
if seg_pos < n1:
|
|
||||||
r, g, b_ = color0
|
|
||||||
else:
|
|
||||||
r, g, b_ = color1
|
|
||||||
o = b * 3
|
|
||||||
buf[o] = g
|
|
||||||
buf[o + 1] = r
|
|
||||||
buf[o + 2] = b_
|
|
||||||
strip_len_bytes = num_leds * 3
|
|
||||||
return buf, strip_len_bytes
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_chase_buffers(driver, color0, color1, n1, n2, cache):
|
|
||||||
"""Build or refresh per-strip double buffers for the chase pattern."""
|
|
||||||
strips = driver.strips
|
|
||||||
key = (
|
|
||||||
color0,
|
|
||||||
color1,
|
|
||||||
int(n1),
|
|
||||||
int(n2),
|
|
||||||
tuple(s.num_leds for s in strips),
|
|
||||||
)
|
|
||||||
if cache.get("key") == key and cache.get("data") is not None:
|
|
||||||
return cache["data"], cache["cumulative_leds"], cache["total_ring_leds"]
|
|
||||||
|
|
||||||
if not strips:
|
|
||||||
cache["key"] = key
|
|
||||||
cache["data"] = []
|
|
||||||
cache["cumulative_leds"] = []
|
|
||||||
cache["total_ring_leds"] = 0
|
|
||||||
return cache["data"], cache["cumulative_leds"], cache["total_ring_leds"]
|
|
||||||
|
|
||||||
cumulative_leds = [0]
|
|
||||||
for s in strips[:-1]:
|
|
||||||
cumulative_leds.append(cumulative_leds[-1] + s.num_leds)
|
|
||||||
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
|
||||||
|
|
||||||
chase_data = []
|
|
||||||
for idx, s in enumerate(strips):
|
|
||||||
buf, strip_len_bytes = _make_chase_double(
|
|
||||||
s.num_leds,
|
|
||||||
cumulative_leds[idx],
|
|
||||||
total_ring_leds,
|
|
||||||
color0,
|
|
||||||
color1,
|
|
||||||
n1,
|
|
||||||
n2,
|
|
||||||
)
|
|
||||||
chase_data.append((buf, strip_len_bytes))
|
|
||||||
|
|
||||||
cache["key"] = key
|
|
||||||
cache["data"] = chase_data
|
|
||||||
cache["cumulative_leds"] = cumulative_leds
|
|
||||||
cache["total_ring_leds"] = total_ring_leds
|
|
||||||
return chase_data, cumulative_leds, total_ring_leds
|
|
||||||
|
|
||||||
|
|
||||||
class Chase:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
self._buffers_cache = {}
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating around the full ring.
|
|
||||||
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)."""
|
|
||||||
colors = preset.c or []
|
|
||||||
if not colors:
|
|
||||||
return
|
|
||||||
|
|
||||||
# If only one color provided, use it for both colors
|
|
||||||
if len(colors) < 2:
|
|
||||||
base0 = colors[0]
|
|
||||||
base1 = colors[0]
|
|
||||||
else:
|
|
||||||
base0 = colors[0]
|
|
||||||
base1 = colors[1]
|
|
||||||
|
|
||||||
# Apply preset/global brightness
|
|
||||||
color0 = self.driver.apply_brightness(base0, preset.b)
|
|
||||||
color1 = self.driver.apply_brightness(base1, preset.b)
|
|
||||||
|
|
||||||
n1 = max(1, int(getattr(preset, "n1", 1)) or 1) # LEDs of color 0
|
|
||||||
n2 = max(1, int(getattr(preset, "n2", 1)) or 1) # LEDs of color 1
|
|
||||||
n3 = int(getattr(preset, "n3", 0) or 0) # step on even steps
|
|
||||||
n4 = int(getattr(preset, "n4", 0) or 0) # step on odd steps
|
|
||||||
|
|
||||||
if n3 == 0 and n4 == 0:
|
|
||||||
# Nothing to move; default to simple forward motion
|
|
||||||
n3 = 1
|
|
||||||
n4 = 1
|
|
||||||
|
|
||||||
chase_data, cumulative_leds, total_ring_leds = _ensure_chase_buffers(
|
|
||||||
self.driver, color0, color1, n1, n2, self._buffers_cache
|
|
||||||
)
|
|
||||||
|
|
||||||
strips = self.driver.strips
|
|
||||||
|
|
||||||
def show_frame(chase_pos):
|
|
||||||
for i, (strip, (buf, strip_len_bytes)) in enumerate(zip(strips, chase_data)):
|
|
||||||
# head in [0, strip_len_bytes) so DMA read head..head+strip_len_bytes stays in double buffer
|
|
||||||
head = ((chase_pos + cumulative_leds[i]) * 3) % strip_len_bytes
|
|
||||||
strip.show(buf, head)
|
|
||||||
|
|
||||||
# Helper to compute head position from current step_count
|
|
||||||
def head_from_step(step_count):
|
|
||||||
if step_count % 2 == 0:
|
|
||||||
# Even steps: (step_count//2) pairs of (n3+n4) plus one extra n3
|
|
||||||
pos = (step_count // 2) * (n3 + n4) + n3
|
|
||||||
else:
|
|
||||||
# Odd steps: ((step_count+1)//2) pairs of (n3+n4)
|
|
||||||
pos = ((step_count + 1) // 2) * (n3 + n4)
|
|
||||||
if total_ring_leds <= 0:
|
|
||||||
return 0
|
|
||||||
pos %= total_ring_leds
|
|
||||||
if pos < 0:
|
|
||||||
pos += total_ring_leds
|
|
||||||
return pos
|
|
||||||
|
|
||||||
# Single-step mode: render one frame, then stop
|
|
||||||
if not preset.a:
|
|
||||||
step_count = int(self.driver.step)
|
|
||||||
chase_pos = head_from_step(step_count)
|
|
||||||
|
|
||||||
show_frame(chase_pos)
|
|
||||||
|
|
||||||
# Advance step for next trigger
|
|
||||||
self.driver.step = step_count + 1
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
|
|
||||||
# Auto mode: continuous loop driven by delay d
|
|
||||||
transition_duration = max(10, int(getattr(preset, "d", 50)) or 10)
|
|
||||||
last_update = utime.ticks_ms() - transition_duration
|
|
||||||
|
|
||||||
while True:
|
|
||||||
current_time = utime.ticks_ms()
|
|
||||||
if utime.ticks_diff(current_time, last_update) >= transition_duration:
|
|
||||||
# Rebuild buffers if geometry/colors changed
|
|
||||||
chase_data, cumulative_leds, total_ring_leds = _ensure_chase_buffers(
|
|
||||||
self.driver, color0, color1, n1, n2, self._buffers_cache
|
|
||||||
)
|
|
||||||
|
|
||||||
step_count = int(self.driver.step)
|
|
||||||
chase_pos = head_from_step(step_count)
|
|
||||||
|
|
||||||
show_frame(chase_pos)
|
|
||||||
|
|
||||||
# Advance step for next frame
|
|
||||||
self.driver.step = step_count + 1
|
|
||||||
last_update = current_time
|
|
||||||
|
|
||||||
# Yield once per tick so other logic can run
|
|
||||||
yield
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import utime
|
|
||||||
|
|
||||||
|
|
||||||
class Circle:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
"""Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4"""
|
|
||||||
head = 0
|
|
||||||
tail = 0
|
|
||||||
|
|
||||||
# Calculate timing from preset
|
|
||||||
head_rate = max(1, int(preset.n1)) # n1 = head moves per second
|
|
||||||
tail_rate = max(1, int(preset.n3)) # n3 = tail moves per second
|
|
||||||
max_length = max(1, int(preset.n2)) # n2 = max length
|
|
||||||
min_length = max(0, int(preset.n4)) # n4 = min length
|
|
||||||
|
|
||||||
head_delay = 1000 // head_rate # ms between head movements
|
|
||||||
tail_delay = 1000 // tail_rate # ms between tail movements
|
|
||||||
|
|
||||||
last_head_move = utime.ticks_ms()
|
|
||||||
last_tail_move = utime.ticks_ms()
|
|
||||||
|
|
||||||
phase = "growing" # "growing", "shrinking", or "off"
|
|
||||||
|
|
||||||
# Support up to two colors (like chase). If only one color is provided,
|
|
||||||
# use black for the second; if none, default to white.
|
|
||||||
colors = preset.c
|
|
||||||
if not colors:
|
|
||||||
base0 = base1 = (255, 255, 255)
|
|
||||||
elif len(colors) == 1:
|
|
||||||
base0 = colors[0]
|
|
||||||
base1 = (0, 0, 0)
|
|
||||||
else:
|
|
||||||
base0 = colors[0]
|
|
||||||
base1 = colors[1]
|
|
||||||
|
|
||||||
color0 = self.driver.apply_brightness(base0, preset.b)
|
|
||||||
color1 = self.driver.apply_brightness(base1, preset.b)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
current_time = utime.ticks_ms()
|
|
||||||
|
|
||||||
# Background: use second color during the "off" phase, otherwise clear to black
|
|
||||||
if phase == "off":
|
|
||||||
self.driver.n.fill(color1)
|
|
||||||
else:
|
|
||||||
self.driver.n.fill((0, 0, 0))
|
|
||||||
|
|
||||||
# Calculate segment length
|
|
||||||
segment_length = (head - tail) % self.driver.num_leds
|
|
||||||
if segment_length == 0 and head != tail:
|
|
||||||
segment_length = self.driver.num_leds
|
|
||||||
|
|
||||||
# Draw segment from tail to head as a solid color (no per-LED alternation)
|
|
||||||
current_color = color0
|
|
||||||
for i in range(segment_length + 1):
|
|
||||||
led_pos = (tail + i) % self.driver.num_leds
|
|
||||||
self.driver.n[led_pos] = current_color
|
|
||||||
|
|
||||||
# Move head continuously at n1 LEDs per second
|
|
||||||
if utime.ticks_diff(current_time, last_head_move) >= head_delay:
|
|
||||||
head = (head + 1) % self.driver.num_leds
|
|
||||||
last_head_move = current_time
|
|
||||||
|
|
||||||
# Tail behavior based on phase
|
|
||||||
if phase == "growing":
|
|
||||||
# Growing phase: tail stays at 0 until max length reached
|
|
||||||
if segment_length >= max_length:
|
|
||||||
phase = "shrinking"
|
|
||||||
elif phase == "shrinking":
|
|
||||||
# Shrinking phase: move tail forward at n3 LEDs per second
|
|
||||||
if utime.ticks_diff(current_time, last_tail_move) >= tail_delay:
|
|
||||||
tail = (tail + 1) % self.driver.num_leds
|
|
||||||
last_tail_move = current_time
|
|
||||||
|
|
||||||
# Check if we've reached min length
|
|
||||||
current_length = (head - tail) % self.driver.num_leds
|
|
||||||
if current_length == 0 and head != tail:
|
|
||||||
current_length = self.driver.num_leds
|
|
||||||
|
|
||||||
# For min_length = 0, we need at least 1 LED (the head)
|
|
||||||
if min_length == 0 and current_length <= 1:
|
|
||||||
phase = "off" # All LEDs off for 1 step
|
|
||||||
elif min_length > 0 and current_length <= min_length:
|
|
||||||
phase = "growing" # Cycle repeats
|
|
||||||
else: # phase == "off"
|
|
||||||
# Off phase: second color fills the ring for 1 step, then restart
|
|
||||||
tail = head # Reset tail to head position to start fresh
|
|
||||||
phase = "growing"
|
|
||||||
|
|
||||||
self.driver.n.write()
|
|
||||||
|
|
||||||
# Yield once per tick so other logic can run
|
|
||||||
yield
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
class DoubleCircle:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
"""
|
|
||||||
DoubleCircle: symmetric band around a center index on the logical ring.
|
|
||||||
|
|
||||||
- n1: center index on the logical ring (0-based, on reference strip 0)
|
|
||||||
- n2: radius of the band (max distance from center)
|
|
||||||
- n3: direction mode
|
|
||||||
0 → LEDs start ALL OFF and turn ON n4 LEDs at a time outward from n1 toward n1±n2
|
|
||||||
1 → LEDs start ALL ON within radius n2 and turn OFF n4 LEDs at a time inward toward n1
|
|
||||||
- n4: step size in LEDs per update
|
|
||||||
- c[0]: base color used for the band
|
|
||||||
"""
|
|
||||||
num_leds = self.driver.num_leds
|
|
||||||
if num_leds <= 0:
|
|
||||||
while True:
|
|
||||||
yield
|
|
||||||
|
|
||||||
colors = preset.c or []
|
|
||||||
base1 = colors[0] if len(colors) >= 1 else (255, 255, 255)
|
|
||||||
off = (0, 0, 0)
|
|
||||||
|
|
||||||
# Apply preset/global brightness
|
|
||||||
color_on = self.driver.apply_brightness(base1, preset.b)
|
|
||||||
color_off = off
|
|
||||||
|
|
||||||
# Center index and radius from preset; clamp center to ring length
|
|
||||||
center = int(getattr(preset, "n1", 0)) % num_leds
|
|
||||||
radius = max(1, int(getattr(preset, "n2", 0)) or 1)
|
|
||||||
mode = int(getattr(preset, "n3", 0) or 0) # 0 = grow band outward, 1 = shrink band inward
|
|
||||||
step_size = max(1, int(getattr(preset, "n4", 1)) or 1)
|
|
||||||
|
|
||||||
num_strips = len(self.driver.strips)
|
|
||||||
|
|
||||||
# Current "front" of the band, as a distance from center
|
|
||||||
# mode 0: grow band outward (0 → radius)
|
|
||||||
# mode 1: shrink band inward (radius → 0)
|
|
||||||
if mode == 0:
|
|
||||||
current = 0
|
|
||||||
else:
|
|
||||||
current = radius
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# Draw current frame based on current radius
|
|
||||||
for i in range(num_leds):
|
|
||||||
# Shortest circular distance from i to center
|
|
||||||
forward = (i - center) % num_leds
|
|
||||||
backward = (center - i) % num_leds
|
|
||||||
dist = forward if forward < backward else backward
|
|
||||||
|
|
||||||
if dist > radius:
|
|
||||||
c = color_off
|
|
||||||
else:
|
|
||||||
if mode == 0:
|
|
||||||
# Grow outward: lit if within current radius
|
|
||||||
c = color_on if dist <= current else color_off
|
|
||||||
else:
|
|
||||||
# Shrink inward: lit if within current radius (band contracts toward center)
|
|
||||||
c = color_on if dist <= current else color_off
|
|
||||||
|
|
||||||
for strip_idx in range(num_strips):
|
|
||||||
self.driver.set(strip_idx, i, c)
|
|
||||||
|
|
||||||
self.driver.show_all()
|
|
||||||
|
|
||||||
# Update current radius for next frame
|
|
||||||
if mode == 0:
|
|
||||||
if current >= radius:
|
|
||||||
# Finished growing; hold final frame
|
|
||||||
while True:
|
|
||||||
yield
|
|
||||||
current = min(radius, current + step_size)
|
|
||||||
else:
|
|
||||||
if current <= 0:
|
|
||||||
# Finished shrinking; hold final frame
|
|
||||||
while True:
|
|
||||||
yield
|
|
||||||
current = max(0, current - step_size)
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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,64 +0,0 @@
|
|||||||
import utime
|
|
||||||
|
|
||||||
|
|
||||||
class Pulse:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
self.driver.off()
|
|
||||||
|
|
||||||
# Get colors from preset
|
|
||||||
colors = preset.c
|
|
||||||
if not colors:
|
|
||||||
colors = [(255, 255, 255)]
|
|
||||||
|
|
||||||
color_index = 0
|
|
||||||
cycle_start = utime.ticks_ms()
|
|
||||||
|
|
||||||
# State machine based pulse using a single generator loop
|
|
||||||
while True:
|
|
||||||
# Read current timing parameters from preset
|
|
||||||
attack_ms = max(0, int(preset.n1)) # Attack time in ms
|
|
||||||
hold_ms = max(0, int(preset.n2)) # Hold time in ms
|
|
||||||
decay_ms = max(0, int(preset.n3)) # Decay time in ms
|
|
||||||
delay_ms = max(0, int(preset.d))
|
|
||||||
|
|
||||||
total_ms = attack_ms + hold_ms + decay_ms + delay_ms
|
|
||||||
if total_ms <= 0:
|
|
||||||
total_ms = 1
|
|
||||||
|
|
||||||
now = utime.ticks_ms()
|
|
||||||
elapsed = utime.ticks_diff(now, cycle_start)
|
|
||||||
|
|
||||||
base_color = colors[color_index % len(colors)]
|
|
||||||
|
|
||||||
if elapsed < attack_ms and attack_ms > 0:
|
|
||||||
# Attack: fade 0 -> 1
|
|
||||||
factor = elapsed / attack_ms
|
|
||||||
color = tuple(int(c * factor) for c in base_color)
|
|
||||||
self.driver.fill(self.driver.apply_brightness(color, preset.b))
|
|
||||||
elif elapsed < attack_ms + hold_ms:
|
|
||||||
# Hold: full brightness
|
|
||||||
self.driver.fill(self.driver.apply_brightness(base_color, preset.b))
|
|
||||||
elif elapsed < attack_ms + hold_ms + decay_ms and decay_ms > 0:
|
|
||||||
# Decay: fade 1 -> 0
|
|
||||||
dec_elapsed = elapsed - attack_ms - hold_ms
|
|
||||||
factor = max(0.0, 1.0 - (dec_elapsed / decay_ms))
|
|
||||||
color = tuple(int(c * factor) for c in base_color)
|
|
||||||
self.driver.fill(self.driver.apply_brightness(color, preset.b))
|
|
||||||
elif elapsed < total_ms:
|
|
||||||
# Delay phase: LEDs off between pulses
|
|
||||||
self.driver.fill((0, 0, 0))
|
|
||||||
else:
|
|
||||||
# End of cycle, move to next color and restart timing
|
|
||||||
color_index += 1
|
|
||||||
cycle_start = now
|
|
||||||
if not preset.a:
|
|
||||||
break
|
|
||||||
# Skip drawing this tick, start next cycle
|
|
||||||
yield
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Yield once per tick
|
|
||||||
yield
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import utime
|
|
||||||
|
|
||||||
|
|
||||||
def _hue_to_rgb(hue):
|
|
||||||
"""Hue 0..360 -> (r, g, b). Simple HSV with S=V=1."""
|
|
||||||
h = hue % 360
|
|
||||||
x = 1 - abs((h / 60) % 2 - 1)
|
|
||||||
if h < 60:
|
|
||||||
r, g, b = 1, x, 0
|
|
||||||
elif h < 120:
|
|
||||||
r, g, b = x, 1, 0
|
|
||||||
elif h < 180:
|
|
||||||
r, g, b = 0, 1, x
|
|
||||||
elif h < 240:
|
|
||||||
r, g, b = 0, x, 1
|
|
||||||
elif h < 300:
|
|
||||||
r, g, b = x, 0, 1
|
|
||||||
else:
|
|
||||||
r, g, b = 1, 0, x
|
|
||||||
return (int(r * 255), int(g * 255), int(b * 255))
|
|
||||||
|
|
||||||
|
|
||||||
def _make_rainbow_double(num_leds, brightness=1.0):
|
|
||||||
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len_bytes).
|
|
||||||
DMA reads double_buf[head:head+strip_len] with no copy."""
|
|
||||||
n = 2 * num_leds
|
|
||||||
double_buf = bytearray(n * 3)
|
|
||||||
for i in range(n):
|
|
||||||
hue = (i / n) * 360 * 2
|
|
||||||
r, g, b = _hue_to_rgb(hue)
|
|
||||||
double_buf[i * 3] = int(g * brightness) & 0xFF
|
|
||||||
double_buf[i * 3 + 1] = int(r * brightness) & 0xFF
|
|
||||||
double_buf[i * 3 + 2] = int(b * brightness) & 0xFF
|
|
||||||
strip_len_bytes = num_leds * 3
|
|
||||||
return (double_buf, strip_len_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_buffers(driver, preset, buffers_cache):
|
|
||||||
"""Build or refresh per-strip double buffers with current brightness. Returns (rainbow_data, cumulative_bytes)."""
|
|
||||||
effective = (preset.b * driver.b) / (255 * 255)
|
|
||||||
key = (preset.b, driver.b)
|
|
||||||
if buffers_cache.get("key") == key and buffers_cache.get("data"):
|
|
||||||
return buffers_cache["data"], buffers_cache["cumulative_bytes"]
|
|
||||||
strips = driver.strips
|
|
||||||
rainbow_data = [_make_rainbow_double(s.num_leds, effective) for s in strips]
|
|
||||||
cumulative_bytes = [0]
|
|
||||||
for s in strips:
|
|
||||||
cumulative_bytes.append(cumulative_bytes[-1] + s.num_leds * 3)
|
|
||||||
buffers_cache["key"] = key
|
|
||||||
buffers_cache["data"] = rainbow_data
|
|
||||||
buffers_cache["cumulative_bytes"] = cumulative_bytes
|
|
||||||
return rainbow_data, cumulative_bytes
|
|
||||||
|
|
||||||
|
|
||||||
class Rainbow:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
self._buffers_cache = {}
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
step_amount = max(1, int(preset.n1)) # n1 = bytes to advance per frame (speed)
|
|
||||||
total_ring_bytes = self.driver.num_leds * 3
|
|
||||||
# Phase in bytes; driver.step kept in 0..255 for compatibility
|
|
||||||
phase = (self.driver.step * total_ring_bytes) // 256
|
|
||||||
|
|
||||||
rainbow_data, cumulative_bytes = _ensure_buffers(
|
|
||||||
self.driver, preset, self._buffers_cache
|
|
||||||
)
|
|
||||||
strips = self.driver.strips
|
|
||||||
|
|
||||||
def show_frame(phase):
|
|
||||||
for i, (strip, (double_buf, strip_len_bytes)) in enumerate(zip(strips, rainbow_data)):
|
|
||||||
head = (phase + cumulative_bytes[i]) % strip_len_bytes
|
|
||||||
strip.show(double_buf, head)
|
|
||||||
self.driver.step = (phase * 256) // total_ring_bytes
|
|
||||||
|
|
||||||
# Single step then stop
|
|
||||||
if not preset.a:
|
|
||||||
show_frame(phase)
|
|
||||||
phase = (phase + step_amount) % total_ring_bytes
|
|
||||||
self.driver.step = (phase * 256) // total_ring_bytes
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
|
|
||||||
last_update = utime.ticks_ms()
|
|
||||||
sleep_ms = max(1, int(preset.d))
|
|
||||||
|
|
||||||
while True:
|
|
||||||
current_time = utime.ticks_ms()
|
|
||||||
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
|
|
||||||
rainbow_data, cumulative_bytes = _ensure_buffers(
|
|
||||||
self.driver, preset, self._buffers_cache
|
|
||||||
)
|
|
||||||
show_frame(phase)
|
|
||||||
phase = (phase + step_amount) % total_ring_bytes
|
|
||||||
last_update = current_time
|
|
||||||
yield
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import utime
|
|
||||||
import math
|
|
||||||
|
|
||||||
|
|
||||||
class Roll:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
"""Roll: all strips show a shared color gradient palette, cycling out of phase.
|
|
||||||
|
|
||||||
- All strips participate; each frame shows N discrete colors
|
|
||||||
(one per strip), from color1 to color2.
|
|
||||||
- Over time, each strip cycles through all colors, out of phase with the
|
|
||||||
others, creating a smooth rolling band around the hoop.
|
|
||||||
- n4: direction (0 = clockwise, 1 = anti-clockwise)
|
|
||||||
- c[0]: head color (full intensity)
|
|
||||||
- c[1]: tail color (usually darker or off)
|
|
||||||
"""
|
|
||||||
colors = preset.c
|
|
||||||
base1 = colors[0] if colors else (255, 255, 255)
|
|
||||||
base2 = colors[1] if len(colors) > 1 else (0, 0, 0)
|
|
||||||
color1 = self.driver.apply_brightness(base1, preset.b)
|
|
||||||
color2 = self.driver.apply_brightness(base2, preset.b)
|
|
||||||
|
|
||||||
n_segments = self.driver.n.num_strips if hasattr(self.driver.n, "num_strips") else 1
|
|
||||||
# n3: number of full rotations before stopping (0 = continuous)
|
|
||||||
max_rotations = int(getattr(preset, "n3", 0) or 0)
|
|
||||||
# n4: direction (0=cw, 1=ccw); default clockwise if missing
|
|
||||||
clockwise = int(getattr(preset, "n4", 0)) == 0
|
|
||||||
|
|
||||||
step = self.driver.step
|
|
||||||
|
|
||||||
# Precompute one shared buffer per brightness level (one per strip),
|
|
||||||
# using the longest strip length so any strip can DMA from it safely.
|
|
||||||
strips_list = self.driver.strips
|
|
||||||
if not strips_list or n_segments <= 0:
|
|
||||||
while True:
|
|
||||||
yield
|
|
||||||
|
|
||||||
max_leds = max(s.num_leds for s in strips_list)
|
|
||||||
|
|
||||||
# Build N discrete color buffers forming a gradient from color1 to color2.
|
|
||||||
buffers = []
|
|
||||||
for j in range(n_segments):
|
|
||||||
if n_segments > 1:
|
|
||||||
t = j / (n_segments - 1)
|
|
||||||
else:
|
|
||||||
t = 0.0
|
|
||||||
# Linear interpolation between color1 and color2
|
|
||||||
r = int(color1[0] + (color2[0] - color1[0]) * t)
|
|
||||||
g = int(color1[1] + (color2[1] - color1[1]) * t)
|
|
||||||
b = int(color1[2] + (color2[2] - color1[2]) * t)
|
|
||||||
|
|
||||||
buf = bytearray(max_leds * 3)
|
|
||||||
for i in range(max_leds):
|
|
||||||
o = i * 3
|
|
||||||
buf[o] = g & 0xFF
|
|
||||||
buf[o + 1] = r & 0xFF
|
|
||||||
buf[o + 2] = b & 0xFF
|
|
||||||
buffers.append(buf)
|
|
||||||
|
|
||||||
def draw(step_index):
|
|
||||||
# Each strip picks a buffer index offset by its strip index so that:
|
|
||||||
# - all brightness levels are visible simultaneously (one per strip)
|
|
||||||
# - over time, each strip cycles through all brightness levels
|
|
||||||
try:
|
|
||||||
self.driver.last_roll_head = step_index % n_segments
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for strip_idx, strip in enumerate(strips_list):
|
|
||||||
if strip_idx < 0 or strip_idx >= n_segments:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if clockwise:
|
|
||||||
buf_index = (step_index + strip_idx) % n_segments
|
|
||||||
else:
|
|
||||||
buf_index = (step_index - strip_idx) % n_segments
|
|
||||||
buf = buffers[buf_index]
|
|
||||||
|
|
||||||
# Show the shared buffer; WS2812B will read num_leds*3 bytes.
|
|
||||||
strip.show(buf, 0)
|
|
||||||
|
|
||||||
if not preset.a:
|
|
||||||
draw(step)
|
|
||||||
self.driver.step = step + 1
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
|
|
||||||
# Auto mode: advance based on preset.d (ms) for smooth, controllable speed
|
|
||||||
delay_ms = max(10, int(getattr(preset, "d", 60)) or 10)
|
|
||||||
last_update = utime.ticks_ms() - delay_ms
|
|
||||||
rotations_done = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
now = utime.ticks_ms()
|
|
||||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
|
||||||
draw(step)
|
|
||||||
step += 1
|
|
||||||
self.driver.step = step
|
|
||||||
last_update = now
|
|
||||||
# Count full rotations if requested: one rotation per n_segments steps
|
|
||||||
if max_rotations > 0 and n_segments > 0 and (step % n_segments) == 0:
|
|
||||||
rotations_done += 1
|
|
||||||
if rotations_done >= max_rotations:
|
|
||||||
# Hold the final frame and stop advancing; keep yielding so
|
|
||||||
# the generator stays alive without changing the LEDs.
|
|
||||||
while True:
|
|
||||||
yield
|
|
||||||
yield
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import utime
|
|
||||||
|
|
||||||
|
|
||||||
RED = (255, 0, 0)
|
|
||||||
|
|
||||||
|
|
||||||
class ScaleTest:
|
|
||||||
"""
|
|
||||||
Animated test for the scale() helper.
|
|
||||||
|
|
||||||
A single red pixel moves along the reference strip (strip 0). For each other
|
|
||||||
strip, the position is mapped using:
|
|
||||||
|
|
||||||
n2 = scale(l1, l2, n1)
|
|
||||||
|
|
||||||
so that all lit pixels stay aligned by proportional position along the strips.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
strips = self.driver.strips
|
|
||||||
if not strips:
|
|
||||||
return
|
|
||||||
|
|
||||||
src_strip_idx = 0
|
|
||||||
l1 = self.driver.strip_length(src_strip_idx)
|
|
||||||
if l1 <= 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
step = self.driver.step
|
|
||||||
delay_ms = max(1, int(getattr(preset, "d", 30) or 30))
|
|
||||||
last_update = utime.ticks_ms()
|
|
||||||
color = self.driver.apply_brightness(RED, getattr(preset, "b", 255))
|
|
||||||
|
|
||||||
while True:
|
|
||||||
now = utime.ticks_ms()
|
|
||||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
|
||||||
n1 = step % l1
|
|
||||||
|
|
||||||
# Clear all strips
|
|
||||||
for strip in strips:
|
|
||||||
strip.fill((0, 0, 0))
|
|
||||||
|
|
||||||
# Light mapped position on each strip using Presets.set/show
|
|
||||||
for dst_strip_idx, _ in enumerate(strips):
|
|
||||||
self.driver.set(dst_strip_idx, n1, color)
|
|
||||||
self.driver.show(dst_strip_idx)
|
|
||||||
|
|
||||||
step += 1
|
|
||||||
self.driver.step = step
|
|
||||||
last_update = now
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
class Segments:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
# Apply preset/global brightness once per color
|
|
||||||
ranges = [
|
|
||||||
(preset.n1, preset.n2),
|
|
||||||
(preset.n3, preset.n4),
|
|
||||||
(preset.n5, preset.n6),
|
|
||||||
(preset.n7, preset.n8),
|
|
||||||
]
|
|
||||||
|
|
||||||
for n, color in enumerate(preset.c):
|
|
||||||
self.driver.fill_n(color, ranges[n][0], ranges[n][1])
|
|
||||||
|
|
||||||
self.driver.show_all()
|
|
||||||
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import utime
|
|
||||||
|
|
||||||
|
|
||||||
class SegmentsTransition:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
"""
|
|
||||||
SegmentsTransition: fade from whatever is currently on the strips
|
|
||||||
to a new static Segments layout defined by n1–n8 and c[0..3].
|
|
||||||
|
|
||||||
- Uses the existing strip buffers as the starting state.
|
|
||||||
- Target state matches the Segments pattern: up to 4 colored bands
|
|
||||||
along the logical reference strip, mapped to all physical strips.
|
|
||||||
- Transition duration is taken from preset.d (ms), minimum 50ms.
|
|
||||||
"""
|
|
||||||
strips = self.driver.strips
|
|
||||||
if not strips:
|
|
||||||
while True:
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Snapshot starting GRB buffers (already scaled by per-strip brightness)
|
|
||||||
start_bufs = [bytes(strip.ar) for strip in strips]
|
|
||||||
|
|
||||||
# Prepare target buffers (same length as each strip's ar)
|
|
||||||
target_bufs = [bytearray(len(strip.ar)) for strip in strips]
|
|
||||||
|
|
||||||
# Base colors (up to 4), missing ones default to black
|
|
||||||
colors = list(preset.c) if getattr(preset, "c", None) else []
|
|
||||||
while len(colors) < 4:
|
|
||||||
colors.append((0, 0, 0))
|
|
||||||
|
|
||||||
# Apply preset/global brightness once per color
|
|
||||||
bright_colors = [
|
|
||||||
self.driver.apply_brightness(colors[0], preset.b),
|
|
||||||
self.driver.apply_brightness(colors[1], preset.b),
|
|
||||||
self.driver.apply_brightness(colors[2], preset.b),
|
|
||||||
self.driver.apply_brightness(colors[3], preset.b),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Logical reference length for all strips (from scale_map[0])
|
|
||||||
ref_len = len(self.driver.scale_map[0]) if self.driver.scale_map else 0
|
|
||||||
if ref_len <= 0:
|
|
||||||
# Fallback: nothing to do, just hold current state
|
|
||||||
while True:
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Helper to clamp and normalize a logical range [a, b] (inclusive) over ref_len.
|
|
||||||
# Returns (start, end_exclusive) suitable for range(start, end_exclusive).
|
|
||||||
def norm_range(a, b):
|
|
||||||
a = int(a)
|
|
||||||
b = int(b)
|
|
||||||
if a > b:
|
|
||||||
a, b = b, a
|
|
||||||
if b < 0 or a >= ref_len:
|
|
||||||
return None
|
|
||||||
a = max(0, a)
|
|
||||||
b = min(ref_len - 1, b)
|
|
||||||
if a > b:
|
|
||||||
return None
|
|
||||||
return a, b + 1
|
|
||||||
|
|
||||||
raw_ranges = [
|
|
||||||
(getattr(preset, "n1", 0), getattr(preset, "n2", -1), bright_colors[0]),
|
|
||||||
(getattr(preset, "n3", 0), getattr(preset, "n4", -1), bright_colors[1]),
|
|
||||||
(getattr(preset, "n5", 0), getattr(preset, "n6", -1), bright_colors[2]),
|
|
||||||
(getattr(preset, "n7", 0), getattr(preset, "n8", -1), bright_colors[3]),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Build target buffers using the same logical indexing idea as Segments
|
|
||||||
for strip_idx, strip in enumerate(strips):
|
|
||||||
bright = strip.brightness
|
|
||||||
scale_map = self.driver.scale_map[strip_idx]
|
|
||||||
buf = target_bufs[strip_idx]
|
|
||||||
n_leds = strip.num_leds
|
|
||||||
|
|
||||||
# Start from black everywhere
|
|
||||||
for i in range(len(buf)):
|
|
||||||
buf[i] = 0
|
|
||||||
|
|
||||||
# Apply each logical range to this strip
|
|
||||||
for a, b, color in raw_ranges:
|
|
||||||
rng = norm_range(a, b)
|
|
||||||
if not rng:
|
|
||||||
continue
|
|
||||||
start, end = rng
|
|
||||||
r, g, bl = color
|
|
||||||
for logical_idx in range(start, end):
|
|
||||||
if logical_idx < 0 or logical_idx >= len(scale_map):
|
|
||||||
continue
|
|
||||||
phys_idx = scale_map[logical_idx]
|
|
||||||
if phys_idx < 0 or phys_idx >= n_leds:
|
|
||||||
continue
|
|
||||||
base = phys_idx * 3
|
|
||||||
if base + 2 >= len(buf):
|
|
||||||
continue
|
|
||||||
buf[base] = int(g * bright)
|
|
||||||
buf[base + 1] = int(r * bright)
|
|
||||||
buf[base + 2] = int(bl * bright)
|
|
||||||
|
|
||||||
# Duration in ms for the whole transition (slower by default)
|
|
||||||
# If preset.d is provided, use it; otherwise default to a slow 3000ms fade.
|
|
||||||
raw_d = int(getattr(preset, "d", 3000) or 3000)
|
|
||||||
duration = max(1000, raw_d) # enforce at least 1s for a clearly visible transition
|
|
||||||
start_time = utime.ticks_ms()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
now = utime.ticks_ms()
|
|
||||||
elapsed = utime.ticks_diff(now, start_time)
|
|
||||||
|
|
||||||
if elapsed >= duration:
|
|
||||||
# Final frame: commit target buffers and hold, then update all strips together
|
|
||||||
for strip, target in zip(strips, target_bufs):
|
|
||||||
ar = strip.ar
|
|
||||||
for i in range(len(ar)):
|
|
||||||
ar[i] = target[i]
|
|
||||||
self.driver.show_all()
|
|
||||||
while True:
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Interpolation factor in [0,1]
|
|
||||||
factor = elapsed / duration
|
|
||||||
inv = 1.0 - factor
|
|
||||||
|
|
||||||
# Blend from start to target in GRB space per byte
|
|
||||||
for idx, strip in enumerate(strips):
|
|
||||||
start_buf = start_bufs[idx]
|
|
||||||
target_buf = target_bufs[idx]
|
|
||||||
ar = strip.ar
|
|
||||||
for i in range(len(ar)):
|
|
||||||
ar[i] = int(start_buf[i] * inv + target_buf[i] * factor)
|
|
||||||
self.driver.show_all()
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
"""Spin: continues from Grab — segment (10 each side of center) moves slowly up to the top. Preset color, n1 = rate."""
|
|
||||||
|
|
||||||
import utime
|
|
||||||
|
|
||||||
SPAN = 0 # LEDs on each side of center (match Grab)
|
|
||||||
LUT_SIZE = 256 # gradient lookup table entries
|
|
||||||
|
|
||||||
|
|
||||||
class Spin:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
strips = self.driver.strips
|
|
||||||
|
|
||||||
self.driver.fill((0, 0, 0))
|
|
||||||
self.driver.show_all()
|
|
||||||
active_indices = (0, 4)
|
|
||||||
c0 = preset.c[0]
|
|
||||||
c1 = preset.c[1]
|
|
||||||
|
|
||||||
# Precompute gradient LUT: t in [0,1] maps to (r,g,b)
|
|
||||||
lut = []
|
|
||||||
for k in range(LUT_SIZE):
|
|
||||||
t = k / (LUT_SIZE - 1) if LUT_SIZE > 1 else 1
|
|
||||||
r = int(c0[0] + (c1[0] - c0[0]) * t)
|
|
||||||
g = int(c0[1] + (c1[1] - c0[1]) * t)
|
|
||||||
b = int(c0[2] + (c1[2] - c0[2]) * t)
|
|
||||||
lut.append((r, g, b))
|
|
||||||
|
|
||||||
# For each active strip we expand from just outside the grab center
|
|
||||||
# left: from (mid - SPAN) down to 0
|
|
||||||
# right: from (mid + SPAN) up to end
|
|
||||||
midpoints = self.driver.strip_midpoints
|
|
||||||
rate = max(1, int(preset.n1) or 1)
|
|
||||||
delay_ms = max(1, int(preset.d) or 1)
|
|
||||||
margin = max(0, int(preset.n2) or 0)
|
|
||||||
|
|
||||||
# Track current extents of each arm
|
|
||||||
left = {}
|
|
||||||
right = {}
|
|
||||||
for idx in active_indices:
|
|
||||||
if 0 <= idx < len(strips):
|
|
||||||
mid = midpoints[idx]
|
|
||||||
left[idx] = mid - SPAN # inner edge of left arm
|
|
||||||
right[idx] = mid + SPAN + 1 # inner edge of right arm
|
|
||||||
|
|
||||||
last_update = utime.ticks_ms()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
now = utime.ticks_ms()
|
|
||||||
if utime.ticks_diff(now, last_update) < delay_ms:
|
|
||||||
yield
|
|
||||||
continue
|
|
||||||
last_update = now
|
|
||||||
|
|
||||||
for idx in active_indices:
|
|
||||||
if idx < 0 or idx >= len(strips):
|
|
||||||
continue
|
|
||||||
strip = strips[idx]
|
|
||||||
n = strip.num_leds
|
|
||||||
mid = midpoints[idx]
|
|
||||||
|
|
||||||
# Expand arms at the same rate on both sides
|
|
||||||
step = max(1, rate)
|
|
||||||
new_left = max(margin, left[idx] - step)
|
|
||||||
new_right = min(n - margin, right[idx] + step)
|
|
||||||
|
|
||||||
# Left arm: c1 at outer, c0 at inner. Right arm: c0 at inner, c1 at outer.
|
|
||||||
left_len = max(0, (mid - SPAN) - new_left)
|
|
||||||
right_len = max(0, new_right - (mid + SPAN + 1))
|
|
||||||
bright = strip.brightness
|
|
||||||
ar = strip.ar
|
|
||||||
|
|
||||||
for j, i in enumerate(range(new_left, mid - SPAN)):
|
|
||||||
if 0 <= i < n:
|
|
||||||
t = 1 - j / (left_len - 1) if left_len > 1 else 0
|
|
||||||
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
|
||||||
r, g, b = lut[lut_idx]
|
|
||||||
base = i * 3
|
|
||||||
ar[base] = int(g * bright)
|
|
||||||
ar[base + 1] = int(r * bright)
|
|
||||||
ar[base + 2] = int(b * bright)
|
|
||||||
|
|
||||||
for j, i in enumerate(range(mid + SPAN + 1, new_right)):
|
|
||||||
if 0 <= i < n:
|
|
||||||
t = j / (right_len - 1) if right_len > 1 else 0
|
|
||||||
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
|
||||||
r, g, b = lut[lut_idx]
|
|
||||||
base = i * 3
|
|
||||||
ar[base] = int(g * bright)
|
|
||||||
ar[base + 1] = int(r * bright)
|
|
||||||
ar[base + 2] = int(b * bright)
|
|
||||||
|
|
||||||
left[idx] = new_left
|
|
||||||
right[idx] = new_right
|
|
||||||
|
|
||||||
# Show only on this strip
|
|
||||||
strip.show()
|
|
||||||
|
|
||||||
yield
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""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()
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import utime
|
|
||||||
|
|
||||||
|
|
||||||
class Transition:
|
|
||||||
def __init__(self, driver):
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
def run(self, preset):
|
|
||||||
"""Transition between colors, blending over `delay` ms."""
|
|
||||||
colors = preset.c
|
|
||||||
if not colors:
|
|
||||||
self.driver.off()
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only one color: just keep it on
|
|
||||||
if len(colors) == 1:
|
|
||||||
while True:
|
|
||||||
self.driver.fill(self.driver.apply_brightness(colors[0], preset.b))
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
|
|
||||||
color_index = 0
|
|
||||||
start_time = utime.ticks_ms()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if not colors:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Get current and next color based on live list
|
|
||||||
c1 = colors[color_index % len(colors)]
|
|
||||||
c2 = colors[(color_index + 1) % len(colors)]
|
|
||||||
|
|
||||||
duration = max(10, int(preset.d)) # At least 10ms
|
|
||||||
now = utime.ticks_ms()
|
|
||||||
elapsed = utime.ticks_diff(now, start_time)
|
|
||||||
|
|
||||||
if elapsed >= duration:
|
|
||||||
# End of this transition step
|
|
||||||
if not preset.a:
|
|
||||||
# One-shot: transition from first to second color only
|
|
||||||
self.driver.fill(self.driver.apply_brightness(c2, preset.b))
|
|
||||||
break
|
|
||||||
# Auto: move to next pair
|
|
||||||
color_index = (color_index + 1) % len(colors)
|
|
||||||
start_time = now
|
|
||||||
yield
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Interpolate between c1 and c2
|
|
||||||
factor = elapsed / duration
|
|
||||||
interpolated = tuple(
|
|
||||||
int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3)
|
|
||||||
)
|
|
||||||
self.driver.fill(self.driver.apply_brightness(interpolated, preset.b))
|
|
||||||
|
|
||||||
yield
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
class Preset:
|
|
||||||
def __init__(self, data):
|
|
||||||
# Set default values for all preset attributes
|
|
||||||
self.p = "off"
|
|
||||||
self.d = 100
|
|
||||||
self.b = 127
|
|
||||||
self.c = [(255, 255, 255)]
|
|
||||||
self.a = True
|
|
||||||
self.n1 = 0
|
|
||||||
self.n2 = 0
|
|
||||||
self.n3 = 0
|
|
||||||
self.n4 = 0
|
|
||||||
self.n5 = 0
|
|
||||||
self.n6 = 0
|
|
||||||
self.n7 = 0
|
|
||||||
self.n8 = 0
|
|
||||||
|
|
||||||
# Override defaults with provided data
|
|
||||||
self.edit(data)
|
|
||||||
|
|
||||||
def edit(self, data=None):
|
|
||||||
if not data:
|
|
||||||
return False
|
|
||||||
for key, value in data.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pattern(self):
|
|
||||||
return self.p
|
|
||||||
|
|
||||||
@pattern.setter
|
|
||||||
def pattern(self, value):
|
|
||||||
self.p = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def delay(self):
|
|
||||||
return self.d
|
|
||||||
|
|
||||||
@delay.setter
|
|
||||||
def delay(self, value):
|
|
||||||
self.d = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def brightness(self):
|
|
||||||
return self.b
|
|
||||||
|
|
||||||
@brightness.setter
|
|
||||||
def brightness(self, value):
|
|
||||||
self.b = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def colors(self):
|
|
||||||
return self.c
|
|
||||||
|
|
||||||
@colors.setter
|
|
||||||
def colors(self, value):
|
|
||||||
self.c = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auto(self):
|
|
||||||
return self.a
|
|
||||||
|
|
||||||
@auto.setter
|
|
||||||
def auto(self, value):
|
|
||||||
self.a = value
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
"p": self.p,
|
|
||||||
"d": self.d,
|
|
||||||
"b": self.b,
|
|
||||||
"c": self.c,
|
|
||||||
"a": self.a,
|
|
||||||
"n1": self.n1,
|
|
||||||
"n2": self.n2,
|
|
||||||
"n3": self.n3,
|
|
||||||
"n4": self.n4,
|
|
||||||
"n5": self.n5,
|
|
||||||
"n6": self.n6,
|
|
||||||
"n7": self.n7,
|
|
||||||
"n8": self.n8,
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
from machine import Pin
|
|
||||||
from ws2812 import WS2812B
|
|
||||||
from preset import Preset
|
|
||||||
from patterns import (
|
|
||||||
Blink,
|
|
||||||
Rainbow,
|
|
||||||
Pulse,
|
|
||||||
Transition,
|
|
||||||
Chase,
|
|
||||||
Circle,
|
|
||||||
DoubleCircle,
|
|
||||||
Roll,
|
|
||||||
Calibration,
|
|
||||||
Test,
|
|
||||||
Grab,
|
|
||||||
Spin,
|
|
||||||
Lift,
|
|
||||||
Flare,
|
|
||||||
Hook,
|
|
||||||
Pose,
|
|
||||||
Segments,
|
|
||||||
SegmentsTransition,
|
|
||||||
)
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class _LogicalRing:
|
|
||||||
"""
|
|
||||||
Lightweight logical ring over all strips.
|
|
||||||
Used by patterns that expect driver.n (e.g. Circle, Roll legacy API).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, driver):
|
|
||||||
self._driver = driver
|
|
||||||
self.num_strips = len(driver.strips)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return self._driver.num_leds
|
|
||||||
|
|
||||||
def fill(self, color):
|
|
||||||
# Apply color to all logical positions across all strips
|
|
||||||
for i in range(self._driver.num_leds):
|
|
||||||
for strip_idx in range(self.num_strips):
|
|
||||||
self._driver.set(strip_idx, i, color)
|
|
||||||
|
|
||||||
def __setitem__(self, index, color):
|
|
||||||
if index < 0 or index >= self._driver.num_leds:
|
|
||||||
return
|
|
||||||
for strip_idx in range(self.num_strips):
|
|
||||||
self._driver.set(strip_idx, index, color)
|
|
||||||
|
|
||||||
def write(self):
|
|
||||||
self._driver.show_all()
|
|
||||||
|
|
||||||
|
|
||||||
# Order: strips[0]=physical 1 … strips[7]=physical 8. (pin, num_leds, midpoint_index).
|
|
||||||
STRIP_CONFIG = (
|
|
||||||
(6, 291, 291 // 2), # 1
|
|
||||||
(29, 290, 290 // 2-1), # 2
|
|
||||||
(3, 283, 283 // 2), # 3
|
|
||||||
(28, 278, 278 // 2-1), # 4
|
|
||||||
(2, 278, 275 // 2), # 5 (bottom of hoop)
|
|
||||||
(0, 283, 278 // 2-1), # 6
|
|
||||||
(4, 290, 283 // 2), # 7
|
|
||||||
(7, 291, 290 // 2-1), # 8
|
|
||||||
)
|
|
||||||
|
|
||||||
class Presets:
|
|
||||||
def __init__(self):
|
|
||||||
self.scale_map = []
|
|
||||||
self.strips = []
|
|
||||||
self.strip_midpoints = [] # midpoint LED index per strip (from STRIP_CONFIG)
|
|
||||||
|
|
||||||
state_machine = 0
|
|
||||||
for entry in STRIP_CONFIG:
|
|
||||||
pin, num_leds = entry[0], entry[1]
|
|
||||||
mid = entry[2] if len(entry) >= 3 else num_leds // 2
|
|
||||||
self.strip_midpoints.append(mid)
|
|
||||||
self.strips.append(WS2812B(num_leds, pin, state_machine, brightness=1.0))
|
|
||||||
state_machine += 1
|
|
||||||
self.scale_map.append(self.create_scale_map(num_leds))
|
|
||||||
|
|
||||||
# Single logical strip using strip 0 as reference for patterns (n[i], .fill(), .write())
|
|
||||||
# WS2812B with brightness=1.0 so Presets.apply_brightness() does all scaling (NeoPixel drop-in)
|
|
||||||
# Reference logical length for patterns that use driver.num_leds (Rainbow/Chase/Circle, etc.)
|
|
||||||
self.num_leds = self.strips[0].num_leds if self.strips else 0
|
|
||||||
# Legacy logical ring interface for patterns expecting driver.n
|
|
||||||
self.n = _LogicalRing(self)
|
|
||||||
self.step = 0
|
|
||||||
# Remember which strip was last used as the roll head (for flare, etc.)
|
|
||||||
self.last_roll_head = 0
|
|
||||||
# Global brightness (0–255), controlled via UART/JSON {"b": <value>}
|
|
||||||
self.b = 255
|
|
||||||
|
|
||||||
self.generator = None
|
|
||||||
self.presets = {}
|
|
||||||
self.selected = None
|
|
||||||
|
|
||||||
# Register all pattern methods
|
|
||||||
self.patterns = {
|
|
||||||
"off": self.off,
|
|
||||||
"on": self.on,
|
|
||||||
"blink": Blink(self).run,
|
|
||||||
"rainbow": Rainbow(self).run,
|
|
||||||
"pulse": Pulse(self).run,
|
|
||||||
"transition": Transition(self).run,
|
|
||||||
"chase": Chase(self).run,
|
|
||||||
"circle": Circle(self).run,
|
|
||||||
"double_circle": DoubleCircle(self).run,
|
|
||||||
"roll": Roll(self).run,
|
|
||||||
"calibration": Calibration(self).run,
|
|
||||||
"test": Test(self).run,
|
|
||||||
"grab": Grab(self).run,
|
|
||||||
"spin": Spin(self).run,
|
|
||||||
"lift": Lift(self).run,
|
|
||||||
"flare": Flare(self).run,
|
|
||||||
"hook": Hook(self).run,
|
|
||||||
"pose": Pose(self).run,
|
|
||||||
"segments": Segments(self).run,
|
|
||||||
"segments_transition": SegmentsTransition(self).run,
|
|
||||||
"point": Segments(self).run, # backwards-compatible alias
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Strip geometry utilities -------------------------------------------------
|
|
||||||
|
|
||||||
def strip_length(self, strip_idx):
|
|
||||||
"""Return number of LEDs for a physical strip index."""
|
|
||||||
if 0 <= strip_idx < len(self.strips):
|
|
||||||
return self.strips[strip_idx].num_leds
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
"""Save the presets to a file."""
|
|
||||||
with open("presets.json", "w") as f:
|
|
||||||
json.dump({name: preset.to_dict() for name, preset in self.presets.items()}, f)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
"""Load presets from a file."""
|
|
||||||
try:
|
|
||||||
with open("presets.json", "r") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
except OSError:
|
|
||||||
# Create an empty presets file if missing
|
|
||||||
self.presets = {}
|
|
||||||
self.save()
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.presets = {}
|
|
||||||
for name, preset_data in data.items():
|
|
||||||
if "c" in preset_data:
|
|
||||||
preset_data["c"] = [tuple(color) for color in preset_data["c"]]
|
|
||||||
self.presets[name] = Preset(preset_data)
|
|
||||||
if self.presets:
|
|
||||||
print("Loaded presets:")
|
|
||||||
#for name in sorted(self.presets.keys()):
|
|
||||||
# print(f" {name}: {self.presets[name].to_dict()}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def edit(self, name, data):
|
|
||||||
"""Create or update a preset with the given name."""
|
|
||||||
if name in self.presets:
|
|
||||||
# Update existing preset
|
|
||||||
self.presets[name].edit(data)
|
|
||||||
else:
|
|
||||||
# Create new preset
|
|
||||||
self.presets[name] = Preset(data)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def delete(self, name):
|
|
||||||
if name in self.presets:
|
|
||||||
del self.presets[name]
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def tick(self):
|
|
||||||
if self.generator is None:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
next(self.generator)
|
|
||||||
except StopIteration:
|
|
||||||
self.generator = None
|
|
||||||
|
|
||||||
def select(self, preset_name, step=None):
|
|
||||||
if preset_name is None:
|
|
||||||
return False
|
|
||||||
print(f"Selecting preset: {preset_name}")
|
|
||||||
preset = None
|
|
||||||
pattern_key = preset_name
|
|
||||||
if preset_name in self.presets:
|
|
||||||
preset = self.presets[preset_name]
|
|
||||||
pattern_key = preset.p
|
|
||||||
if pattern_key not in self.patterns:
|
|
||||||
return False
|
|
||||||
# Run by pattern name (works for saved presets and built-ins like calibration, off, test)
|
|
||||||
if preset is None:
|
|
||||||
preset = Preset({"p": pattern_key}) if pattern_key != "off" else None
|
|
||||||
if step is not None:
|
|
||||||
self.step = step
|
|
||||||
elif pattern_key == "off" or self.selected != preset_name:
|
|
||||||
self.step = 0
|
|
||||||
self.generator = self.patterns[pattern_key](preset)
|
|
||||||
self.selected = preset_name
|
|
||||||
return True
|
|
||||||
|
|
||||||
def apply_brightness(self, color, brightness_override=None):
|
|
||||||
# Combine per-preset brightness (override) with global brightness self.b
|
|
||||||
local = brightness_override if brightness_override is not None else 255
|
|
||||||
# Scale preset brightness by global brightness
|
|
||||||
effective_brightness = int(local * self.b / 255)
|
|
||||||
return tuple(int(c * effective_brightness / 255) for c in color)
|
|
||||||
|
|
||||||
def off(self, preset=None):
|
|
||||||
self.fill((0, 0, 0))
|
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
def on(self, preset):
|
|
||||||
colors = preset.c
|
|
||||||
color = colors[0] if colors else (255, 255, 255)
|
|
||||||
self.fill(self.apply_brightness(color, preset.b))
|
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
def fill(self, color):
|
|
||||||
for strip in self.strips:
|
|
||||||
strip.fill(color)
|
|
||||||
|
|
||||||
def fill_n(self, color, n1, n2):
|
|
||||||
for i in range(n1, n2):
|
|
||||||
for strip_idx in range(8):
|
|
||||||
self.set(strip_idx, i, color)
|
|
||||||
|
|
||||||
|
|
||||||
def set(self, strip, index, color):
|
|
||||||
if index >= self.strips[0].num_leds:
|
|
||||||
return False
|
|
||||||
self.strips[strip].set(self.scale_map[strip][index], color)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def create_scale_map(self, num_leds):
|
|
||||||
ref_len = STRIP_CONFIG[0][1]
|
|
||||||
return [int(i * num_leds / ref_len) for i in range(ref_len)]
|
|
||||||
|
|
||||||
def show_all(self):
|
|
||||||
for strip in self.strips:
|
|
||||||
strip.show()
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
def convert_and_reorder_colors(colors, settings_or_color_order):
|
|
||||||
"""Convert hex color strings to RGB tuples and reorder based on device color order.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
colors: List of colors, either hex strings like "#FF0000" or RGB tuples like (255, 0, 0)
|
|
||||||
settings_or_color_order: Either a Settings object or a color_order string (e.g., "rgb", "grb")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of RGB tuples reordered according to device color order
|
|
||||||
"""
|
|
||||||
# Get channel order from settings or color_order string
|
|
||||||
if hasattr(settings_or_color_order, 'get_rgb_channel_order'):
|
|
||||||
# It's a Settings object
|
|
||||||
channel_order = settings_or_color_order.get_rgb_channel_order()
|
|
||||||
elif isinstance(settings_or_color_order, str):
|
|
||||||
# It's a color_order string, convert to channel order
|
|
||||||
color_order = settings_or_color_order.lower()
|
|
||||||
color_orders = {
|
|
||||||
"rgb": (1, 3, 5),
|
|
||||||
"rbg": (1, 5, 3),
|
|
||||||
"grb": (3, 1, 5),
|
|
||||||
"gbr": (3, 5, 1),
|
|
||||||
"brg": (5, 1, 3),
|
|
||||||
"bgr": (5, 3, 1)
|
|
||||||
}
|
|
||||||
hex_indices = color_orders.get(color_order, (1, 3, 5))
|
|
||||||
# Map hex string positions to RGB channel indices
|
|
||||||
hex_to_channel = {1: 0, 3: 1, 5: 2}
|
|
||||||
channel_order = tuple(hex_to_channel[pos] for pos in hex_indices)
|
|
||||||
else:
|
|
||||||
# Assume it's already a channel order tuple
|
|
||||||
channel_order = settings_or_color_order
|
|
||||||
|
|
||||||
converted_colors = []
|
|
||||||
for color in colors:
|
|
||||||
# Convert "#RRGGBB" to (R, G, B)
|
|
||||||
if isinstance(color, str) and color.startswith("#"):
|
|
||||||
r = int(color[1:3], 16)
|
|
||||||
g = int(color[3:5], 16)
|
|
||||||
b = int(color[5:7], 16)
|
|
||||||
rgb = (r, g, b)
|
|
||||||
# Reorder based on device color order
|
|
||||||
reordered = (rgb[channel_order[0]], rgb[channel_order[1]], rgb[channel_order[2]])
|
|
||||||
converted_colors.append(reordered)
|
|
||||||
elif isinstance(color, (list, tuple)) and len(color) == 3:
|
|
||||||
# Already a tuple/list, just reorder
|
|
||||||
rgb = tuple(color)
|
|
||||||
reordered = (rgb[channel_order[0]], rgb[channel_order[1]], rgb[channel_order[2]])
|
|
||||||
converted_colors.append(reordered)
|
|
||||||
else:
|
|
||||||
# Keep as-is if not recognized format
|
|
||||||
converted_colors.append(color)
|
|
||||||
return converted_colors
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import sys
|
|
||||||
if "lib" not in sys.path:
|
|
||||||
sys.path.insert(0, "lib")
|
|
||||||
if "../lib" not in sys.path:
|
|
||||||
sys.path.insert(0, "../lib")
|
|
||||||
from ws2812 import WS2812B
|
|
||||||
import time
|
|
||||||
|
|
||||||
# --- Chase test: pregenerated double buffer per strip, show via head offset (same as rainbow) ---
|
|
||||||
|
|
||||||
# (pin, num_leds) per strip — same config as rainbow
|
|
||||||
STRIP_CONFIG = (
|
|
||||||
(2, 291),
|
|
||||||
(3, 290),
|
|
||||||
(4, 283),
|
|
||||||
(7, 278),
|
|
||||||
(0, 275),
|
|
||||||
(28, 278),
|
|
||||||
(29, 283),
|
|
||||||
(6, 290),
|
|
||||||
)
|
|
||||||
|
|
||||||
strips = []
|
|
||||||
sm = 0
|
|
||||||
for pin, num_leds in STRIP_CONFIG:
|
|
||||||
print(pin, num_leds)
|
|
||||||
ws = WS2812B(num_leds, pin, sm, brightness=1.0)
|
|
||||||
strips.append(ws)
|
|
||||||
sm += 1
|
|
||||||
|
|
||||||
cumulative_leds = [0]
|
|
||||||
for ws in strips[:-1]:
|
|
||||||
cumulative_leds.append(cumulative_leds[-1] + ws.num_leds)
|
|
||||||
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
|
||||||
|
|
||||||
# Chase: color1 n1 long, then color2 n2 long, stepping n3 pixels
|
|
||||||
COLOR1 = (255, 0, 0) # red
|
|
||||||
COLOR2 = (0, 0, 255) # blue
|
|
||||||
N1 = 24 # length of color1 segment
|
|
||||||
N2 = 24 # length of color2 segment
|
|
||||||
STEP = 1 # step size in pixels per frame
|
|
||||||
|
|
||||||
|
|
||||||
def make_chase_double(num_leds, cumulative_leds, total_ring_leds, color1, color2, n1, n2):
|
|
||||||
"""Pregenerate strip double buffer with repeating segments:
|
|
||||||
color1 for n1 pixels, then color2 for n2 pixels, around the full ring. GRB order."""
|
|
||||||
n = 2 * num_leds
|
|
||||||
buf = bytearray(n * 3)
|
|
||||||
pattern_len = n1 + n2
|
|
||||||
for b in range(n):
|
|
||||||
# Position of this pixel along the logical ring
|
|
||||||
pos = (2 * cumulative_leds - b) % total_ring_leds
|
|
||||||
seg_pos = pos % pattern_len
|
|
||||||
if seg_pos < n1:
|
|
||||||
r, grn, b_ = color1[0], color1[1], color1[2]
|
|
||||||
else:
|
|
||||||
r, grn, b_ = color2[0], color2[1], color2[2]
|
|
||||||
o = b * 3
|
|
||||||
buf[o] = grn
|
|
||||||
buf[o + 1] = r
|
|
||||||
buf[o + 2] = b_
|
|
||||||
return buf
|
|
||||||
|
|
||||||
|
|
||||||
# Pregenerate one double buffer per strip
|
|
||||||
chase_buffers = [
|
|
||||||
make_chase_double(ws.num_leds, cumulative_leds[i], total_ring_leds, COLOR1, COLOR2, N1, N2)
|
|
||||||
for i, ws in enumerate(strips)
|
|
||||||
]
|
|
||||||
|
|
||||||
chase_pos = 0
|
|
||||||
while True:
|
|
||||||
for i, strip in enumerate(strips):
|
|
||||||
# head in [0, strip_len) so DMA read head..head+num_leds*3 stays in double buffer (same as rainbow)
|
|
||||||
strip_len = strip.num_leds * 3
|
|
||||||
head = (chase_pos + cumulative_leds[i]) * 3 % strip_len
|
|
||||||
strip.show(chase_buffers[i], head)
|
|
||||||
chase_pos = (chase_pos + STEP) % total_ring_leds
|
|
||||||
time.sleep_ms(40)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
from ws2812 import WS2812B
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
# --- Rainbow pattern (outside ws2812): pregen double buffer, show via head offset ---
|
|
||||||
|
|
||||||
|
|
||||||
# --- Strips + rainbow buffers per strip ---
|
|
||||||
|
|
||||||
strips = []
|
|
||||||
pins = ((2, 291),
|
|
||||||
(3, 290),
|
|
||||||
(4, 283),
|
|
||||||
(7, 278),
|
|
||||||
(0, 275),
|
|
||||||
(28, 278),
|
|
||||||
(29, 283),
|
|
||||||
(6, 290))
|
|
||||||
|
|
||||||
sm = 0
|
|
||||||
|
|
||||||
for pin, num_leds in pins:
|
|
||||||
print(pin, num_leds)
|
|
||||||
ws = WS2812B(num_leds, pin, sm, brightness=1.0) # 1.0 so fill() is visible
|
|
||||||
strips.append(ws)
|
|
||||||
sm += 1
|
|
||||||
ws.fill((8,0,0))
|
|
||||||
ws.show()
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def run_for(p, wdt, duration_ms):
|
|
||||||
"""Run pattern for specified duration."""
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
print("=" * 50)
|
|
||||||
print("Testing Auto and Manual Modes")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Test 1: Rainbow in AUTO mode (continuous)
|
|
||||||
print("\nTest 1: Rainbow pattern in AUTO mode (should run continuously)")
|
|
||||||
p.edit("rainbow_auto", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"b": 128,
|
|
||||||
"d": 50,
|
|
||||||
"n1": 2,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("rainbow_auto")
|
|
||||||
print("Running rainbow_auto for 3 seconds...")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
print("✓ Auto mode: Pattern ran continuously")
|
|
||||||
|
|
||||||
# Test 2: Rainbow in MANUAL mode (one step per tick)
|
|
||||||
print("\nTest 2: Rainbow pattern in MANUAL mode (one step per tick)")
|
|
||||||
p.edit("rainbow_manual", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"b": 128,
|
|
||||||
"d": 50,
|
|
||||||
"n1": 2,
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.select("rainbow_manual")
|
|
||||||
print("Calling tick() 5 times (should advance 5 steps)...")
|
|
||||||
for i in range(5):
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(100) # Small delay to see changes
|
|
||||||
print(f" Tick {i+1}: generator={'active' if p.generator is not None else 'stopped'}")
|
|
||||||
|
|
||||||
# Check if generator stopped after one cycle
|
|
||||||
if p.generator is None:
|
|
||||||
print("✓ Manual mode: Generator stopped after one step (as expected)")
|
|
||||||
else:
|
|
||||||
print("⚠ Manual mode: Generator still active (may need multiple ticks)")
|
|
||||||
|
|
||||||
# Test 3: Pulse in AUTO mode (continuous cycles)
|
|
||||||
print("\nTest 3: Pulse pattern in AUTO mode (should pulse continuously)")
|
|
||||||
p.edit("pulse_auto", {
|
|
||||||
"p": "pulse",
|
|
||||||
"b": 128,
|
|
||||||
"d": 100,
|
|
||||||
"n1": 500, # Attack
|
|
||||||
"n2": 200, # Hold
|
|
||||||
"n3": 500, # Decay
|
|
||||||
"c": [(255, 0, 0)],
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("pulse_auto")
|
|
||||||
print("Running pulse_auto for 3 seconds...")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
print("✓ Auto mode: Pulse ran continuously")
|
|
||||||
|
|
||||||
# Test 4: Pulse in MANUAL mode (one cycle then stop)
|
|
||||||
print("\nTest 4: Pulse pattern in MANUAL mode (one cycle then stop)")
|
|
||||||
p.edit("pulse_manual", {
|
|
||||||
"p": "pulse",
|
|
||||||
"b": 128,
|
|
||||||
"d": 100,
|
|
||||||
"n1": 300, # Attack
|
|
||||||
"n2": 200, # Hold
|
|
||||||
"n3": 300, # Decay
|
|
||||||
"c": [(0, 255, 0)],
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.select("pulse_manual")
|
|
||||||
print("Running pulse_manual until generator stops...")
|
|
||||||
tick_count = 0
|
|
||||||
max_ticks = 200 # Safety limit
|
|
||||||
while p.generator is not None and tick_count < max_ticks:
|
|
||||||
p.tick()
|
|
||||||
tick_count += 1
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
if p.generator is None:
|
|
||||||
print(f"✓ Manual mode: Pulse completed one cycle after {tick_count} ticks")
|
|
||||||
else:
|
|
||||||
print(f"⚠ Manual mode: Pulse still running after {tick_count} ticks")
|
|
||||||
|
|
||||||
# Test 5: Transition in AUTO mode (continuous transitions)
|
|
||||||
print("\nTest 5: Transition pattern in AUTO mode (continuous transitions)")
|
|
||||||
p.edit("transition_auto", {
|
|
||||||
"p": "transition",
|
|
||||||
"b": 128,
|
|
||||||
"d": 500,
|
|
||||||
"c": [(255, 0, 0), (0, 255, 0), (0, 0, 255)],
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("transition_auto")
|
|
||||||
print("Running transition_auto for 3 seconds...")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
print("✓ Auto mode: Transition ran continuously")
|
|
||||||
|
|
||||||
# Test 6: Transition in MANUAL mode (one transition then stop)
|
|
||||||
print("\nTest 6: Transition pattern in MANUAL mode (one transition then stop)")
|
|
||||||
p.edit("transition_manual", {
|
|
||||||
"p": "transition",
|
|
||||||
"b": 128,
|
|
||||||
"d": 500,
|
|
||||||
"c": [(255, 0, 0), (0, 255, 0)],
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.select("transition_manual")
|
|
||||||
print("Running transition_manual until generator stops...")
|
|
||||||
tick_count = 0
|
|
||||||
max_ticks = 200
|
|
||||||
while p.generator is not None and tick_count < max_ticks:
|
|
||||||
p.tick()
|
|
||||||
tick_count += 1
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
if p.generator is None:
|
|
||||||
print(f"✓ Manual mode: Transition completed after {tick_count} ticks")
|
|
||||||
else:
|
|
||||||
print(f"⚠ Manual mode: Transition still running after {tick_count} ticks")
|
|
||||||
|
|
||||||
# Test 7: Switching between auto and manual modes
|
|
||||||
print("\nTest 7: Switching between auto and manual modes")
|
|
||||||
p.edit("switch_test", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"b": 128,
|
|
||||||
"d": 50,
|
|
||||||
"n1": 2,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("switch_test")
|
|
||||||
print("Running in auto mode for 1 second...")
|
|
||||||
run_for(p, wdt, 1000)
|
|
||||||
|
|
||||||
# Switch to manual mode by editing the preset
|
|
||||||
print("Switching to manual mode...")
|
|
||||||
p.edit("switch_test", {"a": False})
|
|
||||||
p.select("switch_test") # Re-select to apply changes
|
|
||||||
|
|
||||||
print("Calling tick() 3 times in manual mode...")
|
|
||||||
for i in range(3):
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(100)
|
|
||||||
print(f" Tick {i+1}: generator={'active' if p.generator is not None else 'stopped'}")
|
|
||||||
|
|
||||||
# Switch back to auto mode
|
|
||||||
print("Switching back to auto mode...")
|
|
||||||
p.edit("switch_test", {"a": True})
|
|
||||||
p.select("switch_test")
|
|
||||||
print("Running in auto mode for 1 second...")
|
|
||||||
run_for(p, wdt, 1000)
|
|
||||||
print("✓ Successfully switched between auto and manual modes")
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
print("\nCleaning up...")
|
|
||||||
p.edit("cleanup_off", {"p": "off"})
|
|
||||||
p.select("cleanup_off")
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(100)
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("All tests completed!")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
# Create blink preset (use short-key fields: p=pattern, b=brightness, d=delay, c=colors)
|
|
||||||
p.edit("test_blink", {
|
|
||||||
"p": "blink",
|
|
||||||
"b": 64,
|
|
||||||
"d": 200,
|
|
||||||
"c": [(255, 0, 0), (0, 0, 255)],
|
|
||||||
})
|
|
||||||
p.select("test_blink")
|
|
||||||
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < 1500:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def run_for(p, wdt, ms):
|
|
||||||
"""Helper: run current pattern for given ms using tick()."""
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
# Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)
|
|
||||||
print("Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)")
|
|
||||||
p.edit("chase1", {
|
|
||||||
"p": "chase",
|
|
||||||
"b": 255,
|
|
||||||
"d": 200,
|
|
||||||
"n1": 5,
|
|
||||||
"n2": 5,
|
|
||||||
"n3": 1,
|
|
||||||
"n4": 1,
|
|
||||||
"c": [(255, 0, 0), (0, 255, 0)],
|
|
||||||
})
|
|
||||||
p.select("chase1")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
|
|
||||||
# Test 2: Forward and backward (n3=2, n4=-1)
|
|
||||||
print("Test 2: Forward and backward (n3=2, n4=-1)")
|
|
||||||
p.edit("chase2", {
|
|
||||||
"p": "chase",
|
|
||||||
"n1": 3,
|
|
||||||
"n2": 3,
|
|
||||||
"n3": 2,
|
|
||||||
"n4": -1,
|
|
||||||
"d": 150,
|
|
||||||
"c": [(0, 0, 255), (255, 255, 0)],
|
|
||||||
})
|
|
||||||
p.select("chase2")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
|
|
||||||
# Test 3: Large segments (n1=10, n2=5)
|
|
||||||
print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)")
|
|
||||||
p.edit("chase3", {
|
|
||||||
"p": "chase",
|
|
||||||
"n1": 10,
|
|
||||||
"n2": 5,
|
|
||||||
"n3": 3,
|
|
||||||
"n4": 3,
|
|
||||||
"d": 200,
|
|
||||||
"c": [(255, 128, 0), (128, 0, 255)],
|
|
||||||
})
|
|
||||||
p.select("chase3")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
|
|
||||||
# Test 4: Fast movement (n3=5, n4=5)
|
|
||||||
print("Test 4: Fast movement (n3=5, n4=5)")
|
|
||||||
p.edit("chase4", {
|
|
||||||
"p": "chase",
|
|
||||||
"n1": 4,
|
|
||||||
"n2": 4,
|
|
||||||
"n3": 5,
|
|
||||||
"n4": 5,
|
|
||||||
"d": 100,
|
|
||||||
"c": [(255, 0, 255), (0, 255, 255)],
|
|
||||||
})
|
|
||||||
p.select("chase4")
|
|
||||||
run_for(p, wdt, 2000)
|
|
||||||
|
|
||||||
# Test 5: Backward movement (n3=-2, n4=-2)
|
|
||||||
print("Test 5: Backward movement (n3=-2, n4=-2)")
|
|
||||||
p.edit("chase5", {
|
|
||||||
"p": "chase",
|
|
||||||
"n1": 6,
|
|
||||||
"n2": 4,
|
|
||||||
"n3": -2,
|
|
||||||
"n4": -2,
|
|
||||||
"d": 200,
|
|
||||||
"c": [(255, 255, 255), (0, 0, 0)],
|
|
||||||
})
|
|
||||||
p.select("chase5")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
|
|
||||||
# Test 6: Alternating forward/backward (n3=3, n4=-2)
|
|
||||||
print("Test 6: Alternating forward/backward (n3=3, n4=-2)")
|
|
||||||
p.edit("chase6", {
|
|
||||||
"p": "chase",
|
|
||||||
"n1": 5,
|
|
||||||
"n2": 5,
|
|
||||||
"n3": 3,
|
|
||||||
"n4": -2,
|
|
||||||
"d": 250,
|
|
||||||
"c": [(255, 0, 0), (0, 255, 0)],
|
|
||||||
})
|
|
||||||
p.select("chase6")
|
|
||||||
run_for(p, wdt, 4000)
|
|
||||||
|
|
||||||
# Test 7: Manual mode - advance one step per beat
|
|
||||||
print("Test 7: Manual mode chase (auto=False, n3=2, n4=1)")
|
|
||||||
p.edit("chase_manual", {
|
|
||||||
"p": "chase",
|
|
||||||
"n1": 4,
|
|
||||||
"n2": 4,
|
|
||||||
"n3": 2,
|
|
||||||
"n4": 1,
|
|
||||||
"d": 200,
|
|
||||||
"c": [(255, 255, 0), (0, 255, 255)],
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.step = 0 # Reset step counter
|
|
||||||
print(" Advancing pattern with 10 beats (select + tick)...")
|
|
||||||
for i in range(10):
|
|
||||||
p.select("chase_manual") # Simulate beat - restarts generator
|
|
||||||
p.tick() # Advance one step
|
|
||||||
utime.sleep_ms(500) # Pause to see the pattern
|
|
||||||
wdt.feed()
|
|
||||||
print(f" Beat {i+1}: step={p.step}")
|
|
||||||
|
|
||||||
# Test 8: Verify step increments correctly in manual mode
|
|
||||||
print("Test 8: Verify step increments (auto=False)")
|
|
||||||
p.edit("chase_manual2", {
|
|
||||||
"p": "chase",
|
|
||||||
"n1": 3,
|
|
||||||
"n2": 3,
|
|
||||||
"n3": 1,
|
|
||||||
"n4": 1,
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.step = 0
|
|
||||||
initial_step = p.step
|
|
||||||
p.select("chase_manual2")
|
|
||||||
p.tick()
|
|
||||||
final_step = p.step
|
|
||||||
print(f" Step updated from {initial_step} to {final_step} (expected: 1)")
|
|
||||||
if final_step == 1:
|
|
||||||
print(" ✓ Step increment working correctly")
|
|
||||||
else:
|
|
||||||
print(f" ✗ Step increment mismatch! Expected 1, got {final_step}")
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
print("Test complete, turning off")
|
|
||||||
p.edit("cleanup_off", {"p": "off"})
|
|
||||||
p.select("cleanup_off")
|
|
||||||
run_for(p, wdt, 100)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def run_for(p, wdt, ms):
|
|
||||||
"""Helper: run current pattern for given ms using tick()."""
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
# Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)
|
|
||||||
print("Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)")
|
|
||||||
p.edit("circle1", {
|
|
||||||
"p": "circle",
|
|
||||||
"b": 255,
|
|
||||||
"n1": 50, # Head moves 50 LEDs/second
|
|
||||||
"n2": 100, # Max length 100 LEDs
|
|
||||||
"n3": 200, # Tail moves 200 LEDs/second
|
|
||||||
"n4": 0, # Min length 0 LEDs
|
|
||||||
"c": [(255, 0, 0)], # Red
|
|
||||||
})
|
|
||||||
p.select("circle1")
|
|
||||||
run_for(p, wdt, 5000)
|
|
||||||
|
|
||||||
# Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)
|
|
||||||
print("Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)")
|
|
||||||
p.edit("circle2", {
|
|
||||||
"p": "circle",
|
|
||||||
"n1": 20,
|
|
||||||
"n2": 50,
|
|
||||||
"n3": 100,
|
|
||||||
"n4": 0,
|
|
||||||
"c": [(0, 255, 0)], # Green
|
|
||||||
})
|
|
||||||
p.select("circle2")
|
|
||||||
run_for(p, wdt, 5000)
|
|
||||||
|
|
||||||
# Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)
|
|
||||||
print("Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)")
|
|
||||||
p.edit("circle3", {
|
|
||||||
"p": "circle",
|
|
||||||
"n1": 100,
|
|
||||||
"n2": 30,
|
|
||||||
"n3": 20,
|
|
||||||
"n4": 0,
|
|
||||||
"c": [(0, 0, 255)], # Blue
|
|
||||||
})
|
|
||||||
p.select("circle3")
|
|
||||||
run_for(p, wdt, 5000)
|
|
||||||
|
|
||||||
# Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)
|
|
||||||
print("Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)")
|
|
||||||
p.edit("circle4", {
|
|
||||||
"p": "circle",
|
|
||||||
"n1": 50,
|
|
||||||
"n2": 40,
|
|
||||||
"n3": 100,
|
|
||||||
"n4": 10,
|
|
||||||
"c": [(255, 255, 0)], # Yellow
|
|
||||||
})
|
|
||||||
p.select("circle4")
|
|
||||||
run_for(p, wdt, 5000)
|
|
||||||
|
|
||||||
# Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)
|
|
||||||
print("Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)")
|
|
||||||
p.edit("circle5", {
|
|
||||||
"p": "circle",
|
|
||||||
"n1": 200,
|
|
||||||
"n2": 20,
|
|
||||||
"n3": 200,
|
|
||||||
"n4": 0,
|
|
||||||
"c": [(255, 0, 255)], # Magenta
|
|
||||||
})
|
|
||||||
p.select("circle5")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
|
|
||||||
# Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)
|
|
||||||
print("Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)")
|
|
||||||
p.edit("circle6", {
|
|
||||||
"p": "circle",
|
|
||||||
"n1": 10,
|
|
||||||
"n2": 25,
|
|
||||||
"n3": 10,
|
|
||||||
"n4": 0,
|
|
||||||
"c": [(0, 255, 255)], # Cyan
|
|
||||||
})
|
|
||||||
p.select("circle6")
|
|
||||||
run_for(p, wdt, 5000)
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
print("Test complete, turning off")
|
|
||||||
p.edit("cleanup_off", {"p": "off"})
|
|
||||||
p.select("cleanup_off")
|
|
||||||
run_for(p, wdt, 100)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
# Create an "off" preset (use short-key field `p` for pattern)
|
|
||||||
p.edit("test_off", {"p": "off"})
|
|
||||||
p.select("test_off")
|
|
||||||
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < 200:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
# Create presets for on and off using the short-key fields that Presets expects
|
|
||||||
# Preset fields:
|
|
||||||
# p = pattern name, b = brightness, d = delay, c = list of (r,g,b) colors
|
|
||||||
p.edit("test_on", {
|
|
||||||
"p": "on",
|
|
||||||
"b": 64,
|
|
||||||
"d": 120,
|
|
||||||
"c": [(255, 0, 0), (0, 0, 255)],
|
|
||||||
})
|
|
||||||
p.edit("test_off", {"p": "off"})
|
|
||||||
|
|
||||||
# ON phase
|
|
||||||
p.select("test_on")
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < 800:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
# OFF phase
|
|
||||||
p.select("test_off")
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < 100:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def run_for(p, wdt, ms):
|
|
||||||
"""Helper: run current pattern for given ms using tick()."""
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
# Test 1: Simple single-color pulse
|
|
||||||
print("Test 1: Single-color pulse (attack=500, hold=500, decay=500, delay=500)")
|
|
||||||
p.edit("pulse1", {
|
|
||||||
"p": "pulse",
|
|
||||||
"b": 255,
|
|
||||||
"c": [(255, 0, 0)],
|
|
||||||
"n1": 500, # attack ms
|
|
||||||
"n2": 500, # hold ms
|
|
||||||
"n3": 500, # decay ms
|
|
||||||
"d": 500, # delay ms between pulses
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("pulse1")
|
|
||||||
run_for(p, wdt, 5000)
|
|
||||||
|
|
||||||
# Test 2: Faster pulse
|
|
||||||
print("Test 2: Fast pulse (attack=100, hold=100, decay=100, delay=100)")
|
|
||||||
p.edit("pulse2", {
|
|
||||||
"p": "pulse",
|
|
||||||
"n1": 100,
|
|
||||||
"n2": 100,
|
|
||||||
"n3": 100,
|
|
||||||
"d": 100,
|
|
||||||
"c": [(0, 255, 0)],
|
|
||||||
})
|
|
||||||
p.select("pulse2")
|
|
||||||
run_for(p, wdt, 4000)
|
|
||||||
|
|
||||||
# Test 3: Multi-color pulse cycle
|
|
||||||
print("Test 3: Multi-color pulse (red -> green -> blue)")
|
|
||||||
p.edit("pulse3", {
|
|
||||||
"p": "pulse",
|
|
||||||
"n1": 300,
|
|
||||||
"n2": 300,
|
|
||||||
"n3": 300,
|
|
||||||
"d": 200,
|
|
||||||
"c": [(255, 0, 0), (0, 255, 0), (0, 0, 255)],
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("pulse3")
|
|
||||||
run_for(p, wdt, 6000)
|
|
||||||
|
|
||||||
# Test 4: One-shot pulse (auto=False)
|
|
||||||
print("Test 4: Single pulse, auto=False")
|
|
||||||
p.edit("pulse4", {
|
|
||||||
"p": "pulse",
|
|
||||||
"n1": 400,
|
|
||||||
"n2": 0,
|
|
||||||
"n3": 400,
|
|
||||||
"d": 0,
|
|
||||||
"c": [(255, 255, 255)],
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.select("pulse4")
|
|
||||||
# Run long enough to allow one full pulse cycle
|
|
||||||
run_for(p, wdt, 1500)
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
print("Test complete, turning off")
|
|
||||||
p.edit("cleanup_off", {"p": "off"})
|
|
||||||
p.select("cleanup_off")
|
|
||||||
run_for(p, wdt, 200)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def run_for(p, wdt, ms):
|
|
||||||
"""Helper: run current pattern for given ms using tick()."""
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
# Test 1: Basic rainbow with auto=True (continuous)
|
|
||||||
print("Test 1: Basic rainbow (auto=True, n1=1)")
|
|
||||||
p.edit("rainbow1", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"b": 255,
|
|
||||||
"d": 100,
|
|
||||||
"n1": 1,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("rainbow1")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
|
|
||||||
# Test 2: Fast rainbow
|
|
||||||
print("Test 2: Fast rainbow (low delay, n1=1)")
|
|
||||||
p.edit("rainbow2", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"d": 50,
|
|
||||||
"n1": 1,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("rainbow2")
|
|
||||||
run_for(p, wdt, 2000)
|
|
||||||
|
|
||||||
# Test 3: Slow rainbow
|
|
||||||
print("Test 3: Slow rainbow (high delay, n1=1)")
|
|
||||||
p.edit("rainbow3", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"d": 500,
|
|
||||||
"n1": 1,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("rainbow3")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
|
|
||||||
# Test 4: Low brightness rainbow
|
|
||||||
print("Test 4: Low brightness rainbow (n1=1)")
|
|
||||||
p.edit("rainbow4", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"b": 64,
|
|
||||||
"d": 100,
|
|
||||||
"n1": 1,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("rainbow4")
|
|
||||||
run_for(p, wdt, 2000)
|
|
||||||
|
|
||||||
# Test 5: Single-step rainbow (auto=False)
|
|
||||||
print("Test 5: Single-step rainbow (auto=False, n1=1)")
|
|
||||||
p.edit("rainbow5", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"b": 255,
|
|
||||||
"d": 100,
|
|
||||||
"n1": 1,
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.step = 0
|
|
||||||
for i in range(10):
|
|
||||||
p.select("rainbow5")
|
|
||||||
# One tick advances the generator one frame when auto=False
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(100)
|
|
||||||
wdt.feed()
|
|
||||||
|
|
||||||
# Test 6: Verify step updates correctly
|
|
||||||
print("Test 6: Verify step updates (auto=False, n1=1)")
|
|
||||||
p.edit("rainbow6", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"n1": 1,
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
initial_step = p.step
|
|
||||||
p.select("rainbow6")
|
|
||||||
p.tick()
|
|
||||||
final_step = p.step
|
|
||||||
print(f"Step updated from {initial_step} to {final_step} (expected increment: 1)")
|
|
||||||
|
|
||||||
# Test 7: Fast step increment (n1=5)
|
|
||||||
print("Test 7: Fast rainbow (n1=5, auto=True)")
|
|
||||||
p.edit("rainbow7", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"b": 255,
|
|
||||||
"d": 100,
|
|
||||||
"n1": 5,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("rainbow7")
|
|
||||||
run_for(p, wdt, 2000)
|
|
||||||
|
|
||||||
# Test 8: Very fast step increment (n1=10)
|
|
||||||
print("Test 8: Very fast rainbow (n1=10, auto=True)")
|
|
||||||
p.edit("rainbow8", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"n1": 10,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("rainbow8")
|
|
||||||
run_for(p, wdt, 2000)
|
|
||||||
|
|
||||||
# Test 9: Verify n1 controls step increment (auto=False)
|
|
||||||
print("Test 9: Verify n1 step increment (auto=False, n1=5)")
|
|
||||||
p.edit("rainbow9", {
|
|
||||||
"p": "rainbow",
|
|
||||||
"n1": 5,
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.step = 0
|
|
||||||
initial_step = p.step
|
|
||||||
p.select("rainbow9")
|
|
||||||
p.tick()
|
|
||||||
final_step = p.step
|
|
||||||
expected_step = (initial_step + 5) % 256
|
|
||||||
print(f"Step updated from {initial_step} to {final_step} (expected: {expected_step})")
|
|
||||||
if final_step == expected_step:
|
|
||||||
print("✓ n1 step increment working correctly")
|
|
||||||
else:
|
|
||||||
print(f"✗ Step increment mismatch! Expected {expected_step}, got {final_step}")
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
print("Test complete, turning off")
|
|
||||||
p.edit("cleanup_off", {"p": "off"})
|
|
||||||
p.select("cleanup_off")
|
|
||||||
run_for(p, wdt, 100)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import utime
|
|
||||||
from machine import WDT
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def run_for(p, wdt, ms):
|
|
||||||
"""Helper: run current pattern for given ms using tick()."""
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
|
||||||
wdt.feed()
|
|
||||||
p.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
s = Settings()
|
|
||||||
pin = s.get("led_pin", 10)
|
|
||||||
num = s.get("num_leds", 30)
|
|
||||||
|
|
||||||
p = Presets(pin=pin, num_leds=num)
|
|
||||||
wdt = WDT(timeout=10000)
|
|
||||||
|
|
||||||
# Test 1: Simple two-color transition
|
|
||||||
print("Test 1: Two-color transition (red <-> blue, delay=1000)")
|
|
||||||
p.edit("transition1", {
|
|
||||||
"p": "transition",
|
|
||||||
"b": 255,
|
|
||||||
"d": 1000, # transition duration
|
|
||||||
"c": [(255, 0, 0), (0, 0, 255)],
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("transition1")
|
|
||||||
run_for(p, wdt, 6000)
|
|
||||||
|
|
||||||
# Test 2: Multi-color transition
|
|
||||||
print("Test 2: Multi-color transition (red -> green -> blue -> white)")
|
|
||||||
p.edit("transition2", {
|
|
||||||
"p": "transition",
|
|
||||||
"d": 800,
|
|
||||||
"c": [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)],
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("transition2")
|
|
||||||
run_for(p, wdt, 8000)
|
|
||||||
|
|
||||||
# Test 3: One-shot transition (auto=False)
|
|
||||||
print("Test 3: One-shot transition (auto=False)")
|
|
||||||
p.edit("transition3", {
|
|
||||||
"p": "transition",
|
|
||||||
"d": 1000,
|
|
||||||
"c": [(255, 0, 0), (0, 255, 0)],
|
|
||||||
"a": False,
|
|
||||||
})
|
|
||||||
p.select("transition3")
|
|
||||||
# Run long enough for a single transition step
|
|
||||||
run_for(p, wdt, 2000)
|
|
||||||
|
|
||||||
# Test 4: Single-color behavior (should just stay on)
|
|
||||||
print("Test 4: Single-color transition (should hold color)")
|
|
||||||
p.edit("transition4", {
|
|
||||||
"p": "transition",
|
|
||||||
"c": [(0, 0, 255)],
|
|
||||||
"d": 500,
|
|
||||||
"a": True,
|
|
||||||
})
|
|
||||||
p.select("transition4")
|
|
||||||
run_for(p, wdt, 3000)
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
print("Test complete, turning off")
|
|
||||||
p.edit("cleanup_off", {"p": "off"})
|
|
||||||
p.select("cleanup_off")
|
|
||||||
run_for(p, wdt, 200)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import sys
|
|
||||||
# So "from ws2812 import WS2812B" finds pico/lib when run from device / or test/
|
|
||||||
if "lib" not in sys.path:
|
|
||||||
sys.path.insert(0, "lib")
|
|
||||||
if "../lib" not in sys.path:
|
|
||||||
sys.path.insert(0, "../lib")
|
|
||||||
from ws2812 import WS2812B
|
|
||||||
import time
|
|
||||||
|
|
||||||
# --- Rainbow pattern (outside ws2812): pregen double buffer, show via head offset ---
|
|
||||||
|
|
||||||
def hue_to_rgb(hue):
|
|
||||||
"""Hue 0..360 -> (r, g, b). Simple HSV with S=V=1."""
|
|
||||||
h = hue % 360
|
|
||||||
x = 1 - abs((h / 60) % 2 - 1)
|
|
||||||
if h < 60:
|
|
||||||
r, g, b = 1, x, 0
|
|
||||||
elif h < 120:
|
|
||||||
r, g, b = x, 1, 0
|
|
||||||
elif h < 180:
|
|
||||||
r, g, b = 0, 1, x
|
|
||||||
elif h < 240:
|
|
||||||
r, g, b = 0, x, 1
|
|
||||||
elif h < 300:
|
|
||||||
r, g, b = x, 0, 1
|
|
||||||
else:
|
|
||||||
r, g, b = 1, 0, x
|
|
||||||
return (int(r * 255), int(g * 255), int(b * 255))
|
|
||||||
|
|
||||||
|
|
||||||
def make_rainbow_double(num_leds, brightness=1.0):
|
|
||||||
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len).
|
|
||||||
head must be in 0..strip_len-1 so DMA reads double_buf[head:head+strip_len] with no copy."""
|
|
||||||
n = 2 * num_leds
|
|
||||||
double_buf = bytearray(n * 3)
|
|
||||||
for i in range(n):
|
|
||||||
hue = (i / n) * 360 * 2
|
|
||||||
r, g, b = hue_to_rgb(hue)
|
|
||||||
g = int(g * brightness) & 0xFF
|
|
||||||
r = int(r * brightness) & 0xFF
|
|
||||||
b = int(b * brightness) & 0xFF
|
|
||||||
o = i * 3
|
|
||||||
double_buf[o] = g
|
|
||||||
double_buf[o + 1] = r
|
|
||||||
double_buf[o + 2] = b
|
|
||||||
strip_len = num_leds * 3
|
|
||||||
return (double_buf, strip_len)
|
|
||||||
|
|
||||||
|
|
||||||
def show_rainbow(strip, double_buf, strip_len, head):
|
|
||||||
"""DMA reads directly from double_buf at head; no copy. head in 0..strip_len-1."""
|
|
||||||
strip.show(double_buf, head)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Strips + rainbow buffers per strip ---
|
|
||||||
# Each strip can have a different length; buffers and phase are per-strip.
|
|
||||||
# Strip config must match pico/src/main.py pins.
|
|
||||||
STRIP_CONFIG = (
|
|
||||||
(7, 291),
|
|
||||||
(3, 290),
|
|
||||||
(6, 283),
|
|
||||||
(28, 278),
|
|
||||||
(29, 275),
|
|
||||||
(4, 278),
|
|
||||||
(0, 283),
|
|
||||||
(2, 290),
|
|
||||||
)
|
|
||||||
|
|
||||||
strips = []
|
|
||||||
sm = 0
|
|
||||||
for pin, num_leds in STRIP_CONFIG:
|
|
||||||
print(pin, num_leds)
|
|
||||||
ws = WS2812B(num_leds, pin, sm, brightness=1.0) # 1.0 so fill() is visible
|
|
||||||
strips.append(ws)
|
|
||||||
sm += 1
|
|
||||||
|
|
||||||
# Cumulative LED count before each strip; total ring size
|
|
||||||
cumulative_leds = [0]
|
|
||||||
for ws in strips[:-1]:
|
|
||||||
cumulative_leds.append(cumulative_leds[-1] + ws.num_leds)
|
|
||||||
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
|
||||||
bytes_per_cycle = total_ring_leds * 3
|
|
||||||
|
|
||||||
# One rainbow double buffer per strip (length = 2 * num_leds for that strip)
|
|
||||||
now = time.ticks_ms()
|
|
||||||
rainbow_data = [make_rainbow_double(ws.num_leds, ws.brightness) for ws in strips]
|
|
||||||
# Global phase in bytes; each strip: head = (phase + cumulative_leds[i]*3) % strip_len[i]
|
|
||||||
print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
|
||||||
rainbow_head = 0
|
|
||||||
step = 3
|
|
||||||
|
|
||||||
while True:
|
|
||||||
now = time.ticks_ms()
|
|
||||||
for i, (strip, (double_buf, strip_len)) in enumerate(zip(strips, rainbow_data)):
|
|
||||||
head = (rainbow_head + cumulative_leds[i] * 3) % strip_len
|
|
||||||
show_rainbow(strip, double_buf, strip_len, head)
|
|
||||||
rainbow_head = (rainbow_head + step) % bytes_per_cycle
|
|
||||||
#print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
|
||||||
time.sleep_ms(10)
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
"""
|
|
||||||
On-device visual test for the Roll pattern via Presets.
|
|
||||||
|
|
||||||
This exercises src/patterns/roll.py (gradient from color1 to color2 across strips),
|
|
||||||
not the low-level WS2812 driver.
|
|
||||||
|
|
||||||
Usage (from pico/ dir or project root with adjusted paths):
|
|
||||||
|
|
||||||
mpremote connect <device> cp src/*.py :
|
|
||||||
mpremote connect <device> cp src/patterns/*.py :patterns
|
|
||||||
mpremote connect <device> cp lib/*.py :
|
|
||||||
mpremote connect <device> cp test/roll.py :
|
|
||||||
mpremote connect <device> run roll.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import utime
|
|
||||||
|
|
||||||
from presets import Presets, Preset
|
|
||||||
|
|
||||||
|
|
||||||
def make_roll_preset(name, color1, color2, delay_ms=60, brightness=255, direction=0):
|
|
||||||
"""
|
|
||||||
Helper to build a Preset for the 'roll' pattern.
|
|
||||||
|
|
||||||
- color1: head color (full intensity)
|
|
||||||
- color2: tail color (end of gradient)
|
|
||||||
- direction: 0 = clockwise, 1 = anti-clockwise
|
|
||||||
"""
|
|
||||||
data = {
|
|
||||||
"p": "roll",
|
|
||||||
"c": [color1, color2],
|
|
||||||
"b": brightness,
|
|
||||||
"d": delay_ms,
|
|
||||||
"n4": direction,
|
|
||||||
"a": True, # animated
|
|
||||||
}
|
|
||||||
return name, Preset(data)
|
|
||||||
|
|
||||||
|
|
||||||
def run_for(presets, duration_ms):
|
|
||||||
"""Tick the current pattern for duration_ms."""
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
|
|
||||||
presets.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
presets = Presets()
|
|
||||||
presets.load()
|
|
||||||
|
|
||||||
num_leds = presets.strip_length(0)
|
|
||||||
if num_leds <= 0:
|
|
||||||
print("No strips; aborting roll test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Starting roll pattern gradient tests via Presets...")
|
|
||||||
|
|
||||||
# A few different roll presets to compare:
|
|
||||||
roll_presets = []
|
|
||||||
|
|
||||||
# 1. White → off, clockwise (50% brightness, faster)
|
|
||||||
roll_presets.append(
|
|
||||||
make_roll_preset(
|
|
||||||
"roll_white_off_cw",
|
|
||||||
color1=(255, 255, 255),
|
|
||||||
color2=(0, 0, 0),
|
|
||||||
delay_ms=120,
|
|
||||||
brightness=128,
|
|
||||||
direction=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Warm white → cool blue, clockwise (50% brightness, faster)
|
|
||||||
roll_presets.append(
|
|
||||||
make_roll_preset(
|
|
||||||
"roll_warm_cool_cw",
|
|
||||||
color1=(255, 200, 100),
|
|
||||||
color2=(0, 0, 255),
|
|
||||||
delay_ms=130,
|
|
||||||
brightness=128,
|
|
||||||
direction=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Red → green, counter-clockwise (50% brightness, faster)
|
|
||||||
roll_presets.append(
|
|
||||||
make_roll_preset(
|
|
||||||
"roll_red_green_ccw",
|
|
||||||
color1=(255, 0, 0),
|
|
||||||
color2=(0, 255, 0),
|
|
||||||
delay_ms=110,
|
|
||||||
brightness=128,
|
|
||||||
direction=1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register presets and run them one after another
|
|
||||||
for name, preset_obj in roll_presets:
|
|
||||||
presets.presets[name] = preset_obj
|
|
||||||
|
|
||||||
for name, _preset in roll_presets:
|
|
||||||
print("Running roll preset:", name)
|
|
||||||
presets.select(name)
|
|
||||||
run_for(presets, duration_ms=8000)
|
|
||||||
|
|
||||||
print("Roll pattern Presets test finished. Turning off LEDs.")
|
|
||||||
presets.select("off")
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from neopixel import NeoPixel
|
|
||||||
from machine import Pin
|
|
||||||
|
|
||||||
p = NeoPixel(Pin(6), 291)
|
|
||||||
p.fill((255, 255, 255))
|
|
||||||
p.write()
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"""
|
|
||||||
On-device test that turns all LEDs on via Presets and verifies strip 0 (pin 6).
|
|
||||||
|
|
||||||
Usage (from pico/ dir or project root with adjusted paths):
|
|
||||||
|
|
||||||
mpremote connect <device> cp src/*.py :
|
|
||||||
mpremote connect <device> cp src/patterns/*.py :patterns
|
|
||||||
mpremote connect <device> cp lib/*.py :
|
|
||||||
mpremote connect <device> cp test/test_all_on_presets.py :
|
|
||||||
mpremote connect <device> run test_all_on_presets.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
from presets import Presets, Preset
|
|
||||||
|
|
||||||
|
|
||||||
def verify_strip0_on(presets, expected_color):
|
|
||||||
"""Check that every LED on strip 0 matches expected_color."""
|
|
||||||
if not presets.strips:
|
|
||||||
print("No strips; skipping strip-0 on test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
strip = presets.strips[0]
|
|
||||||
r_exp, g_exp, b_exp = expected_color
|
|
||||||
|
|
||||||
for i in range(strip.num_leds):
|
|
||||||
o = i * 3
|
|
||||||
g = strip.ar[o]
|
|
||||||
r = strip.ar[o + 1]
|
|
||||||
b = strip.ar[o + 2]
|
|
||||||
if (r, g, b) != (r_exp, g_exp, b_exp):
|
|
||||||
raise AssertionError(
|
|
||||||
"Strip 0 LED %d: got (%d,%d,%d), expected (%d,%d,%d)"
|
|
||||||
% (i, r, g, b, r_exp, g_exp, b_exp)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
presets = Presets()
|
|
||||||
|
|
||||||
if not presets.strips:
|
|
||||||
print("No strips; skipping all-on-presets test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Full-brightness white via the built-in 'on' pattern.
|
|
||||||
base_color = (255, 255, 255)
|
|
||||||
brightness = 255
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"p": "on",
|
|
||||||
"c": [base_color],
|
|
||||||
"b": brightness,
|
|
||||||
}
|
|
||||||
|
|
||||||
name = "test_all_on_presets"
|
|
||||||
preset = Preset(data)
|
|
||||||
presets.presets[name] = preset
|
|
||||||
|
|
||||||
presets.select(name)
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
# Compute the color actually written by the pattern after brightness scaling.
|
|
||||||
expected_color = presets.apply_brightness(base_color, brightness)
|
|
||||||
|
|
||||||
verify_strip0_on(presets, expected_color)
|
|
||||||
|
|
||||||
print("test_all_on_presets: OK")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
"""
|
|
||||||
On-device test that exercises the Chase pattern via Presets.
|
|
||||||
|
|
||||||
Usage (from pico/ dir or project root with adjusted paths):
|
|
||||||
|
|
||||||
mpremote connect <device> cp src/*.py :
|
|
||||||
mpremote connect <device> cp src/patterns/*.py :patterns
|
|
||||||
mpremote connect <device> cp lib/*.py :
|
|
||||||
mpremote connect <device> cp test/test_chase_via_presets.py :
|
|
||||||
mpremote connect <device> run test_chase_via_presets.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import utime
|
|
||||||
|
|
||||||
from presets import Presets, Preset
|
|
||||||
|
|
||||||
|
|
||||||
def snapshot_strip_colors(presets, strip_idx=0, max_leds=32):
|
|
||||||
"""Return a list of (r,g,b) tuples for the first max_leds of the given strip."""
|
|
||||||
strip = presets.strips[strip_idx]
|
|
||||||
num = min(strip.num_leds, max_leds)
|
|
||||||
out = []
|
|
||||||
for i in range(num):
|
|
||||||
o = i * 3
|
|
||||||
g = strip.ar[o]
|
|
||||||
r = strip.ar[o + 1]
|
|
||||||
b = strip.ar[o + 2]
|
|
||||||
out.append((r, g, b))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def expected_chase_color(i, num_leds, step_count, color0, color1, n1, n2, n3, n4):
|
|
||||||
"""Mirror the position logic from patterns/chase.py for a single logical LED."""
|
|
||||||
segment_length = n1 + n2
|
|
||||||
|
|
||||||
if step_count % 2 == 0:
|
|
||||||
position = (step_count // 2) * (n3 + n4) + n3
|
|
||||||
else:
|
|
||||||
position = ((step_count + 1) // 2) * (n3 + n4)
|
|
||||||
|
|
||||||
max_pos = num_leds + segment_length
|
|
||||||
position = position % max_pos
|
|
||||||
if position < 0:
|
|
||||||
position += max_pos
|
|
||||||
|
|
||||||
relative_pos = (i - position) % segment_length
|
|
||||||
if relative_pos < 0:
|
|
||||||
relative_pos = (relative_pos + segment_length) % segment_length
|
|
||||||
|
|
||||||
return color0 if relative_pos < n1 else color1
|
|
||||||
|
|
||||||
|
|
||||||
def test_chase_single_step_via_presets():
|
|
||||||
presets = Presets()
|
|
||||||
|
|
||||||
num_leds = presets.num_leds
|
|
||||||
if num_leds <= 0:
|
|
||||||
print("No strips; skipping chase test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Simple alternating colors with known lengths.
|
|
||||||
base_color0 = (10, 0, 0)
|
|
||||||
base_color1 = (0, 0, 20)
|
|
||||||
|
|
||||||
# Use full brightness so apply_brightness is identity.
|
|
||||||
brightness = 255
|
|
||||||
|
|
||||||
n1 = 2
|
|
||||||
n2 = 3
|
|
||||||
# Same step size on even/odd for easier reasoning.
|
|
||||||
n3 = 1
|
|
||||||
n4 = 1
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"p": "chase",
|
|
||||||
"c": [base_color0, base_color1],
|
|
||||||
"b": brightness,
|
|
||||||
"d": 0,
|
|
||||||
"a": False, # single-step mode
|
|
||||||
"n1": n1,
|
|
||||||
"n2": n2,
|
|
||||||
"n3": n3,
|
|
||||||
"n4": n4,
|
|
||||||
}
|
|
||||||
|
|
||||||
name = "test_chase_pattern"
|
|
||||||
preset = Preset(data)
|
|
||||||
presets.presets[name] = preset
|
|
||||||
|
|
||||||
# Select and run one tick; this should render exactly one chase frame for step 0.
|
|
||||||
presets.select(name, step=0)
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
# Colors after brightness scaling (driver.apply_brightness is used in the pattern).
|
|
||||||
color0 = presets.apply_brightness(base_color0, brightness)
|
|
||||||
color1 = presets.apply_brightness(base_color1, brightness)
|
|
||||||
|
|
||||||
# Snapshot first few LEDs of strip 0 and compare against expected pattern for step 0.
|
|
||||||
colors = snapshot_strip_colors(presets, strip_idx=0, max_leds=16)
|
|
||||||
step_count = 0
|
|
||||||
|
|
||||||
for i, actual in enumerate(colors):
|
|
||||||
expected = expected_chase_color(
|
|
||||||
i, num_leds, step_count, color0, color1, n1, n2, n3, n4
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
actual == expected
|
|
||||||
), "LED %d: got %r, expected %r" % (i, actual, expected)
|
|
||||||
|
|
||||||
print("test_chase_single_step_via_presets: OK")
|
|
||||||
|
|
||||||
|
|
||||||
def test_chase_multiple_steps_via_presets():
|
|
||||||
"""Render several steps and verify pattern advances correctly."""
|
|
||||||
presets = Presets()
|
|
||||||
|
|
||||||
num_leds = presets.num_leds
|
|
||||||
if num_leds <= 0:
|
|
||||||
print("No strips; skipping chase multi-step test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
base_color0 = (10, 0, 0)
|
|
||||||
base_color1 = (0, 0, 20)
|
|
||||||
brightness = 255
|
|
||||||
|
|
||||||
n1 = 2
|
|
||||||
n2 = 3
|
|
||||||
n3 = 1
|
|
||||||
n4 = 1
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"p": "chase",
|
|
||||||
"c": [base_color0, base_color1],
|
|
||||||
"b": brightness,
|
|
||||||
"d": 0,
|
|
||||||
"a": False,
|
|
||||||
"n1": n1,
|
|
||||||
"n2": n2,
|
|
||||||
"n3": n3,
|
|
||||||
"n4": n4,
|
|
||||||
}
|
|
||||||
|
|
||||||
name = "test_chase_pattern_multi"
|
|
||||||
preset = Preset(data)
|
|
||||||
presets.presets[name] = preset
|
|
||||||
|
|
||||||
color0 = presets.apply_brightness(base_color0, brightness)
|
|
||||||
color1 = presets.apply_brightness(base_color1, brightness)
|
|
||||||
|
|
||||||
# In non-auto mode (a=False), the Chase pattern advances one step per
|
|
||||||
# invocation of the generator, and Presets is expected to call select()
|
|
||||||
# again for each beat. Emulate that here by re-selecting with an
|
|
||||||
# explicit step value for each frame we want to test.
|
|
||||||
for step_count in range(4):
|
|
||||||
presets.select(name, step=step_count)
|
|
||||||
presets.tick()
|
|
||||||
colors = snapshot_strip_colors(presets, strip_idx=0, max_leds=16)
|
|
||||||
|
|
||||||
for i, actual in enumerate(colors):
|
|
||||||
expected = expected_chase_color(
|
|
||||||
i, num_leds, step_count, color0, color1, n1, n2, n3, n4
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
actual == expected
|
|
||||||
), "step %d, LED %d: got %r, expected %r" % (
|
|
||||||
step_count,
|
|
||||||
i,
|
|
||||||
actual,
|
|
||||||
expected,
|
|
||||||
)
|
|
||||||
|
|
||||||
print("test_chase_multiple_steps_via_presets: OK")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
test_chase_single_step_via_presets()
|
|
||||||
test_chase_multiple_steps_via_presets()
|
|
||||||
# Give a brief pause so message is visible if run interactively.
|
|
||||||
utime.sleep_ms(100)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
"""
|
|
||||||
On-device test for the double_circle pattern using mpremote.
|
|
||||||
|
|
||||||
Usage (from pico/ dir or project root with adjusted paths):
|
|
||||||
|
|
||||||
mpremote connect <device> cp src/*.py :
|
|
||||||
mpremote connect <device> cp src/patterns/*.py :patterns
|
|
||||||
mpremote connect <device> cp lib/*.py :
|
|
||||||
mpremote connect <device> cp test/test_double_circle.py :
|
|
||||||
mpremote connect <device> run test_double_circle.py
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Instantiates Presets
|
|
||||||
- Creates a few in-memory 'double_circle' presets with different centers, widths, and colors
|
|
||||||
- Selects each one so you can visually confirm the symmetric bands and color gradients
|
|
||||||
"""
|
|
||||||
|
|
||||||
from presets import Presets, Preset
|
|
||||||
|
|
||||||
|
|
||||||
def make_double_circle_preset(
|
|
||||||
name, center, half_width, colors, direction=0, step_size=1, brightness=255
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Helper to build a Preset for the 'double_circle' pattern.
|
|
||||||
|
|
||||||
center: logical index (0-based, on reference strip 0)
|
|
||||||
half_width: number of LEDs each side of center
|
|
||||||
colors: [color1, color2] where each color is (r,g,b)
|
|
||||||
"""
|
|
||||||
cs = list(colors)[:2]
|
|
||||||
while len(cs) < 2:
|
|
||||||
cs.append((0, 0, 0))
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"p": "double_circle",
|
|
||||||
"c": cs,
|
|
||||||
"b": brightness,
|
|
||||||
"n1": center,
|
|
||||||
"n2": half_width,
|
|
||||||
"n3": direction,
|
|
||||||
"n4": step_size,
|
|
||||||
}
|
|
||||||
return name, Preset(data)
|
|
||||||
|
|
||||||
|
|
||||||
def show_and_wait(presets, name, preset_obj, wait_ms):
|
|
||||||
"""Select a static double_circle preset and hold it for wait_ms."""
|
|
||||||
presets.presets[name] = preset_obj
|
|
||||||
presets.select(name)
|
|
||||||
# DoubleCircle draws immediately in run(), then just yields; one tick is enough.
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
import utime
|
|
||||||
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < wait_ms:
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
presets = Presets()
|
|
||||||
presets.load()
|
|
||||||
|
|
||||||
num_leds = presets.strip_length(0)
|
|
||||||
if num_leds <= 0:
|
|
||||||
print("No strips; aborting double_circle test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Starting double_circle pattern test...")
|
|
||||||
|
|
||||||
quarter = num_leds // 4
|
|
||||||
half = num_leds // 2
|
|
||||||
|
|
||||||
dc_presets = []
|
|
||||||
|
|
||||||
# 1. Center at top (0), moderate width, color1 at center (n3=0)
|
|
||||||
dc_presets.append(
|
|
||||||
make_double_circle_preset(
|
|
||||||
"dc_top_red_to_blue",
|
|
||||||
center=0,
|
|
||||||
half_width=quarter,
|
|
||||||
colors=[(255, 0, 0), (0, 0, 255)],
|
|
||||||
direction=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Center at bottom (half), narrow band, color1 at endpoints (n3=1)
|
|
||||||
dc_presets.append(
|
|
||||||
make_double_circle_preset(
|
|
||||||
"dc_bottom_green_to_purple",
|
|
||||||
center=half,
|
|
||||||
half_width=quarter // 2,
|
|
||||||
colors=[(0, 255, 0), (128, 0, 128)],
|
|
||||||
direction=1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Center at quarter, wide band, both directions for comparison
|
|
||||||
dc_presets.append(
|
|
||||||
make_double_circle_preset(
|
|
||||||
"dc_quarter_white_to_cyan_inward",
|
|
||||||
center=quarter,
|
|
||||||
half_width=half,
|
|
||||||
colors=[(255, 255, 255), (0, 255, 255)],
|
|
||||||
direction=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dc_presets.append(
|
|
||||||
make_double_circle_preset(
|
|
||||||
"dc_quarter_white_to_cyan_outward",
|
|
||||||
center=quarter,
|
|
||||||
half_width=half,
|
|
||||||
colors=[(255, 255, 255), (0, 255, 255)],
|
|
||||||
direction=1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. Explicit test: n1 = 50, n2 = 40 (half of 80) inward
|
|
||||||
dc_presets.append(
|
|
||||||
make_double_circle_preset(
|
|
||||||
"dc_n1_50_n2_40_inward",
|
|
||||||
center=50,
|
|
||||||
half_width=40,
|
|
||||||
colors=[(255, 100, 0), (0, 0, 0)],
|
|
||||||
direction=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 5. Explicit test: n1 = num_leds//2, n2 = num_leds//4 outward, stepping as fast as possible
|
|
||||||
center_half = num_leds // 2
|
|
||||||
radius_quarter = max(1, num_leds // 4)
|
|
||||||
dc_presets.append(
|
|
||||||
make_double_circle_preset(
|
|
||||||
"dc_n1_half_n2_quarter_outward",
|
|
||||||
center=center_half,
|
|
||||||
half_width=radius_quarter,
|
|
||||||
colors=[(0, 150, 255), (0, 0, 0)],
|
|
||||||
direction=1,
|
|
||||||
step_size=radius_quarter, # jump to full radius in one step
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Show each for ~4 seconds
|
|
||||||
for name, preset_obj in dc_presets:
|
|
||||||
print("Showing double_circle preset:", name)
|
|
||||||
show_and_wait(presets, name, preset_obj, wait_ms=4000)
|
|
||||||
|
|
||||||
print("Double_circle pattern test finished. Turning off LEDs.")
|
|
||||||
presets.select("off")
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,694 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Test ESPNow receive functionality - runs on MicroPython device."""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import utime
|
|
||||||
from settings import Settings
|
|
||||||
from presets import Presets
|
|
||||||
from utils import convert_and_reorder_colors
|
|
||||||
|
|
||||||
|
|
||||||
class MockESPNow:
|
|
||||||
"""Mock ESPNow for testing that can send messages."""
|
|
||||||
def __init__(self):
|
|
||||||
self.messages = []
|
|
||||||
self.active_state = False
|
|
||||||
|
|
||||||
def active(self, state):
|
|
||||||
self.active_state = state
|
|
||||||
|
|
||||||
def any(self):
|
|
||||||
"""Return True if there are messages."""
|
|
||||||
return len(self.messages) > 0
|
|
||||||
|
|
||||||
def recv(self):
|
|
||||||
"""Receive a message (removes it from queue)."""
|
|
||||||
if self.messages:
|
|
||||||
return self.messages.pop(0)
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def send_message(self, host, msg_data):
|
|
||||||
"""Send a message by adding it to the queue (testing helper)."""
|
|
||||||
if isinstance(msg_data, dict):
|
|
||||||
msg = json.dumps(msg_data)
|
|
||||||
else:
|
|
||||||
msg = msg_data
|
|
||||||
self.messages.append((host, msg))
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Clear all messages (testing helper)."""
|
|
||||||
self.messages = []
|
|
||||||
|
|
||||||
|
|
||||||
from machine import WDT
|
|
||||||
|
|
||||||
def get_wdt():
|
|
||||||
"""Get a real WDT instance for tests."""
|
|
||||||
return WDT(timeout=10000) # 10 second timeout for tests
|
|
||||||
|
|
||||||
|
|
||||||
def run_main_loop_iterations(espnow, patterns, settings, wdt, max_iterations=10):
|
|
||||||
"""Run main loop iterations until no messages or max reached."""
|
|
||||||
iterations = 0
|
|
||||||
results = []
|
|
||||||
|
|
||||||
while iterations < max_iterations:
|
|
||||||
wdt.feed()
|
|
||||||
patterns.tick()
|
|
||||||
|
|
||||||
if espnow.any():
|
|
||||||
host, msg = espnow.recv()
|
|
||||||
data = json.loads(msg)
|
|
||||||
|
|
||||||
if data.get("v") != "1":
|
|
||||||
results.append(("version_rejected", data))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "presets" in data:
|
|
||||||
for name, preset_data in data["presets"].items():
|
|
||||||
# Convert hex color strings to RGB tuples and reorder based on device color order
|
|
||||||
if "colors" in preset_data:
|
|
||||||
preset_data["colors"] = convert_and_reorder_colors(preset_data["colors"], settings)
|
|
||||||
patterns.edit(name, preset_data)
|
|
||||||
results.append(("presets_processed", list(data["presets"].keys())))
|
|
||||||
|
|
||||||
if settings.get("name") in data.get("select", {}):
|
|
||||||
select_list = data["select"][settings.get("name")]
|
|
||||||
# Select value is always a list: ["preset_name"] or ["preset_name", step]
|
|
||||||
if select_list:
|
|
||||||
preset_name = select_list[0]
|
|
||||||
step = select_list[1] if len(select_list) > 1 else None
|
|
||||||
if patterns.select(preset_name, step=step):
|
|
||||||
results.append(("selected", preset_name))
|
|
||||||
|
|
||||||
iterations += 1
|
|
||||||
|
|
||||||
# Stop if no more messages
|
|
||||||
if not espnow.any():
|
|
||||||
break
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def test_version_check():
|
|
||||||
"""Test that messages with wrong version are rejected."""
|
|
||||||
print("Test 1: Version check")
|
|
||||||
settings = Settings()
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
# Send message with wrong version
|
|
||||||
mock_espnow.send_message(b"\xaa\xaa\xaa\xaa\xaa\xaa", {"v": "2", "presets": {"test": {"pattern": "on"}}})
|
|
||||||
results = run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
assert len([r for r in results if r[0] == "version_rejected"]) > 0, "Should reject wrong version"
|
|
||||||
assert "test" not in patterns.presets, "Preset should not be created"
|
|
||||||
print(" ✓ Version check passed")
|
|
||||||
|
|
||||||
# Send message with correct version
|
|
||||||
mock_espnow.clear()
|
|
||||||
mock_espnow.send_message(b"\xaa\xaa\xaa\xaa\xaa\xaa", {"v": "1", "presets": {"test": {"pattern": "on"}}})
|
|
||||||
results = run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
assert len([r for r in results if r[0] == "presets_processed"]) > 0, "Should process correct version"
|
|
||||||
assert "test" in patterns.presets, "Preset should be created"
|
|
||||||
print(" ✓ Correct version accepted")
|
|
||||||
|
|
||||||
|
|
||||||
def test_preset_creation():
|
|
||||||
"""Test preset creation from ESPNow messages."""
|
|
||||||
print("\nTest 2: Preset creation")
|
|
||||||
settings = Settings()
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
msg = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"test_blink": {
|
|
||||||
"pattern": "blink",
|
|
||||||
"colors": ["#FF0000", "#00FF00"],
|
|
||||||
"delay": 200,
|
|
||||||
"brightness": 128
|
|
||||||
},
|
|
||||||
"test_rainbow": {
|
|
||||||
"pattern": "rainbow",
|
|
||||||
"delay": 100,
|
|
||||||
"n1": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_espnow.send_message(b"\xbb\xbb\xbb\xbb\xbb\xbb", msg)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
assert "test_blink" in patterns.presets, "test_blink preset should exist"
|
|
||||||
assert "test_rainbow" in patterns.presets, "test_rainbow preset should exist"
|
|
||||||
|
|
||||||
# Check preset values
|
|
||||||
blink_preset = patterns.presets["test_blink"]
|
|
||||||
assert blink_preset.pattern == "blink", "Pattern should be blink"
|
|
||||||
assert blink_preset.delay == 200, "Delay should be 200"
|
|
||||||
assert blink_preset.brightness == 128, "Brightness should be 128"
|
|
||||||
|
|
||||||
rainbow_preset = patterns.presets["test_rainbow"]
|
|
||||||
assert rainbow_preset.pattern == "rainbow", "Pattern should be rainbow"
|
|
||||||
assert rainbow_preset.n1 == 2, "n1 should be 2"
|
|
||||||
|
|
||||||
print(" ✓ Presets created correctly")
|
|
||||||
|
|
||||||
|
|
||||||
def test_color_conversion():
|
|
||||||
"""Test hex color string conversion and reordering."""
|
|
||||||
print("\nTest 3: Color conversion")
|
|
||||||
settings = Settings()
|
|
||||||
settings["color_order"] = "rgb" # Default RGB order
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
msg = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"test_colors": {
|
|
||||||
"pattern": "on",
|
|
||||||
"colors": ["#FF0000", "#00FF00", "#0000FF"] # Red, Green, Blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_espnow.send_message(b"\xcc\xcc\xcc\xcc\xcc\xcc", msg)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
preset = patterns.presets["test_colors"]
|
|
||||||
assert len(preset.colors) == 3, "Should have 3 colors"
|
|
||||||
assert preset.colors[0] == (255, 0, 0), "First color should be red (255,0,0)"
|
|
||||||
assert preset.colors[1] == (0, 255, 0), "Second color should be green (0,255,0)"
|
|
||||||
assert preset.colors[2] == (0, 0, 255), "Third color should be blue (0,0,255)"
|
|
||||||
print(" ✓ Colors converted correctly (RGB order)")
|
|
||||||
|
|
||||||
# Test GRB order
|
|
||||||
settings["color_order"] = "grb"
|
|
||||||
patterns2 = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow2 = MockESPNow()
|
|
||||||
msg2 = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"test_grb": {
|
|
||||||
"pattern": "on",
|
|
||||||
"colors": ["#FF0000"] # Red in RGB, should become (0, 255, 0) in GRB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow2.send_message(b"\xdd\xdd\xdd\xdd\xdd\xdd", msg2)
|
|
||||||
wdt2 = get_wdt()
|
|
||||||
run_main_loop_iterations(mock_espnow2, patterns2, settings, wdt2)
|
|
||||||
preset2 = patterns2.presets["test_grb"]
|
|
||||||
assert preset2.colors[0] == (0, 255, 0), "GRB: Red should become green (0,255,0)"
|
|
||||||
print(" ✓ Colors reordered correctly (GRB order)")
|
|
||||||
|
|
||||||
|
|
||||||
def test_preset_update():
|
|
||||||
"""Test that editing an existing preset updates it."""
|
|
||||||
print("\nTest 4: Preset update")
|
|
||||||
settings = Settings()
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
# Create initial preset
|
|
||||||
msg1 = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"test_update": {
|
|
||||||
"pattern": "blink",
|
|
||||||
"delay": 100,
|
|
||||||
"brightness": 64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xee\xee\xee\xee\xee\xee", msg1)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.presets["test_update"].delay == 100, "Initial delay should be 100"
|
|
||||||
|
|
||||||
# Update preset
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg2 = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"test_update": {
|
|
||||||
"pattern": "blink",
|
|
||||||
"delay": 200,
|
|
||||||
"brightness": 128
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xff\xff\xff\xff\xff\xff", msg2)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.presets["test_update"].delay == 200, "Updated delay should be 200"
|
|
||||||
assert patterns.presets["test_update"].brightness == 128, "Updated brightness should be 128"
|
|
||||||
print(" ✓ Preset updated correctly")
|
|
||||||
|
|
||||||
|
|
||||||
def test_select():
|
|
||||||
"""Test preset selection."""
|
|
||||||
print("\nTest 5: Preset selection")
|
|
||||||
settings = Settings()
|
|
||||||
settings["name"] = "device1"
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
# Create presets
|
|
||||||
msg1 = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"preset1": {"pattern": "on", "colors": [(255, 0, 0)]},
|
|
||||||
"preset2": {"pattern": "rainbow", "delay": 50}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\x11\x11\x11\x11\x11\x11", msg1)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
# Select preset
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg2 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"device1": ["preset1"],
|
|
||||||
"device2": ["preset2"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\x22\x22\x22\x22\x22\x22", msg2)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.selected == "preset1", "Should select preset1"
|
|
||||||
print(" ✓ Preset selected correctly")
|
|
||||||
|
|
||||||
|
|
||||||
def test_full_message():
|
|
||||||
"""Test a full message with presets and select."""
|
|
||||||
print("\nTest 6: Full message (presets + select)")
|
|
||||||
settings = Settings()
|
|
||||||
settings["name"] = "test_device"
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
msg = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"my_preset": {
|
|
||||||
"pattern": "pulse",
|
|
||||||
"colors": ["#FF0000", "#00FF00"],
|
|
||||||
"delay": 150,
|
|
||||||
"n1": 500,
|
|
||||||
"n2": 200,
|
|
||||||
"n3": 500
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"select": {
|
|
||||||
"test_device": ["my_preset"],
|
|
||||||
"other_device": ["other_preset"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_espnow.send_message(b"\x44\x44\x44\x44\x44\x44", msg)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
assert "my_preset" in patterns.presets, "Preset should be created"
|
|
||||||
assert patterns.selected == "my_preset", "Preset should be selected"
|
|
||||||
|
|
||||||
preset = patterns.presets["my_preset"]
|
|
||||||
assert preset.pattern == "pulse", "Pattern should be pulse"
|
|
||||||
assert preset.delay == 150, "Delay should be 150"
|
|
||||||
assert preset.n1 == 500, "n1 should be 500"
|
|
||||||
print(" ✓ Full message processed correctly")
|
|
||||||
|
|
||||||
|
|
||||||
def test_switch_presets():
|
|
||||||
"""Test switching between different presets."""
|
|
||||||
print("\nTest 7: Switch between presets")
|
|
||||||
settings = Settings()
|
|
||||||
settings["name"] = "switch_device"
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
# Create multiple presets
|
|
||||||
msg1 = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"preset_blink": {"pattern": "blink", "delay": 200, "colors": [(255, 0, 0)]},
|
|
||||||
"preset_rainbow": {"pattern": "rainbow", "delay": 100, "n1": 2},
|
|
||||||
"preset_pulse": {"pattern": "pulse", "delay": 150, "n1": 500, "n2": 200, "n3": 500}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\x55\x55\x55\x55\x55\x55", msg1)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
# Select and run first preset for 2 seconds
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg2 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"switch_device": ["preset_blink"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\x66\x66\x66\x66\x66\x66", msg2)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.selected == "preset_blink", "Should select preset_blink"
|
|
||||||
print(" ✓ Selected preset_blink, running for 2 seconds...")
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
|
|
||||||
wdt.feed()
|
|
||||||
patterns.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
# Switch to second preset and run for 2 seconds
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg3 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"switch_device": ["preset_rainbow"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\x77\x77\x77\x77\x77\x77", msg3)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.selected == "preset_rainbow", "Should switch to preset_rainbow"
|
|
||||||
print(" ✓ Switched to preset_rainbow, running for 2 seconds...")
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
|
|
||||||
wdt.feed()
|
|
||||||
patterns.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
# Switch to third preset and run for 2 seconds
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg4 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"switch_device": ["preset_pulse"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\x88\x88\x88\x88\x88\x88", msg4)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.selected == "preset_pulse", "Should switch to preset_pulse"
|
|
||||||
print(" ✓ Switched to preset_pulse, running for 2 seconds...")
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
|
|
||||||
wdt.feed()
|
|
||||||
patterns.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
# Switch back to first preset and run for 2 seconds
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg5 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"switch_device": ["preset_blink"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\x99\x99\x99\x99\x99\x99", msg5)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.selected == "preset_blink", "Should switch back to preset_blink"
|
|
||||||
print(" ✓ Switched back to preset_blink, running for 2 seconds...")
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
|
|
||||||
wdt.feed()
|
|
||||||
patterns.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
print(" ✓ Preset switching works correctly")
|
|
||||||
|
|
||||||
|
|
||||||
def test_beat_functionality():
|
|
||||||
"""Test beat functionality - calling select() again with same preset restarts pattern."""
|
|
||||||
print("\nTest 8: Beat functionality")
|
|
||||||
settings = Settings()
|
|
||||||
settings["name"] = "beat_device"
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
# Create presets with manual mode
|
|
||||||
msg1 = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"beat_rainbow": {"pattern": "rainbow", "delay": 100, "n1": 1, "auto": False},
|
|
||||||
"beat_chase": {"pattern": "chase", "delay": 200, "n1": 4, "n2": 4, "n3": 2, "n4": 1, "auto": False},
|
|
||||||
"beat_pulse": {"pattern": "pulse", "delay": 150, "n1": 300, "n2": 100, "n3": 300, "auto": False}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xaa\xaa\xaa\xaa\xaa\xaa", msg1)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
# Test 1: Beat with rainbow (manual mode) - should advance one step per beat
|
|
||||||
print(" Test 8.1: Beat with rainbow (manual mode)")
|
|
||||||
patterns.step = 0
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg2 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"beat_device": ["beat_rainbow"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xbb\xbb\xbb\xbb\xbb\xbb", msg2)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.selected == "beat_rainbow", "Should select beat_rainbow"
|
|
||||||
initial_step = patterns.step
|
|
||||||
|
|
||||||
# First beat - advance one step
|
|
||||||
mock_espnow.clear()
|
|
||||||
mock_espnow.send_message(b"\xcc\xcc\xcc\xcc\xcc\xcc", msg2) # Same select message = beat
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
|
|
||||||
# tick() is already called in run_main_loop_iterations, so step should be incremented
|
|
||||||
assert patterns.step == (initial_step + 1) % 256, f"Step should increment from {initial_step} to {(initial_step + 1) % 256}, got {patterns.step}"
|
|
||||||
|
|
||||||
# Second beat - advance another step
|
|
||||||
mock_espnow.clear()
|
|
||||||
mock_espnow.send_message(b"\xdd\xdd\xdd\xdd\xdd\xdd", msg2) # Beat again
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
|
|
||||||
assert patterns.step == (initial_step + 2) % 256, f"Step should increment to {(initial_step + 2) % 256}, got {patterns.step}"
|
|
||||||
print(" ✓ Rainbow beat advances one step per beat")
|
|
||||||
|
|
||||||
# Test 2: Beat with chase (manual mode) - should advance one step per beat
|
|
||||||
print(" Test 8.2: Beat with chase (manual mode)")
|
|
||||||
patterns.step = 0
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg3 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"beat_device": ["beat_chase"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xee\xee\xee\xee\xee\xee", msg3)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.selected == "beat_chase", "Should select beat_chase"
|
|
||||||
initial_step = patterns.step
|
|
||||||
|
|
||||||
# First beat
|
|
||||||
mock_espnow.clear()
|
|
||||||
mock_espnow.send_message(b"\xff\xff\xff\xff\xff\xff", msg3) # Beat
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
|
|
||||||
# tick() is already called in run_main_loop_iterations
|
|
||||||
assert patterns.step == initial_step + 1, f"Chase step should increment from {initial_step} to {initial_step + 1}, got {patterns.step}"
|
|
||||||
|
|
||||||
# Second beat
|
|
||||||
mock_espnow.clear()
|
|
||||||
mock_espnow.send_message(b"\x11\x11\x11\x11\x11\x11", msg3) # Beat again
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
|
|
||||||
assert patterns.step == initial_step + 2, f"Chase step should increment to {initial_step + 2}, got {patterns.step}"
|
|
||||||
print(" ✓ Chase beat advances one step per beat")
|
|
||||||
|
|
||||||
# Test 3: Beat with pulse (manual mode) - should restart full cycle
|
|
||||||
print(" Test 8.3: Beat with pulse (manual mode)")
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg4 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"beat_device": ["beat_pulse"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\x22\x22\x22\x22\x22\x22", msg4)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.selected == "beat_pulse", "Should select beat_pulse"
|
|
||||||
assert patterns.generator is not None, "Generator should be active"
|
|
||||||
|
|
||||||
# First beat - should restart generator
|
|
||||||
initial_generator = patterns.generator
|
|
||||||
mock_espnow.clear()
|
|
||||||
mock_espnow.send_message(b"\x33\x33\x33\x33\x33\x33", msg4) # Beat
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
assert patterns.generator is not None, "Generator should still be active after beat"
|
|
||||||
assert patterns.generator != initial_generator, "Generator should be restarted (new instance)"
|
|
||||||
print(" ✓ Pulse beat restarts generator for full cycle")
|
|
||||||
|
|
||||||
# Test 4: Multiple beats in sequence
|
|
||||||
print(" Test 8.4: Multiple beats in sequence")
|
|
||||||
patterns.step = 0
|
|
||||||
mock_espnow.clear()
|
|
||||||
mock_espnow.send_message(b"\x44\x44\x44\x44\x44\x44", msg2) # Select rainbow
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
# Send 5 beats
|
|
||||||
for i in range(5):
|
|
||||||
mock_espnow.clear()
|
|
||||||
mock_espnow.send_message(b"\x55\x55\x55\x55\x55\x55", msg2) # Beat
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=5)
|
|
||||||
# tick() is already called in run_main_loop_iterations
|
|
||||||
wdt.feed()
|
|
||||||
utime.sleep_ms(50)
|
|
||||||
|
|
||||||
assert patterns.step == 5, f"After 5 beats, step should be 5, got {patterns.step}"
|
|
||||||
print(" ✓ Multiple beats work correctly")
|
|
||||||
|
|
||||||
print(" ✓ Beat functionality works correctly")
|
|
||||||
|
|
||||||
|
|
||||||
def test_select_with_step():
|
|
||||||
"""Test selecting a preset with an explicit step value."""
|
|
||||||
print("\nTest 9: Select with step value")
|
|
||||||
settings = Settings()
|
|
||||||
settings["name"] = "step_device"
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
mock_espnow = MockESPNow()
|
|
||||||
wdt = get_wdt()
|
|
||||||
|
|
||||||
# Create preset
|
|
||||||
msg1 = {
|
|
||||||
"v": "1",
|
|
||||||
"presets": {
|
|
||||||
"step_preset": {"pattern": "rainbow", "delay": 100, "n1": 1, "auto": False}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xaa\xaa\xaa\xaa\xaa\xaa", msg1)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt)
|
|
||||||
|
|
||||||
# Select with explicit step value
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg2 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"step_device": ["step_preset", 10]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xbb\xbb\xbb\xbb\xbb\xbb", msg2)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=2)
|
|
||||||
# Ensure tick() is called after select() to advance the step
|
|
||||||
patterns.tick()
|
|
||||||
|
|
||||||
assert patterns.selected == "step_preset", "Should select step_preset"
|
|
||||||
# Step is set to 10, then tick() advances it, so it should be 11
|
|
||||||
assert patterns.step == 11, f"Step should be set to 10 then advanced to 11 by tick(), got {patterns.step}"
|
|
||||||
print(" ✓ Step value set correctly")
|
|
||||||
|
|
||||||
# Select without step (should use default behavior)
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg3 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"step_device": ["step_preset"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xcc\xcc\xcc\xcc\xcc\xcc", msg3)
|
|
||||||
initial_step = patterns.step # Should be 11
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=2)
|
|
||||||
# Ensure tick() is called after select() to advance the step
|
|
||||||
patterns.tick()
|
|
||||||
# Since it's the same preset, step should not be reset, but tick() will advance it
|
|
||||||
# So step should be initial_step + 1 (one tick call)
|
|
||||||
assert patterns.step == initial_step + 1, f"Step should advance from {initial_step} to {initial_step + 1} (not reset), got {patterns.step}"
|
|
||||||
print(" ✓ Step preserved when selecting same preset without step (tick advances it)")
|
|
||||||
|
|
||||||
# Select different preset with step
|
|
||||||
patterns.edit("other_preset", {"p": "rainbow", "a": False})
|
|
||||||
mock_espnow.clear()
|
|
||||||
msg4 = {
|
|
||||||
"v": "1",
|
|
||||||
"select": {
|
|
||||||
"step_device": ["other_preset", 5]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_espnow.send_message(b"\xdd\xdd\xdd\xdd\xdd\xdd", msg4)
|
|
||||||
run_main_loop_iterations(mock_espnow, patterns, settings, wdt, max_iterations=2)
|
|
||||||
# Ensure tick() is called after select() to advance the step
|
|
||||||
patterns.tick()
|
|
||||||
|
|
||||||
assert patterns.selected == "other_preset", "Should select other_preset"
|
|
||||||
# Step is set to 5, then tick() advances it, so it should be 6
|
|
||||||
assert patterns.step == 6, f"Step should be set to 5 then advanced to 6 by tick(), got {patterns.step}"
|
|
||||||
print(" ✓ Step set correctly when switching presets")
|
|
||||||
|
|
||||||
|
|
||||||
def test_preset_save_load():
|
|
||||||
"""Test saving and loading presets to/from JSON."""
|
|
||||||
print("\nTest 10: Preset save/load")
|
|
||||||
settings = Settings()
|
|
||||||
patterns = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
|
|
||||||
patterns.edit("saved_preset", {
|
|
||||||
"p": "blink",
|
|
||||||
"d": 150,
|
|
||||||
"b": 200,
|
|
||||||
"c": [(1, 2, 3), (4, 5, 6)],
|
|
||||||
"a": False,
|
|
||||||
"n1": 1,
|
|
||||||
"n2": 2,
|
|
||||||
"n3": 3,
|
|
||||||
"n4": 4,
|
|
||||||
"n5": 5,
|
|
||||||
"n6": 6,
|
|
||||||
})
|
|
||||||
assert patterns.save(), "Save should return True"
|
|
||||||
|
|
||||||
reloaded = Presets(settings["led_pin"], settings["num_leds"])
|
|
||||||
assert reloaded.load(), "Load should return True"
|
|
||||||
|
|
||||||
preset = reloaded.presets.get("saved_preset")
|
|
||||||
assert preset is not None, "Preset should be loaded"
|
|
||||||
assert preset.p == "blink", "Pattern should be blink"
|
|
||||||
assert preset.d == 150, "Delay should be 150"
|
|
||||||
assert preset.b == 200, "Brightness should be 200"
|
|
||||||
assert preset.c == [(1, 2, 3), (4, 5, 6)], "Colors should be restored as tuples"
|
|
||||||
assert preset.a is False, "Auto should be False"
|
|
||||||
assert (preset.n1, preset.n2, preset.n3, preset.n4, preset.n5, preset.n6) == (1, 2, 3, 4, 5, 6), "n1-n6 should match"
|
|
||||||
try:
|
|
||||||
os.remove("presets.json")
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
print(" ✓ Preset save/load works correctly")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run all tests."""
|
|
||||||
print("=" * 60)
|
|
||||||
print("ESPNow Receive Functionality Tests")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
test_version_check()
|
|
||||||
test_preset_creation()
|
|
||||||
test_color_conversion()
|
|
||||||
test_preset_update()
|
|
||||||
test_select()
|
|
||||||
test_full_message()
|
|
||||||
test_switch_presets()
|
|
||||||
test_beat_functionality()
|
|
||||||
test_select_with_step()
|
|
||||||
test_preset_save_load()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("All tests passed! ✓")
|
|
||||||
print("=" * 60)
|
|
||||||
except AssertionError as e:
|
|
||||||
print("\n✗ Test failed:", e)
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
print("\n✗ Unexpected error:", e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
"""
|
|
||||||
On-device test for Presets.fill_n() using mpremote.
|
|
||||||
|
|
||||||
Usage (from pico/ dir or project root with adjusted paths):
|
|
||||||
|
|
||||||
mpremote connect <device> cp src/*.py :
|
|
||||||
mpremote connect <device> cp src/patterns/*.py :patterns
|
|
||||||
mpremote connect <device> cp lib/*.py :
|
|
||||||
mpremote connect <device> cp test/test_fill_n.py :
|
|
||||||
mpremote connect <device> run test_fill_n.py
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Instantiates Presets
|
|
||||||
- Calls fill_n() with a simple range
|
|
||||||
- Lets you visually confirm that all strips show the same proportional segment
|
|
||||||
and that equal-length strip pairs have identical lit indices.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
presets = Presets()
|
|
||||||
presets.load()
|
|
||||||
|
|
||||||
# Choose a simple test range on the reference strip (strip 0).
|
|
||||||
ref_len = presets.strip_length(0)
|
|
||||||
if ref_len <= 0:
|
|
||||||
print("No strips or invalid length; aborting fill_n test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Use a central segment so it's easy to see.
|
|
||||||
start = ref_len // 4
|
|
||||||
end = 3 * ref_len // 4
|
|
||||||
print("Running fill_n test from", start, "to", end, "on reference strip 0.")
|
|
||||||
|
|
||||||
color = (0, 50, 0) # dim green
|
|
||||||
|
|
||||||
# First, clear everything
|
|
||||||
for strip in presets.strips:
|
|
||||||
strip.fill((0, 0, 0))
|
|
||||||
strip.show()
|
|
||||||
|
|
||||||
# Apply fill_n, which will use scale() internally.
|
|
||||||
presets.fill_n(color, start, end)
|
|
||||||
|
|
||||||
print("fill_n test applied; visually inspect strips.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
"""
|
|
||||||
On-device test that exercises Segments and multiple SegmentsTransition presets via Presets.
|
|
||||||
|
|
||||||
Usage (from pico/ dir or project root with adjusted paths):
|
|
||||||
|
|
||||||
mpremote connect <device> cp src/*.py :
|
|
||||||
mpremote connect <device> cp src/patterns/*.py :patterns
|
|
||||||
mpremote connect <device> cp lib/*.py :
|
|
||||||
mpremote connect <device> cp test/test_multi_patterns.py :
|
|
||||||
mpremote connect <device> run test_multi_patterns.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import utime
|
|
||||||
|
|
||||||
from presets import Presets, Preset
|
|
||||||
|
|
||||||
|
|
||||||
def run_for(presets, duration_ms):
|
|
||||||
"""Tick the current pattern for duration_ms."""
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
|
|
||||||
presets.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def make_segments_preset(name, colors, n_values, brightness=255):
|
|
||||||
"""
|
|
||||||
Helper to build a Preset for the 'segments' pattern.
|
|
||||||
|
|
||||||
colors: list of up to 4 (r,g,b) tuples
|
|
||||||
n_values: list/tuple of 8 ints [n1..n8]
|
|
||||||
"""
|
|
||||||
cs = list(colors)[:4]
|
|
||||||
while len(cs) < 4:
|
|
||||||
cs.append((0, 0, 0))
|
|
||||||
|
|
||||||
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
|
|
||||||
data = {
|
|
||||||
"p": "segments",
|
|
||||||
"c": cs,
|
|
||||||
"b": brightness,
|
|
||||||
"n1": n1,
|
|
||||||
"n2": n2,
|
|
||||||
"n3": n3,
|
|
||||||
"n4": n4,
|
|
||||||
"n5": n5,
|
|
||||||
"n6": n6,
|
|
||||||
"n7": n7,
|
|
||||||
"n8": n8,
|
|
||||||
}
|
|
||||||
return name, Preset(data)
|
|
||||||
|
|
||||||
|
|
||||||
def make_segments_transition_preset(name, colors, n_values, duration_ms=1000, brightness=255):
|
|
||||||
"""
|
|
||||||
Helper to build a Preset for the 'segments_transition' pattern.
|
|
||||||
|
|
||||||
Starts from whatever is currently displayed and fades to the
|
|
||||||
new segments layout over duration_ms.
|
|
||||||
"""
|
|
||||||
cs = list(colors)[:4]
|
|
||||||
while len(cs) < 4:
|
|
||||||
cs.append((0, 0, 0))
|
|
||||||
|
|
||||||
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
|
|
||||||
data = {
|
|
||||||
"p": "segments_transition",
|
|
||||||
"c": cs,
|
|
||||||
"b": brightness,
|
|
||||||
"d": duration_ms,
|
|
||||||
"n1": n1,
|
|
||||||
"n2": n2,
|
|
||||||
"n3": n3,
|
|
||||||
"n4": n4,
|
|
||||||
"n5": n5,
|
|
||||||
"n6": n6,
|
|
||||||
"n7": n7,
|
|
||||||
"n8": n8,
|
|
||||||
}
|
|
||||||
return name, Preset(data)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
presets = Presets()
|
|
||||||
presets.load()
|
|
||||||
|
|
||||||
num_leds = presets.strip_length(0)
|
|
||||||
if num_leds <= 0:
|
|
||||||
print("No strips; aborting multi-pattern test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Starting multi-pattern test with Presets...")
|
|
||||||
|
|
||||||
quarter = num_leds // 4
|
|
||||||
half = num_leds // 2
|
|
||||||
|
|
||||||
# 1. Static segments: simple R/G/B bands
|
|
||||||
name_static, preset_static = make_segments_preset(
|
|
||||||
"mp_segments_static",
|
|
||||||
colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)],
|
|
||||||
n_values=[
|
|
||||||
0,
|
|
||||||
quarter - 1, # red
|
|
||||||
quarter,
|
|
||||||
2 * quarter - 1, # green
|
|
||||||
2 * quarter,
|
|
||||||
3 * quarter - 1, # blue
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2a. Segments transition: fade from previous buffer to new colors (slow)
|
|
||||||
name_trans1, preset_trans1 = make_segments_transition_preset(
|
|
||||||
"mp_segments_transition_1",
|
|
||||||
colors=[(255, 255, 255), (255, 0, 255), (0, 255, 255)],
|
|
||||||
n_values=[
|
|
||||||
0,
|
|
||||||
half - 1, # white on first half
|
|
||||||
half,
|
|
||||||
num_leds - 1, # magenta on second half
|
|
||||||
0,
|
|
||||||
-1, # cyan unused in this example
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
],
|
|
||||||
duration_ms=3000,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2b. Segments transition: fade between two different segment layouts
|
|
||||||
name_trans2, preset_trans2 = make_segments_transition_preset(
|
|
||||||
"mp_segments_transition_2",
|
|
||||||
colors=[(255, 0, 0), (0, 0, 255)],
|
|
||||||
n_values=[
|
|
||||||
0,
|
|
||||||
quarter - 1, # red first quarter
|
|
||||||
quarter,
|
|
||||||
2 * quarter - 1, # blue second quarter
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
],
|
|
||||||
duration_ms=4000,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2c. Segments transition: thin moving band (center quarter only)
|
|
||||||
band_start = quarter // 2
|
|
||||||
band_end = band_start + quarter
|
|
||||||
name_trans3, preset_trans3 = make_segments_transition_preset(
|
|
||||||
"mp_segments_transition_3",
|
|
||||||
colors=[(0, 255, 0)],
|
|
||||||
n_values=[
|
|
||||||
band_start,
|
|
||||||
band_end - 1, # green band in the middle
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
],
|
|
||||||
duration_ms=5000,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2d. Segments transition: full-ring warm white fade
|
|
||||||
name_trans4, preset_trans4 = make_segments_transition_preset(
|
|
||||||
"mp_segments_transition_4",
|
|
||||||
colors=[(255, 200, 100)],
|
|
||||||
n_values=[
|
|
||||||
0,
|
|
||||||
num_leds - 1, # entire strip
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
],
|
|
||||||
duration_ms=6000,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2e. Segments transition: alternating warm/cool halves
|
|
||||||
name_trans5, preset_trans5 = make_segments_transition_preset(
|
|
||||||
"mp_segments_transition_5",
|
|
||||||
colors=[(255, 180, 100), (100, 180, 255)],
|
|
||||||
n_values=[
|
|
||||||
0,
|
|
||||||
half - 1, # warm first half
|
|
||||||
half,
|
|
||||||
num_leds - 1, # cool second half
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
],
|
|
||||||
duration_ms=5000,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2f. Segments transition: narrow red band near start
|
|
||||||
narrow_start = num_leds // 16
|
|
||||||
narrow_end = narrow_start + max(4, num_leds // 32)
|
|
||||||
name_trans6, preset_trans6 = make_segments_transition_preset(
|
|
||||||
"mp_segments_transition_6",
|
|
||||||
colors=[(255, 0, 0)],
|
|
||||||
n_values=[
|
|
||||||
narrow_start,
|
|
||||||
narrow_end - 1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
],
|
|
||||||
duration_ms=4000,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register presets in Presets and run them in sequence
|
|
||||||
presets.presets[name_static] = preset_static
|
|
||||||
presets.presets[name_trans1] = preset_trans1
|
|
||||||
presets.presets[name_trans2] = preset_trans2
|
|
||||||
presets.presets[name_trans3] = preset_trans3
|
|
||||||
presets.presets[name_trans4] = preset_trans4
|
|
||||||
presets.presets[name_trans5] = preset_trans5
|
|
||||||
presets.presets[name_trans6] = preset_trans6
|
|
||||||
|
|
||||||
print("Showing static segments...")
|
|
||||||
presets.select(name_static)
|
|
||||||
presets.tick() # draw once
|
|
||||||
run_for(presets, 3000)
|
|
||||||
|
|
||||||
print("Running segments transition 1 (fading to new half/half layout)...")
|
|
||||||
presets.select(name_trans1)
|
|
||||||
run_for(presets, 3500)
|
|
||||||
|
|
||||||
print("Running segments transition 2 (fading to quarter-band layout)...")
|
|
||||||
presets.select(name_trans2)
|
|
||||||
run_for(presets, 4500)
|
|
||||||
|
|
||||||
print("Running segments transition 3 (fading to center green band)...")
|
|
||||||
presets.select(name_trans3)
|
|
||||||
run_for(presets, 5500)
|
|
||||||
|
|
||||||
print("Running segments transition 4 (fading to full warm white ring)...")
|
|
||||||
presets.select(name_trans4)
|
|
||||||
run_for(presets, 6500)
|
|
||||||
|
|
||||||
print("Running segments transition 5 (fading to warm/cool halves)...")
|
|
||||||
presets.select(name_trans5)
|
|
||||||
run_for(presets, 5500)
|
|
||||||
|
|
||||||
print("Running segments transition 6 (fading to narrow red band)...")
|
|
||||||
presets.select(name_trans6)
|
|
||||||
run_for(presets, 4500)
|
|
||||||
|
|
||||||
print("Multi-pattern test finished. Turning off LEDs.")
|
|
||||||
presets.select("off")
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
"""
|
|
||||||
Test the Presets.scale() helper on-device with mpremote.
|
|
||||||
|
|
||||||
Usage (from project root):
|
|
||||||
|
|
||||||
mpremote connect <device> cp pico/src/*.py : &&
|
|
||||||
mpremote connect <device> cp pico/src/patterns/*.py :patterns &&
|
|
||||||
mpremote connect <device> cp pico/lib/*.py : &&
|
|
||||||
mpremote connect <device> cp tests/test_scale.py : &&
|
|
||||||
mpremote connect <device> run test_scale.py
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Creates a minimal Presets instance
|
|
||||||
- Runs a few numeric test cases for scale()
|
|
||||||
- Optionally displays a short visual check on the LEDs
|
|
||||||
"""
|
|
||||||
|
|
||||||
from presets import Presets
|
|
||||||
|
|
||||||
|
|
||||||
def numeric_tests(presets):
|
|
||||||
"""
|
|
||||||
Numeric sanity checks for scale() using the actual strip config.
|
|
||||||
|
|
||||||
We treat strip 0 as the reference and print the mapped indices for
|
|
||||||
a few positions on each other strip.
|
|
||||||
"""
|
|
||||||
print("Numeric scale() tests (from strip 0):")
|
|
||||||
ref_len = presets.strip_length(0)
|
|
||||||
if ref_len <= 0:
|
|
||||||
print(" strip 0 length <= 0; skipping numeric tests.")
|
|
||||||
return
|
|
||||||
|
|
||||||
test_positions = [0, ref_len // 2, ref_len - 1]
|
|
||||||
for pos in test_positions:
|
|
||||||
print(" pos on strip 0:", pos)
|
|
||||||
for dst_idx in range(len(presets.strips)):
|
|
||||||
dst_len = presets.strip_length(dst_idx)
|
|
||||||
if dst_len <= 0:
|
|
||||||
continue
|
|
||||||
n2 = presets.scale(dst_idx, pos)
|
|
||||||
print(" -> strip", dst_idx, "len", dst_len, "pos", n2)
|
|
||||||
|
|
||||||
|
|
||||||
def visual_test(presets):
|
|
||||||
"""
|
|
||||||
Simple visual test:
|
|
||||||
- Use strip 0 as reference
|
|
||||||
- Move a pixel along strip 0
|
|
||||||
- Map position to all other strips with scale()
|
|
||||||
"""
|
|
||||||
import utime
|
|
||||||
|
|
||||||
strips = presets.strips
|
|
||||||
if not strips:
|
|
||||||
print("No strips available for visual test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
src_strip_idx = 0
|
|
||||||
l1 = presets.strip_length(src_strip_idx)
|
|
||||||
if l1 <= 0:
|
|
||||||
print("strip_length(0) <= 0; aborting visual test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
color = (50, 0, 0) # dim red so it doesn't blind you
|
|
||||||
|
|
||||||
# Run once across the full length of the reference strip,
|
|
||||||
# jumping 10 LEDs at a time.
|
|
||||||
step_size = 10
|
|
||||||
steps = (l1 + step_size - 1) // step_size
|
|
||||||
print("Starting visual scale() test with 10-LED jumps:", steps, "steps...")
|
|
||||||
for step in range(steps):
|
|
||||||
n1 = (step * step_size) % l1
|
|
||||||
|
|
||||||
# Clear all strips
|
|
||||||
for strip in strips:
|
|
||||||
strip.fill((0, 0, 0))
|
|
||||||
|
|
||||||
# Light mapped position on each strip using Presets.set/show
|
|
||||||
for dst_strip_idx, _ in enumerate(strips):
|
|
||||||
presets.set(dst_strip_idx, n1, color)
|
|
||||||
presets.show(dst_strip_idx)
|
|
||||||
|
|
||||||
print("Visual test finished.")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
presets = Presets()
|
|
||||||
presets.load()
|
|
||||||
numeric_tests(presets)
|
|
||||||
# Comment this in/out depending on whether you want the LEDs to run:
|
|
||||||
try:
|
|
||||||
visual_test(presets)
|
|
||||||
except Exception as e:
|
|
||||||
print("Visual test error:", e)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
"""
|
|
||||||
On-device test for the Segments pattern using mpremote.
|
|
||||||
|
|
||||||
Usage (from pico/ dir or project root with adjusted paths):
|
|
||||||
|
|
||||||
mpremote connect <device> cp src/*.py :
|
|
||||||
mpremote connect <device> cp src/patterns/*.py :patterns
|
|
||||||
mpremote connect <device> cp lib/*.py :
|
|
||||||
mpremote connect <device> cp test/test_segments.py :
|
|
||||||
mpremote connect <device> run test_segments.py
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Instantiates Presets
|
|
||||||
- Creates a few in-memory 'point' (Segments) presets with different ranges/colors
|
|
||||||
- Selects each one so you can visually confirm the segments
|
|
||||||
"""
|
|
||||||
|
|
||||||
from presets import Presets, Preset
|
|
||||||
|
|
||||||
|
|
||||||
def make_segments_preset(name, colors, n_values, brightness=255):
|
|
||||||
"""
|
|
||||||
Helper to build a Preset for the 'segments' pattern (key 'point').
|
|
||||||
|
|
||||||
colors: list of up to 4 (r,g,b) tuples
|
|
||||||
n_values: list/tuple of 8 ints [n1..n8]
|
|
||||||
"""
|
|
||||||
# Pad or trim colors to 4 entries
|
|
||||||
cs = list(colors)[:4]
|
|
||||||
while len(cs) < 4:
|
|
||||||
cs.append((0, 0, 0))
|
|
||||||
|
|
||||||
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
|
|
||||||
data = {
|
|
||||||
"p": "segments", # pattern key for Segments
|
|
||||||
"c": cs,
|
|
||||||
"b": brightness,
|
|
||||||
"n1": n1,
|
|
||||||
"n2": n2,
|
|
||||||
"n3": n3,
|
|
||||||
"n4": n4,
|
|
||||||
"n5": n5,
|
|
||||||
"n6": n6,
|
|
||||||
"n7": n7,
|
|
||||||
"n8": n8,
|
|
||||||
# 'a' is not used by segments; it's static
|
|
||||||
}
|
|
||||||
return name, Preset(data)
|
|
||||||
|
|
||||||
|
|
||||||
def show_and_wait(presets, name, preset_obj, wait_ms):
|
|
||||||
"""Select a static segments preset and hold it for wait_ms."""
|
|
||||||
presets.presets[name] = preset_obj
|
|
||||||
presets.select(name)
|
|
||||||
# Segments draws immediately in run(), then just yields; one tick is enough.
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
import utime
|
|
||||||
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < wait_ms:
|
|
||||||
# Keep ticking in case other logic ever depends on it
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
presets = Presets()
|
|
||||||
presets.load()
|
|
||||||
|
|
||||||
num_leds = presets.strip_length(0)
|
|
||||||
if num_leds <= 0:
|
|
||||||
print("No strips; aborting segments test.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Starting segments pattern test...")
|
|
||||||
|
|
||||||
quarter = num_leds // 4
|
|
||||||
half = num_leds // 2
|
|
||||||
|
|
||||||
segments_presets = []
|
|
||||||
|
|
||||||
# 1. Single band: first quarter, red
|
|
||||||
segments_presets.append(
|
|
||||||
make_segments_preset(
|
|
||||||
"segments_red_q1",
|
|
||||||
colors=[(255, 0, 0)],
|
|
||||||
n_values=[0, quarter - 1, 0, -1, 0, -1, 0, -1],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Two bands: red first half, green second half
|
|
||||||
segments_presets.append(
|
|
||||||
make_segments_preset(
|
|
||||||
"segments_red_green_halves",
|
|
||||||
colors=[(255, 0, 0), (0, 255, 0)],
|
|
||||||
n_values=[0, half - 1, half, num_leds - 1, 0, -1, 0, -1],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Three bands: R, G, B quarters
|
|
||||||
segments_presets.append(
|
|
||||||
make_segments_preset(
|
|
||||||
"segments_rgb_quarters",
|
|
||||||
colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)],
|
|
||||||
n_values=[
|
|
||||||
0,
|
|
||||||
quarter - 1, # red
|
|
||||||
quarter,
|
|
||||||
2 * quarter - 1, # green
|
|
||||||
2 * quarter,
|
|
||||||
3 * quarter - 1, # blue
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Show each for ~4 seconds
|
|
||||||
for name, preset_obj in segments_presets:
|
|
||||||
print("Showing segments preset:", name)
|
|
||||||
show_and_wait(presets, name, preset_obj, wait_ms=4000)
|
|
||||||
|
|
||||||
print("Segments pattern test finished. Turning off LEDs.")
|
|
||||||
presets.select("off")
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/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)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/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.")
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/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.")
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
"""
|
|
||||||
On-device test for the Spin pattern using mpremote and Presets.
|
|
||||||
|
|
||||||
Usage (from pico/ dir or project root with adjusted paths):
|
|
||||||
|
|
||||||
mpremote connect <device> cp src/*.py :
|
|
||||||
mpremote connect <device> cp src/patterns/*.py :patterns
|
|
||||||
mpremote connect <device> cp lib/*.py :
|
|
||||||
mpremote connect <device> cp presets.json :
|
|
||||||
mpremote connect <device> cp test/test_spin.py :
|
|
||||||
mpremote connect <device> run test_spin.py
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Instantiates Presets
|
|
||||||
- Creates a few in-memory spin presets with different parameters
|
|
||||||
- Runs each one for a short time so you can visually compare behaviour
|
|
||||||
"""
|
|
||||||
|
|
||||||
import utime
|
|
||||||
from presets import Presets, Preset
|
|
||||||
|
|
||||||
|
|
||||||
def make_spin_preset(
|
|
||||||
name,
|
|
||||||
color_inner,
|
|
||||||
color_outer,
|
|
||||||
rate=4,
|
|
||||||
delay_ms=30,
|
|
||||||
margin=0,
|
|
||||||
brightness=255,
|
|
||||||
):
|
|
||||||
"""Helper to build a Preset dict for the spin pattern."""
|
|
||||||
data = {
|
|
||||||
"p": "spin",
|
|
||||||
"c": [color_inner, color_outer],
|
|
||||||
"b": brightness,
|
|
||||||
"d": delay_ms,
|
|
||||||
"n1": rate, # expansion step per tick
|
|
||||||
"n2": margin, # margin from strip ends
|
|
||||||
"a": True,
|
|
||||||
}
|
|
||||||
return name, Preset(data)
|
|
||||||
|
|
||||||
|
|
||||||
def run_preset(presets, name, preset_obj, duration_ms):
|
|
||||||
"""Run a given spin preset for duration_ms using the existing tick loop."""
|
|
||||||
# Start each preset from a blank frame so both sides are balanced.
|
|
||||||
presets.select("off")
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
presets.presets[name] = preset_obj
|
|
||||||
presets.select(name)
|
|
||||||
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
|
|
||||||
presets.tick()
|
|
||||||
utime.sleep_ms(10)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
presets = Presets()
|
|
||||||
presets.load()
|
|
||||||
|
|
||||||
# Ensure we start from a blank frame.
|
|
||||||
presets.select("off")
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
print("Starting spin pattern test...")
|
|
||||||
|
|
||||||
# Use strip 0 length to derive a reasonable margin.
|
|
||||||
ref_len = presets.strip_length(0)
|
|
||||||
margin_small = ref_len // 16 if ref_len > 0 else 0
|
|
||||||
margin_large = ref_len // 8 if ref_len > 0 else 0
|
|
||||||
|
|
||||||
spin_presets = []
|
|
||||||
|
|
||||||
# 1. Slow spin, warm white to orange, small margin
|
|
||||||
spin_presets.append(
|
|
||||||
make_spin_preset(
|
|
||||||
"spin_slow_warm",
|
|
||||||
color_inner=(255, 200, 120),
|
|
||||||
color_outer=(255, 100, 0),
|
|
||||||
rate=2,
|
|
||||||
delay_ms=40,
|
|
||||||
margin=margin_small,
|
|
||||||
brightness=255,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Medium spin, cyan to magenta, larger margin
|
|
||||||
spin_presets.append(
|
|
||||||
make_spin_preset(
|
|
||||||
"spin_medium_cyan_magenta",
|
|
||||||
color_inner=(0, 255, 180),
|
|
||||||
color_outer=(255, 0, 180),
|
|
||||||
rate=4,
|
|
||||||
delay_ms=30,
|
|
||||||
margin=margin_large,
|
|
||||||
brightness=255,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Fast spin, white to off (fade outwards), no margin
|
|
||||||
spin_presets.append(
|
|
||||||
make_spin_preset(
|
|
||||||
"spin_fast_white",
|
|
||||||
color_inner=(255, 255, 255),
|
|
||||||
color_outer=(0, 0, 0),
|
|
||||||
rate=6,
|
|
||||||
delay_ms=20,
|
|
||||||
margin=0,
|
|
||||||
brightness=255,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run each spin preset for about 6 seconds
|
|
||||||
for name, preset_obj in spin_presets:
|
|
||||||
print("Running spin preset:", name)
|
|
||||||
run_preset(presets, name, preset_obj, duration_ms=6000)
|
|
||||||
|
|
||||||
print("Spin pattern test finished. Turning off LEDs.")
|
|
||||||
presets.select("off")
|
|
||||||
presets.tick()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
618
presets.json
618
presets.json
@@ -1,618 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
src/boot.py
Normal file
9
src/boot.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import settings
|
||||||
|
import wifi
|
||||||
|
from settings import Settings
|
||||||
|
|
||||||
|
s = Settings()
|
||||||
|
|
||||||
|
name = s.get('name', 'led')
|
||||||
|
password = s.get("ap_password", "")
|
||||||
|
wifi.ap(name, password)
|
||||||
@@ -93,8 +93,8 @@ class PIO_DMA_Transfer():
|
|||||||
self.dma_chan.CTRL_TRIG.INCR_WRITE = 0
|
self.dma_chan.CTRL_TRIG.INCR_WRITE = 0
|
||||||
self.dma_chan.CTRL_TRIG.INCR_READ = 1
|
self.dma_chan.CTRL_TRIG.INCR_READ = 1
|
||||||
|
|
||||||
def start_transfer(self, buffer, offset=0):
|
def start_transfer(self, buffer):
|
||||||
self.dma_chan.READ_ADDR_REG = uctypes.addressof(buffer) + offset
|
self.dma_chan.READ_ADDR_REG = uctypes.addressof(buffer)
|
||||||
self.dma_chan.CTRL_TRIG.EN = 1
|
self.dma_chan.CTRL_TRIG.EN = 1
|
||||||
|
|
||||||
def transfer_count(self):
|
def transfer_count(self):
|
||||||
54
src/main.py
Normal file
54
src/main.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import asyncio
|
||||||
|
import aioespnow
|
||||||
|
from settings import Settings
|
||||||
|
from web import web
|
||||||
|
from patterns import Patterns
|
||||||
|
import gc
|
||||||
|
import utime
|
||||||
|
import machine
|
||||||
|
import time
|
||||||
|
import wifi
|
||||||
|
import json
|
||||||
|
from p2p import p2p
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
patterns = Patterns(selected=settings["pattern"])
|
||||||
|
if settings["color_order"] == "rbg": color_order = (1, 5, 3)
|
||||||
|
else: color_order = (1, 3, 5)
|
||||||
|
patterns.set_color1(tuple(int(settings["color1"][i:i+2], 16) for i in color_order))
|
||||||
|
patterns.set_color2(tuple(int(settings["color2"][i:i+2], 16) for i in color_order))
|
||||||
|
patterns.set_brightness(int(settings["brightness"]))
|
||||||
|
patterns.set_delay(int(settings["delay"]))
|
||||||
|
|
||||||
|
async def tick():
|
||||||
|
while True:
|
||||||
|
patterns.tick()
|
||||||
|
await asyncio.sleep_ms(0)
|
||||||
|
|
||||||
|
async def system():
|
||||||
|
while True:
|
||||||
|
gc.collect()
|
||||||
|
for i in range(60):
|
||||||
|
wdt.feed()
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
w = web(settings, patterns)
|
||||||
|
print(settings)
|
||||||
|
# start the server in a bacakground task
|
||||||
|
print("Starting")
|
||||||
|
server = asyncio.create_task(w.start_server(host="0.0.0.0", port=80))
|
||||||
|
wdt = machine.WDT(timeout=10000)
|
||||||
|
wdt.feed()
|
||||||
|
|
||||||
|
asyncio.create_task(tick())
|
||||||
|
asyncio.create_task(p2p(settings, patterns))
|
||||||
|
asyncio.create_task(system())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# cleanup before ending the application
|
||||||
|
await server
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
20
src/p2p.py
Normal file
20
src/p2p.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
print(data)
|
||||||
|
if "names" not in data or settings.get("name") in data.get("names", []):
|
||||||
|
if "step" in settings and isinstance(settings["step"], int):
|
||||||
|
patterns.set_pattern_step(settings["step"])
|
||||||
|
else:
|
||||||
|
settings.set_settings(data.get("settings", {}), patterns, data.get("save", False))
|
||||||
|
print("should not print")
|
||||||
514
src/patterns.py
Normal file
514
src/patterns.py
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
from machine import Pin
|
||||||
|
from neopixel import NeoPixel
|
||||||
|
import utime
|
||||||
|
import random
|
||||||
|
|
||||||
|
# 8 strips of 270 leds
|
||||||
|
strips = [(1, 270), (2, 277),
|
||||||
|
(3, 280), (4, 270),
|
||||||
|
(5, 270), (6, 270),
|
||||||
|
(7, 270), (10,270)]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Patterns:
|
||||||
|
def __init__(self,color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100):
|
||||||
|
self.strips = []
|
||||||
|
# Initialize all 8 strips
|
||||||
|
for pin, num_leds in strips:
|
||||||
|
strip = NeoPixel(Pin(pin, Pin.OUT), num_leds)
|
||||||
|
self.strips.append(strip)
|
||||||
|
self.pattern_step = 0
|
||||||
|
self.last_update = utime.ticks_ms()
|
||||||
|
self.delay = delay
|
||||||
|
self.brightness = brightness
|
||||||
|
self.patterns = {
|
||||||
|
"off": self.off,
|
||||||
|
"on" : self.on,
|
||||||
|
"color_wipe": self.color_wipe,
|
||||||
|
"rainbow_cycle": self.rainbow_cycle,
|
||||||
|
"rainbow_spiral": self.rainbow_spiral, # Rainbow cycle per strip
|
||||||
|
"rainbow_strips": self.rainbow_strips, # New: Single color per strip, rainbow between strips
|
||||||
|
"theater_chase": self.theater_chase,
|
||||||
|
"blink": self.blink,
|
||||||
|
"color_transition": self.color_transition, # Added new pattern
|
||||||
|
"flicker": self.flicker,
|
||||||
|
"scanner": self.scanner, # New: Single direction scanner
|
||||||
|
"bidirectional_scanner": self.bidirectional_scanner, # New: Bidirectional scanner
|
||||||
|
"strip_cycle": self.strip_cycle, # New: Cycle through strips
|
||||||
|
"external": None
|
||||||
|
}
|
||||||
|
self.selected = selected
|
||||||
|
# Ensure colors list always starts with at least two for robust transition handling
|
||||||
|
self.colors = [color1, color2] if color1 != color2 else [color1, (255, 255, 255)] # Fallback if initial colors are same
|
||||||
|
if not self.colors: # Ensure at least one color exists
|
||||||
|
self.colors = [(0, 0, 0)]
|
||||||
|
|
||||||
|
self.transition_duration = delay * 50 # Default transition duration
|
||||||
|
self.hold_duration = delay * 10 # Default hold duration at each color
|
||||||
|
self.transition_step = 0 # Current step in the transition
|
||||||
|
self.current_color_idx = 0 # Index of the color currently being held/transitioned from
|
||||||
|
self.current_color = self.colors[self.current_color_idx] # The actual blended color
|
||||||
|
|
||||||
|
self.hold_start_time = utime.ticks_ms() # Time when the current color hold started
|
||||||
|
|
||||||
|
# New attributes for scanner patterns
|
||||||
|
self.scanner_direction = 1 # 1 for forward, -1 for backward
|
||||||
|
self.scanner_tail_length = 3 # Number of trailing pixels
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
self.pattern_step=0
|
||||||
|
self.last_update = utime.ticks_ms() - self.delay
|
||||||
|
if self.selected == "color_transition":
|
||||||
|
self.transition_step = 0
|
||||||
|
self.current_color_idx = 0
|
||||||
|
self.current_color = self.colors[self.current_color_idx]
|
||||||
|
self.hold_start_time = utime.ticks_ms() # Reset hold time
|
||||||
|
# Reset scanner specific variables
|
||||||
|
self.scanner_direction = 1
|
||||||
|
self.tick()
|
||||||
|
|
||||||
|
def set_pattern_step(self, step):
|
||||||
|
self.pattern_step = step
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
if self.patterns[self.selected]:
|
||||||
|
self.patterns[self.selected]()
|
||||||
|
|
||||||
|
def update_num_leds(self, pin, num_leds):
|
||||||
|
# Find and update the specific strip
|
||||||
|
for i, (strip_pin, _) in enumerate(strips):
|
||||||
|
if strip_pin == pin:
|
||||||
|
self.strips[i] = NeoPixel(Pin(pin, Pin.OUT), num_leds)
|
||||||
|
self.pattern_step = 0
|
||||||
|
break
|
||||||
|
|
||||||
|
def set_delay(self, delay):
|
||||||
|
self.delay = delay
|
||||||
|
# Update transition duration and hold duration when delay changes
|
||||||
|
self.transition_duration = self.delay * 50
|
||||||
|
self.hold_duration = self.delay * 10
|
||||||
|
|
||||||
|
|
||||||
|
def set_brightness(self, brightness):
|
||||||
|
self.brightness = brightness
|
||||||
|
|
||||||
|
def set_color1(self, color):
|
||||||
|
if len(self.colors) > 0:
|
||||||
|
self.colors[0] = color
|
||||||
|
if self.selected == "color_transition":
|
||||||
|
# If the first color is changed, potentially reset transition
|
||||||
|
# to start from this new color if we were about to transition from it
|
||||||
|
if self.current_color_idx == 0:
|
||||||
|
self.transition_step = 0
|
||||||
|
self.current_color = self.colors[0]
|
||||||
|
self.hold_start_time = utime.ticks_ms()
|
||||||
|
else:
|
||||||
|
self.colors.append(color)
|
||||||
|
|
||||||
|
|
||||||
|
def set_color2(self, color):
|
||||||
|
if len(self.colors) > 1:
|
||||||
|
self.colors[1] = color
|
||||||
|
elif len(self.colors) == 1:
|
||||||
|
self.colors.append(color)
|
||||||
|
else: # List is empty
|
||||||
|
self.colors.append((0,0,0)) # Dummy color
|
||||||
|
self.colors.append(color)
|
||||||
|
|
||||||
|
|
||||||
|
def set_colors(self, colors):
|
||||||
|
if colors and len(colors) >= 2:
|
||||||
|
self.colors = colors
|
||||||
|
if self.selected == "color_transition":
|
||||||
|
self.sync() # Reset transition if new color list is provided
|
||||||
|
elif colors and len(colors) == 1:
|
||||||
|
self.colors = [colors[0], (255,255,255)] # Add a default second color
|
||||||
|
if self.selected == "color_transition":
|
||||||
|
print("Warning: 'color_transition' requires at least two colors. Adding a default second color.")
|
||||||
|
self.sync()
|
||||||
|
else:
|
||||||
|
print("Error: set_colors requires a list of at least one color.")
|
||||||
|
self.colors = [(0,0,0), (255,255,255)] # Fallback
|
||||||
|
if self.selected == "color_transition":
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
def set_color(self, num, color):
|
||||||
|
# Changed: More robust index check
|
||||||
|
if 0 <= num < len(self.colors):
|
||||||
|
self.colors[num] = color
|
||||||
|
# If the changed color is part of the current or next transition,
|
||||||
|
# restart the transition for smoother updates
|
||||||
|
if self.selected == "color_transition":
|
||||||
|
current_from_idx = self.current_color_idx
|
||||||
|
current_to_idx = (self.current_color_idx + 1) % len(self.colors)
|
||||||
|
if num == current_from_idx or num == current_to_idx:
|
||||||
|
# If we change a color involved in the current transition,
|
||||||
|
# it's best to restart the transition state for smoothness.
|
||||||
|
self.transition_step = 0
|
||||||
|
self.current_color_idx = current_from_idx # Stay at the current starting color
|
||||||
|
self.current_color = self.colors[self.current_color_idx]
|
||||||
|
self.hold_start_time = utime.ticks_ms() # Reset hold
|
||||||
|
return True
|
||||||
|
elif num == len(self.colors): # Allow setting a new color at the end
|
||||||
|
self.colors.append(color)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_color(self, color):
|
||||||
|
self.colors.append(color)
|
||||||
|
if self.selected == "color_transition" and len(self.colors) == 2:
|
||||||
|
# If we just added the second color needed for transition
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
|
||||||
|
def del_color(self, num):
|
||||||
|
# Changed: More robust index check and using del for lists
|
||||||
|
if 0 <= num < len(self.colors):
|
||||||
|
del self.colors[num]
|
||||||
|
# If the color being deleted was part of the current transition,
|
||||||
|
# re-evaluate the current_color_idx
|
||||||
|
if self.selected == "color_transition":
|
||||||
|
if len(self.colors) < 2: # Need at least two colors for transition
|
||||||
|
print("Warning: Not enough colors for 'color_transition'. Switching to 'on'.")
|
||||||
|
self.select("on") # Or some other default
|
||||||
|
else:
|
||||||
|
# Adjust index if it's out of bounds after deletion or was the one transitioning from
|
||||||
|
self.current_color_idx %= len(self.colors)
|
||||||
|
self.transition_step = 0
|
||||||
|
self.current_color = self.colors[self.current_color_idx]
|
||||||
|
self.hold_start_time = utime.ticks_ms()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def apply_brightness(self, color, brightness_override=None):
|
||||||
|
effective_brightness = brightness_override if brightness_override is not None else self.brightness
|
||||||
|
return tuple(int(c * effective_brightness / 255) for c in color)
|
||||||
|
|
||||||
|
def select(self, pattern):
|
||||||
|
if pattern in self.patterns:
|
||||||
|
self.selected = pattern
|
||||||
|
self.sync() # Reset pattern state when selecting a new pattern
|
||||||
|
if pattern == "color_transition":
|
||||||
|
if len(self.colors) < 2:
|
||||||
|
print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.")
|
||||||
|
self.selected = "on" # Fallback if not enough colors
|
||||||
|
self.sync() # Re-sync for the new pattern
|
||||||
|
else:
|
||||||
|
self.transition_step = 0
|
||||||
|
self.current_color_idx = 0 # Start from the first color in the list
|
||||||
|
self.current_color = self.colors[self.current_color_idx]
|
||||||
|
self.hold_start_time = utime.ticks_ms() # Reset hold timer
|
||||||
|
self.transition_duration = self.delay * 50 # Initialize transition duration
|
||||||
|
self.hold_duration = self.delay * 10 # Initialize hold duration
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set(self, i, color):
|
||||||
|
# Find which strip contains LED i
|
||||||
|
current_pos = 0
|
||||||
|
for strip in self.strips:
|
||||||
|
if i < current_pos + len(strip):
|
||||||
|
strip[i - current_pos] = color
|
||||||
|
return
|
||||||
|
current_pos += len(strip)
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
for strip in self.strips:
|
||||||
|
strip.write()
|
||||||
|
|
||||||
|
def fill(self, color=None):
|
||||||
|
fill_color = color if color is not None else self.colors[0]
|
||||||
|
for strip in self.strips:
|
||||||
|
for i in range(len(strip)):
|
||||||
|
strip[i] = fill_color
|
||||||
|
self.write()
|
||||||
|
|
||||||
|
def off(self):
|
||||||
|
self.fill((0, 0, 0))
|
||||||
|
|
||||||
|
def on(self):
|
||||||
|
self.fill(self.apply_brightness(self.colors[0]))
|
||||||
|
|
||||||
|
def color_wipe(self):
|
||||||
|
color = self.apply_brightness(self.colors[0])
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
|
||||||
|
# Calculate total LEDs dynamically
|
||||||
|
total_leds = sum(len(strip) for strip in self.strips)
|
||||||
|
if self.pattern_step < total_leds:
|
||||||
|
# Clear all LEDs
|
||||||
|
self.fill((0, 0, 0))
|
||||||
|
# Set the current LED
|
||||||
|
self.set(self.pattern_step, color)
|
||||||
|
self.write()
|
||||||
|
self.pattern_step += 1
|
||||||
|
else:
|
||||||
|
self.pattern_step = 0
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def rainbow_cycle(self):
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay/5:
|
||||||
|
def wheel(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)
|
||||||
|
|
||||||
|
total_leds = sum(len(strip) for strip in self.strips)
|
||||||
|
for i in range(total_leds):
|
||||||
|
rc_index = (i * 256 // total_leds) + self.pattern_step
|
||||||
|
self.set(i, self.apply_brightness(wheel(rc_index & 255)))
|
||||||
|
self.write()
|
||||||
|
self.pattern_step = (self.pattern_step + 1) % 256
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def theater_chase(self):
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
|
||||||
|
total_leds = sum(len(strip) for strip in self.strips)
|
||||||
|
for i in range(total_leds):
|
||||||
|
if (i + self.pattern_step) % 3 == 0:
|
||||||
|
self.set(i, self.apply_brightness(self.colors[0]))
|
||||||
|
else:
|
||||||
|
self.set(i, (0, 0, 0))
|
||||||
|
self.write()
|
||||||
|
self.pattern_step = (self.pattern_step + 1) % 3
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def blink(self):
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
|
||||||
|
if self.pattern_step % 2 == 0:
|
||||||
|
self.fill(self.apply_brightness(self.colors[0]))
|
||||||
|
else:
|
||||||
|
self.fill((0, 0, 0))
|
||||||
|
self.pattern_step = (self.pattern_step + 1) % 2
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def color_transition(self):
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
|
||||||
|
# Check for hold duration first
|
||||||
|
if utime.ticks_diff(current_time, self.hold_start_time) < self.hold_duration:
|
||||||
|
# Still in hold phase, just display the current solid color
|
||||||
|
self.fill(self.apply_brightness(self.current_color))
|
||||||
|
self.last_update = current_time # Keep updating last_update to avoid skipping frames
|
||||||
|
return
|
||||||
|
|
||||||
|
# If hold duration is over, proceed with transition
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
|
||||||
|
num_colors = len(self.colors)
|
||||||
|
if num_colors < 2:
|
||||||
|
# Should not happen if select handles it, but as a safeguard
|
||||||
|
self.select("on")
|
||||||
|
return
|
||||||
|
|
||||||
|
from_color = self.colors[self.current_color_idx]
|
||||||
|
to_color_idx = (self.current_color_idx + 1) % num_colors
|
||||||
|
to_color = self.colors[to_color_idx]
|
||||||
|
|
||||||
|
# Calculate interpolation factor (0.0 to 1.0)
|
||||||
|
# transition_step goes from 0 to transition_duration - 1
|
||||||
|
if self.transition_duration > 0:
|
||||||
|
interp_factor = self.transition_step / self.transition_duration
|
||||||
|
else:
|
||||||
|
interp_factor = 1.0 # Immediately transition if duration is zero
|
||||||
|
|
||||||
|
# Interpolate each color component
|
||||||
|
r = int(from_color[0] + (to_color[0] - from_color[0]) * interp_factor)
|
||||||
|
g = int(from_color[1] + (to_color[1] - from_color[1]) * interp_factor)
|
||||||
|
b = int(from_color[2] + (to_color[2] - from_color[2]) * interp_factor)
|
||||||
|
|
||||||
|
self.current_color = (r, g, b)
|
||||||
|
self.fill(self.apply_brightness(self.current_color))
|
||||||
|
|
||||||
|
self.transition_step += self.delay # Advance the transition step by the delay
|
||||||
|
|
||||||
|
if self.transition_step >= self.transition_duration:
|
||||||
|
# Transition complete, move to the next color and reset for hold phase
|
||||||
|
self.current_color_idx = to_color_idx
|
||||||
|
self.current_color = self.colors[self.current_color_idx] # Ensure current_color is the exact target color
|
||||||
|
self.transition_step = 0 # Reset transition progress
|
||||||
|
self.hold_start_time = current_time # Start hold phase for the new color
|
||||||
|
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def flicker(self):
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay/5:
|
||||||
|
base_color = self.colors[0]
|
||||||
|
# Increase the range for flicker_brightness_offset
|
||||||
|
# Changed from self.brightness // 4 to self.brightness // 2 (or even self.brightness for max intensity)
|
||||||
|
flicker_brightness_offset = random.randint(-int(self.brightness // 1.5), int(self.brightness // 1.5))
|
||||||
|
flicker_brightness = max(0, min(255, self.brightness + flicker_brightness_offset))
|
||||||
|
|
||||||
|
flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness)
|
||||||
|
self.fill(flicker_color)
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def scanner(self):
|
||||||
|
"""
|
||||||
|
Mimics a 'Knight Rider' style scanner, moving in one direction.
|
||||||
|
"""
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
|
||||||
|
self.fill((0, 0, 0)) # Clear all LEDs
|
||||||
|
|
||||||
|
# Calculate the head and tail position
|
||||||
|
head_pos = self.pattern_step
|
||||||
|
color = self.apply_brightness(self.colors[0])
|
||||||
|
total_leds = sum(len(strip) for strip in self.strips)
|
||||||
|
|
||||||
|
# Draw the head
|
||||||
|
if 0 <= head_pos < total_leds:
|
||||||
|
self.set(head_pos, color)
|
||||||
|
|
||||||
|
# Draw the trailing pixels with decreasing brightness
|
||||||
|
for i in range(1, self.scanner_tail_length + 1):
|
||||||
|
tail_pos = head_pos - i
|
||||||
|
if 0 <= tail_pos < total_leds:
|
||||||
|
# Calculate fading color for tail
|
||||||
|
# Example: linear fade from full brightness to off
|
||||||
|
fade_factor = 1.0 - (i / (self.scanner_tail_length + 1))
|
||||||
|
faded_color = tuple(int(c * fade_factor) for c in color)
|
||||||
|
self.set(tail_pos, faded_color)
|
||||||
|
|
||||||
|
self.write()
|
||||||
|
|
||||||
|
self.pattern_step += 1
|
||||||
|
if self.pattern_step >= total_leds + self.scanner_tail_length:
|
||||||
|
self.pattern_step = 0 # Reset to start
|
||||||
|
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def bidirectional_scanner(self):
|
||||||
|
"""
|
||||||
|
Mimics a 'Knight Rider' style scanner, moving back and forth.
|
||||||
|
"""
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay/100:
|
||||||
|
self.fill((0, 0, 0)) # Clear all LEDs
|
||||||
|
|
||||||
|
color = self.apply_brightness(self.colors[0])
|
||||||
|
total_leds = sum(len(strip) for strip in self.strips)
|
||||||
|
|
||||||
|
# Calculate the head position based on direction
|
||||||
|
head_pos = self.pattern_step
|
||||||
|
|
||||||
|
# Draw the head
|
||||||
|
if 0 <= head_pos < total_leds:
|
||||||
|
self.set(head_pos, color)
|
||||||
|
|
||||||
|
# Draw the trailing pixels with decreasing brightness
|
||||||
|
for i in range(1, self.scanner_tail_length + 1):
|
||||||
|
tail_pos = head_pos - (i * self.scanner_direction)
|
||||||
|
if 0 <= tail_pos < total_leds:
|
||||||
|
fade_factor = 1.0 - (i / (self.scanner_tail_length + 1))
|
||||||
|
faded_color = tuple(int(c * fade_factor) for c in color)
|
||||||
|
self.set(tail_pos, faded_color)
|
||||||
|
|
||||||
|
self.write()
|
||||||
|
|
||||||
|
self.pattern_step += self.scanner_direction
|
||||||
|
|
||||||
|
# Change direction if boundaries are reached
|
||||||
|
if self.scanner_direction == 1 and self.pattern_step >= total_leds:
|
||||||
|
self.scanner_direction = -1
|
||||||
|
self.pattern_step = total_leds - 1 # Start moving back from the last LED
|
||||||
|
elif self.scanner_direction == -1 and self.pattern_step < 0:
|
||||||
|
self.scanner_direction = 1
|
||||||
|
self.pattern_step = 0 # Start moving forward from the first LED
|
||||||
|
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def strip_cycle(self):
|
||||||
|
"""
|
||||||
|
Cycles through each strip, turning them on and off one by one.
|
||||||
|
"""
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
|
||||||
|
# Turn off the previous strip
|
||||||
|
prev_strip = (self.pattern_step - 1) % len(self.strips)
|
||||||
|
for i in range(len(self.strips[prev_strip])):
|
||||||
|
self.strips[prev_strip][i] = (0, 0, 0)
|
||||||
|
|
||||||
|
# Turn on the current strip
|
||||||
|
current_strip = self.pattern_step % len(self.strips)
|
||||||
|
color = self.apply_brightness(self.colors[0])
|
||||||
|
|
||||||
|
for i in range(len(self.strips[current_strip])):
|
||||||
|
self.strips[current_strip][i] = color
|
||||||
|
|
||||||
|
self.write()
|
||||||
|
|
||||||
|
# Move to next strip
|
||||||
|
self.pattern_step += 1
|
||||||
|
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def rainbow_spiral(self):
|
||||||
|
"""
|
||||||
|
Creates a rainbow effect that cycles through each strip individually.
|
||||||
|
Each strip shows its own rainbow pattern.
|
||||||
|
"""
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay/5:
|
||||||
|
def wheel(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)
|
||||||
|
|
||||||
|
# Apply rainbow to each strip individually
|
||||||
|
for strip_idx, strip in enumerate(self.strips):
|
||||||
|
strip_length = len(strip)
|
||||||
|
for i in range(strip_length):
|
||||||
|
# Each strip gets its own rainbow cycle with offset based on strip index
|
||||||
|
rc_index = (i * 256 // strip_length) + self.pattern_step + (strip_idx * 32)
|
||||||
|
strip[i] = self.apply_brightness(wheel(rc_index & 255))
|
||||||
|
|
||||||
|
self.write()
|
||||||
|
self.pattern_step = (self.pattern_step + 1) % 256
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
def rainbow_strips(self):
|
||||||
|
"""
|
||||||
|
Each strip is a single color, rainbow effect is between strips.
|
||||||
|
Creates a rainbow pattern across the 8 strips.
|
||||||
|
"""
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, self.last_update) >= self.delay/5:
|
||||||
|
def wheel(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)
|
||||||
|
|
||||||
|
# Each strip gets a single color from the rainbow
|
||||||
|
for strip_idx, strip in enumerate(self.strips):
|
||||||
|
# Calculate rainbow position for this strip
|
||||||
|
rainbow_pos = (strip_idx * 32 + self.pattern_step) % 256
|
||||||
|
color = self.apply_brightness(wheel(rainbow_pos))
|
||||||
|
|
||||||
|
# Fill entire strip with this single color
|
||||||
|
for i in range(len(strip)):
|
||||||
|
strip[i] = color
|
||||||
|
|
||||||
|
self.write()
|
||||||
|
self.pattern_step = (self.pattern_step + 1) % 256
|
||||||
|
self.last_update = current_time
|
||||||
113
src/settings.py
Normal file
113
src/settings.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import json
|
||||||
|
import wifi
|
||||||
|
import ubinascii
|
||||||
|
import machine
|
||||||
|
|
||||||
|
class Settings(dict):
|
||||||
|
SETTINGS_FILE = "/settings.json"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.load() # Load settings from file during initialization
|
||||||
|
if self["color_order"] == "rbg": self.color_order = (1, 5, 3)
|
||||||
|
else: self.color_order = (1, 3, 5)
|
||||||
|
|
||||||
|
def set_defaults(self):
|
||||||
|
self["led_pin"] = 10
|
||||||
|
self["num_leds"] = 50
|
||||||
|
self["pattern"] = "on"
|
||||||
|
self["color1"] = "#00ff00"
|
||||||
|
self["color2"] = "#ff0000"
|
||||||
|
self["delay"] = 100
|
||||||
|
self["brightness"] = 10
|
||||||
|
self["color_order"] = "rgb"
|
||||||
|
self["name"] = f"led-hoop"
|
||||||
|
self["ap_password"] = ""
|
||||||
|
self["id"] = 0
|
||||||
|
|
||||||
|
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 set_settings(self, data, patterns, save):
|
||||||
|
try:
|
||||||
|
print(data)
|
||||||
|
for key, value in data.items():
|
||||||
|
print(key, value)
|
||||||
|
if key == "colors":
|
||||||
|
buff = []
|
||||||
|
for color in value:
|
||||||
|
buff.append(tuple(int(color[i:i+2], 16) for i in self.color_order))
|
||||||
|
patterns.set_colors(buff)
|
||||||
|
elif key == "color1":
|
||||||
|
patterns.set_color1(tuple(int(value[i:i+2], 16) for i in self.color_order)) # Convert hex to RGB
|
||||||
|
elif key == "color2":
|
||||||
|
patterns.set_color2(tuple(int(value[i:i+2], 16) for i in self.color_order)) # Convert hex to RGB
|
||||||
|
elif key == "num_leds":
|
||||||
|
patterns.update_num_leds(self["led_pin"], value)
|
||||||
|
elif key == "pattern":
|
||||||
|
if not patterns.select(value):
|
||||||
|
return "Pattern doesn't exist", 400
|
||||||
|
elif key == "delay":
|
||||||
|
delay = int(data["delay"])
|
||||||
|
patterns.set_delay(delay)
|
||||||
|
elif key == "brightness":
|
||||||
|
brightness = int(data["brightness"])
|
||||||
|
patterns.set_brightness(brightness)
|
||||||
|
elif key == "name":
|
||||||
|
self[key] = value
|
||||||
|
self.save()
|
||||||
|
machine.reset()
|
||||||
|
elif key == "color_order":
|
||||||
|
if value == "rbg": self.color_order = (1, 5, 3)
|
||||||
|
else: self.color_order = (1, 3, 5)
|
||||||
|
pass
|
||||||
|
elif key == "id":
|
||||||
|
pass
|
||||||
|
elif key == "led_pin":
|
||||||
|
patterns.update_num_leds(value, self["num_leds"])
|
||||||
|
else:
|
||||||
|
return "Invalid key", 400
|
||||||
|
self[key] = value
|
||||||
|
#print(self)
|
||||||
|
patterns.sync()
|
||||||
|
if save:
|
||||||
|
self.save()
|
||||||
|
return "OK", 200
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
return "Bad request", 400
|
||||||
|
|
||||||
|
# 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()
|
||||||
109
src/static/main.css
Normal file
109
src/static/main.css
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
input[type="text"],
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="range"],
|
||||||
|
input[type="color"] {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 25px;
|
||||||
|
background: #d3d3d3;
|
||||||
|
outline: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
input[type="range"]:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
background: #4caf50;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
background: #4caf50;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
#pattern_buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
#pattern_buttons button {
|
||||||
|
flex: 1 0 calc(33.333% - 10px);
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
#pattern_buttons button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#pattern_buttons button {
|
||||||
|
flex: 1 0 calc(50% - 10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#connection-status {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block; /* Or block, depending on where you put it */
|
||||||
|
margin-left: 10px; /* Adjust spacing as needed */
|
||||||
|
vertical-align: middle; /* Align with nearby text */
|
||||||
|
background-color: grey; /* Default: Unknown */
|
||||||
|
}
|
||||||
|
|
||||||
|
#connection-status.connecting {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connection-status.open {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connection-status.closing,
|
||||||
|
#connection-status.closed {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color_order_form label,
|
||||||
|
#color_order_form input[type="radio"] {
|
||||||
|
/* Ensures they behave as inline elements */
|
||||||
|
display: inline-block;
|
||||||
|
/* Adds some space between them for readability */
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle; /* Aligns them nicely if heights vary */
|
||||||
|
}
|
||||||
244
src/static/main.js
Normal file
244
src/static/main.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
let delayTimeout;
|
||||||
|
let brightnessTimeout;
|
||||||
|
let colorTimeout;
|
||||||
|
let color2Timeout;
|
||||||
|
let ws; // Variable to hold the WebSocket connection
|
||||||
|
let connectionStatusElement; // Variable to hold the connection status element
|
||||||
|
|
||||||
|
// Function to update the connection status indicator
|
||||||
|
function updateConnectionStatus(status) {
|
||||||
|
if (!connectionStatusElement) {
|
||||||
|
connectionStatusElement = document.getElementById("connection-status");
|
||||||
|
}
|
||||||
|
if (connectionStatusElement) {
|
||||||
|
connectionStatusElement.className = ""; // Clear existing classes
|
||||||
|
connectionStatusElement.classList.add(status);
|
||||||
|
// Optionally, you could also update text content based on status
|
||||||
|
// connectionStatusElement.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to establish WebSocket connection
|
||||||
|
function connectWebSocket() {
|
||||||
|
// Determine the WebSocket URL based on the current location
|
||||||
|
const wsUrl = `ws://${window.location.host}/ws`;
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
updateConnectionStatus("connecting"); // Indicate connecting state
|
||||||
|
|
||||||
|
ws.onopen = function (event) {
|
||||||
|
console.log("WebSocket connection opened:", event);
|
||||||
|
updateConnectionStatus("open"); // Indicate open state
|
||||||
|
// Optionally, you could send an initial message here
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function (event) {
|
||||||
|
console.log("WebSocket message received:", event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function (event) {
|
||||||
|
console.error("WebSocket error:", event);
|
||||||
|
updateConnectionStatus("closed"); // Indicate error state (treat as closed)
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function (event) {
|
||||||
|
if (event.wasClean) {
|
||||||
|
console.log(
|
||||||
|
`WebSocket connection closed cleanly, code=${event.code}, reason=${event.reason}`,
|
||||||
|
);
|
||||||
|
updateConnectionStatus("closed"); // Indicate closed state
|
||||||
|
} else {
|
||||||
|
console.error("WebSocket connection died");
|
||||||
|
updateConnectionStatus("closed"); // Indicate closed state
|
||||||
|
}
|
||||||
|
// Attempt to reconnect after a delay
|
||||||
|
setTimeout(connectWebSocket, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to send data over WebSocket
|
||||||
|
function sendWebSocketData(data) {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
console.log("Sending data over WebSocket:", data);
|
||||||
|
ws.send(JSON.stringify(data));
|
||||||
|
} else {
|
||||||
|
console.error("WebSocket is not connected. Cannot send data:", data);
|
||||||
|
// You might want to queue messages or handle this in a different way
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the post and get functions for now, they might still be useful
|
||||||
|
async function post(path, data) {
|
||||||
|
console.log(`POST to ${path}`, data);
|
||||||
|
try {
|
||||||
|
const response = await fetch(path, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during POST request:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(path) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(path);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during GET request:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateColor(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
clearTimeout(colorTimeout);
|
||||||
|
colorTimeout = setTimeout(function () {
|
||||||
|
const color = document.getElementById("color").value;
|
||||||
|
sendWebSocketData({ color1: color });
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateColor2(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
clearTimeout(color2Timeout);
|
||||||
|
color2Timeout = setTimeout(function () {
|
||||||
|
const color = document.getElementById("color2").value;
|
||||||
|
sendWebSocketData({ color2: color });
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePattern(pattern) {
|
||||||
|
sendWebSocketData({ pattern: pattern });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBrightness(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
clearTimeout(brightnessTimeout);
|
||||||
|
brightnessTimeout = setTimeout(function () {
|
||||||
|
const brightness = document.getElementById("brightness").value;
|
||||||
|
sendWebSocketData({ brightness: brightness });
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDelay(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
clearTimeout(delayTimeout);
|
||||||
|
delayTimeout = setTimeout(function () {
|
||||||
|
const delay = document.getElementById("delay").value;
|
||||||
|
sendWebSocketData({ delay: delay });
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateNumLeds(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const numLeds = document.getElementById("num_leds").value;
|
||||||
|
sendWebSocketData({ num_leds: parseInt(numLeds) });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateName(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const name = document.getElementById("name").value;
|
||||||
|
sendWebSocketData({ name: name });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateID(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = document.getElementById("id").value;
|
||||||
|
sendWebSocketData({ id: parseInt(id) });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateLedPin(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const ledpin = document.getElementById("led_pin").value;
|
||||||
|
sendWebSocketData({ led_pin: parseInt(ledpin) });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRadioChange(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log("Selected color order:", event.target.value);
|
||||||
|
// Add your specific logic here
|
||||||
|
if (event.target.value === "rgb") {
|
||||||
|
console.log("RGB order selected!");
|
||||||
|
} else if (event.target.value === "rbg") {
|
||||||
|
console.log("RBG order selected!");
|
||||||
|
}
|
||||||
|
sendWebSocketData({ color_order: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPatternButtons(patterns) {
|
||||||
|
const container = document.getElementById("pattern_buttons");
|
||||||
|
container.innerHTML = ""; // Clear previous buttons
|
||||||
|
|
||||||
|
patterns.forEach((pattern) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.textContent = pattern;
|
||||||
|
button.value = pattern;
|
||||||
|
button.addEventListener("click", async function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
await updatePattern(pattern);
|
||||||
|
});
|
||||||
|
container.appendChild(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async function () {
|
||||||
|
// Get the connection status element once the DOM is ready
|
||||||
|
connectionStatusElement = document.getElementById("connection-status");
|
||||||
|
|
||||||
|
// Establish WebSocket connection on page load
|
||||||
|
connectWebSocket();
|
||||||
|
|
||||||
|
document.getElementById("color").addEventListener("input", updateColor);
|
||||||
|
document.getElementById("color2").addEventListener("input", updateColor2);
|
||||||
|
document.getElementById("delay").addEventListener("input", updateDelay);
|
||||||
|
document
|
||||||
|
.getElementById("brightness")
|
||||||
|
.addEventListener("input", updateBrightness);
|
||||||
|
document
|
||||||
|
.getElementById("num_leds_form")
|
||||||
|
.addEventListener("submit", updateNumLeds);
|
||||||
|
document.getElementById("name_form").addEventListener("submit", updateName);
|
||||||
|
document.getElementById("id_form").addEventListener("submit", updateID);
|
||||||
|
document
|
||||||
|
.getElementById("led_pin_form")
|
||||||
|
.addEventListener("submit", updateLedPin);
|
||||||
|
document.getElementById("delay").addEventListener("touchend", updateDelay);
|
||||||
|
document
|
||||||
|
.getElementById("brightness")
|
||||||
|
.addEventListener("touchend", updateBrightness);
|
||||||
|
|
||||||
|
document.getElementById("rgb").addEventListener("change", handleRadioChange);
|
||||||
|
document.getElementById("rbg").addEventListener("change", handleRadioChange);
|
||||||
|
document.querySelectorAll(".pattern_button").forEach((button) => {
|
||||||
|
console.log(button.value);
|
||||||
|
button.addEventListener("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await updatePattern(button.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to toggle the display of the settings menu
|
||||||
|
function selectSettings() {
|
||||||
|
const settingsMenu = document.getElementById("settings_menu");
|
||||||
|
controls = document.getElementById("controls");
|
||||||
|
settingsMenu.style.display = "block";
|
||||||
|
controls.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectControls() {
|
||||||
|
const settingsMenu = document.getElementById("settings_menu");
|
||||||
|
controls = document.getElementById("controls");
|
||||||
|
settingsMenu.style.display = "none";
|
||||||
|
controls.style.display = "block";
|
||||||
|
}
|
||||||
124
src/templates/index.html
Normal file
124
src/templates/index.html
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{% args settings, patterns, mac %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{settings['name']}}</title>
|
||||||
|
<script src="static/main.js"></script>
|
||||||
|
<link rel="stylesheet" href="static/main.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{settings['name']}}</h1>
|
||||||
|
<button onclick="selectControls()">Controls</button>
|
||||||
|
<button onclick="selectSettings()">Settings</button>
|
||||||
|
|
||||||
|
<!-- Main LED Controls -->
|
||||||
|
<div id="controls">
|
||||||
|
<div id="pattern_buttons">
|
||||||
|
{% for p in patterns %}
|
||||||
|
<button class="pattern_button" value="{{p}}">{{p}}</button>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Pattern buttons will be inserted here -->
|
||||||
|
</div>
|
||||||
|
<form id="delay_form" method="post" action="/delay">
|
||||||
|
<label for="delay">Delay:</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="delay"
|
||||||
|
name="delay"
|
||||||
|
min="1"
|
||||||
|
max="1000"
|
||||||
|
value="{{settings['delay']}}"
|
||||||
|
step="10"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<form id="brightness_form" method="post" action="/brightness">
|
||||||
|
<label for="brightness">Brightness:</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="brightness"
|
||||||
|
name="brightness"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value="{{settings['brightness']}}"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<form id="color_form" method="post" action="/color">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
id="color"
|
||||||
|
name="color"
|
||||||
|
value="{{settings['color1']}}"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<form id="color2_form" method="post" action="/color2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
id="color2"
|
||||||
|
name="color2"
|
||||||
|
value="{{settings['color2']}}"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Menu for num_leds, Wi-Fi SSID, and Password -->
|
||||||
|
|
||||||
|
<div id="settings_menu" style="display: none">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
|
||||||
|
<form id="name_form" method="post" action="/name">
|
||||||
|
<label for="name">Name:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="num_leds"
|
||||||
|
value="{{settings['name']}}"
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Update Name" />
|
||||||
|
</form>
|
||||||
|
<form id="id_form" method="post" action="/id">
|
||||||
|
<label for="id">ID:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="id"
|
||||||
|
name="id"
|
||||||
|
value="{{settings['id']}}"
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Update ID" />
|
||||||
|
</form>
|
||||||
|
<!-- Separate form for submitting num_leds -->
|
||||||
|
<form id="num_leds_form" method="post" action="/num_leds">
|
||||||
|
<label for="num_leds">Number of LEDs:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="num_leds"
|
||||||
|
name="num_leds"
|
||||||
|
value="{{settings['num_leds']}}"
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Update Number of LEDs" />
|
||||||
|
</form>
|
||||||
|
<form id="led_pin_form" method="post" action="/led_pin">
|
||||||
|
<label for="num_leds">Led pin:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="led_pin"
|
||||||
|
name="led_pin"
|
||||||
|
value="{{settings['led_pin']}}"
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Update Led Pin" />
|
||||||
|
</form>
|
||||||
|
<form id="color_order_form">
|
||||||
|
<label for="rgb">RGB:</label>
|
||||||
|
<input type="radio" id="rgb" name="color_order" value="rgb" {{'checked' if settings["color_order"]=="rgb" else ''}} />
|
||||||
|
<label for="rbg">RBG</label>
|
||||||
|
<input type="radio" id="rbg" name="color_order" value="rbg" {{'checked' if settings["color_order"]=="rbg" else ''}}/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>Mac address: {{mac}}</p>
|
||||||
|
</div>
|
||||||
|
<div id="connection-status"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
94
src/templates/index_html.py
Normal file
94
src/templates/index_html.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Autogenerated file
|
||||||
|
def render(settings, patterns):
|
||||||
|
yield """<!DOCTYPE html>
|
||||||
|
<html lang=\"en\">
|
||||||
|
<head>
|
||||||
|
<meta charset=\"UTF-8\">
|
||||||
|
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
|
||||||
|
<title>LED Control</title>
|
||||||
|
<script src=\"static/main.js\"></script>
|
||||||
|
<link rel=\"stylesheet\" href=\"static/main.css\">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Control LEDs</h1>
|
||||||
|
<button onclick=\"selectControls()\">Controls</button>
|
||||||
|
<button onclick=\"selectSettings()\">Settings</button>
|
||||||
|
|
||||||
|
<!-- Main LED Controls -->
|
||||||
|
<div id=\"controls\">
|
||||||
|
<div id=\"pattern_buttons\">
|
||||||
|
"""
|
||||||
|
for p in patterns:
|
||||||
|
yield """ <button class=\"pattern_button\" value=\""""
|
||||||
|
yield str(p)
|
||||||
|
yield """\">"""
|
||||||
|
yield str(p)
|
||||||
|
yield """</button>
|
||||||
|
"""
|
||||||
|
yield """
|
||||||
|
<!-- Pattern buttons will be inserted here -->
|
||||||
|
</div>
|
||||||
|
<form id=\"delay_form\" method=\"post\" action=\"/delay\">
|
||||||
|
<label for=\"delay\">Delay:</label>
|
||||||
|
<input type=\"range\" id=\"delay\" name=\"delay\" min=\"1\" max=\"1000\" value=\""""
|
||||||
|
yield str(settings['delay'])
|
||||||
|
yield """\" step=\"10\">
|
||||||
|
</form>
|
||||||
|
<form id=\"brightness_form\" method=\"post\" action=\"/brightness\">
|
||||||
|
<label for=\"brightness\">Brightness:</label>
|
||||||
|
<input type=\"range\" id=\"brightness\" name=\"brightness\" min=\"0\" max=\"100\" value=\""""
|
||||||
|
yield str(settings['brightness'])
|
||||||
|
yield """\" step=\"1\">
|
||||||
|
</form>
|
||||||
|
<form id=\"color_form\" method=\"post\" action=\"/color\">
|
||||||
|
<input type=\"color\" id=\"color\" name=\"color\" value=\""""
|
||||||
|
yield str(settings['color1'])
|
||||||
|
yield """\">
|
||||||
|
</form>
|
||||||
|
<form id=\"color2_form\" method=\"post\" action=\"/color2\">
|
||||||
|
<input type=\"color\" id=\"color2\" name=\"color2\" value=\""""
|
||||||
|
yield str(settings['color2'])
|
||||||
|
yield """\">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Menu for num_leds, Wi-Fi SSID, and Password -->
|
||||||
|
|
||||||
|
<div id=\"settings_menu\" style=\"display: none;\">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
|
||||||
|
<!-- Separate form for submitting num_leds -->
|
||||||
|
<form id=\"num_leds_form\" method=\"post\" action=\"/num_leds\">
|
||||||
|
<label for=\"num_leds\">Number of LEDs:</label>
|
||||||
|
<input type=\"text\" id=\"num_leds\" name=\"num_leds\" value=\""""
|
||||||
|
yield str(settings['num_leds'])
|
||||||
|
yield """\">
|
||||||
|
<input type=\"submit\" value=\"Update Number of LEDs\">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Form for Wi-Fi SSID and password -->
|
||||||
|
<form id=\"wifi_form\" method=\"post\" action=\"/wifi_settings\">
|
||||||
|
<label for=\"ssid\">Wi-Fi SSID:</label>
|
||||||
|
<input type=\"text\" id=\"ssid\" name=\"ssid\" value=\""""
|
||||||
|
yield str(settings['wifi']['ssid'])
|
||||||
|
yield """\">
|
||||||
|
<br>
|
||||||
|
<label for=\"password\">Wi-Fi Password:</label>
|
||||||
|
<input type=\"password\" id=\"password\" name=\"password\">
|
||||||
|
<br>
|
||||||
|
<label for=\"ip\">Wi-Fi IP:</label>
|
||||||
|
<input type=\"ip\" id=\"ip\" name=\"ip\" value=\""""
|
||||||
|
yield str(settings.get('wifi', {}).get('ip', ''))
|
||||||
|
yield """\">
|
||||||
|
<br>
|
||||||
|
<label for=\"gateway\">Wi-Fi Gateway:</label>
|
||||||
|
<input type=\"gateway\" id=\"gateway\" name=\"gateway\" value=\""""
|
||||||
|
yield str(settings.get('wifi', {}).get('gateway', ''))
|
||||||
|
yield """\">
|
||||||
|
<br>
|
||||||
|
<input type=\"submit\" value=\"Save Wi-Fi Settings\">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
43
src/web (1).py
Normal file
43
src/web (1).py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from microdot import Microdot, send_file, Response
|
||||||
|
from microdot.utemplate import Template
|
||||||
|
from microdot.websocket import with_websocket
|
||||||
|
import machine
|
||||||
|
import wifi
|
||||||
|
import json
|
||||||
|
|
||||||
|
def web(settings, patterns):
|
||||||
|
app = Microdot()
|
||||||
|
Response.default_content_type = 'text/html'
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def index_hnadler(request):
|
||||||
|
mac = wifi.get_mac().hex()
|
||||||
|
return Template('/index.html').render(settings=settings, patterns=patterns.patterns.keys(), mac=mac)
|
||||||
|
|
||||||
|
@app.route("/static/<path:path>")
|
||||||
|
def static_handler(request, path):
|
||||||
|
if '..' in path:
|
||||||
|
# Directory traversal is not allowed
|
||||||
|
return 'Not found', 404
|
||||||
|
return send_file('static/' + path)
|
||||||
|
|
||||||
|
@app.post("/settings")
|
||||||
|
def settings_handler(request):
|
||||||
|
# Keep the POST handler for compatibility or alternative usage if needed
|
||||||
|
# For WebSocket updates, the /ws handler is now primary
|
||||||
|
return settings.set_settings(request.body.decode('utf-8'), patterns)
|
||||||
|
|
||||||
|
@app.route("/ws")
|
||||||
|
@with_websocket
|
||||||
|
async def ws(request, ws):
|
||||||
|
while True:
|
||||||
|
data = await ws.receive()
|
||||||
|
if data:
|
||||||
|
|
||||||
|
# Process the received data
|
||||||
|
_, status_code = settings.set_settings(json.loads(data), patterns, True)
|
||||||
|
#await ws.send(status_code)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return app
|
||||||
43
src/web.py
Normal file
43
src/web.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from microdot import Microdot, send_file, Response
|
||||||
|
from microdot.utemplate import Template
|
||||||
|
from microdot.websocket import with_websocket
|
||||||
|
import machine
|
||||||
|
import wifi
|
||||||
|
import json
|
||||||
|
|
||||||
|
def web(settings, patterns):
|
||||||
|
app = Microdot()
|
||||||
|
Response.default_content_type = 'text/html'
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def index_hnadler(request):
|
||||||
|
mac = wifi.get_mac().hex()
|
||||||
|
return Template('/index.html').render(settings=settings, patterns=patterns.patterns.keys(), mac=mac)
|
||||||
|
|
||||||
|
@app.route("/static/<path:path>")
|
||||||
|
def static_handler(request, path):
|
||||||
|
if '..' in path:
|
||||||
|
# Directory traversal is not allowed
|
||||||
|
return 'Not found', 404
|
||||||
|
return send_file('static/' + path)
|
||||||
|
|
||||||
|
@app.post("/settings")
|
||||||
|
def settings_handler(request):
|
||||||
|
# Keep the POST handler for compatibility or alternative usage if needed
|
||||||
|
# For WebSocket updates, the /ws handler is now primary
|
||||||
|
return settings.set_settings(request.body.decode('utf-8'), patterns)
|
||||||
|
|
||||||
|
@app.route("/ws")
|
||||||
|
@with_websocket
|
||||||
|
async def ws(request, ws):
|
||||||
|
while True:
|
||||||
|
data = await ws.receive()
|
||||||
|
if data:
|
||||||
|
|
||||||
|
# Process the received data
|
||||||
|
_, status_code = settings.set_settings(json.loads(data), patterns, True)
|
||||||
|
#await ws.send(status_code)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return app
|
||||||
39
src/wifi.py
Normal file
39
src/wifi.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import network
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
def connect(ssid, password, ip, gateway):
|
||||||
|
|
||||||
|
try:
|
||||||
|
sta_if = network.WLAN(network.STA_IF)
|
||||||
|
if not sta_if.isconnected():
|
||||||
|
if ssid == "" or password == "":
|
||||||
|
print("Missing ssid or password")
|
||||||
|
return None
|
||||||
|
if ip != "" and gateway != "":
|
||||||
|
sta_if.ifconfig((ip, '255.255.255.0', gateway, '1.1.1.1'))
|
||||||
|
print('connecting to network...')
|
||||||
|
sta_if.active(True)
|
||||||
|
sta_if.connect(ssid, password)
|
||||||
|
sleep(0.1)
|
||||||
|
if sta_if.isconnected():
|
||||||
|
return sta_if.ifconfig()
|
||||||
|
return None
|
||||||
|
return sta_if.ifconfig()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to wifi {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ap(ssid, password):
|
||||||
|
ap_if = network.WLAN(network.AP_IF)
|
||||||
|
ap_mac = ap_if.config('mac')
|
||||||
|
print(ssid)
|
||||||
|
ap_if.active(True)
|
||||||
|
ap_if.config(essid=ssid, password=password)
|
||||||
|
ap_if.active(False)
|
||||||
|
ap_if.active(True)
|
||||||
|
print(ap_if.ifconfig())
|
||||||
|
|
||||||
|
def get_mac():
|
||||||
|
ap_if = network.WLAN(network.AP_IF)
|
||||||
|
return ap_if.config('mac')
|
||||||
@@ -28,13 +28,9 @@ class WS2812B:
|
|||||||
self.brightness = brightness
|
self.brightness = brightness
|
||||||
self.invert = invert
|
self.invert = invert
|
||||||
self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3)
|
self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3)
|
||||||
self.dma.start_transfer(self.ar)
|
|
||||||
|
|
||||||
|
def show(self):
|
||||||
def show(self, array=None, offset=0):
|
self.pio_dma.start_transfer(self.ar)
|
||||||
if array is None:
|
|
||||||
array = self.ar
|
|
||||||
self.pio_dma.start_transfer(array, offset)
|
|
||||||
|
|
||||||
def set(self, i, color):
|
def set(self, i, color):
|
||||||
self.ar[i*3] = int(color[1]*self.brightness)
|
self.ar[i*3] = int(color[1]*self.brightness)
|
||||||
@@ -59,7 +55,7 @@ class WS2812B:
|
|||||||
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)
|
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
num_leds, pin, sm, brightness = 293, 2, 0, 0.1
|
num_leds, pin, sm, brightness = 10, 0, 0, 1
|
||||||
ws0 = WS2812B(num_leds, pin, sm, brightness)
|
ws0 = WS2812B(num_leds, pin, sm, brightness)
|
||||||
while True:
|
while True:
|
||||||
for color in ws0.COLORS:
|
for color in ws0.COLORS:
|
||||||
Reference in New Issue
Block a user