63 Commits

Author SHA1 Message Date
ae407ab3aa receiver: no change to protocol; revert temporary color-index handling; radiate uses ticks_us timing; development: ensure dev.py upload workflow retained 2025-10-04 01:10:46 +13:00
e516b49eb8 Add segmented_movement pattern with alternating forward/backward movement
- Add n4 parameter support to main.py ESP NOW receiver
- Implement segmented_movement pattern with configurable parameters:
  * n1: segment length (number of LEDs per segment)
  * n2: spacing between segments
  * n3: forward movement speed (positions per beat)
  * n4: backward movement speed (positions per beat)
- Pattern alternates between forward and backward movement each beat
- If only n3 or n4 is set, moves in that direction every beat
- Draws repeating segments with spacing across entire LED strip
- Add Pipfile script to run dev.py directly with arguments
2025-10-03 19:56:24 +13:00
355d113e32 Fix rainbow pattern synchronization in LED bar
- Use controller's step for synchronization instead of internal step counter
- Rainbow pattern now syncs with controller timing like n_chase pattern
- Prevents rainbow from running independently and out of sync
- Uses beat_index % 256 for full color wheel cycling
2025-09-19 01:29:48 +12:00
d715af4344 Fix n_chase pattern to properly chase through all LED positions
- Replace oscillating behavior with proper chasing movement
- Use pattern_step for internal tracking instead of controller's step
- Calculate position relative to chase head: (i - pattern_step) % num_leds
- Chase head moves through all LED positions with n3 step multiplier
- n1 controls width of lit chase segment
2025-09-19 00:22:25 +12:00
67c4a1a6f6 Update LED bar to handle message type field
- Process 't' field to distinguish between beat ('b') and update ('u') messages
- Beat messages: execute pattern immediately using current parameters
- Update messages: only update parameters, don't execute pattern
- Maintains backward compatibility with default to beat if 't' not specified
- Enables proper synchronization between controller and bars
2025-09-18 22:10:23 +12:00
748ad4b507 Add n3 step rate functionality to patterns 2025-09-18 20:35:21 +12:00
1275d60aaa Make alternating pattern timing independent of n1
- Changed alternating pattern to return delay/2 instead of delay
- Each phase now lasts delay/2, making full cycle equal to delay
- n1 now only controls ON/OFF segment width, not timing
2025-09-18 19:11:35 +12:00
d8e853183b main: enforce event-driven behavior; run selected pattern once per message; clarify comments; fix pattern lookup 2025-09-17 20:20:41 +12:00
8cfb3e156b patterns: add rainbow, specto, and radiate (out then dark-out)
radiate: origins every n1, step by delay, stop when full, dark wave outward, ensure strip off at end, run once

alternating: use n1 as ON width and n2 as OFF width; phase via self.step

pulse: attack (n1), hold (delay), decay (n2); stop at end

tests: add specto sweep (n1_sequence) and radiate demo; include n index per message; use nested {name:{...}} schema; support iterations/repeat-delay
2025-09-16 22:28:51 +12:00
d599af271b patterns: alternating uses n1 (on) and n2 (off); ensure visible ON color; return delay; phase via self.step
test: WS client sends nested {name:{...}}; add iterations and repeat-delay; include n per message; use n1/n2 for alternating
2025-09-16 21:22:47 +12:00
93560a253e patterns: fix blink timing; slow alternating; unify self-test with absolute tick scheduling 2025-09-15 14:12:43 +12:00
d68817ea18 Pipfile.lock: update lockfile 2025-09-15 12:58:51 +12:00
a7a2274a59 Pipfile: sync dependencies 2025-09-15 12:58:45 +12:00
df838dc4d6 settings: adjust defaults and color order handling 2025-09-15 12:58:39 +12:00
4ec48b9f8f main: update loop/test harness configuration 2025-09-15 12:58:30 +12:00
1456ed8a6e boot: minor adjustments 2025-09-15 12:58:20 +12:00
80d5a66fab patterns: centralize timing in tick(); remove selected-delay coupling; update self-test to use per-config durations 2025-09-15 12:56:57 +12:00
44cb35d1aa Split into pattern and low level methods 2025-09-05 23:29:18 +12:00
fc080f7796 Add watchfiles 2025-08-26 22:53:48 +12:00
70fe5a0cdc Add watchfiles 2025-08-11 22:15:21 +12:00
2a7b5527a5 Move gc and wdt to function 2025-08-03 19:39:25 +12:00
50545e3170 Remove random patterns 2025-08-03 19:29:10 +12:00
d2826a0f63 Swtich to names isntead of ids 2025-07-12 10:22:17 +12:00
87fc74bb51 Add flicker pattern 2025-07-12 10:21:43 +12:00
03f3f02da8 Remove wifi client 2025-06-19 19:13:13 +12:00
524db5e979 Move espnow to seperate file 2025-06-19 19:05:08 +12:00
279416cded Add set_pattern_step 2025-06-19 19:03:22 +12:00
fbd14f2e16 If no ids run set_settings 2025-06-12 21:32:24 +12:00
1989f6f5c9 Switch to list for colors 2025-06-12 21:29:31 +12:00
a19b1e86f2 Have to save when using espnow 2025-06-08 13:18:28 +12:00
c63e907204 espnow if id is 0 call set_settings 2025-06-04 21:07:15 +12:00
b7920e224f Add color order 2025-06-04 21:02:55 +12:00
42e92dafc8 Add led pin setting 2025-06-04 20:03:51 +12:00
0b6eb9724f Add device ID 2025-06-04 19:54:06 +12:00
55ef5c1580 Move json load out of set_settings 2025-06-04 19:22:09 +12:00
c15f9787a7 Sync after going through all the keys 2025-06-02 00:32:43 +12:00
3d0078f118 Update leds straight away after a sync 2025-06-02 00:18:25 +12:00
9e72dba035 Check all keys before returning 2025-06-02 00:16:58 +12:00
3d7dd754eb Wifi and ESPNOW don't work at the same time 2025-06-02 00:15:31 +12:00
2dd20fa51b Enable garabage collection 2025-05-28 21:19:56 +12:00
d33bd6b0e4 Enable watchdog timer 2025-05-28 21:17:23 +12:00
8902adf18c Fix color transition 2025-05-24 13:09:28 +12:00
9abd425f46 Add wifi 2025-05-22 22:02:49 +12:00
ee28b5805d Change title and h1 to "name" 2025-05-22 22:02:29 +12:00
ec29dbdd01 Add color order 2025-05-19 22:00:35 +12:00
3fa9377438 Add set_settings to Settings class 2025-05-19 21:59:43 +12:00
ec049b52c0 Only check wifi settings if not connected 2025-05-19 19:35:29 +12:00
a009ea85bc Add wifi settings 2025-05-19 19:32:53 +12:00
bd2e6e56cf Check if ssid, password, ip and gateway are "" 2025-05-19 19:31:30 +12:00
37c7280a15 Get ap password from settings 2025-05-19 19:28:11 +12:00
2f10d4cabd Fix rgb order 2025-05-19 19:24:17 +12:00
385dcffe68 Add led pin in settings 2025-05-19 19:22:38 +12:00
fa0578349b Delete index_html.py 2025-05-18 21:28:44 +12:00
4a36ff0da0 Update main.py 2025-05-18 21:28:41 +12:00
bd4046572c Update main.css 2025-05-18 21:28:38 +12:00
fdd299b063 Update main.js 2025-05-18 21:28:34 +12:00
a44ef2d0ad Update index.html 2025-05-18 21:28:29 +12:00
2d1208e223 Update web.py 2025-05-18 21:28:21 +12:00
67279a8f46 Update wifi.py 2025-05-18 21:28:14 +12:00
a52ac3df99 Update Pipfile.lock 2025-05-18 21:27:58 +12:00
c4356cf354 Switch to web socket 2025-05-18 21:26:33 +12:00
0c219e0697 Add websocket settings endpoint 2025-05-12 22:22:05 +12:00
cee8c20176 Always sync 2025-05-12 22:21:11 +12:00
18 changed files with 2140 additions and 825 deletions

71
8_BAR_SETUP.md Normal file
View File

@@ -0,0 +1,71 @@
# 8-LED Bar System Setup
This system supports 8 LED bars working together, each with unique names "100" through "107".
## Quick Setup
### 1. Configure Each LED Bar
Each LED bar needs a unique name. Run the configuration script on each bar:
```bash
python configure_bar.py
```
Then enter the bar name (100, 101, 102, etc.) when prompted.
### 2. Update Bar Names (Optional)
To change the bar names, edit `/home/jimmy/projects/lighting-controller/src/bar_config.py`:
```python
LED_BAR_NAMES = [
"100", # Bar 1
"101", # Bar 2
"102", # Bar 3
"103", # Bar 4
"104", # Bar 5
"105", # Bar 6
"106", # Bar 7
"107", # Bar 8
]
```
### 3. Default Settings
All bars use the same default settings defined in `bar_config.py`:
```python
DEFAULT_BAR_SETTINGS = {
"pattern": "pulse",
"delay": 100,
"colors": [(0, 255, 0)], # Default green
"brightness": 100,
"num_leds": 200,
"n1": 10,
"n2": 10,
"n3": 1,
"n": 0,
}
```
## How It Works
1. **Lighting Controller** sends ESP-NOW messages to all bars simultaneously
2. **Each LED Bar** listens for messages addressed to its unique name
3. **All bars** receive the same pattern/color/brightness settings
4. **Synchronized effects** across all 8 bars
## Current Features
- ✅ All bars show the same pattern simultaneously
- ✅ Individual bar addressing (100-107)
- ✅ Optimized JSON payloads with defaults deduplication
- ✅ Easy configuration via `bar_config.py`
- ✅ MIDI control for all bars
- ✅ n3 step rate functionality
## Future Enhancements
- Sequential patterns (bar 1 → bar 2 → bar 3...)
- Wave effects across bars
- Individual bar control
- Master/slave synchronization
- Physical arrangement awareness

View File

@@ -7,8 +7,13 @@ name = "pypi"
mpremote = "*" mpremote = "*"
pyserial = "*" pyserial = "*"
esptool = "*" esptool = "*"
watchfiles = "*"
uvicorn = "*"
[dev-packages] [dev-packages]
[requires] [requires]
python_version = "3.12" python_version = "3.12"
[scripts]
dev = "./dev.py"

748
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "8b14bb293b7e7117ffc89c2bc92d7aa2290e8f68be7fc0f073f2b3f7f959ef71" "sha256": "53809b70ded7a2b3e577a8a4263fbadbb722d1e8d92eb016e134b0776fd40f6b"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -16,285 +16,325 @@
] ]
}, },
"default": { "default": {
"argcomplete": { "anyio": {
"hashes": [ "hashes": [
"sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61", "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6",
"sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392" "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"
], ],
"markers": "sys_platform != 'win32'", "markers": "python_version >= '3.9'",
"version": "==3.5.3" "version": "==4.10.0"
}, },
"bitarray": { "bitarray": {
"hashes": [ "hashes": [
"sha256:000df24c183011b5d27c23d79970f49b6762e5bb5aacd25da9c3e9695c693222", "sha256:002b73bf4a9f7b3ecb02260bd4dd332a6ee4d7f74ee9779a1ef342a36244d0cf",
"sha256:0027b8f3bb2bba914c79115e96a59b9924aafa1a578223a7c4f0a7242d349842", "sha256:01e3ba46c2dee6d47a4ab22561a01d8ee6772f681defc9fcb357097a055e48cf",
"sha256:00f9a88c56e373009ac3c73c55205cfbd9683fbd247e2f9a64bae3da78795252", "sha256:03dc877ec286b7f2813185ea6bc5f1f5527fd859e61038d38768883b134e06b3",
"sha256:041c889e69c847b8a96346650e50f728b747ae176889199c49a3f31ae1de0e23", "sha256:03eeab48f376c3cd988add2b75c20d2d084b6fcc9a164adb0dc390ef152255b4",
"sha256:0879f839ec8f079fa60c3255966c2e1aa7196699a234d4e5b7898fbc321901b5", "sha256:05ee46a734b5110c5ac483815da4379f7622f4316362872ec7c0ed16db4b0148",
"sha256:0b555006a7dea53f6bebc616a4d0249cecbf8f1fadf77860120a2e5dbdc2f167", "sha256:0751596f60f33df66245b2dafa3f7fbe13cb7ac91dd14ead87d8c2eec57cb3ed",
"sha256:0b655c3110e315219e266b2732609fddb0857bc69593de29f3c2ba74b7d3f51a", "sha256:08c114cf02a63e13ce6d70bc5b9e7bdcfa8d5db17cece207cfa085c4bc4a7a0c",
"sha256:0cecaf2981c9cd2054547f651537b4f4939f9fe225d3fc2b77324b597c124e40", "sha256:0ed4a87eda16e2f95d536152c5acccae07841fbdda3b9a752f3dbf43e39f4d6b",
"sha256:0e104f9399144fab6a892d379ba1bb4275e56272eb465059beef52a77b4e5ce6", "sha256:101230b8074919970433ef79866570989157ade3421246d4c3afb7a994fdc614",
"sha256:0ef5c787c8263c082a73219a69eb60a500e157a4ac69d1b8515ad836b0e71fb4", "sha256:11fcfdf272549a3d876f10d8422bcd5f675750aa746ce04ff04937ec3bb2329e",
"sha256:12f19ede03e685c5c588ab5ed63167999295ffab5e1126c5fe97d12c0718c18f", "sha256:160f449bb91686f8fc9984200e78b8d793b79e382decf7eb1dc9948d7c21b36f",
"sha256:1414a7102a3c4986f241480544f5c99f5d32258fb9b85c9c04e84e48c490ab35", "sha256:16426a843b1bc9c552a7c97d6d7555e69730c2de1e2f560503d3fc0e7f6d8005",
"sha256:147542299f458bdb177f798726e5f7d39ab8491de4182c3c6d9885ed275a3c2b", "sha256:1f1575cc0f66aa70a0bb5cb57c8d9d1b7d541d920455169c6266919bf804dc20",
"sha256:150b7b29c36d9f1a24779aea723fdfc73d1c1c161dc0ea14990da27d4e947092", "sha256:1f7a8fc5085450635a539c47c9fce6d441b4a973686f88fc220aa20e3921fe55",
"sha256:153d7c416a70951dcfa73487af05d2f49c632e95602f1620cd9a651fa2033695", "sha256:1fb0a46ae4b8d244a3fb80c3055717baa3dec6be17938e6871042a8d5b4ce670",
"sha256:184972c96e1c7e691be60c3792ca1a51dd22b7f25d96ebea502fe3c9b554f25d", "sha256:2965fd8ba31b04c42e4b696fad509dc5ab50663efca6eb06bb3b6d08587f3a09",
"sha256:18abdce7ab5d2104437c39670821cba0b32fdb9b2da9e6d17a4ff295362bd9dc", "sha256:2b524306104c1296f1e91d74ee4ccbeeea621f6a13e44addf0bb630a1839fd72",
"sha256:2055206ed653bee0b56628f6a4d248d53e5660228d355bbec0014bdfa27050ae", "sha256:2db04b165a57499fbcfe0eaa2f7752f118552bbcfab2163a43fef8d95f4ae745",
"sha256:20f30373f0af9cb583e4122348cefde93c82865dbcbccc4997108b3d575ece84", "sha256:3092f6bbf4a75b1e6f14a5b1030e27c435f341afeb23987115e45a25cc68ba91",
"sha256:22b00f65193fafb13aa644e16012c8b49e7d5cbb6bb72825105ff89aadaa01e3", "sha256:30a2fc37698820cbf9b51d5f801219ef4bed828a04f3307072b8f983dc422a0e",
"sha256:251cd5bd47f542893b2b61860eded54f34920ea47fd5bff038d85e7a2f7ae99b", "sha256:3110b98c5dfb31dc1cf82d8b0c32e3fa6d6d0b268ff9f2a1599165770c1af80f",
"sha256:2855cc01ee370f7e6e3ec97eebe44b1453c83fb35080313145e2c8c3c5243afb", "sha256:33f604bffd06b170637f8a48ddcf42074ed1e1980366ac46058e065ce04bfe2a",
"sha256:2ac67b658fa5426503e9581a3fb44a26a3b346c1abd17105735f07db572195b3", "sha256:340c524c7c934b61d1985d805bffe7609180fb5d16ece6ce89b51aa535b936f2",
"sha256:2d9fe3ee51afeb909b68f97e14c6539ace3f4faa99b21012e610bbe7315c388d", "sha256:37a6a8382864a1defb5b370b66a635e04358c7334054457bbbb8645610cd95b2",
"sha256:2da91ab3633c66999c2a352f0ca9ae064f553e5fc0eca231d28e7e305b83e942", "sha256:3875578748b484638f6ea776f534e9088cfb15eee131aac051036cba40fd5d05",
"sha256:2dad7ba2af80f9ec1dd988c3aca7992408ec0d0b4c215b65d353d95ab0070b10", "sha256:38b0261483c59bb39ae9300ad46bf0bbf431ab604266382d986a349c96171b36",
"sha256:34fc13da3518f14825b239374734fce93c1a9299ed7b558c3ec1d659ec7e4c70", "sha256:3b9a2eb7d2e0e9c2f25256d2663c0a2a4798fe3110e3ddbbb1a7b71740b4de08",
"sha256:369b6d457af94af901d632c7e625ca6caf0a7484110fc91c6290ce26bc4f1478", "sha256:3bb3cf22c3c03ae698647e6766314149c9cf04aa2018d9f48d5efddc3ced2764",
"sha256:37be5482b9df3105bad00fdf7dc65244e449b130867c3879c9db1db7d72e508b", "sha256:3db0648536f3e08afa7ceb928153c39913f98fd50a5c3adf92a4d0d4268f213e",
"sha256:3963b80a68aedcd722a9978d261ae53cb9bb6a8129cc29790f0f10ce5aca287a", "sha256:3dc654da62b3a3027b7c922f7e9f4b27feaabd5d38b2a98ea98de5e8107c72f2",
"sha256:39b38a3d45dac39d528c87b700b81dfd5e8dc8e9e1a102503336310ef837c3fd", "sha256:4079857566077f290d35e23ff0e8ba593069c139ae85b0d152b9fa476494f50a",
"sha256:3cd565253889940b4ec4768d24f101d9fe111cad4606fdb203ea16f9797cf9ed", "sha256:44f468fb4857fff86c65bec5e2fb67067789e40dad69258e9bb78fc6a6df49e7",
"sha256:3d47bc4ff9b0e1624d613563c6fa7b80aebe7863c56c3df5ab238bb7134e8755", "sha256:45660e2fabcdc1bab9699a468b312f47956300d41d6a2ea91c8f067572aaf38a",
"sha256:3fa5d8e4b28388b337face6ce4029be73585651a44866901513df44be9a491ab", "sha256:477b9456eb7d70f385dc8f097a1d66ee40771b62e47b3b3e33406dcfbc1c6a3b",
"sha256:42bf1b222c698b467097f58b9f59dc850dfa694dde4e08237407a6a103757aa3", "sha256:481239cd0966f965c2b8fa78b88614be5f12a64e7773bb5feecc567d39bb2dd5",
"sha256:43b6c7c4f4a7b80e86e24a76f4c6b9b67d03229ea16d7d403520616535c32196", "sha256:4a83d247420b147d4b3cba0335e484365e117dc1cfe5ab35acd6a0817ad9244f",
"sha256:44c3e78b60070389b824d5a654afa1c893df723153c81904088d4922c3cfb6ac", "sha256:53d2abeabb91a822e9d76420c9b44980edd2d6b21767c7bb9cb2b1b4cf091049",
"sha256:4683bff52f5a0fd523fb5d3138161ef87611e63968e1fcb6cf4b0c6a86970fe0", "sha256:55c31bc3d2c9e48741c812ee5ce4607c6f33e33f339831c214d923ffc7777d21",
"sha256:47ccf9887bd595d4a0536f2310f0dcf89e17ab83b8befa7dc8727b8017120fda", "sha256:567d6891cb1ddbfd0051fcff3cb1bb86efc82ec818d9c5f98c37d59c1d23cc96",
"sha256:4800c91a14656789d2e67d9513359e23e8a534c8ee1482bb9b517a4cfc845200", "sha256:57b9df5d38ab49c13eaa9e0152fdfa8501fc23987f6dcf421b73484bfe573918",
"sha256:4817d73d995bd2b977d9cde6050be8d407791cf1f84c8047fa0bea88c1b815bc", "sha256:59ddb8a9f47ec807009c69e582d0de1c86c005f9f614557f4cebc7b8ac9b7d28",
"sha256:4839d3b64af51e4b8bb4a602563b98b9faeb34fd6c00ed23d7834e40a9d080fc", "sha256:61b9f3cf3a55322baed8f0532b73bce77d688a01446c179392c4056ab74eb551",
"sha256:4ac2027ca650a7302864ed2528220d6cc6921501b383e9917afc7a2424a1e36d", "sha256:639389b023315596e0293f85999645f47ec3dc28c892e51242dde6176c91486b",
"sha256:4cb5702dd667f4bb10fed056ffdc4ddaae8193a52cd74cb2cdb54e71f4ef2dd1", "sha256:64d1143e90299ba8c967324840912a63a903494b1870a52f6675bda53dc332f7",
"sha256:53e002ac1073ac70e323a7a4bfa9ab95e7e1a85c79160799e265563f342b1557", "sha256:6542e1cfe060badd160cd383ad93a84871595c14bb05fb8129f963248affd946",
"sha256:545d36332de81e4742a845a80df89530ff193213a50b4cbef937ed5a44c0e5e5", "sha256:69687ef16d501c9217675af36fa3c68c009c03e184b07d22ba245e5c01d47e6b",
"sha256:572a61fba7e3a710a8324771322fba8488d134034d349dcd036a7aef74723a80", "sha256:6f7e1cdf0abb11718e655bb258920453b1e89c2315e9019f60f0775704b12a8c",
"sha256:57d5ef854f8ec434f2ffd9ddcefc25a10848393fe2976e2be2c8c773cf5fef42", "sha256:7378055c9f456c5bb034ac313d9a9028fc6597619a0b16584099adba5a589fdb",
"sha256:5ddbf71a97ad1d6252e6e93d2d703b624d0a5b77c153b12f9ea87d83e1250e0c", "sha256:78103afbd0a94ac4c1f0b4014545fd149b968d5ea423aaa3b1f6e2c3fc19423e",
"sha256:5fa4b4d9fa90124b33b251ef74e44e737021f253dc7a9174e1b39f097451f7ca", "sha256:79038bf1a7b13d243e51f4b6909c6997c2ba2bffc45bcae264704308a2d17198",
"sha256:628f93e9c2c23930bd1cfe21c634d6c84ec30f45f23e69aefe1fcd262186d7bb", "sha256:795b1760418ab750826420ae24f06f392c08e21dc234f0a369a69cc00444f8ec",
"sha256:648e7ce794928e8d11343b5da8ecc5b910af75a82ea1a4264d5d0a55c3785faa", "sha256:7998dfb1e9e0255fb8553abb019c3e7f558925de4edc8604243775ff9dd3898d",
"sha256:656db7bdf1d81ec3b57b3cad7ec7276765964bcfd0eb81c5d1331f385298169c", "sha256:7afc740ad45ee0e0cef055765faf64789c2c183eb4aa3ecb8cecdb4b607396b3",
"sha256:666e44b0458bb2894b64264a29f2cc7b5b2cbcc4c5e9cedfe1fdbde37a8e329a", "sha256:7b4a41dc183d7d16750634f65566205990f94144755a39f33da44c0350c3e1a8",
"sha256:66a33a537e781eac3a352397ce6b07eedf3a8380ef4a804f8844f3f45e335544", "sha256:7f825ebedcad87a2825ddb6cf62f6d7d5b7a56ddaf7c93eef4b974e7ddc16408",
"sha256:66d6134b7bb737b88f1d16478ad0927c571387f6054f4afa5557825a4c1b78e2", "sha256:7f9f9bb2c5cc1f679605ebbeb72f46fc395d850b93fa7de7addd502a1dc66e99",
"sha256:67a0b56dd02f2713f6f52cacb3f251afd67c94c5f0748026d307d87a81a8e15c", "sha256:7fdf059d4e3acec44f512ebe247718ae511fde632e2b06992022df8e637385a6",
"sha256:6c33129b49196aa7965ac0f16fcde7b6ad8614b606caf01669a0277cef1afe1d", "sha256:81e4648c09103bc18f488957c1e0863d2397bab6625c0e6771891f151ee0bd96",
"sha256:6d2a2ce73f9897268f58857ad6893a1a6680c5a6b28f79d21c7d33285a5ae646", "sha256:8489bff00a1f81ac0754355772e76775878c32a42f16f01d427c3645546761c4",
"sha256:71ad0139c95c9acf4fb62e203b428f9906157b15eecf3f30dc10b55919225896", "sha256:851398428f5604c53371b72c5e0a28163274264ada4a08cd1eafe65fde1f68d0",
"sha256:7814c9924a0b30ecd401f02f082d8697fc5a5be3f8d407efa6e34531ff3c306a", "sha256:87a29b8a4cc72af6118954592dcd4e49223420470ccc3f8091c255f6c7330bb1",
"sha256:787db8da5e9e29be712f7a6bce153c7bc8697ccc2c38633e347bb9c82475d5c9", "sha256:8b8e07374d60040b24d1a158895d9758424db13be63d4b2fe1870e37f9dec009",
"sha256:7cb885c043000924554fe2124d13084c8fdae03aec52c4086915cd4cb87fe8be", "sha256:8d4aa56782368269eb9402caf7378b2a5ada6f05eb9c7edc2362be258973fd7e",
"sha256:7cd021ada988e73d649289cee00428b75564c46d55fbdcb0e3402e504b0ae5ea", "sha256:97c448a20aded59727261468873d9b11dfdcce5a6338a359135667d5e3f1d070",
"sha256:7e51e7f8289bf6bb631e1ef2a8f5e9ca287985ff518fe666abbdfdb6a848cb26", "sha256:98373c273e01a5a7c17103ecb617de7c9980b7608351d58c72198e3525f0002e",
"sha256:7e9eee03f187cef1e54a4545124109ee0afc84398628b4b32ebb4852b4a66393", "sha256:98e4a17f55f3cbf6fe06cc79234269572f234467c8355b6758eb252073f78e6b",
"sha256:7edb83089acbf2c86c8002b96599071931dc4ea5e1513e08306f6f7df879a48b", "sha256:99124e39658b2f72d296819ec03418609dd4f1b275b00289c2f278a19da6f9c0",
"sha256:7f1c24be7519f16a47b7e2ad1a1ef73023d34d8cbe1a3a59b185fc14baabb132", "sha256:9ad0df7886cb9d6d2ff75e87d323108a0e32bdca5c9918071681864129ce8ea8",
"sha256:8330912be6cb8e2fbfe8eb69f82dee139d605730cadf8d50882103af9ac83bb4", "sha256:9bfdfe2e2af434d3f4e47250f693657334e34a7ec557cd703b129a814422b4b8",
"sha256:8a9eb510cde3fa78c2e302bece510bf5ed494ec40e6b082dec753d6e22d5d1b1", "sha256:9faa4c6fcb19a31240ad389426699a99df481b6576f7286471e24efbf1b44dfc",
"sha256:8c9733d2ff9b7838ac04bf1048baea153174753e6a47312be14c83c6a395424b", "sha256:a048e41e1cb0c1a37021269d02698e30d2a7cc9a0205dd3390e0807745b76dae",
"sha256:904c1d5e3bd24f0c0d37a582d2461312033c91436a6a4f3bdeeceb4bea4a899d", "sha256:a05982bb49c73463cb0f0f4bed2d8da82631708a2c2d1926107ba99651b419ec",
"sha256:928b8b6dfcd015e1a81334cfdac02815da2a2407854492a80cf8a3a922b04052", "sha256:a23b5f13f9b292004e94b0b13fead4dae79c7512db04dc817ff2c2478298e04a",
"sha256:9502c2230d59a4ace2fddfd770dad8e8b414cbd99517e7e56c55c20997c28b8d", "sha256:a393b0f881eff94440f72846a6f0f95b983594a0a50af81c41ed18107420d6a7",
"sha256:96cf0898f8060b2d3ae491762ae871b071212ded97ff9e1e3a5229e9fefe544c", "sha256:a3b6bd81c77d9925809b714980cd30b1831a86bd090316d37cab124d92af1daf",
"sha256:98a4070ddafabddaee70b2aa7cc6286cf73c37984169ab03af1782da2351059a", "sha256:a43f4631ecb87bedc510568fef67db53f2a20c4a5953a9d1e07457e7b1d14911",
"sha256:9929051feeaf8d948cc0b1c9ce57748079a941a1a15c89f6014edf18adaade84", "sha256:a569c993942ac26c6c590639ed6712c6c9c3f0c8d287a067bf2a60eb615f3c6b",
"sha256:996d1b83eb904589f40974538223eaed1ab0f62be8a5105c280b9bd849e685c4", "sha256:a5b89349f05431270d1ccc7321aaab91c42ff33f463868779e502438b7f0e668",
"sha256:9c6e52005e91803eb4e08c0a08a481fb55ddce97f926bae1f6fa61b3396b5b61", "sha256:ac39319e6322c2c093a660c02cea6bb3b1ae53d049b573d4781df8896e443e04",
"sha256:9e3727ab63dfb6bde00b281934e2212bb7529ea3006c0031a556a84d2268bea5", "sha256:acc56700963f63307ac096689d4547e8061028a66bb78b90e42c5da2898898fb",
"sha256:a0255bd05ec7165e512c115423a5255a3f301417973d20a80fc5bfc3f3640bcb", "sha256:b723f9d10f7d8259f010b87fa66e924bb4d67927d9dcff4526a755e9ee84fef4",
"sha256:a2083dc20f0d828a7cdf7a16b20dae56aab0f43dc4f347a3b3039f6577992b03", "sha256:b99a0347bc6131046c19e056a113daa34d7df99f1f45510161bc78bc8461a470",
"sha256:a3c36b2fcfebe15ad1c10a90c1d52a42bebe960adcbce340fef867203028fbe7", "sha256:bc0880011b86f81c5353ce4abaeb2472d942ba2320985166a2a3dd4f783563a9",
"sha256:a4f49ac31734fe654a68e2515c0da7f5bbdf2d52755ba09a42ac406f1f08c9d0", "sha256:be2f40045432e8aa33d9fd5cb43c91b0c61d77d3d8810f88e84e2e46411c27a7",
"sha256:a667ea05ba1ea81b722682276dbef1d36990f8908cf51e570099fd505a89f931", "sha256:bebb17125373c499beea009cc5bced757bde52bcb3fa1d6335650e6c2d8111d7",
"sha256:a754c1464e7b946b1cac7300c582c6fba7d66e535cd1dab76d998ad285ac5a37", "sha256:befac6644c6f304a1b6a7948a04095682849c426cebcc44cb2459aa92d3e1735",
"sha256:a817ad70c1aff217530576b4f037dd9b539eb2926603354fcac605d824082ad1", "sha256:c1f4880bcb6fb7a8e2ab89128032b3dcf59e1e877ff4493b11c8bf7c3a5b3df2",
"sha256:aa54c7e1da8cf4be0aab941ea284ec64033ede5d6de3fd47d75e77cafe986e9d", "sha256:c3e014f7295b9327fa6f0b3e55a3fd485abac98be145b9597e0cdbb05c44ad07",
"sha256:ab37da66a8736ad5a75a58034180e92c41e864da0152b84e71fcc253a2f69cd4", "sha256:c427dfcce13a8c814556dfe7c110b8ef61b8fab5fca0d856d4890856807321dc",
"sha256:ac06dd72ee1e1b6e312504d06f75220b5894af1fb58f0c20643698f5122aea76", "sha256:c44cf0059633470c6bb415091def546adbeb5dcfa91cc3fcb1ac16593f14e52a",
"sha256:aca0a9cd376beaccd9f504961de83e776dd209c2de5a4c78dc87a78edf61839b", "sha256:c4e04c12f507942f1ddf215cb3a08c244d24051cdd2ba571060166ce8a92be16",
"sha256:acc07211a59e2f245e9a06f28fa374d094fb0e71cf5366eef52abbb826ddc81e", "sha256:c65257899bb8faf6a111297b4ff0066324a6b901318582c0453a01422c3bcd5a",
"sha256:aef404d5400d95c6ec86664df9924bde667c8865f8e33c9b7bd79823d53b3e5d", "sha256:c6c48cf5a92244ef3df4161c8625ee1890bb3d931db9a9f3b699e61a037cd58a",
"sha256:b1047999f1797c3ea7b7c85261649249c243308dcf3632840d076d18fa72f142", "sha256:c9bf2bf29854f165a47917b8782b6cf3a7d602971bf454806208d0cbb96f797a",
"sha256:b7d09ef06ba57bea646144c29764bf6b870fb3c5558ca098191e07b6a1d40bf7", "sha256:ca4b6298c89b92d6b0a67dfc5f98d68ae92b08101d227263ef2033b9c9a03a72",
"sha256:bcf0150ae0bcc4aa97bdfcb231b37bad1a59083c1b5012643b266012bf420e68", "sha256:cc76ad7453816318d794248fba4032967eaffd992d76e5d1af10ef9d46589770",
"sha256:bcf524a087b143ba736aebbb054bb399d49e77cf7c04ed24c728e411adc82bfa", "sha256:cd7f6bfa2a36fb91b7dec9ddf905716f2ed0c3675d2b63c69b7530c9d211e715",
"sha256:beeb79e476d19b91fd6a3439853e4e5ba1b3b475920fa40d62bde719c8af786f", "sha256:d12c45da97b2f31d0233e15f8d68731cfa86264c9f04b2669b9fdf46aaf68e1f",
"sha256:bf90aba4cff9e72e24ecdefe33bad608f147a23fa5c97790a5bab0e72fe62b6d", "sha256:d160173efdad8a57c22e422a034196df3d84753672c497aee2f94bd5b128f8dd",
"sha256:c23286abba0cb509733c6ce8f4013cd951672c332b2e184dbefbd7331cd234c8", "sha256:d2b1ed363a4ef5622dccbf7822f01b51195062c4f382b28c9bd125d046d0324c",
"sha256:c2945e0390d1329c585c584c6b6d78be017d9c6a1288f9c92006fe907f69cc28", "sha256:d30e7daaf228e3d69cdd8b02c0dd4199cec034c4b93c80109f56f4675a6db957",
"sha256:c756a92cf1c1abf01e56a4cc40cb89f0ff9147f2a0be5b557ec436a23ff464d8", "sha256:d3f38373d9b2629dedc559e647010541cc4ec4ad9bea560e2eb1017e6a00d9ef",
"sha256:c9e9fef0754867d88e948ce8351c9fd7e507d8514e0f242fd67c907b9cdf98b3", "sha256:d7e274ac1975e55ebfb8166cce27e13dc99120c1d6ce9e490d7a716b9be9abb5",
"sha256:ca79f02a98cbda1472449d440592a2fe2ad96fe55515a0447fa8864a38017cf8", "sha256:d877759842ff9eb16d9c2b8b497953a7d994d4b231c171515f0bf3a2ae185c0c",
"sha256:cb7302dbcfcb676f0b66f15891f091d0233c4fc23e1d4b9dc9b9e958156e347f", "sha256:da3dfd2776226e15d3288a3a24c7975f9ee160ba198f2efa66bc28c5ba76d792",
"sha256:cb98d5b6eac4b2cf2a5a69f60a9c499844b8bea207059e9fc45c752436e6bb49", "sha256:db0441e80773d747a1ed9edfb9f75e7acb68ce8627583bbb6f770b7ec49f0064",
"sha256:cc83ea003dd75e9ade3291ef0585577dd5524aec0c8c99305c0aaa2a7570d6db", "sha256:dbbaa147cf28b3e87738c624d390a3a9e2a5dfef4316f4c38b4ecaf3155a3eab",
"sha256:ce249ed981f428a8b61538ca82d3875847733d579dd40084ab8246549160f8a4", "sha256:ddc646cec4899a137c134b13818469e4178a251d77f9f4b23229267e3da78cfb",
"sha256:cf0cc2e91dd38122dec2e6541efa99aafb0a62e118179218181eff720b4b8153", "sha256:df7cc9584614f495f474a5ded365cf72decbcee4efcdc888d2943f8a794c789e",
"sha256:d1a199e6d7c3bad5ba9d0e4dc00dde70ee7d111c9dfc521247fa646ef59fa57e", "sha256:dfde50ae55e075dcd5801e2c3ea0e749c849ed2cbbee991af0f97f1bdbadb2a6",
"sha256:d1d5abf1d6d910599ac16afdd9a0ed3e24f3b46af57f3070cf2792f236f36e0b", "sha256:e15e70a3cf5bb519e2448524d689c02ff6bcd4750587a517e2bffee06065bf27",
"sha256:d3f761184b93092077c7f6b7dad7bd4e671c1620404a76620da7872ceb576a94", "sha256:e3572889fcb87e5ca94add412d8b365dbb7b59773a4362e52caa556e5fd98643",
"sha256:d756bfeb62ca4fe65d2af7a39249d442c05070c047d03729ad6cd4c2e9b0f0bd", "sha256:e39f5e85e1e3d7d84ac2217cd095b3678306c979e991532df47012880e02215d",
"sha256:d8c36ddc1923bcc4c11b9994c54eaae25034812a42400b7b8a86fe6d242166a2", "sha256:e501bd27c795105aaba02b5212ecd1bb552ca2ee2ede53e5a8cb74deee0e2052",
"sha256:dbe1084935b942fab206e609fa1ed3f46ad1f2612fb4833e177e9b2a5e006c96", "sha256:e62892645f6a214eefb58a42c3ed2501af2e40a797844e0e09ec1e400ce75f3d",
"sha256:dc1937a0ff2671797d35243db4b596329842480d125a65e9fe964bcffaf16dfc", "sha256:e75eb1734046291c554d9addecca9a8785bdf5d53a64f525569f8549da863dde",
"sha256:dfea514e665af278b2e1d4deb542de1cd4f77413bee83dd15ae16175976ea8d5", "sha256:e84cff8e8fe71903a6cf873fb3c8731df8bd7c1dac878e7a0fe19d8e2ef39aa9",
"sha256:e008b7b4ce6c7f7a54b250c45c28d4243cc2a3bbfd5298fa7dac92afda229842", "sha256:ea60cf85b4e5a78b5a41eed3a65abc3839a50d915c6e0f6966cbcf81b85991bd",
"sha256:e0e7f24a0b01e6e6a0191c50b06ca8edfdec1988d9d2b264d669d2487f4f4680", "sha256:ec3fd30622180cbe2326d48c14a4ab7f98a504b104bdca7dda88b134adad6e31",
"sha256:e15c94d79810c5ab90ddf4d943f71f14332890417be896ca253f21fa3d78d2b1", "sha256:eccc6829035c8b7b391a0aa124fade54932bb937dd1079f2740b9f1bde829226",
"sha256:e56ba8be5f17dee0ffa6d6ce85251e062ded2faa3cbd2558659c671e6c3bf96d", "sha256:eda67136343db96752e58ef36ac37116f36cba40961e79fd0e9bd858f5a09b38",
"sha256:e89ea59a3ed86a6eb150d016ed28b1bedf892802d0ed32b5659d3199440f3ced", "sha256:ef5a99a8d1a5c47b4cf85925d1420fc4ee584c98be8efc548651447b3047242f",
"sha256:e91d46d12781a14ccb8b284566b14933de4e3b29f8bc5e1c17de7a2001ad3b5b", "sha256:f0795e2be2aa8afd013635f30ffe599cc00f1bbaca2d1d19b6187b4d1c58fb44",
"sha256:ea40e98d751ed4b255db4a88fe8fb743374183f78470b9e9305aab186bf28ede", "sha256:f31d8c2168bf2a52e4539232392352832c2296e07e0e14b6e06a44da574099ba",
"sha256:eb27c01b747649afd7e1c342961680893df6d8d81f832a6f04d8c8e03a8a54cc", "sha256:f41a4b57cbc128a699e9d716a56c90c7fc76554e680fe2962f49cc4d8688b051",
"sha256:ec5b0f2d13da53e0975ac15ecbe8badb463bdb0bebaa09457f4df3320421915c", "sha256:f583a1fb180a123c00064fab1a3bfb9d43e574b6474be1be3f6469e0331e3e2e",
"sha256:ee040ad3b7dfa05e459713099f16373c1f2a6f68b43cb0575a66718e7a5daef4", "sha256:f7c531722e8c3901f6bb303db464cac98ab44ed422c0fd0c762baa4a8d49ffa1",
"sha256:f12cc7c7638074918cdcc7491aff897df921b092ffd877227892d2686e98f876", "sha256:f8ab90410b2ba5b8276657c66941bcaae556a38be8dd81630a7647e8735f0a20",
"sha256:f536fc4d1a683025f9caef0bebeafd60384054579ffe0825bb9bd8c59f8c55b8", "sha256:fa05460dc4f57358680b977b4a254d331b24c8beb501319b998625fd6a22654b",
"sha256:f71f24b58e75a889b9915e3197865302467f13e7390efdea5b6afc7424b3a2ea", "sha256:fbe1ef622748d2edb3dd4fef933b934e90e479f9831dfe31bda3fdc16bf5287f",
"sha256:f75fc0198c955d840b836059bd43e0993edbf119923029ca60c4fc017cefa54a", "sha256:fdb7af369df317527d697c5bb37ab944bb9a17ea1a5e82e47d5c7c638f3ccdd6",
"sha256:f785af6b7cb07a9b1e5db0dea9ef9e3e8bb3d74874a0a61303eab9c16acc1999", "sha256:fe1f1f4010244cb07f6a079854a12e1627e4fb9ea99d672f2ceccaf6653ca514",
"sha256:fbb645477595ce2a0fbb678d1cfd08d3b896e5d56196d40fb9e114eeab9382b3", "sha256:fe2493d3f49e314e573022ead4d8c845c9748979b7eb95e815429fe947c4bde2",
"sha256:fcef31b062f756ba7eebcd7890c5d5de84b9d64ee877325257bcc9782288564a", "sha256:ffd112646486a31ea5a45aa1eca0e2cd90b6a12f67e848e50349e324c24cc2e7"
"sha256:fe606e728842389943a939258809dc5db2de831b1d2e0118515059e87f7bbc1a",
"sha256:fef4e3b3f2084b4dae3e5316b44cda72587dcc81f68b4eb2dbda1b8d15261b61",
"sha256:ffd94b4803811c738e504a4b499fb2f848b2f7412d71e6b517508217c1d7929d"
], ],
"version": "==3.0.0" "version": "==3.7.1"
}, },
"bitstring": { "bitstring": {
"hashes": [ "hashes": [
"sha256:3282a896814813f8fe5fa09dbafac842c57aace1d3bfd94546c6f1ed9aafcbe1", "sha256:69d1587f0ac18dc7d93fc7e80d5f447161a33e57027e726dc18a0a8bacf1711a",
"sha256:81800bc4e00b6508716adbae648e741256355c8dfd19541f76482fb89bee0313" "sha256:a08bc09d3857216d4c0f412a1611056f1cc2b64fd254fb1e8a0afba7cfa1a95a"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.3.0" "version": "==4.3.1"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
"sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
"sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
"sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
"sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
"sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
"sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
"sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
"sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
"sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
"sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
"sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
"sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
"sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
"sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
"sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
"sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
"sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
"sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
"sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
"sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
"sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
"sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
"sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
"sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
"sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
"sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
"sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
"sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
"sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
"sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
"sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
"sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
"sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
"sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
"sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
"sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
"sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
"sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
"sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
"sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
"sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
"sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
"sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
"sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
"sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
"sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
"sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
"sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
"sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
"sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
"sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
"sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
"sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
"sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
"sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
"sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
"sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
"sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
"sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
"sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
"sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
"sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
"sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
"sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
"sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
"sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
"sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
], ],
"markers": "platform_python_implementation != 'PyPy'", "markers": "platform_python_implementation != 'PyPy'",
"version": "==1.17.1" "version": "==2.0.0"
},
"click": {
"hashes": [
"sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202",
"sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"
],
"markers": "python_version >= '3.10'",
"version": "==8.2.1"
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34",
"sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513",
"sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5",
"sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c",
"sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63",
"sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130",
"sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae",
"sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443",
"sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59",
"sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee",
"sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf",
"sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27",
"sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde",
"sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971",
"sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8",
"sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339",
"sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6",
"sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90",
"sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691",
"sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3",
"sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083",
"sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6",
"sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1",
"sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3",
"sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8",
"sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2",
"sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4" "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7",
"sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141",
"sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3",
"sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9",
"sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4",
"sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4",
"sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b",
"sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252",
"sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17",
"sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b",
"sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd"
], ],
"markers": "python_version >= '3.7' 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": "==44.0.0" "version": "==45.0.7"
},
"ecdsa": {
"hashes": [
"sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a",
"sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.19.0"
}, },
"esptool": { "esptool": {
"hashes": [ "hashes": [
"sha256:dc4ef26b659e1a8dcb019147c0ea6d94980b34de99fbe09121c7941c8b254531" "sha256:05cc4732eb2a9a7766c9e3531f7943d76ff0ca06dc9cd308d1d3d0b72f74aac2"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.10'",
"version": "==4.8.1" "version": "==5.0.2"
},
"h11": {
"hashes": [
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
],
"markers": "python_version >= '3.8'",
"version": "==0.16.0"
},
"idna": {
"hashes": [
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
],
"markers": "python_version >= '3.6'",
"version": "==3.10"
}, },
"intelhex": { "intelhex": {
"hashes": [ "hashes": [
@@ -303,22 +343,54 @@
], ],
"version": "==2.3.0" "version": "==2.3.0"
}, },
"markdown-it-py": {
"hashes": [
"sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147",
"sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"
],
"markers": "python_version >= '3.10'",
"version": "==4.0.0"
},
"mdurl": {
"hashes": [
"sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
"sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
],
"markers": "python_version >= '3.7'",
"version": "==0.1.2"
},
"mpremote": { "mpremote": {
"hashes": [ "hashes": [
"sha256:1a3c16d255748cfe54d4a897908651fc8286233173f7c7b2a0e56ae4b9fa940e", "sha256:7f347318fb6d3bb8f89401d399a05efba39b51c74f747cebe92d3c6a9a4ee0b4",
"sha256:d3ae3d0a0ae7713c537be2b6afadd11c7cde5f1750ea1260f6667bb80071b15b" "sha256:daed9b795fdf98edb0c9c4f7f892bf66f075ec5e728bdcc4ab0915abf23d5d17"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.4'", "markers": "python_version >= '3.4'",
"version": "==1.24.1" "version": "==1.26.0"
},
"platformdirs": {
"hashes": [
"sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85",
"sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"
],
"markers": "python_version >= '3.9'",
"version": "==4.4.0"
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
"sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
"sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
],
"markers": "implementation_name != 'PyPy'",
"version": "==2.23"
},
"pygments": {
"hashes": [
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.22" "version": "==2.19.2"
}, },
"pyserial": { "pyserial": {
"hashes": [ "hashes": [
@@ -394,13 +466,159 @@
], ],
"version": "==1.7.0" "version": "==1.7.0"
}, },
"six": { "rich": {
"hashes": [ "hashes": [
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f",
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_full_version >= '3.8.0'",
"version": "==1.17.0" "version": "==14.1.0"
},
"rich-click": {
"hashes": [
"sha256:c3fa81ed8a671a10de65a9e20abf642cfdac6fdb882db1ef465ee33919fbcfe2",
"sha256:fd98c0ab9ddc1cf9c0b7463f68daf28b4d0033a74214ceb02f761b3ff2af3136"
],
"markers": "python_version >= '3.7'",
"version": "==1.8.9"
},
"sniffio": {
"hashes": [
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
],
"markers": "python_version >= '3.7'",
"version": "==1.3.1"
},
"typing-extensions": {
"hashes": [
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
],
"markers": "python_version < '3.13'",
"version": "==4.15.0"
},
"uvicorn": {
"hashes": [
"sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a",
"sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==0.35.0"
},
"watchfiles": {
"hashes": [
"sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a",
"sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f",
"sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6",
"sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3",
"sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7",
"sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a",
"sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259",
"sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297",
"sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1",
"sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c",
"sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a",
"sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b",
"sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb",
"sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc",
"sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b",
"sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339",
"sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9",
"sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df",
"sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb",
"sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4",
"sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5",
"sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc",
"sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c",
"sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8",
"sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433",
"sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12",
"sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30",
"sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0",
"sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86",
"sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c",
"sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5",
"sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866",
"sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb",
"sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2",
"sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e",
"sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575",
"sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f",
"sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a",
"sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f",
"sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d",
"sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277",
"sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9",
"sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf",
"sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92",
"sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72",
"sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b",
"sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68",
"sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa",
"sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc",
"sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b",
"sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd",
"sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4",
"sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7",
"sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792",
"sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9",
"sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0",
"sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297",
"sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef",
"sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179",
"sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d",
"sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea",
"sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5",
"sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee",
"sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82",
"sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011",
"sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e",
"sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4",
"sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf",
"sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db",
"sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20",
"sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4",
"sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575",
"sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa",
"sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c",
"sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f",
"sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f",
"sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267",
"sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018",
"sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2",
"sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d",
"sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd",
"sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47",
"sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb",
"sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29",
"sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147",
"sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8",
"sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670",
"sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587",
"sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97",
"sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c",
"sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5",
"sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e",
"sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e",
"sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6",
"sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc",
"sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e",
"sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8",
"sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895",
"sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7",
"sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432",
"sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc",
"sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633",
"sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f",
"sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77",
"sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12",
"sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==1.1.0"
} }
}, },
"develop": {} "develop": {}

58
configure_bar.py Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
LED Bar Configuration Script
Updates the settings.json file for each LED bar with its unique name
"""
import json
import os
# LED Bar names/IDs
LED_BAR_NAMES = ["100", "101", "102", "103", "104", "105", "106", "107"]
def update_bar_settings(bar_name, settings_file="settings.json"):
"""Update the settings.json file with the bar name"""
if not os.path.exists(settings_file):
print(f"Error: {settings_file} not found")
return False
# Read current settings
with open(settings_file, 'r') as f:
settings = json.load(f)
# Update the name
settings["name"] = bar_name
# Write back to file
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=4)
print(f"Updated {settings_file} with name: {bar_name}")
return True
def main():
print("LED Bar Configuration Script")
print("=" * 40)
print("Available bar names:", LED_BAR_NAMES)
print()
while True:
print("Enter bar name to configure (or 'quit' to exit):")
bar_name = input("> ").strip()
if bar_name.lower() == 'quit':
break
if bar_name not in LED_BAR_NAMES:
print(f"Invalid bar name. Must be one of: {LED_BAR_NAMES}")
continue
if update_bar_settings(bar_name):
print(f"Successfully configured LED bar as '{bar_name}'")
else:
print("Failed to update settings")
print()
if __name__ == "__main__":
main()

428
patterns.py Normal file
View File

@@ -0,0 +1,428 @@
import utime
import random
from patterns_base import PatternBase # Import PatternBase
class Patterns(PatternBase): # Inherit from PatternBase
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100):
super().__init__(pin, num_leds, color1, color2, brightness, selected, delay) # Call parent constructor
# Pattern-specific initializations
self.on_width = 1 # Default on width
self.off_width = 2 # Default off width (so total segment is 3, matching original behavior)
self.n1 = 0 # Default start of fill range
self.n2 = self.num_leds - 1 # Default end of fill range
self.oneshot = False # New: One-shot flag for patterns like fill_range
self.patterns = {
"off": self.off,
"on" : self.on,
"color_wipe": self.color_wipe,
"rainbow_cycle": self.rainbow_cycle,
"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
"fill_range": self.fill_range, # New: Fill from n1 to n2
"n_chase": self.n_chase, # New: N1 on, N2 off repeating chase
"alternating": self.alternating, # New: N1 on/off, N2 off/on alternating chase
"external": None,
"pulse": self.pulse
}
# Beat-related functionality removed
# self.selected is already initialized in PatternBase, but we need to ensure it uses our patterns dict
# self.selected = selected # Handled by PatternBase
# Ensure colors list always starts with at least two for robust transition handling
# self.colors handled by PatternBase
# Transition attributes handled by PatternBase
# Scanner attributes handled by PatternBase
# self.run handled by PatternBase
def set_on_width(self, on_width):
self.on_width = on_width
def set_off_width(self, off_width):
self.off_width = off_width
def set_on_off_width(self, on_width, off_width):
self.on_width = on_width
self.off_width = off_width
self.sync()
def set_fill_range(self, n1, n2):
self.n1 = n1
self.n2 = n2
self.sync()
def set_oneshot(self, oneshot_value):
self.oneshot = oneshot_value
if self.oneshot: # Reset pattern step if enabling one-shot
self.pattern_step = 0
self.sync()
def select(self, pattern):
if pattern in self.patterns:
super().select(pattern) # Use parent select to set self.selected and self.transition_step
self.run = True # Set run flag
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 off(self):
self.fill((0, 0, 0))
return self.delay
def on(self):
self.fill(self.apply_brightness(self.colors[0]))
return self.delay
def color_wipe(self):
color = self.apply_brightness(self.colors[0])
current_time = utime.ticks_ms()
if self.pattern_step < self.num_leds:
for i in range(self.num_leds):
self.n[i] = (0, 0, 0)
self.n[self.pattern_step] = self.apply_brightness(color)
self.n.write()
self.pattern_step += 1
else:
self.pattern_step = 0
self.last_update = current_time
return self.delay
def rainbow_cycle(self):
current_time = utime.ticks_ms()
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)
for i in range(self.num_leds):
rc_index = (i * 256 // self.num_leds) + self.pattern_step
self.n[i] = self.apply_brightness(wheel(rc_index & 255))
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 256
self.last_update = current_time
return max(1, int(self.delay // 5))
def theater_chase(self):
current_time = utime.ticks_ms()
segment_length = self.on_width + self.off_width
for i in range(self.num_leds):
if (i + self.pattern_step) % segment_length < self.on_width:
self.n[i] = self.apply_brightness(self.colors[0])
else:
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % segment_length
self.last_update = current_time
return self.delay
def blink(self):
current_time = utime.ticks_ms()
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
return self.delay
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 self.delay
# 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 self.delay
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
return self.delay
def flicker(self):
current_time = utime.ticks_ms()
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
return max(1, int(self.delay // 5))
def scanner(self):
"""
Mimics a 'Knight Rider' style scanner, moving in one direction.
"""
current_time = utime.ticks_ms()
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])
# Draw the head
if 0 <= head_pos < self.num_leds:
self.n[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 < self.num_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.n[tail_pos] = faded_color
self.n.write()
self.pattern_step += 1
if self.pattern_step >= self.num_leds + self.scanner_tail_length:
self.pattern_step = 0 # Reset to start
self.last_update = current_time
return self.delay
def bidirectional_scanner(self):
"""
Mimics a 'Knight Rider' style scanner, moving back and forth.
"""
current_time = utime.ticks_ms()
self.fill((0, 0, 0)) # Clear all LEDs
color = self.apply_brightness(self.colors[0])
# Calculate the head position based on direction
head_pos = self.pattern_step
# Draw the head
if 0 <= head_pos < self.num_leds:
self.n[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 < self.num_leds:
fade_factor = 1.0 - (i / (self.scanner_tail_length + 1))
faded_color = tuple(int(c * fade_factor) for c in color)
self.n[tail_pos] = faded_color
self.n.write()
self.pattern_step += self.scanner_direction
# Change direction if boundaries are reached
if self.scanner_direction == 1 and self.pattern_step >= self.num_leds:
self.scanner_direction = -1
self.pattern_step = self.num_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
return self.delay
def fill_range(self):
"""
Fills a range of LEDs from n1 to n2 with a solid color.
If self.oneshot is True, it fills once and then turns off the LEDs.
"""
current_time = utime.ticks_ms()
if self.oneshot and self.pattern_step >= 1:
self.fill((0, 0, 0)) # Turn off LEDs if one-shot already happened
else:
color = self.apply_brightness(self.colors[0])
for i in range(self.n1, self.n2 + 1):
self.n[i] = color
self.n.write()
self.last_update = current_time
return self.delay
self.last_update = current_time
return self.delay
def n_chase(self):
"""
A theater chase pattern using n1 for on-width and n2 for off-width.
"""
current_time = utime.ticks_ms()
segment_length = self.n1 + self.n2
if segment_length == 0: # Avoid division by zero
self.fill((0,0,0))
self.n.write()
self.last_update = current_time
return self.delay
for i in range(self.num_leds):
if (i + self.pattern_step) % segment_length < self.n1:
self.n[i] = self.apply_brightness(self.colors[0])
else:
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % segment_length
self.last_update = current_time
return self.delay
def alternating(self):
"""
An alternating pattern where n1 LEDs are ON/OFF and n2 LEDs are OFF/ON globally, without moving.
"""
current_time = utime.ticks_ms()
total_segment_length = self.n1 + self.n2
if total_segment_length == 0:
self.fill((0,0,0))
self.n.write()
self.last_update = current_time
return self.delay
# current_phase will alternate between 0 and 1
current_phase = self.pattern_step % 2
for i in range(self.num_leds):
# Position within a single repeating segment (n1 + n2)
pos_in_segment = i % total_segment_length
if current_phase == 0: # State 0: n1 ON, n2 OFF
if pos_in_segment < self.n1:
self.n[i] = self.apply_brightness(self.colors[0]) # n1 is ON
else:
self.n[i] = (0, 0, 0) # n2 is OFF
else: # State 1: n1 OFF, n2 ON
if pos_in_segment < self.n1:
self.n[i] = (0, 0, 0) # n1 is OFF
else:
self.n[i] = self.apply_brightness(self.colors[0]) # n2 is ON
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 2 # Toggle between 0 and 1
self.last_update = current_time
return self.delay * 2
def pulse(self):
if self.pattern_step == 0:
self.fill(self.apply_brightness(self.colors[0]))
self.pattern_step = 1
self.last_update = utime.ticks_ms()
if utime.ticks_diff(utime.ticks_ms(), self.last_update) > self.delay:
self.fill((0, 0, 0))
print(utime.ticks_diff(utime.ticks_ms(), self.last_update))
self.run = False
return self.delay
if __name__ == "__main__":
import time
from machine import WDT
wdt = WDT(timeout=2000) # Enable watchdog with a 2 second timeout
p = Patterns(pin=4, num_leds=60, color1=(255,0,0), color2=(0,0,255), brightness=127, selected="off", delay=100)
print(p.colors, p.brightness)
tests = [
("off", {"duration_ms": 500}),
("on", {"duration_ms": 500}),
("color_wipe", {"delay": 200, "duration_ms": 1000}),
("rainbow_cycle", {"delay": 100, "duration_ms": 2500}),
("theater_chase", {"on_width": 3, "off_width": 3, "delay": 1000, "duration_ms": 2500}),
("blink", {"delay": 500, "duration_ms": 2000}),
("color_transition", {"delay": 150, "colors": [(255,0,0),(0,255,0),(0,0,255)], "duration_ms": 5000}),
("flicker", {"delay": 100, "duration_ms": 2000}),
("scanner", {"delay": 150, "duration_ms": 2500}),
("bidirectional_scanner", {"delay": 50, "duration_ms": 2500}),
("fill_range", {"n1": 10, "n2": 20, "delay": 500, "duration_ms": 2000}),
("n_chase", {"n1": 5, "n2": 5, "delay": 2000, "duration_ms": 2500}),
("alternating", {"n1": 5, "n2": 5, "delay": 500, "duration_ms": 2500}),
("pulse", {"delay": 100, "duration_ms": 700}),
]
print("\n--- Running pattern self-test ---")
for name, cfg in tests:
print(f"\nPattern: {name}")
# apply simple config helpers
if "delay" in cfg:
p.set_delay(cfg["delay"])
if "on_width" in cfg:
p.set_on_width(cfg["on_width"])
if "off_width" in cfg:
p.set_off_width(cfg["off_width"])
if "n1" in cfg and "n2" in cfg:
p.set_fill_range(cfg["n1"], cfg["n2"])
if "colors" in cfg:
p.set_colors(cfg["colors"])
p.select(name)
# run per configured duration using absolute-scheduled tick(next_due_ms)
start = utime.ticks_ms()
duration_ms = cfg["duration_ms"]
delay = cfg.get("delay", 0)
next_due = utime.ticks_ms() - 1 # force immediate first call
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
delay = p.tick(delay)
wdt.feed()
print("\n--- Test routine finished ---")

View File

@@ -5,4 +5,5 @@ from settings import Settings
s = Settings() s = Settings()
name = s.get('name', 'led') name = s.get('name', 'led')
wifi.ap(name, '') password = s.get("ap_password", "")
# wifi.ap(name, password)

View File

@@ -1,6 +1,6 @@
import asyncio
import aioespnow import patterns
from settings import Settings, set_settings from settings import Settings
from web import web from web import web
from patterns import Patterns from patterns import Patterns
import gc import gc
@@ -8,53 +8,83 @@ import utime
import machine import machine
import time import time
import wifi import wifi
import json
from p2p import p2p
import espnow
import network
def main():
async def main():
settings = Settings() settings = Settings()
patterns = Patterns(4, settings["num_leds"], selected=settings["pattern"])
patterns.set_color1(tuple(int(settings["color1"][i:i+2], 16) for i in (1, 5, 3)))
patterns.set_color2(tuple(int(settings["color2"][i:i+2], 16) for i in (1, 5, 3)))
patterns.set_brightness(int(settings["brightness"]))
patterns.set_delay(int(settings["delay"]))
w = web(settings, patterns)
print(settings) 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()
async def tick(): if settings.get("color_order", "rgb") == "rbg":
while True: color_order = (1, 5, 3)
patterns.tick() else:
await asyncio.sleep_ms(1) color_order = (1, 3, 5)
patterns = Patterns(settings["led_pin"], settings["num_leds"], selected="off")
async def espnow(): sta_if = network.WLAN(network.STA_IF)
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support sta_if.active(True)
e.active(True)
async for mac, msg in e:
print(msg)
set_settings(msg, settings, patterns)
asyncio.create_task(tick()) e = espnow.ESPNow()
asyncio.create_task(espnow()) e.config(rxbuf=1024)
e.active(True)
# Increase buffer size for 8-bar payloads (default 526 bytes might be too small) # Set to 1KB to handle larger multi-bar payloads
wdt = machine.WDT(timeout=10000)
wdt.feed()
while True: while True:
# advance pattern based on its own returned schedule
# due = patterns.tick(due)
wdt.feed()
#print(time.localtime()) # Drain all pending packets and only process the latest
last_msg = None
while True:
host, msg = e.recv(0)
if not msg:
break
last_msg = msg
# gc.collect() if last_msg:
for i in range(60): try:
#wdt.feed() data = json.loads(last_msg)
await asyncio.sleep_ms(500) print(data)
defaults = data.get("d", {})
bar = data.get(settings.get("name"), {})
# Check message type
message_type = defaults.get("t", "b") # Default to beat if not specified
# Always update parameters from message
patterns.brightness = bar.get("br", defaults.get("br", patterns.brightness))
patterns.delay = bar.get("dl", defaults.get("dl", patterns.delay))
patterns.colors = bar.get("cl", defaults.get("cl", patterns.colors))
patterns.n1 = bar.get("n1", defaults.get("n1", patterns.n1))
patterns.n2 = bar.get("n2", defaults.get("n2", patterns.n2))
patterns.n3 = bar.get("n3", defaults.get("n3", patterns.n3))
patterns.n4 = bar.get("n4", defaults.get("n4", patterns.n4))
patterns.step = bar.get("s", defaults.get("s", patterns.step))
# Print received parameters
print(f"Params: br={patterns.brightness}, dl={patterns.delay}, n1={patterns.n1}, n2={patterns.n2}, n3={patterns.n3}, n4={patterns.n4}, step={patterns.step}")
# Only execute pattern if it's a beat message
if message_type == "b": # Beat message
selected_pattern = bar.get("pt", defaults.get("pt", "off"))
if selected_pattern in patterns.patterns:
# Run the selected pattern ONCE in response to this beat message
patterns.patterns[selected_pattern]()
else:
print(f"Pattern {selected_pattern} not found")
elif message_type == "u": # Update message
# Just update parameters, don't execute pattern
print(f"Parameters updated: brightness={patterns.brightness}, delay={patterns.delay}")
else:
print(f"Unknown message type: {message_type}")
except Exception as ex:
print(f"Failed to load espnow data {last_msg}: {ex}")
continue
# cleanup before ending the application main()
await server
asyncio.run(main())

20
src/p2p.py Normal file
View 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")

View File

@@ -1,291 +1,453 @@
from machine import Pin
from neopixel import NeoPixel
import utime import utime
import random import random
from patterns_base import PatternBase # Import PatternBase
class Patterns: class Patterns(PatternBase): # Inherit from PatternBase
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100): def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) super().__init__(pin, num_leds, color1, color2, brightness, selected, delay) # Call parent constructor
self.num_leds = num_leds
self.pattern_step = 0 # Pattern-specific initializations
self.last_update = utime.ticks_ms() self.on_width = 1 # Default on width
self.delay = delay self.off_width = 2 # Default off width (so total segment is 3, matching original behavior)
self.brightness = brightness self.n1 = 0 # Default start of fill range
self.n2 = self.num_leds - 1 # Default end of fill range
self.n3 = 1 # Default step factor
self.n4 = 0
self.oneshot = False # New: One-shot flag for patterns like fill_range
self.patterns = { self.patterns = {
"on": self.on,
"off": self.off, "off": self.off,
"on" : self.on, "flicker": self.flicker,
"color_wipe": self.color_wipe_step, "fill_range": self.fill_range,
"rainbow_cycle": self.rainbow_cycle_step, "n_chase": self.n_chase,
"theater_chase": self.theater_chase_step, "alternating": self.alternating,
"blink": self.blink_step, "pulse": self.pulse,
"random_color_wipe": self.random_color_wipe_step, "rainbow": self.rainbow,
"random_rainbow_cycle": self.random_rainbow_cycle_step, "specto": self.specto,
"random_theater_chase": self.random_theater_chase_step, "radiate": self.radiate,
"random_blink": self.random_blink_step, "segmented_movement": self.segmented_movement,
"color_transition": self.color_transition_step, # Shortened pattern names for optimized JSON payloads
"external": None "o": self.off,
} "f": self.flicker,
self.selected = selected "fr": self.fill_range,
self.color1 = color1 "nc": self.n_chase,
self.color2 = color2 "a": self.alternating,
self.transition_duration = 50 # Duration of color transition in milliseconds "p": self.pulse,
self.transition_step = 0 "r": self.rainbow,
"s": self.specto,
def sync(self): "rd": self.radiate,
self.pattern_step=0 "sm": self.segmented_movement,
self.last_update = utime.ticks_ms() }
self.step = 0
def tick(self): def on(self):
if self.patterns[self.selected]: """Turn on all LEDs with current color"""
self.patterns[self.selected]() self.fill(self.apply_brightness(self.colors[0]))
def update_num_leds(self, pin, num_leds):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.pattern_step = 0
def set_delay(self, delay):
self.delay = delay
def set_brightness(self, brightness):
self.brightness = brightness
def set_color1(self, color):
print(color)
self.color1 = self.apply_brightness(color)
def set_color2(self, color):
self.color2 = self.apply_brightness(color)
def apply_brightness(self, color):
return tuple(int(c * self.brightness / 255) for c in color)
def select(self, pattern):
if pattern in self.patterns:
self.selected = pattern
return True
return False
def set(self, i, color):
self.n[i] = color
def write(self):
self.n.write()
def fill(self):
for i in range(self.num_leds):
self.n[i] = self.color1
self.n.write() self.n.write()
return self.delay
def off(self): def off(self):
color = self.color1 """Turn off all LEDs"""
self.color1 = (0,0,0) self.fill((0, 0, 0))
self.fill() self.n.write()
self.color1 = color return self.delay
def on(self):
color = self.color1
self.color1 = self.apply_brightness(self.color1)
self.fill()
self.color1 = color
def color_wipe_step(self):
color = self.apply_brightness(self.color1)
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
if self.pattern_step < self.num_leds:
for i in range(self.num_leds):
self.n[i] = (0, 0, 0)
self.n[self.pattern_step] = self.apply_brightness(color)
self.n.write()
self.pattern_step += 1
else:
self.pattern_step = 0
self.last_update = current_time
def rainbow_cycle_step(self): def flicker(self):
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay/5: base_color = self.colors[0]
def wheel(pos): # Use fixed minimum brightness of 10, flicker between 10 and full brightness
if pos < 85: # Use n3 as step rate multiplier to control how fast patterns step
return (pos * 3, 255 - pos * 3, 0) min_brightness = 10
elif pos < 170: step_rate = max(1, int(self.n3))
pos -= 85 flicker_brightness_offset = random.randint(-int(self.brightness // 1.5), int(self.brightness // 1.5))
return (255 - pos * 3, 0, pos * 3) flicker_brightness = max(min_brightness, min(255, self.brightness + flicker_brightness_offset))
else:
pos -= 170
return (0, pos * 3, 255 - pos * 3)
for i in range(self.num_leds): flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness)
rc_index = (i * 256 // self.num_leds) + self.pattern_step self.fill(flicker_color)
self.n[i] = self.apply_brightness(wheel(rc_index & 255)) self.last_update = current_time
return max(1, int(self.delay // (5 * step_rate)))
def fill_range(self):
"""
Fills a range of LEDs from n1 to n2 with a solid color.
If self.oneshot is True, it fills once and then turns off the LEDs.
"""
current_time = utime.ticks_ms()
if self.oneshot and self.pattern_step >= 1:
self.fill((0, 0, 0)) # Turn off LEDs if one-shot already happened
else:
color = self.apply_brightness(self.colors[0])
for i in range(self.n1, self.n2 + 1):
self.n[i] = color
self.n.write() self.n.write()
self.pattern_step = (self.pattern_step + 1) % 256
self.last_update = current_time self.last_update = current_time
return self.delay
self.last_update = current_time
return self.delay
def theater_chase_step(self): def n_chase(self):
"""
A theater chase pattern using n1 for on-width and n2 for off-width.
"""
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay: step_rate = max(1, int(self.n3))
for i in range(self.num_leds): segment_length = self.n1 + self.n2
if (i + self.pattern_step) % 3 == 0: if segment_length == 0: # Avoid division by zero
self.n[i] = self.apply_brightness(self.color1) self.fill((0,0,0))
else:
self.n[i] = (0, 0, 0)
self.n.write() self.n.write()
self.pattern_step = (self.pattern_step + 1) % 3
self.last_update = current_time self.last_update = current_time
return self.delay
def blink_step(self):
current_time = utime.ticks_ms() # Use controller's step for synchronization, but scale it for chasing
if utime.ticks_diff(current_time, self.last_update) >= self.delay: chase_step = (self.step * step_rate) % self.num_leds
if self.pattern_step % 2 == 0:
for i in range(self.num_leds):
self.n[i] = self.apply_brightness(self.color1)
else:
for i in range(self.num_leds):
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 2
self.last_update = current_time
def random_color_wipe_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
if self.pattern_step < self.num_leds:
for i in range(self.num_leds):
self.n[i] = (0, 0, 0)
self.n[self.pattern_step] = self.apply_brightness(color)
self.n.write()
self.pattern_step += 1
else:
self.pattern_step = 0
self.last_update = current_time
def random_rainbow_cycle_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
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)
random_offset = random.randint(0, 255)
for i in range(self.num_leds):
rc_index = (i * 256 // self.num_leds) + self.pattern_step + random_offset
self.n[i] = self.apply_brightness(wheel(rc_index & 255))
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 256
self.last_update = current_time
def random_theater_chase_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
for i in range(self.num_leds):
if (i + self.pattern_step) % 3 == 0:
self.n[i] = self.apply_brightness(color)
else:
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 3
self.last_update = current_time
def random_blink_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
if self.pattern_step % 2 == 0:
for i in range(self.num_leds):
self.n[i] = self.apply_brightness(color)
else:
for i in range(self.num_leds):
self.n[i] = (0, 0, 0)
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 2
self.last_update = current_time
def color_transition_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
# Calculate transition factor based on elapsed time
transition_factor = (self.pattern_step * 100) / self.transition_duration
if transition_factor > 100:
transition_factor = 100
color = self.interpolate_color(self.color1, self.color2, transition_factor / 100)
# Apply the interpolated color to all LEDs for i in range(self.num_leds):
for i in range(self.num_leds): # Calculate position relative to the chase head
self.n[i] = self.apply_brightness(color) pos_from_head = (i - chase_step) % self.num_leds
self.n.write() if pos_from_head < self.n1:
self.n[i] = self.apply_brightness(self.colors[0])
self.pattern_step += self.delay else:
if self.pattern_step > self.transition_duration:
self.pattern_step = 0
self.last_update = current_time
def interpolate_color(self, color1, color2, factor):
return (
int(color1[0] + (color2[0] - color1[0]) * factor),
int(color1[1] + (color2[1] - color1[1]) * factor),
int(color1[2] + (color2[2] - color1[2]) * factor)
)
def two_steps_forward_one_step_back_step(self):
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, self.last_update) >= self.delay:
# Move forward 2 steps and backward 1 step
if self.direction == 1: # Moving forward
if self.scanner_position < self.num_leds - 2:
self.scanner_position += 2 # Move forward 2 steps
else:
self.direction = -1 # Change direction to backward
else: # Moving backward
if self.scanner_position > 0:
self.scanner_position -= 1 # Move backward 1 step
else:
self.direction = 1 # Change direction to forward
# Set all LEDs to off
for i in range(self.num_leds):
self.n[i] = (0, 0, 0) self.n[i] = (0, 0, 0)
self.n.write()
# Don't update internal step - use controller's step for sync
self.last_update = current_time
return self.delay
def alternating(self):
# Use n1 as ON width and n2 as OFF width
segment_on = max(0, int(self.n1))
segment_off = max(0, int(self.n2))
total_segment_length = segment_on + segment_off
if total_segment_length <= 0:
self.fill((0, 0, 0))
self.n.write()
return self.delay
current_phase = self.step % 2
active_color = self.apply_brightness(self.colors[0])
for i in range(self.num_leds):
pos_in_segment = i % total_segment_length
if current_phase == 0:
# ON then OFF
if pos_in_segment < segment_on:
self.n[i] = active_color
else:
self.n[i] = (0, 0, 0)
else:
# OFF then ON
if pos_in_segment < segment_on:
self.n[i] = (0, 0, 0)
else:
self.n[i] = active_color
self.n.write()
# Don't update step - use the step value sent from controller for synchronization
return max(1, int(self.delay // 2))
def pulse(self):
# Envelope: attack=n1 ms, hold=delay ms, decay=n2 ms
attack_ms = max(0, int(self.n1))
hold_ms = max(0, int(self.delay))
decay_ms = max(0, int(self.n2))
base = self.colors[0] if len(self.colors) > 0 else (255, 255, 255)
full_brightness = max(0, min(255, int(self.brightness)))
# Attack phase (0 -> full)
if attack_ms > 0:
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < attack_ms:
elapsed = utime.ticks_diff(utime.ticks_ms(), start)
frac = elapsed / attack_ms if attack_ms > 0 else 1.0
b = int(full_brightness * frac)
self.fill(self.apply_brightness(base, brightness_override=b))
else:
self.fill(self.apply_brightness(base, brightness_override=full_brightness))
# Hold phase
if hold_ms > 0:
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < hold_ms:
pass
# Decay phase (full -> 0)
if decay_ms > 0:
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < decay_ms:
elapsed = utime.ticks_diff(utime.ticks_ms(), start)
frac = 1.0 - (elapsed / decay_ms if decay_ms > 0 else 1.0)
if frac < 0:
frac = 0
b = int(full_brightness * frac)
self.fill(self.apply_brightness(base, brightness_override=b))
# Ensure off at the end and stop auto-run
self.fill((0, 0, 0))
self.run = False
return self.delay
def rainbow(self):
# Wheel function to map 0-255 to RGB
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)
step_rate = max(1, int(self.n3))
# Use controller's step for synchronization, scaled for rainbow cycling
rainbow_step = (self.step * step_rate) % 256
for i in range(self.num_leds):
rc_index = (i * 256 // max(1, self.num_leds)) + rainbow_step
self.n[i] = self.apply_brightness(wheel(rc_index & 255))
self.n.write()
# Don't update internal step - use controller's step for sync
return max(1, int(self.delay // 5))
def specto(self):
# Light up LEDs from 0 up to n1 (exclusive) and turn the rest off
count = int(self.n1)
if count < 0:
count = 0
if count > self.num_leds:
count = self.num_leds
color = self.apply_brightness(self.colors[0] if len(self.colors) > 0 else (255, 255, 255))
for i in range(self.num_leds):
self.n[i] = color if i < count else (0, 0, 0)
self.n.write()
return self.delay
def radiate(self):
# Radiate outward from origins spaced every n1 LEDs, stepping each ring by self.delay
sep = max(1, int(self.n1) if self.n1 else 1)
color = self.apply_brightness(self.colors[0] if len(self.colors) > 0 else (255, 255, 255))
# Start with strip off
self.fill((0, 0, 0))
origins = list(range(0, self.num_leds, sep))
radius = 0
lit_total = 0
while True:
drew_any = False
for o in origins:
left = o - radius
right = o + radius
if 0 <= left < self.num_leds:
if self.n[left] == (0, 0, 0):
lit_total += 1
self.n[left] = color
drew_any = True
if 0 <= right < self.num_leds:
if self.n[right] == (0, 0, 0):
lit_total += 1
self.n[right] = color
drew_any = True
self.n.write()
# If we didn't draw anything new, we've reached beyond edges
if not drew_any:
break
# If all LEDs are now lit, immediately proceed to dark sweep
if lit_total >= self.num_leds:
break
# wait self.delay ms before next ring
start = utime.ticks_us()
while utime.ticks_diff(utime.ticks_us(), start) < self.delay:
pass
radius += 1
# Radiate back out (darkness outward): turn off from center to edges
last_radius = max(0, radius - 1)
for r in range(0, last_radius + 1):
for o in origins:
left = o - r
right = o + r
if 0 <= left < self.num_leds:
self.n[left] = (0, 0, 0)
if 0 <= right < self.num_leds:
self.n[right] = (0, 0, 0)
self.n.write()
start = utime.ticks_us()
while utime.ticks_diff(utime.ticks_us(), start) < self.delay:
pass
# ensure all LEDs are off at completion
self.fill((0, 0, 0))
# mark complete so scheduler won't auto-run again until re-selected
self.run = False
return self.delay
def segmented_movement(self):
"""
Segmented movement pattern that alternates forward and backward.
Parameters:
n1: Number of LEDs per segment
n2: Spacing between segments (currently unused)
n3: Forward movement steps per beat
n4: Backward movement steps per beat
Movement: Alternates between moving forward n3 steps and backward n4 steps each beat.
"""
try:
# Get parameters
segment_length = max(1, int(self.n1)) if hasattr(self, 'n1') else 3
segment_spacing = max(0, int(self.n2)) if hasattr(self, 'n2') else 2
forward_step = max(0, int(self.n3)) if hasattr(self, 'n3') else 1
backward_step = max(0, int(self.n4)) if hasattr(self, 'n4') else 0
# Set the current position to the color # Initialize position tracking if not exists
self.n[self.scanner_position] = self.apply_brightness(self.color1) if not hasattr(self, '_sm_position'):
self._sm_position = 0
self._sm_last_step = -1
# Apply the color transition # Check if this is a new beat (step changed)
transition_factor = (self.pattern_step * 100) / self.transition_duration if self.step != self._sm_last_step:
if transition_factor > 100: # Alternate between forward and backward movement
transition_factor = 100 if self.step % 2 == 0:
color = self.interpolate_color(self.color1, self.color2, transition_factor / 100) # Even steps: move forward (if n3 > 0)
self.n[self.scanner_position] = self.apply_brightness(color) if forward_step > 0:
self._sm_position += forward_step
direction = "FWD"
elif backward_step > 0:
# If no forward, still move backward
self._sm_position -= backward_step
direction = "BWD"
else:
direction = "NONE"
else:
# Odd steps: move backward (if n4 > 0)
if backward_step > 0:
self._sm_position -= backward_step
direction = "BWD"
elif forward_step > 0:
# If no backward, still move forward
self._sm_position += forward_step
direction = "FWD"
else:
direction = "NONE"
# Wrap position around strip length
strip_length = self.num_leds + segment_length
self._sm_position = self._sm_position % strip_length
# Update last step
self._sm_last_step = self.step
# DEBUG: Print every beat
if self.step % 5 == 0:
print(f"SM: step={self.step}, dir={direction}, n3={forward_step}, n4={backward_step}, pos={self._sm_position}")
# Clear all LEDs
self.fill((0, 0, 0))
# Get color
color = self.apply_brightness(self.colors[0])
# Calculate segment width (segment + spacing)
segment_width = segment_length + segment_spacing
# Draw multiple segments across the strip
if segment_width > 0:
base_position = int(self._sm_position) % segment_width
# Draw segments starting from base_position
current_pos = base_position
while current_pos < self.num_leds:
# Draw segment from current_pos to current_pos + segment_length
segment_end = min(current_pos + segment_length, self.num_leds)
for i in range(max(0, current_pos), segment_end):
self.n[i] = color
# Move to next segment position
current_pos += segment_width
# Handle wrap-around: draw segments that start before 0
wrap_position = base_position - segment_width
while wrap_position > -segment_length:
if wrap_position < 0:
# Partial segment at start
segment_end = min(wrap_position + segment_length, self.num_leds)
for i in range(0, segment_end):
self.n[i] = color
wrap_position -= segment_width
self.n.write() self.n.write()
self.pattern_step += self.delay return self.delay
if self.pattern_step > self.transition_duration:
self.pattern_step = 0 except Exception as e:
# DEBUG: Print error
print(f"SM Error: {e}")
# If anything goes wrong, turn off LEDs and return
self.fill((0, 0, 0))
self.n.write()
return self.delay
self.last_update = current_time
if __name__ == "__main__": if __name__ == "__main__":
p = Patterns(4, 180) import time
p.set_color1((255,0,0)) from machine import WDT
p.set_color2((0,255,0)) wdt = WDT(timeout=2000) # Enable watchdog with a 2 second timeout
#p.set_delay(10) p = Patterns(pin=4, num_leds=60, color1=(255,0,0), color2=(0,0,255), brightness=127, selected="off", delay=100)
try:
while True: print(p.colors, p.brightness)
for key in p.patterns:
print(key) tests = [
p.select(key) ("off", {"duration_ms": 500}),
for _ in range(2000): ("on", {"duration_ms": 500}),
p.tick() ("color_wipe", {"delay": 200, "duration_ms": 1000}),
utime.sleep_ms(1) ("rainbow_cycle", {"delay": 100, "duration_ms": 2500}),
except KeyboardInterrupt: ("theater_chase", {"on_width": 3, "off_width": 3, "delay": 1000, "duration_ms": 2500}),
p.fill((0, 0, 0)) ("blink", {"delay": 500, "duration_ms": 2000}),
("color_transition", {"delay": 150, "colors": [(255,0,0),(0,255,0),(0,0,255)], "duration_ms": 5000}),
("flicker", {"delay": 100, "duration_ms": 2000}),
("scanner", {"delay": 150, "duration_ms": 2500}),
("bidirectional_scanner", {"delay": 50, "duration_ms": 2500}),
("fill_range", {"n1": 10, "n2": 20, "delay": 500, "duration_ms": 2000}),
("n_chase", {"n1": 5, "n2": 5, "delay": 2000, "duration_ms": 2500}),
("alternating", {"n1": 5, "n2": 5, "delay": 500, "duration_ms": 2500}),
("pulse", {"delay": 100, "duration_ms": 700}),
]
print("\n--- Running pattern self-test ---")
for name, cfg in tests:
print(f"\nPattern: {name}")
# apply simple config helpers
if "delay" in cfg:
p.set_delay(cfg["delay"])
if "on_width" in cfg:
p.set_on_width(cfg["on_width"])
if "off_width" in cfg:
p.set_off_width(cfg["off_width"])
if "n1" in cfg and "n2" in cfg:
p.set_fill_range(cfg["n1"], cfg["n2"])
if "colors" in cfg:
p.set_colors(cfg["colors"])
p.select(name)
# run per configured duration using absolute-scheduled tick(next_due_ms)
start = utime.ticks_ms()
duration_ms = cfg["duration_ms"]
delay = cfg.get("delay", 0)
next_due = utime.ticks_ms() - 1 # force immediate first call
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
delay = p.tick(delay)
wdt.feed()
print("\n--- Test routine finished ---")

70
src/patterns_base.py Normal file
View File

@@ -0,0 +1,70 @@
from machine import Pin
from neopixel import NeoPixel
import utime
class PatternBase:
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
self.pattern_step = 0
self.last_update = utime.ticks_ms()
self.delay = delay
self.brightness = brightness
self.patterns = {}
self.selected = selected
self.run = True
# 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 (moved from Patterns to PatternBase as they are generic enough)
self.scanner_direction = 1 # 1 for forward, -1 for backward
self.scanner_tail_length = 3 # Number of trailing pixels
# Store last pattern-returned delay to use for subsequent gating
self._last_returned_delay = None
def update_num_leds(self, pin, num_leds):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
self.num_leds = num_leds
def set_color(self, num, color):
if 0 <= num < len(self.colors):
self.colors[num] = color
elif num == len(self.colors): # Allow setting a new color at the end
self.colors.append(color)
return True
return False
def del_color(self, num):
if 0 <= num < len(self.colors):
del self.colors[num]
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 write(self):
self.n.write()
def fill(self, color=None):
fill_color = color if color is not None else self.colors[0]
self.n.fill(fill_color)
self.n.write()
def off(self):
self.fill((0, 0, 0))
def on(self):
self.fill(self.apply_brightness(self.colors[0]))

View File

@@ -9,16 +9,14 @@ class Settings(dict):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.load() # Load settings from file during initialization self.load() # Load settings from file during initialization
if self.get("color_order", "rgb") == "rbg": self.color_order = (1, 5, 3)
else: self.color_order = (1, 3, 5)
def set_defaults(self): def set_defaults(self):
self["num_leds"] = 50 self["led_pin"] = 10
self["pattern"] = "on" self["num_leds"] = 119
self["color1"] = "#00ff00" self["color_order"] = "rgb"
self["color2"] = "#ff0000" self["name"] = f"104"
self["delay"] = 100
self["brightness"] = 10
self["name"] = f"led-{ubinascii.hexlify(wifi.get_mac()).decode()}"
self["ap_password"] = ""
def save(self): def save(self):
try: try:
@@ -38,6 +36,73 @@ class Settings(dict):
except Exception as e: except Exception as e:
print(f"Error loading settings") print(f"Error loading settings")
self.set_defaults() self.set_defaults()
self.save()
def set_settings(self, data, patterns, save):
try:
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 == "on_width":
on_width = int(data["on_width"])
patterns.set_on_width(on_width)
elif key == "off_width":
off_width = int(data["off_width"])
on_width = int(data.get("on_width", self["on_width"]))
patterns.set_on_off_width(on_width, off_width)
elif key == "n1":
n1 = int(data["n1"])
n2 = int(data.get("n2", patterns.n2))
patterns.set_fill_range(n1, n2)
elif key == "n2":
n2 = int(data["n2"])
n1 = int(data.get("n1", patterns.n1))
patterns.set_fill_range(n1, n2)
elif key == "oneshot":
oneshot_value = bool(data["oneshot"])
patterns.set_oneshot(oneshot_value)
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 Exception as e:
print(f"An unexpected error occurred in set_settings: {e}")
return "Bad request", 400
# Example usage # Example usage
def main(): def main():
@@ -52,42 +117,7 @@ def main():
print(f"Loaded number of LEDs: {new_settings['num_leds']}") print(f"Loaded number of LEDs: {new_settings['num_leds']}")
print(settings) print(settings)
def set_settings(raw_json, settings, patterns):
try:
data = json.loads(raw_json)
print(data)
for key, value in data.items():
print(key, value)
if key == "color1":
patterns.set_color1(tuple(int(value[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
elif key == "color2":
patterns.set_color2(tuple(int(value[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
elif key == "num_leds":
patterns.update_num_leds(4, 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":
settings[key] = value
settings.save()
machine.reset()
elif key == "sync":
patterns.sync()
return "OK", 200
else:
return "Invalid key", 400
settings[key] = value
settings.save()
return "OK", 200
except (KeyError, ValueError):
return "Bad request", 400
# Run the example # Run the example
if __name__ == "__main__": if __name__ == "__main__":

View File

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

View File

@@ -2,7 +2,72 @@ let delayTimeout;
let brightnessTimeout; let brightnessTimeout;
let colorTimeout; let colorTimeout;
let color2Timeout; 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) { async function post(path, data) {
console.log(`POST to ${path}`, data); console.log(`POST to ${path}`, data);
try { try {
@@ -11,7 +76,7 @@ async function post(path, data) {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(data), // Convert data to JSON string body: JSON.stringify(data),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
@@ -27,7 +92,7 @@ async function get(path) {
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
} }
return await response.json(); // Assuming you are expecting JSON response return await response.json();
} catch (error) { } catch (error) {
console.error("Error during GET request:", error); console.error("Error during GET request:", error);
} }
@@ -36,53 +101,77 @@ async function get(path) {
async function updateColor(event) { async function updateColor(event) {
event.preventDefault(); event.preventDefault();
clearTimeout(colorTimeout); clearTimeout(colorTimeout);
colorTimeout = setTimeout(async function () { colorTimeout = setTimeout(function () {
const color = document.getElementById("color").value; const color = document.getElementById("color").value;
await post("settings", { color1: color }); // Send as JSON sendWebSocketData({ color1: color });
}, 500); }, 500);
} }
async function updateColor2(event) { async function updateColor2(event) {
event.preventDefault(); event.preventDefault();
clearTimeout(color2Timeout); clearTimeout(color2Timeout);
color2Timeout = setTimeout(async function () { color2Timeout = setTimeout(function () {
const color = document.getElementById("color2").value; const color = document.getElementById("color2").value;
await post("/settings", { color2: color }); // Send as JSON sendWebSocketData({ color2: color });
}, 500); }, 500);
} }
async function updatePattern(pattern) { async function updatePattern(pattern) {
await post("/settings", { pattern: pattern }); // Send as JSON sendWebSocketData({ pattern: pattern });
} }
async function updateBrightness(event) { async function updateBrightness(event) {
event.preventDefault(); event.preventDefault();
clearTimeout(brightnessTimeout); clearTimeout(brightnessTimeout);
brightnessTimeout = setTimeout(async function () { brightnessTimeout = setTimeout(function () {
const brightness = document.getElementById("brightness").value; const brightness = document.getElementById("brightness").value;
await post("/settings", { brightness: brightness }); // Send as JSON sendWebSocketData({ brightness: brightness });
}, 500); }, 500);
} }
async function updateDelay(event) { async function updateDelay(event) {
event.preventDefault(); event.preventDefault();
clearTimeout(delayTimeout); clearTimeout(delayTimeout);
delayTimeout = setTimeout(async function () { delayTimeout = setTimeout(function () {
const delay = document.getElementById("delay").value; const delay = document.getElementById("delay").value;
await post("/settings", { delay: delay }); // Send as JSON sendWebSocketData({ delay: delay });
}, 500); }, 500);
} }
async function updateNumLeds(event) { async function updateNumLeds(event) {
event.preventDefault(); event.preventDefault();
const numLeds = document.getElementById("num_leds").value; const numLeds = document.getElementById("num_leds").value;
await post("/settings", { num_leds: parseInt(numLeds) }); // Send as JSON sendWebSocketData({ num_leds: parseInt(numLeds) });
} }
async function updateName(event) { async function updateName(event) {
event.preventDefault(); event.preventDefault();
const name = document.getElementById("name").value; const name = document.getElementById("name").value;
await post("/settings", { name: name }); // Send as JSON 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) { function createPatternButtons(patterns) {
@@ -91,7 +180,7 @@ function createPatternButtons(patterns) {
patterns.forEach((pattern) => { patterns.forEach((pattern) => {
const button = document.createElement("button"); const button = document.createElement("button");
button.type = "button"; // Use 'button' instead of 'submit' button.type = "button";
button.textContent = pattern; button.textContent = pattern;
button.value = pattern; button.value = pattern;
button.addEventListener("click", async function (event) { button.addEventListener("click", async function (event) {
@@ -103,6 +192,12 @@ function createPatternButtons(patterns) {
} }
document.addEventListener("DOMContentLoaded", async function () { 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("color").addEventListener("input", updateColor);
document.getElementById("color2").addEventListener("input", updateColor2); document.getElementById("color2").addEventListener("input", updateColor2);
document.getElementById("delay").addEventListener("input", updateDelay); document.getElementById("delay").addEventListener("input", updateDelay);
@@ -113,11 +208,17 @@ document.addEventListener("DOMContentLoaded", async function () {
.getElementById("num_leds_form") .getElementById("num_leds_form")
.addEventListener("submit", updateNumLeds); .addEventListener("submit", updateNumLeds);
document.getElementById("name_form").addEventListener("submit", updateName); 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("delay").addEventListener("touchend", updateDelay);
document document
.getElementById("brightness") .getElementById("brightness")
.addEventListener("touchend", updateBrightness); .addEventListener("touchend", updateBrightness);
document.getElementById("rgb").addEventListener("change", handleRadioChange);
document.getElementById("rbg").addEventListener("change", handleRadioChange);
document.querySelectorAll(".pattern_button").forEach((button) => { document.querySelectorAll(".pattern_button").forEach((button) => {
console.log(button.value); console.log(button.value);
button.addEventListener("click", async (event) => { button.addEventListener("click", async (event) => {

View File

@@ -4,12 +4,12 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LED Control</title> <title>{{settings['name']}}</title>
<script src="static/main.js"></script> <script src="static/main.js"></script>
<link rel="stylesheet" href="static/main.css" /> <link rel="stylesheet" href="static/main.css" />
</head> </head>
<body> <body>
<h1>Control LEDs</h1> <h1>{{settings['name']}}</h1>
<button onclick="selectControls()">Controls</button> <button onclick="selectControls()">Controls</button>
<button onclick="selectSettings()">Settings</button> <button onclick="selectSettings()">Settings</button>
@@ -79,6 +79,16 @@
/> />
<input type="submit" value="Update Name" /> <input type="submit" value="Update Name" />
</form> </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 --> <!-- Separate form for submitting num_leds -->
<form id="num_leds_form" method="post" action="/num_leds"> <form id="num_leds_form" method="post" action="/num_leds">
<label for="num_leds">Number of LEDs:</label> <label for="num_leds">Number of LEDs:</label>
@@ -90,7 +100,25 @@
/> />
<input type="submit" value="Update Number of LEDs" /> <input type="submit" value="Update Number of LEDs" />
</form> </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> <p>Mac address: {{mac}}</p>
</div> </div>
<div id="connection-status"></div>
</body> </body>
</html> </html>

View File

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

View File

@@ -2,7 +2,6 @@ from microdot import Microdot, send_file, Response
from microdot.utemplate import Template from microdot.utemplate import Template
from microdot.websocket import with_websocket from microdot.websocket import with_websocket
import machine import machine
from settings import set_settings
import wifi import wifi
import json import json
@@ -24,18 +23,21 @@ def web(settings, patterns):
@app.post("/settings") @app.post("/settings")
def settings_handler(request): def settings_handler(request):
return set_settings(request.body.decode('utf-8'), settings, patterns) # 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")
@app.route("/external")
@with_websocket @with_websocket
async def ws(request, ws): async def ws(request, ws):
patterns.select("external")
while True: while True:
data = await ws.receive() data = await ws.receive()
print(data) if data:
for i in range(min(patterns.num_leds, int(len(data)/3))):
patterns.set(i, (data[i*3], data[i*3+1], data[i*3+2])) # Process the received data
patterns.write() _, status_code = settings.set_settings(json.loads(data), patterns, True)
#await ws.send(status_code)
else:
break
return app return app

View File

@@ -1,18 +1,16 @@
import network import network
from machine import Pin
from time import sleep from time import sleep
import ubinascii
from settings import Settings
def connect(ssid, password, ip, gateway): def connect(ssid, password, ip, gateway):
if ssid is None or password is None:
print("Missing ssid or password")
return None
try: try:
sta_if = network.WLAN(network.STA_IF) sta_if = network.WLAN(network.STA_IF)
if ip is not None and gateway is not None:
sta_if.ifconfig((ip, '255.255.255.0', gateway, '1.1.1.1'))
if not sta_if.isconnected(): 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...') print('connecting to network...')
sta_if.active(True) sta_if.active(True)
sta_if.connect(ssid, password) sta_if.connect(ssid, password)
@@ -26,21 +24,16 @@ def connect(ssid, password, ip, gateway):
return None return None
def ap(password): def ap(ssid, password):
ap_if = network.WLAN(network.AP_IF) ap_if = network.WLAN(network.AP_IF)
ap_mac = ap_if.config('mac') ap_mac = ap_if.config('mac')
ssid = f"led-{ubinascii.hexlify(ap_mac).decode()}"
print(ssid) print(ssid)
ap_if.active(True) ap_if.active(True)
ap_if.config(essid=ssid, password="qwerty1234") ap_if.config(essid=ssid, password=password)
ap_if.active(False) ap_if.active(False)
ap_if.active(True) ap_if.active(True)
print(ap_if.ifconfig()) print(ap_if.ifconfig())
def get_mac():
ap_if = network.WLAN(network.AP_IF)
return ap_if.config('mac')

158
test/main.py Normal file
View File

@@ -0,0 +1,158 @@
import asyncio
import json
import argparse
import signal
try:
import websockets # type: ignore
except Exception as e:
print("Please install websockets: pip install websockets")
raise
WS_URI = "ws://192.168.4.1/ws"
# Default pattern suite aligned with current firmware patterns
PATTERN_SUITE = [
{"pattern": "flicker", "delay": 80, "iterations": 30, "repeat_delay": 80, "colors": ["#ffaa00"]},
{"pattern": "fill_range", "n1": 10, "n2": 20, "delay": 400, "iterations": 1, "repeat_delay": 500, "colors": ["#888888"]},
{"pattern": "n_chase", "n1": 5, "n2": 5, "delay": 250, "iterations": 40, "repeat_delay": 120, "colors": ["#00ff88"]},
{"pattern": "alternating", "n1": 6, "n2": 6, "delay": 300, "iterations": 20, "repeat_delay": 300, "colors": ["#ff8800"]},
{"pattern": "pulse", "delay": 200, "iterations": 6, "repeat_delay": 300, "colors": ["#ffffff"]},
]
def build_message(
pattern: str,
n: int | None = None,
delay: int | None = None,
colors: list[str] | None = None,
brightness: int | None = None,
num_leds: int | None = None,
n1: int | None = None,
n2: int | None = None,
name: str = "0",
pattern_step: int | None = None,
):
settings: dict[str, object] = {
"pattern": pattern,
}
if n is not None:
settings["n"] = n
if delay is not None:
settings["delay"] = delay
if colors is not None:
settings["colors"] = colors
if brightness is not None:
settings["brightness"] = brightness
if num_leds is not None:
settings["num_leds"] = num_leds
if n1 is not None:
settings["n1"] = n1
if n2 is not None:
settings["n2"] = n2
if pattern_step is not None:
settings["pattern_step"] = pattern_step
# ESP-NOW-style nested payload keyed by name (e.g., "0")
return {name: settings}
async def send_once(uri: str, payload: dict, hold_ms: int | None = None):
async with websockets.connect(uri) as ws:
await ws.send(json.dumps(payload))
if hold_ms and hold_ms > 0:
await asyncio.sleep(hold_ms / 1000)
async def run_suite(uri: str):
async with websockets.connect(uri) as ws:
for cfg in PATTERN_SUITE:
iterations = int(cfg.get("iterations", 10))
interval_ms = int(cfg.get("interval_ms", cfg.get("delay", 100) or 100))
repeat_ms = int(cfg.get("repeat_delay", interval_ms))
for i in range(iterations):
msg = build_message(
cfg.get("pattern", "off"),
i,
delay=cfg.get("delay"),
colors=cfg.get("colors"),
brightness=cfg.get("brightness", 127),
num_leds=cfg.get("num_leds"),
n1=cfg.get("n1"),
n2=cfg.get("n2"),
name=cfg.get("name", "0"),
pattern_step=cfg.get("pattern_step"),
)
print(msg)
await ws.send(json.dumps(msg))
await asyncio.sleep(repeat_ms / 1000)
def _parse_args():
p = argparse.ArgumentParser(description="WebSocket LED pattern tester")
p.add_argument("--uri", default=WS_URI, help="WebSocket URI, default ws://192.168.4.1/ws")
p.add_argument("--pattern", help="Single pattern to send (overrides suite)")
p.add_argument("--delay", type=int, help="Delay ms")
p.add_argument("--brightness", type=int, help="Brightness 0-255")
p.add_argument("--num-leds", type=int, help="Number of LEDs")
p.add_argument("--colors", nargs="*", help="Hex colors like #ff0000 #00ff00")
p.add_argument("--on-width", type=int)
p.add_argument("--off-width", type=int)
p.add_argument("--n1", type=int)
p.add_argument("--n2", type=int)
p.add_argument("--name", default="0", help="Target name key for nested payload (default: 0)")
p.add_argument("--iterations", type=int, help="How many cycles/messages to send")
p.add_argument("--interval", type=int, help="Interval between messages in ms (default: delay or 100)")
p.add_argument("--repeat-delay", dest="repeat_delay", type=int, help="Delay between repeats in ms (overrides --interval if set)")
p.add_argument("--hold", type=int, default=1500, help="Hold ms for single send")
return p.parse_args()
def _setup_sigint(loop: asyncio.AbstractEventLoop):
for sig in (signal.SIGINT, signal.SIGTERM):
try:
loop.add_signal_handler(sig, loop.stop)
except NotImplementedError:
pass
async def main_async():
args = _parse_args()
if args.pattern:
iterations = int(args.iterations or 1)
interval_ms = int(args.interval or (args.delay if args.delay is not None else 100))
repeat_ms = int(args.repeat_delay or interval_ms)
async with websockets.connect(args.uri) as ws:
for i in range(iterations):
msg = build_message(
pattern=args.pattern,
n=i,
delay=args.delay,
colors=args.colors,
brightness=args.brightness,
num_leds=args.num_leds,
n1=args.n1,
n2=args.n2,
name=args.name,
)
print(msg)
await ws.send(json.dumps(msg))
await asyncio.sleep(repeat_ms / 1000)
else:
await run_suite(args.uri)
def main():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
_setup_sigint(loop)
try:
loop.run_until_complete(main_async())
finally:
try:
loop.run_until_complete(asyncio.sleep(0))
except Exception:
pass
loop.close()
if __name__ == "__main__":
main()