Initial commit
This commit is contained in:
188
.gitignore
vendored
188
.gitignore
vendored
@@ -1,176 +1,30 @@
|
|||||||
# ---> Python
|
# Build files
|
||||||
# Byte-compiled / optimized / DLL files
|
build/
|
||||||
|
sdkconfig
|
||||||
|
sdkconfig.old
|
||||||
|
|
||||||
|
# Binary files
|
||||||
|
*.bin
|
||||||
|
|
||||||
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
.Python
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# UV
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
#uv.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
*.egg-info/
|
||||||
env.bak/
|
dist/
|
||||||
venv.bak/
|
build/
|
||||||
|
|
||||||
# Spyder project settings
|
# IDE
|
||||||
.spyderproject
|
.vscode/
|
||||||
.spyproject
|
.idea/
|
||||||
|
*.swp
|
||||||
# Rope project settings
|
*.swo
|
||||||
.ropeproject
|
*~
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
# Ruff stuff:
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# PyPI configuration file
|
|
||||||
.pypirc
|
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|||||||
22
Pipfile
Normal file
22
Pipfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
mpremote = "*"
|
||||||
|
pyserial = "*"
|
||||||
|
esptool = "*"
|
||||||
|
watchfiles = "*"
|
||||||
|
fastapi = "*"
|
||||||
|
uvicorn = "*"
|
||||||
|
flask = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3"
|
||||||
|
|
||||||
|
[scripts]
|
||||||
|
dev = 'watchfiles "./dev.py /dev/ttyACM0 src reset follow"'
|
||||||
|
web = "uvicorn tool:app --host 0.0.0.0 --port 8080"
|
||||||
37
README.md
37
README.md
@@ -1,2 +1,37 @@
|
|||||||
# led-driver
|
# LED Driver - MicroPython
|
||||||
|
|
||||||
|
MicroPython-based LED driver application for ESP32 microcontrollers.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- MicroPython firmware installed on ESP32
|
||||||
|
- USB cable for programming
|
||||||
|
- Python 3 with pipenv
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pipenv install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Deploy to device:
|
||||||
|
```bash
|
||||||
|
pipenv run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
led-driver/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.py # Main application code
|
||||||
|
│ ├── patterns.py # LED pattern implementations
|
||||||
|
│ ├── patterns_base.py # Base pattern class
|
||||||
|
│ ├── settings.py # Settings management
|
||||||
|
│ └── p2p.py # Peer-to-peer communication
|
||||||
|
├── test/ # Pattern tests
|
||||||
|
├── web_app.py # Web interface
|
||||||
|
├── dev.py # Development tools
|
||||||
|
└── Pipfile # Python dependencies
|
||||||
|
```
|
||||||
|
|||||||
132
dev.py
Executable file
132
dev.py
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import serial
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
|
||||||
|
def upload_src(port):
|
||||||
|
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":"], cwd="src")
|
||||||
|
|
||||||
|
def upload_lib(port):
|
||||||
|
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":"])
|
||||||
|
|
||||||
|
def list_files(port):
|
||||||
|
subprocess.call(["mpremote", "connect", port, "fs", "ls", ":"])
|
||||||
|
|
||||||
|
def reset_device(port):
|
||||||
|
with serial.Serial(port, baudrate=115200) as ser:
|
||||||
|
ser.write(b'\x03\x03\x04')
|
||||||
|
|
||||||
|
def follow_serial(port):
|
||||||
|
with serial.Serial(port, baudrate=115200) as ser:
|
||||||
|
while True:
|
||||||
|
if ser.in_waiting > 0:
|
||||||
|
data = ser.readline().decode('utf-8').strip()
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
def clean_settings(port):
|
||||||
|
subprocess.call(["mpremote", "connect", port, "fs", "rm", ":/settings.json"])
|
||||||
|
|
||||||
|
def flash_firmware(port):
|
||||||
|
# Find MicroPython firmware binary
|
||||||
|
firmware_files = glob.glob("*.bin")
|
||||||
|
if not firmware_files:
|
||||||
|
print("Error: No .bin firmware file found in current directory")
|
||||||
|
print("Please download MicroPython firmware and place it in the project directory")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
firmware = firmware_files[0]
|
||||||
|
if len(firmware_files) > 1:
|
||||||
|
print(f"Warning: Multiple .bin files found, using: {firmware}")
|
||||||
|
|
||||||
|
print(f"Flashing MicroPython firmware: {firmware}")
|
||||||
|
print("Erasing flash...")
|
||||||
|
subprocess.call(["esptool.py", "--port", port, "erase_flash"])
|
||||||
|
|
||||||
|
print(f"Writing firmware to flash...")
|
||||||
|
subprocess.call([
|
||||||
|
"esptool.py",
|
||||||
|
"--port", port,
|
||||||
|
"--baud", "460800",
|
||||||
|
"write_flash", "0",
|
||||||
|
firmware
|
||||||
|
])
|
||||||
|
print("Flash complete!")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
port = "/dev/ttyACM0"
|
||||||
|
commands = []
|
||||||
|
i = 1
|
||||||
|
|
||||||
|
# Parse arguments manually to preserve order
|
||||||
|
while i < len(sys.argv):
|
||||||
|
arg = sys.argv[i]
|
||||||
|
if arg in ["-p", "--port"]:
|
||||||
|
if i + 1 < len(sys.argv):
|
||||||
|
port = sys.argv[i + 1]
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
print(f"Error: {arg} requires a port argument")
|
||||||
|
sys.exit(1)
|
||||||
|
elif arg in ["-s", "--src"]:
|
||||||
|
commands.append(("src", upload_src))
|
||||||
|
i += 1
|
||||||
|
elif arg in ["-r", "--reset"]:
|
||||||
|
commands.append(("reset", reset_device))
|
||||||
|
i += 1
|
||||||
|
elif arg in ["-f", "--follow"]:
|
||||||
|
commands.append(("follow", follow_serial))
|
||||||
|
i += 1
|
||||||
|
elif arg == "--lib":
|
||||||
|
commands.append(("lib", upload_lib))
|
||||||
|
i += 1
|
||||||
|
elif arg == "--ls":
|
||||||
|
commands.append(("ls", list_files))
|
||||||
|
i += 1
|
||||||
|
elif arg == "--clean":
|
||||||
|
commands.append(("clean", clean_settings))
|
||||||
|
i += 1
|
||||||
|
elif arg == "--flash":
|
||||||
|
commands.append(("flash", flash_firmware))
|
||||||
|
i += 1
|
||||||
|
elif arg in ["-h", "--help"]:
|
||||||
|
print("LED Driver development tools")
|
||||||
|
print("\nUsage:")
|
||||||
|
print(" ./dev.py [-p PORT] [FLAGS...]")
|
||||||
|
print("\nFlags:")
|
||||||
|
print(" -p, --port PORT Serial port (default: /dev/ttyACM0)")
|
||||||
|
print(" -s, --src Upload src directory")
|
||||||
|
print(" -r, --reset Reset device")
|
||||||
|
print(" -f, --follow Follow serial output")
|
||||||
|
print(" --lib Upload lib directory")
|
||||||
|
print(" --ls List files on device")
|
||||||
|
print(" --clean Remove settings.json from device")
|
||||||
|
print(" --flash Flash MicroPython firmware")
|
||||||
|
print("\nExamples:")
|
||||||
|
print(" ./dev.py -p /dev/ttyACM0 -s -r -f")
|
||||||
|
print(" ./dev.py --flash -s -r")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print(f"Error: Unknown argument: {arg}")
|
||||||
|
print("Use -h or --help for usage information")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Execute commands in the order they were given
|
||||||
|
if not commands:
|
||||||
|
print("No commands specified. Use -h or --help for usage information.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for cmd_name, cmd_func in commands:
|
||||||
|
if cmd_name == "reset":
|
||||||
|
print("Resetting device...")
|
||||||
|
elif cmd_name == "follow":
|
||||||
|
print("Following serial output (Ctrl+C to exit)...")
|
||||||
|
elif cmd_name == "flash":
|
||||||
|
pass # flash_firmware prints its own messages
|
||||||
|
else:
|
||||||
|
print(f"{cmd_name.capitalize()}...")
|
||||||
|
cmd_func(port)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
34
src/main.py
Normal file
34
src/main.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from settings import Settings
|
||||||
|
from machine import WDT
|
||||||
|
from espnow import ESPNow
|
||||||
|
import network
|
||||||
|
from patterns import Patterns
|
||||||
|
import json
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
print(settings)
|
||||||
|
|
||||||
|
patterns = Patterns(settings["led_pin"], settings["num_leds"], selected=settings["pattern"])
|
||||||
|
patterns.colors = [(8,0,0)]
|
||||||
|
patterns.select("rainbow")
|
||||||
|
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
wdt.feed()
|
||||||
|
|
||||||
|
sta_if = network.WLAN(network.STA_IF)
|
||||||
|
sta_if.active(True)
|
||||||
|
sta_if.disconnect()
|
||||||
|
sta_if.config(channel=1)
|
||||||
|
e = ESPNow()
|
||||||
|
e.active(True)
|
||||||
|
|
||||||
|
|
||||||
|
while True:
|
||||||
|
wdt.feed()
|
||||||
|
patterns.tick()
|
||||||
|
if e.any():
|
||||||
|
host, msg = e.recv()
|
||||||
|
data = json.loads(msg)
|
||||||
|
if settings.get("name") in data.get("names", []):
|
||||||
|
settings.set_settings(data.get("settings", {}), patterns, data.get("save", False))
|
||||||
|
|
||||||
16
src/p2p.py
Normal file
16
src/p2p.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import asyncio
|
||||||
|
import aioespnow
|
||||||
|
import json
|
||||||
|
|
||||||
|
async def p2p(settings, patterns):
|
||||||
|
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
|
||||||
|
e.active(True)
|
||||||
|
async for mac, msg in e:
|
||||||
|
try:
|
||||||
|
data = json.loads(msg)
|
||||||
|
except:
|
||||||
|
print(f"Failed to load espnow data {msg}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "names" not in data or settings.get("name") in data.get("names", []):
|
||||||
|
await settings.set_settings(data.get("settings", {}), patterns, data.get("save", False))
|
||||||
322
src/patterns.py
Normal file
322
src/patterns.py
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
import utime
|
||||||
|
from patterns_base import Patterns as PatternsBase
|
||||||
|
|
||||||
|
class Patterns(PatternsBase):
|
||||||
|
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", delay=100):
|
||||||
|
super().__init__(pin, num_leds, color1, color2, brightness, selected, delay)
|
||||||
|
self.auto = True
|
||||||
|
self.step = 0
|
||||||
|
self.patterns = {
|
||||||
|
"off": self.off,
|
||||||
|
"on" : self.on,
|
||||||
|
"blink": self.blink,
|
||||||
|
"rainbow": self.rainbow,
|
||||||
|
"pulse": self.pulse,
|
||||||
|
"transition": self.transition,
|
||||||
|
"chase": self.chase,
|
||||||
|
"circle": self.circle,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def blink(self):
|
||||||
|
state = True # True = on, False = off
|
||||||
|
last_update = utime.ticks_ms()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, last_update) >= self.delay:
|
||||||
|
if state:
|
||||||
|
self.fill(self.apply_brightness(self.colors[0]))
|
||||||
|
else:
|
||||||
|
self.fill((0, 0, 0))
|
||||||
|
state = not state
|
||||||
|
last_update = current_time
|
||||||
|
# Yield once per tick so other logic can run
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def rainbow(self):
|
||||||
|
step = self.step % 256
|
||||||
|
step_amount = max(1, int(self.n1)) # n1 controls step increment
|
||||||
|
|
||||||
|
# If auto is False, run a single step and then stop
|
||||||
|
if not self.auto:
|
||||||
|
for i in range(self.num_leds):
|
||||||
|
rc_index = (i * 256 // self.num_leds) + step
|
||||||
|
self.n[i] = self.apply_brightness(self.wheel(rc_index & 255))
|
||||||
|
self.n.write()
|
||||||
|
# Increment step by n1 for next manual call
|
||||||
|
self.step = (step + step_amount) % 256
|
||||||
|
# Allow tick() to advance the generator once
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
last_update = utime.ticks_ms()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
sleep_ms = max(1, int(self.delay)) # Access delay directly
|
||||||
|
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
|
||||||
|
for i in range(self.num_leds):
|
||||||
|
rc_index = (i * 256 // self.num_leds) + step
|
||||||
|
self.n[i] = self.apply_brightness(self.wheel(rc_index & 255))
|
||||||
|
self.n.write()
|
||||||
|
step = (step + step_amount) % 256
|
||||||
|
self.step = step
|
||||||
|
last_update = current_time
|
||||||
|
# Yield once per tick so other logic can run
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def pulse(self):
|
||||||
|
self.off()
|
||||||
|
|
||||||
|
# Ensure we have at least one color
|
||||||
|
if not self.colors:
|
||||||
|
self.colors = [(255, 255, 255)]
|
||||||
|
|
||||||
|
color_index = 0
|
||||||
|
cycle_start = utime.ticks_ms()
|
||||||
|
|
||||||
|
# State machine based pulse using a single generator loop
|
||||||
|
while True:
|
||||||
|
# Read current timing parameters each cycle so they can be changed live
|
||||||
|
attack_ms = max(0, int(self.n1)) # Attack time in ms
|
||||||
|
hold_ms = max(0, int(self.n2)) # Hold time in ms
|
||||||
|
decay_ms = max(0, int(self.n3)) # Decay time in ms
|
||||||
|
delay_ms = max(0, int(self.delay))
|
||||||
|
|
||||||
|
total_ms = attack_ms + hold_ms + decay_ms + delay_ms
|
||||||
|
if total_ms <= 0:
|
||||||
|
total_ms = 1
|
||||||
|
|
||||||
|
now = utime.ticks_ms()
|
||||||
|
elapsed = utime.ticks_diff(now, cycle_start)
|
||||||
|
|
||||||
|
base_color = self.colors[color_index % len(self.colors)]
|
||||||
|
|
||||||
|
if elapsed < attack_ms and attack_ms > 0:
|
||||||
|
# Attack: fade 0 -> 1
|
||||||
|
factor = elapsed / attack_ms
|
||||||
|
color = tuple(int(c * factor) for c in base_color)
|
||||||
|
self.fill(self.apply_brightness(color))
|
||||||
|
elif elapsed < attack_ms + hold_ms:
|
||||||
|
# Hold: full brightness
|
||||||
|
self.fill(self.apply_brightness(base_color))
|
||||||
|
elif elapsed < attack_ms + hold_ms + decay_ms and decay_ms > 0:
|
||||||
|
# Decay: fade 1 -> 0
|
||||||
|
dec_elapsed = elapsed - attack_ms - hold_ms
|
||||||
|
factor = max(0.0, 1.0 - (dec_elapsed / decay_ms))
|
||||||
|
color = tuple(int(c * factor) for c in base_color)
|
||||||
|
self.fill(self.apply_brightness(color))
|
||||||
|
elif elapsed < total_ms:
|
||||||
|
# Delay phase: LEDs off between pulses
|
||||||
|
self.fill((0, 0, 0))
|
||||||
|
else:
|
||||||
|
# End of cycle, move to next color and restart timing
|
||||||
|
color_index += 1
|
||||||
|
cycle_start = now
|
||||||
|
if not self.auto:
|
||||||
|
break
|
||||||
|
# Skip drawing this tick, start next cycle
|
||||||
|
yield
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Yield once per tick
|
||||||
|
yield
|
||||||
|
|
||||||
|
def transition(self):
|
||||||
|
"""Transition between colors, blending over `delay` ms."""
|
||||||
|
if not self.colors:
|
||||||
|
self.off()
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only one color: just keep it on
|
||||||
|
if len(self.colors) == 1:
|
||||||
|
while True:
|
||||||
|
self.fill(self.apply_brightness(self.colors[0]))
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
color_index = 0
|
||||||
|
start_time = utime.ticks_ms()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if not self.colors:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get current and next color based on live list
|
||||||
|
c1 = self.colors[color_index % len(self.colors)]
|
||||||
|
c2 = self.colors[(color_index + 1) % len(self.colors)]
|
||||||
|
|
||||||
|
duration = max(10, int(self.delay)) # At least 10ms
|
||||||
|
now = utime.ticks_ms()
|
||||||
|
elapsed = utime.ticks_diff(now, start_time)
|
||||||
|
|
||||||
|
if elapsed >= duration:
|
||||||
|
# End of this transition step
|
||||||
|
if not self.auto and color_index >= 0:
|
||||||
|
# One-shot: transition from first to second color only
|
||||||
|
self.fill(self.apply_brightness(c2))
|
||||||
|
break
|
||||||
|
# Auto: move to next pair
|
||||||
|
color_index = (color_index + 1) % len(self.colors)
|
||||||
|
start_time = now
|
||||||
|
yield
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Interpolate between c1 and c2
|
||||||
|
factor = elapsed / duration
|
||||||
|
interpolated = tuple(
|
||||||
|
int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3)
|
||||||
|
)
|
||||||
|
self.fill(self.apply_brightness(interpolated))
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
def chase(self):
|
||||||
|
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating.
|
||||||
|
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)"""
|
||||||
|
if len(self.colors) < 1:
|
||||||
|
# Need at least 1 color
|
||||||
|
return
|
||||||
|
|
||||||
|
segment_length = 0 # Will be calculated in loop
|
||||||
|
position = 0 # Current position offset
|
||||||
|
step_count = 0 # Track which step we're on
|
||||||
|
|
||||||
|
last_update = utime.ticks_ms()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Access colors, delay, and n values directly for live updates
|
||||||
|
if not self.colors:
|
||||||
|
break
|
||||||
|
# If only one color provided, use it for both colors
|
||||||
|
if len(self.colors) < 2:
|
||||||
|
color0 = self.colors[0]
|
||||||
|
color1 = self.colors[0]
|
||||||
|
else:
|
||||||
|
color0 = self.colors[0]
|
||||||
|
color1 = self.colors[1]
|
||||||
|
|
||||||
|
color0 = self.apply_brightness(color0)
|
||||||
|
color1 = self.apply_brightness(color1)
|
||||||
|
|
||||||
|
n1 = max(1, int(self.n1)) # LEDs of color 0
|
||||||
|
n2 = max(1, int(self.n2)) # LEDs of color 1
|
||||||
|
n3 = int(self.n3) # Step movement on odd steps (can be negative)
|
||||||
|
n4 = int(self.n4) # Step movement on even steps (can be negative)
|
||||||
|
|
||||||
|
segment_length = n1 + n2
|
||||||
|
transition_duration = max(10, int(self.delay))
|
||||||
|
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(current_time, last_update) >= transition_duration:
|
||||||
|
# Clear all LEDs
|
||||||
|
self.n.fill((0, 0, 0))
|
||||||
|
|
||||||
|
# Draw repeating pattern starting at position
|
||||||
|
for i in range(self.num_leds):
|
||||||
|
# Calculate position in the repeating segment
|
||||||
|
relative_pos = (i - position) % segment_length
|
||||||
|
if relative_pos < 0:
|
||||||
|
relative_pos = (relative_pos + segment_length) % segment_length
|
||||||
|
|
||||||
|
# Determine which color based on position in segment
|
||||||
|
if relative_pos < n1:
|
||||||
|
self.n[i] = color0
|
||||||
|
else:
|
||||||
|
self.n[i] = color1
|
||||||
|
|
||||||
|
self.n.write()
|
||||||
|
|
||||||
|
# Move position by n3 or n4 on alternate steps
|
||||||
|
if step_count % 2 == 0:
|
||||||
|
position = position + n3
|
||||||
|
else:
|
||||||
|
position = position + n4
|
||||||
|
|
||||||
|
# Wrap position to keep it reasonable
|
||||||
|
max_pos = self.num_leds + segment_length
|
||||||
|
position = position % max_pos
|
||||||
|
if position < 0:
|
||||||
|
position += max_pos
|
||||||
|
|
||||||
|
step_count += 1
|
||||||
|
last_update = current_time
|
||||||
|
|
||||||
|
# Yield once per tick so other logic can run
|
||||||
|
yield
|
||||||
|
|
||||||
|
def circle(self):
|
||||||
|
"""Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4"""
|
||||||
|
head = 0
|
||||||
|
tail = 0
|
||||||
|
|
||||||
|
# Calculate timing
|
||||||
|
head_rate = max(1, int(self.n1)) # n1 = head moves per second
|
||||||
|
tail_rate = max(1, int(self.n3)) # n3 = tail moves per second
|
||||||
|
max_length = max(1, int(self.n2)) # n2 = max length
|
||||||
|
min_length = max(0, int(self.n4)) # n4 = min length
|
||||||
|
|
||||||
|
head_delay = 1000 // head_rate # ms between head movements
|
||||||
|
tail_delay = 1000 // tail_rate # ms between tail movements
|
||||||
|
|
||||||
|
last_head_move = utime.ticks_ms()
|
||||||
|
last_tail_move = utime.ticks_ms()
|
||||||
|
|
||||||
|
phase = "growing" # "growing", "shrinking", or "off"
|
||||||
|
|
||||||
|
while True:
|
||||||
|
current_time = utime.ticks_ms()
|
||||||
|
|
||||||
|
# Clear all LEDs
|
||||||
|
self.n.fill((0, 0, 0))
|
||||||
|
|
||||||
|
# Calculate segment length
|
||||||
|
segment_length = (head - tail) % self.num_leds
|
||||||
|
if segment_length == 0 and head != tail:
|
||||||
|
segment_length = self.num_leds
|
||||||
|
|
||||||
|
# Draw segment from tail to head
|
||||||
|
color = self.apply_brightness(self.colors[0])
|
||||||
|
for i in range(segment_length + 1):
|
||||||
|
led_pos = (tail + i) % self.num_leds
|
||||||
|
self.n[led_pos] = color
|
||||||
|
|
||||||
|
# Move head continuously at n1 LEDs per second
|
||||||
|
if utime.ticks_diff(current_time, last_head_move) >= head_delay:
|
||||||
|
head = (head + 1) % self.num_leds
|
||||||
|
last_head_move = current_time
|
||||||
|
|
||||||
|
# Tail behavior based on phase
|
||||||
|
if phase == "growing":
|
||||||
|
# Growing phase: tail stays at 0 until max length reached
|
||||||
|
if segment_length >= max_length:
|
||||||
|
phase = "shrinking"
|
||||||
|
elif phase == "shrinking":
|
||||||
|
# Shrinking phase: move tail forward at n3 LEDs per second
|
||||||
|
if utime.ticks_diff(current_time, last_tail_move) >= tail_delay:
|
||||||
|
tail = (tail + 1) % self.num_leds
|
||||||
|
last_tail_move = current_time
|
||||||
|
|
||||||
|
# Check if we've reached min length
|
||||||
|
current_length = (head - tail) % self.num_leds
|
||||||
|
if current_length == 0 and head != tail:
|
||||||
|
current_length = self.num_leds
|
||||||
|
|
||||||
|
# For min_length = 0, we need at least 1 LED (the head)
|
||||||
|
if min_length == 0 and current_length <= 1:
|
||||||
|
phase = "off" # All LEDs off for 1 step
|
||||||
|
elif min_length > 0 and current_length <= min_length:
|
||||||
|
phase = "growing" # Cycle repeats
|
||||||
|
else: # phase == "off"
|
||||||
|
# Off phase: all LEDs off for 1 step, then restart
|
||||||
|
tail = head # Reset tail to head position to start fresh
|
||||||
|
phase = "growing"
|
||||||
|
|
||||||
|
self.n.write()
|
||||||
|
|
||||||
|
# Yield once per tick so other logic can run
|
||||||
|
yield
|
||||||
145
src/patterns_base.py
Normal file
145
src/patterns_base.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
from machine import Pin
|
||||||
|
from neopixel import NeoPixel
|
||||||
|
import utime
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Short-key parameter mapping for convenience setters
|
||||||
|
param_mapping = {
|
||||||
|
"pt": "selected",
|
||||||
|
"pa": "selected",
|
||||||
|
"cl": "colors",
|
||||||
|
"br": "brightness",
|
||||||
|
"dl": "delay",
|
||||||
|
"nl": "num_leds",
|
||||||
|
"co": "color_order",
|
||||||
|
"lp": "led_pin",
|
||||||
|
"n1": "n1",
|
||||||
|
"n2": "n2",
|
||||||
|
"n3": "n3",
|
||||||
|
"n4": "n4",
|
||||||
|
"n5": "n5",
|
||||||
|
"n6": "n6",
|
||||||
|
"auto": "auto",
|
||||||
|
}
|
||||||
|
|
||||||
|
class Patterns:
|
||||||
|
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", 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.auto = False
|
||||||
|
self.patterns = {}
|
||||||
|
self.selected = selected
|
||||||
|
# Ensure colors list always starts with at least two for robust transition handling
|
||||||
|
self.colors = [color1, color2] if color1 != color2 else [color1, (255, 255, 255)] # Fallback if initial colors are same
|
||||||
|
if not self.colors: # Ensure at least one color exists
|
||||||
|
self.colors = [(0, 0, 0)]
|
||||||
|
|
||||||
|
self.transition_duration = delay * 50 # Default transition duration
|
||||||
|
self.hold_duration = delay * 10 # Default hold duration at each color
|
||||||
|
self.transition_step = 0 # Current step in the transition
|
||||||
|
self.current_color_idx = 0 # Index of the color currently being held/transitioned from
|
||||||
|
self.current_color = self.colors[self.current_color_idx] # The actual blended color
|
||||||
|
|
||||||
|
self.hold_start_time = utime.ticks_ms() # Time when the current color hold started
|
||||||
|
|
||||||
|
# New attributes for scanner patterns
|
||||||
|
self.scanner_direction = 1 # 1 for forward, -1 for backward
|
||||||
|
self.scanner_tail_length = 3 # Number of trailing pixels
|
||||||
|
|
||||||
|
self.n1 = 0
|
||||||
|
self.n2 = 0
|
||||||
|
self.n3 = 0
|
||||||
|
self.n4 = 0
|
||||||
|
self.n5 = 0
|
||||||
|
self.n6 = 0
|
||||||
|
|
||||||
|
self.generator = None
|
||||||
|
self.select(self.selected)
|
||||||
|
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
if self.generator is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
next(self.generator)
|
||||||
|
except StopIteration:
|
||||||
|
self.generator = None
|
||||||
|
|
||||||
|
def select(self, pattern):
|
||||||
|
if pattern in self.patterns:
|
||||||
|
self.selected = pattern
|
||||||
|
self.generator = self.patterns[pattern]()
|
||||||
|
print(f"Selected pattern: {pattern}")
|
||||||
|
return True
|
||||||
|
# If pattern doesn't exist, default to "off"
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_param(self, key, value):
|
||||||
|
if key in param_mapping:
|
||||||
|
setattr(self, param_mapping[key], value)
|
||||||
|
return True
|
||||||
|
print(f"Invalid parameter: {key}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
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_color(self, num, color):
|
||||||
|
# Changed: More robust index check
|
||||||
|
if 0 <= num < len(self.colors):
|
||||||
|
self.colors[num] = color
|
||||||
|
# If the changed color is part of the current or next transition,
|
||||||
|
# restart the transition for smoother updates
|
||||||
|
return True
|
||||||
|
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):
|
||||||
|
# Changed: More robust index check and using del for lists
|
||||||
|
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 fill(self, color=None):
|
||||||
|
fill_color = color if color is not None else self.colors[0]
|
||||||
|
for i in range(self.num_leds):
|
||||||
|
self.n[i] = fill_color
|
||||||
|
self.n.write()
|
||||||
|
|
||||||
|
def off(self):
|
||||||
|
self.fill((0, 0, 0))
|
||||||
|
|
||||||
|
def on(self):
|
||||||
|
self.fill(self.apply_brightness(self.colors[0]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def wheel(self, pos):
|
||||||
|
if pos < 85:
|
||||||
|
return (pos * 3, 255 - pos * 3, 0)
|
||||||
|
elif pos < 170:
|
||||||
|
pos -= 85
|
||||||
|
return (255 - pos * 3, 0, pos * 3)
|
||||||
|
else:
|
||||||
|
pos -= 170
|
||||||
|
return (0, pos * 3, 255 - pos * 3)
|
||||||
|
|
||||||
|
|
||||||
131
src/settings.py
Normal file
131
src/settings.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import json
|
||||||
|
import ubinascii
|
||||||
|
import machine
|
||||||
|
import network
|
||||||
|
|
||||||
|
class Settings(dict):
|
||||||
|
SETTINGS_FILE = "/settings.json"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.load() # Load settings from file during initialization
|
||||||
|
self.color_order = self.get_color_order(self["color_order"])
|
||||||
|
|
||||||
|
def set_defaults(self):
|
||||||
|
self["led_pin"] = 10
|
||||||
|
self["num_leds"] = 50
|
||||||
|
self["pattern"] = "on"
|
||||||
|
self["delay"] = 100
|
||||||
|
self["brightness"] = 10
|
||||||
|
self["color_order"] = "rgb"
|
||||||
|
self["name"] = f"led-{ubinascii.hexlify(network.WLAN(network.AP_IF).config('mac')).decode()}"
|
||||||
|
self["ap_password"] = ""
|
||||||
|
self["id"] = 0
|
||||||
|
self["debug"] = False
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
try:
|
||||||
|
j = json.dumps(self)
|
||||||
|
with open(self.SETTINGS_FILE, 'w') as file:
|
||||||
|
file.write(j)
|
||||||
|
print("Settings saved successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving settings: {e}")
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
try:
|
||||||
|
with open(self.SETTINGS_FILE, 'r') as file:
|
||||||
|
loaded_settings = json.load(file)
|
||||||
|
self.update(loaded_settings)
|
||||||
|
print("Settings loaded successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading settings")
|
||||||
|
self.set_defaults()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def set_settings(self, data, patterns, save):
|
||||||
|
try:
|
||||||
|
print(f"Setting settings: {data}")
|
||||||
|
for key, value in data.items():
|
||||||
|
print(key, value)
|
||||||
|
if key == "colors":
|
||||||
|
buff = []
|
||||||
|
for color in value:
|
||||||
|
buff.append(tuple(int(color[i:i+2], 16) for i in self.color_order))
|
||||||
|
patterns.colors = buff
|
||||||
|
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.delay = delay
|
||||||
|
elif key == "brightness":
|
||||||
|
brightness = int(data["brightness"])
|
||||||
|
patterns.brightness = brightness
|
||||||
|
elif key == "n1":
|
||||||
|
patterns.n1 = value
|
||||||
|
elif key == "n2":
|
||||||
|
patterns.n2 = value
|
||||||
|
elif key == "n3":
|
||||||
|
patterns.n3 = value
|
||||||
|
elif key == "n4":
|
||||||
|
patterns.n4 = value
|
||||||
|
elif key == "n5":
|
||||||
|
patterns.n5 = value
|
||||||
|
elif key == "n6":
|
||||||
|
patterns.n6 = value
|
||||||
|
elif key == "name":
|
||||||
|
self[key] = value
|
||||||
|
self.save()
|
||||||
|
machine.reset()
|
||||||
|
elif key == "color_order":
|
||||||
|
self["color_order"] = value
|
||||||
|
self.color_order = self.get_color_order(value)
|
||||||
|
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)
|
||||||
|
if save:
|
||||||
|
self.save()
|
||||||
|
print(self)
|
||||||
|
return "OK", 200
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
return "Bad request", 400
|
||||||
|
|
||||||
|
def get_color_order(self, color_order):
|
||||||
|
"""Convert color order string to tuple of hex string indices."""
|
||||||
|
color_orders = {
|
||||||
|
"rgb": (1, 3, 5),
|
||||||
|
"rbg": (1, 5, 3),
|
||||||
|
"grb": (3, 1, 5),
|
||||||
|
"gbr": (3, 5, 1),
|
||||||
|
"brg": (5, 1, 3),
|
||||||
|
"bgr": (5, 3, 1)
|
||||||
|
}
|
||||||
|
return color_orders.get(color_order.lower(), (1, 3, 5)) # Default to RGB
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
def main():
|
||||||
|
settings = Settings()
|
||||||
|
print(f"Number of LEDs: {settings['num_leds']}")
|
||||||
|
settings['num_leds'] = 100
|
||||||
|
print(f"Updated number of LEDs: {settings['num_leds']}")
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
# Create a new Settings object to test loading
|
||||||
|
new_settings = Settings()
|
||||||
|
print(f"Loaded number of LEDs: {new_settings['num_leds']}")
|
||||||
|
print(settings)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Run the example
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
30
test/patterns/blink.py
Normal file
30
test/patterns/blink.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import utime
|
||||||
|
from machine import WDT
|
||||||
|
from settings import Settings
|
||||||
|
from patterns import Patterns
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = Settings()
|
||||||
|
pin = s.get("led_pin", 10)
|
||||||
|
num = s.get("num_leds", 30)
|
||||||
|
|
||||||
|
p = Patterns(pin=pin, num_leds=num)
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
p.set_param("br", 64)
|
||||||
|
p.set_param("dl", 200)
|
||||||
|
p.set_param("cl", [(255, 0, 0), (0, 0, 255)])
|
||||||
|
p.select("blink")
|
||||||
|
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < 1500:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
100
test/patterns/chase.py
Normal file
100
test/patterns/chase.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import utime
|
||||||
|
from machine import WDT
|
||||||
|
from settings import Settings
|
||||||
|
from patterns import Patterns
|
||||||
|
|
||||||
|
|
||||||
|
def run_for(p, wdt, ms):
|
||||||
|
"""Helper: run current pattern for given ms using tick()."""
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = Settings()
|
||||||
|
pin = s.get("led_pin", 10)
|
||||||
|
num = s.get("num_leds", 30)
|
||||||
|
|
||||||
|
p = Patterns(pin=pin, num_leds=num)
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
|
||||||
|
# Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)
|
||||||
|
print("Test 1: Basic chase (n1=5, n2=5, n3=1, n4=1)")
|
||||||
|
p.set_param("br", 255)
|
||||||
|
p.set_param("dl", 200)
|
||||||
|
p.set_param("n1", 5)
|
||||||
|
p.set_param("n2", 5)
|
||||||
|
p.set_param("n3", 1)
|
||||||
|
p.set_param("n4", 1)
|
||||||
|
p.set_param("cl", [(255, 0, 0), (0, 255, 0)])
|
||||||
|
p.select("chase")
|
||||||
|
run_for(p, wdt, 3000)
|
||||||
|
|
||||||
|
# Test 2: Forward and backward (n3=2, n4=-1)
|
||||||
|
print("Test 2: Forward and backward (n3=2, n4=-1)")
|
||||||
|
p.set_param("n1", 3)
|
||||||
|
p.set_param("n2", 3)
|
||||||
|
p.set_param("n3", 2)
|
||||||
|
p.set_param("n4", -1)
|
||||||
|
p.set_param("dl", 150)
|
||||||
|
p.set_param("cl", [(0, 0, 255), (255, 255, 0)])
|
||||||
|
p.select("chase")
|
||||||
|
run_for(p, wdt, 3000)
|
||||||
|
|
||||||
|
# Test 3: Large segments (n1=10, n2=5)
|
||||||
|
print("Test 3: Large segments (n1=10, n2=5, n3=3, n4=3)")
|
||||||
|
p.set_param("n1", 10)
|
||||||
|
p.set_param("n2", 5)
|
||||||
|
p.set_param("n3", 3)
|
||||||
|
p.set_param("n4", 3)
|
||||||
|
p.set_param("dl", 200)
|
||||||
|
p.set_param("cl", [(255, 128, 0), (128, 0, 255)])
|
||||||
|
p.select("chase")
|
||||||
|
run_for(p, wdt, 3000)
|
||||||
|
|
||||||
|
# Test 4: Fast movement (n3=5, n4=5)
|
||||||
|
print("Test 4: Fast movement (n3=5, n4=5)")
|
||||||
|
p.set_param("n1", 4)
|
||||||
|
p.set_param("n2", 4)
|
||||||
|
p.set_param("n3", 5)
|
||||||
|
p.set_param("n4", 5)
|
||||||
|
p.set_param("dl", 100)
|
||||||
|
p.set_param("cl", [(255, 0, 255), (0, 255, 255)])
|
||||||
|
p.select("chase")
|
||||||
|
run_for(p, wdt, 2000)
|
||||||
|
|
||||||
|
# Test 5: Backward movement (n3=-2, n4=-2)
|
||||||
|
print("Test 5: Backward movement (n3=-2, n4=-2)")
|
||||||
|
p.set_param("n1", 6)
|
||||||
|
p.set_param("n2", 4)
|
||||||
|
p.set_param("n3", -2)
|
||||||
|
p.set_param("n4", -2)
|
||||||
|
p.set_param("dl", 200)
|
||||||
|
p.set_param("cl", [(255, 255, 255), (0, 0, 0)])
|
||||||
|
p.select("chase")
|
||||||
|
run_for(p, wdt, 3000)
|
||||||
|
|
||||||
|
# Test 6: Alternating forward/backward (n3=3, n4=-2)
|
||||||
|
print("Test 6: Alternating forward/backward (n3=3, n4=-2)")
|
||||||
|
p.set_param("n1", 5)
|
||||||
|
p.set_param("n2", 5)
|
||||||
|
p.set_param("n3", 3)
|
||||||
|
p.set_param("n4", -2)
|
||||||
|
p.set_param("dl", 250)
|
||||||
|
p.set_param("cl", [(255, 0, 0), (0, 255, 0)])
|
||||||
|
p.select("chase")
|
||||||
|
run_for(p, wdt, 4000)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
print("Test complete, turning off")
|
||||||
|
p.select("off")
|
||||||
|
run_for(p, wdt, 100)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
94
test/patterns/circle.py
Normal file
94
test/patterns/circle.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import utime
|
||||||
|
from machine import WDT
|
||||||
|
from settings import Settings
|
||||||
|
from patterns import Patterns
|
||||||
|
|
||||||
|
|
||||||
|
def run_for(p, wdt, ms):
|
||||||
|
"""Helper: run current pattern for given ms using tick()."""
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = Settings()
|
||||||
|
pin = s.get("led_pin", 10)
|
||||||
|
num = s.get("num_leds", 30)
|
||||||
|
|
||||||
|
p = Patterns(pin=pin, num_leds=num)
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
|
||||||
|
# Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)
|
||||||
|
print("Test 1: Basic circle (n1=50, n2=100, n3=200, n4=0)")
|
||||||
|
p.set_param("br", 255)
|
||||||
|
p.set_param("n1", 50) # Head moves 50 LEDs/second
|
||||||
|
p.set_param("n2", 100) # Max length 100 LEDs
|
||||||
|
p.set_param("n3", 200) # Tail moves 200 LEDs/second
|
||||||
|
p.set_param("n4", 0) # Min length 0 LEDs
|
||||||
|
p.set_param("cl", [(255, 0, 0)]) # Red
|
||||||
|
p.select("circle")
|
||||||
|
run_for(p, wdt, 5000)
|
||||||
|
|
||||||
|
# Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)
|
||||||
|
print("Test 2: Slow growth, fast shrink (n1=20, n2=50, n3=100, n4=0)")
|
||||||
|
p.set_param("n1", 20)
|
||||||
|
p.set_param("n2", 50)
|
||||||
|
p.set_param("n3", 100)
|
||||||
|
p.set_param("n4", 0)
|
||||||
|
p.set_param("cl", [(0, 255, 0)]) # Green
|
||||||
|
p.select("circle")
|
||||||
|
run_for(p, wdt, 5000)
|
||||||
|
|
||||||
|
# Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)
|
||||||
|
print("Test 3: Fast growth, slow shrink (n1=100, n2=30, n3=20, n4=0)")
|
||||||
|
p.set_param("n1", 100)
|
||||||
|
p.set_param("n2", 30)
|
||||||
|
p.set_param("n3", 20)
|
||||||
|
p.set_param("n4", 0)
|
||||||
|
p.set_param("cl", [(0, 0, 255)]) # Blue
|
||||||
|
p.select("circle")
|
||||||
|
run_for(p, wdt, 5000)
|
||||||
|
|
||||||
|
# Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)
|
||||||
|
print("Test 4: With minimum length (n1=50, n2=40, n3=100, n4=10)")
|
||||||
|
p.set_param("n1", 50)
|
||||||
|
p.set_param("n2", 40)
|
||||||
|
p.set_param("n3", 100)
|
||||||
|
p.set_param("n4", 10)
|
||||||
|
p.set_param("cl", [(255, 255, 0)]) # Yellow
|
||||||
|
p.select("circle")
|
||||||
|
run_for(p, wdt, 5000)
|
||||||
|
|
||||||
|
# Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)
|
||||||
|
print("Test 5: Very fast (n1=200, n2=20, n3=200, n4=0)")
|
||||||
|
p.set_param("n1", 200)
|
||||||
|
p.set_param("n2", 20)
|
||||||
|
p.set_param("n3", 200)
|
||||||
|
p.set_param("n4", 0)
|
||||||
|
p.set_param("cl", [(255, 0, 255)]) # Magenta
|
||||||
|
p.select("circle")
|
||||||
|
run_for(p, wdt, 3000)
|
||||||
|
|
||||||
|
# Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)
|
||||||
|
print("Test 6: Very slow (n1=10, n2=25, n3=10, n4=0)")
|
||||||
|
p.set_param("n1", 10)
|
||||||
|
p.set_param("n2", 25)
|
||||||
|
p.set_param("n3", 10)
|
||||||
|
p.set_param("n4", 0)
|
||||||
|
p.set_param("cl", [(0, 255, 255)]) # Cyan
|
||||||
|
p.select("circle")
|
||||||
|
run_for(p, wdt, 5000)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
print("Test complete, turning off")
|
||||||
|
p.select("off")
|
||||||
|
run_for(p, wdt, 100)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
27
test/patterns/off.py
Normal file
27
test/patterns/off.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import utime
|
||||||
|
from machine import WDT
|
||||||
|
from settings import Settings
|
||||||
|
from patterns import Patterns
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = Settings()
|
||||||
|
pin = s.get("led_pin", 10)
|
||||||
|
num = s.get("num_leds", 30)
|
||||||
|
|
||||||
|
p = Patterns(pin=pin, num_leds=num)
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
p.select("off")
|
||||||
|
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < 200:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
39
test/patterns/on.py
Normal file
39
test/patterns/on.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import utime
|
||||||
|
from machine import WDT
|
||||||
|
from settings import Settings
|
||||||
|
from patterns import Patterns
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = Settings()
|
||||||
|
pin = s.get("led_pin", 10)
|
||||||
|
num = s.get("num_leds", 30)
|
||||||
|
|
||||||
|
p = Patterns(pin=pin, num_leds=num)
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
p.set_param("br", 64)
|
||||||
|
p.set_param("dl", 120)
|
||||||
|
p.set_param("cl", [(255, 0, 0), (0, 0, 255)])
|
||||||
|
|
||||||
|
# ON phase
|
||||||
|
p.select("on")
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < 800:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
# OFF phase
|
||||||
|
p.select("off")
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < 100:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
79
test/patterns/pulse.py
Normal file
79
test/patterns/pulse.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import utime
|
||||||
|
from machine import WDT
|
||||||
|
from settings import Settings
|
||||||
|
from patterns import Patterns
|
||||||
|
|
||||||
|
|
||||||
|
def run_for(p, wdt, ms):
|
||||||
|
"""Helper: run current pattern for given ms using tick()."""
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = Settings()
|
||||||
|
pin = s.get("led_pin", 10)
|
||||||
|
num = s.get("num_leds", 30)
|
||||||
|
|
||||||
|
p = Patterns(pin=pin, num_leds=num)
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
|
||||||
|
# Test 1: Simple single-color pulse
|
||||||
|
print("Test 1: Single-color pulse (attack=500, hold=500, decay=500, delay=500)")
|
||||||
|
p.set_param("br", 255)
|
||||||
|
p.set_param("cl", [(255, 0, 0)]) # Red
|
||||||
|
p.set_param("n1", 500) # attack ms
|
||||||
|
p.set_param("n2", 500) # hold ms
|
||||||
|
p.set_param("n3", 500) # decay ms
|
||||||
|
p.set_param("dl", 500) # delay ms between pulses
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("pulse")
|
||||||
|
run_for(p, wdt, 5000)
|
||||||
|
|
||||||
|
# Test 2: Faster pulse
|
||||||
|
print("Test 2: Fast pulse (attack=100, hold=100, decay=100, delay=100)")
|
||||||
|
p.set_param("n1", 100)
|
||||||
|
p.set_param("n2", 100)
|
||||||
|
p.set_param("n3", 100)
|
||||||
|
p.set_param("dl", 100)
|
||||||
|
p.set_param("cl", [(0, 255, 0)]) # Green
|
||||||
|
p.select("pulse")
|
||||||
|
run_for(p, wdt, 4000)
|
||||||
|
|
||||||
|
# Test 3: Multi-color pulse cycle
|
||||||
|
print("Test 3: Multi-color pulse (red -> green -> blue)")
|
||||||
|
p.set_param("n1", 300)
|
||||||
|
p.set_param("n2", 300)
|
||||||
|
p.set_param("n3", 300)
|
||||||
|
p.set_param("dl", 200)
|
||||||
|
p.set_param("cl", [(255, 0, 0), (0, 255, 0), (0, 0, 255)])
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("pulse")
|
||||||
|
run_for(p, wdt, 6000)
|
||||||
|
|
||||||
|
# Test 4: One-shot pulse (auto=False)
|
||||||
|
print("Test 4: Single pulse, auto=False")
|
||||||
|
p.set_param("n1", 400)
|
||||||
|
p.set_param("n2", 0)
|
||||||
|
p.set_param("n3", 400)
|
||||||
|
p.set_param("dl", 0)
|
||||||
|
p.set_param("cl", [(255, 255, 255)])
|
||||||
|
p.set_param("auto", False)
|
||||||
|
p.select("pulse")
|
||||||
|
# Run long enough to allow one full pulse cycle
|
||||||
|
run_for(p, wdt, 1500)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
print("Test complete, turning off")
|
||||||
|
p.select("off")
|
||||||
|
run_for(p, wdt, 200)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
123
test/patterns/rainbow.py
Normal file
123
test/patterns/rainbow.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import utime
|
||||||
|
from machine import WDT
|
||||||
|
from settings import Settings
|
||||||
|
from patterns import Patterns
|
||||||
|
|
||||||
|
|
||||||
|
def run_for(p, wdt, ms):
|
||||||
|
"""Helper: run current pattern for given ms using tick()."""
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = Settings()
|
||||||
|
pin = s.get("led_pin", 10)
|
||||||
|
num = s.get("num_leds", 30)
|
||||||
|
|
||||||
|
p = Patterns(pin=pin, num_leds=num)
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
|
||||||
|
# Test 1: Basic rainbow with auto=True (continuous)
|
||||||
|
print("Test 1: Basic rainbow (auto=True, n1=1)")
|
||||||
|
p.set_param("br", 255)
|
||||||
|
p.set_param("dl", 100) # Delay affects animation speed
|
||||||
|
p.set_param("n1", 1) # Step increment of 1
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("rainbow")
|
||||||
|
run_for(p, wdt, 3000)
|
||||||
|
|
||||||
|
# Test 2: Fast rainbow
|
||||||
|
print("Test 2: Fast rainbow (low delay, n1=1)")
|
||||||
|
p.set_param("dl", 50)
|
||||||
|
p.set_param("n1", 1)
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("rainbow")
|
||||||
|
run_for(p, wdt, 2000)
|
||||||
|
|
||||||
|
# Test 3: Slow rainbow
|
||||||
|
print("Test 3: Slow rainbow (high delay, n1=1)")
|
||||||
|
p.set_param("dl", 500)
|
||||||
|
p.set_param("n1", 1)
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("rainbow")
|
||||||
|
run_for(p, wdt, 3000)
|
||||||
|
|
||||||
|
# Test 4: Low brightness rainbow
|
||||||
|
print("Test 4: Low brightness rainbow (n1=1)")
|
||||||
|
p.set_param("br", 64)
|
||||||
|
p.set_param("dl", 100)
|
||||||
|
p.set_param("n1", 1)
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("rainbow")
|
||||||
|
run_for(p, wdt, 2000)
|
||||||
|
|
||||||
|
# Test 5: Single-step rainbow (auto=False)
|
||||||
|
print("Test 5: Single-step rainbow (auto=False, n1=1)")
|
||||||
|
p.set_param("br", 255)
|
||||||
|
p.set_param("dl", 100)
|
||||||
|
p.set_param("n1", 1)
|
||||||
|
p.set_param("auto", False)
|
||||||
|
p.step = 0
|
||||||
|
for i in range(10):
|
||||||
|
p.select("rainbow")
|
||||||
|
# One tick advances the generator one frame when auto=False
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(100)
|
||||||
|
wdt.feed()
|
||||||
|
|
||||||
|
# Test 6: Verify step updates correctly
|
||||||
|
print("Test 6: Verify step updates (auto=False, n1=1)")
|
||||||
|
p.set_param("n1", 1)
|
||||||
|
p.set_param("auto", False)
|
||||||
|
initial_step = p.step
|
||||||
|
p.select("rainbow")
|
||||||
|
p.tick()
|
||||||
|
final_step = p.step
|
||||||
|
print(f"Step updated from {initial_step} to {final_step} (expected increment: 1)")
|
||||||
|
|
||||||
|
# Test 7: Fast step increment (n1=5)
|
||||||
|
print("Test 7: Fast rainbow (n1=5, auto=True)")
|
||||||
|
p.set_param("br", 255)
|
||||||
|
p.set_param("dl", 100)
|
||||||
|
p.set_param("n1", 5)
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("rainbow")
|
||||||
|
run_for(p, wdt, 2000)
|
||||||
|
|
||||||
|
# Test 8: Very fast step increment (n1=10)
|
||||||
|
print("Test 8: Very fast rainbow (n1=10, auto=True)")
|
||||||
|
p.set_param("n1", 10)
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("rainbow")
|
||||||
|
run_for(p, wdt, 2000)
|
||||||
|
|
||||||
|
# Test 9: Verify n1 controls step increment (auto=False)
|
||||||
|
print("Test 9: Verify n1 step increment (auto=False, n1=5)")
|
||||||
|
p.set_param("n1", 5)
|
||||||
|
p.set_param("auto", False)
|
||||||
|
p.step = 0
|
||||||
|
initial_step = p.step
|
||||||
|
p.select("rainbow")
|
||||||
|
p.tick()
|
||||||
|
final_step = p.step
|
||||||
|
expected_step = (initial_step + 5) % 256
|
||||||
|
print(f"Step updated from {initial_step} to {final_step} (expected: {expected_step})")
|
||||||
|
if final_step == expected_step:
|
||||||
|
print("✓ n1 step increment working correctly")
|
||||||
|
else:
|
||||||
|
print(f"✗ Step increment mismatch! Expected {expected_step}, got {final_step}")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
print("Test complete, turning off")
|
||||||
|
p.select("off")
|
||||||
|
run_for(p, wdt, 100)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
68
test/patterns/transition.py
Normal file
68
test/patterns/transition.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import utime
|
||||||
|
from machine import WDT
|
||||||
|
from settings import Settings
|
||||||
|
from patterns import Patterns
|
||||||
|
|
||||||
|
|
||||||
|
def run_for(p, wdt, ms):
|
||||||
|
"""Helper: run current pattern for given ms using tick()."""
|
||||||
|
start = utime.ticks_ms()
|
||||||
|
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
|
||||||
|
wdt.feed()
|
||||||
|
p.tick()
|
||||||
|
utime.sleep_ms(10)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = Settings()
|
||||||
|
pin = s.get("led_pin", 10)
|
||||||
|
num = s.get("num_leds", 30)
|
||||||
|
|
||||||
|
p = Patterns(pin=pin, num_leds=num)
|
||||||
|
wdt = WDT(timeout=10000)
|
||||||
|
|
||||||
|
# Test 1: Simple two-color transition
|
||||||
|
print("Test 1: Two-color transition (red <-> blue, delay=1000)")
|
||||||
|
p.set_param("br", 255)
|
||||||
|
p.set_param("dl", 1000) # transition duration
|
||||||
|
p.set_param("cl", [(255, 0, 0), (0, 0, 255)])
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("transition")
|
||||||
|
run_for(p, wdt, 6000)
|
||||||
|
|
||||||
|
# Test 2: Multi-color transition
|
||||||
|
print("Test 2: Multi-color transition (red -> green -> blue -> white)")
|
||||||
|
p.set_param("dl", 800)
|
||||||
|
p.set_param("cl", [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)])
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("transition")
|
||||||
|
run_for(p, wdt, 8000)
|
||||||
|
|
||||||
|
# Test 3: One-shot transition (auto=False)
|
||||||
|
print("Test 3: One-shot transition (auto=False)")
|
||||||
|
p.set_param("dl", 1000)
|
||||||
|
p.set_param("cl", [(255, 0, 0), (0, 255, 0)])
|
||||||
|
p.set_param("auto", False)
|
||||||
|
p.select("transition")
|
||||||
|
# Run long enough for a single transition step
|
||||||
|
run_for(p, wdt, 2000)
|
||||||
|
|
||||||
|
# Test 4: Single-color behavior (should just stay on)
|
||||||
|
print("Test 4: Single-color transition (should hold color)")
|
||||||
|
p.set_param("cl", [(0, 0, 255)])
|
||||||
|
p.set_param("dl", 500)
|
||||||
|
p.set_param("auto", True)
|
||||||
|
p.select("transition")
|
||||||
|
run_for(p, wdt, 3000)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
print("Test complete, turning off")
|
||||||
|
p.select("off")
|
||||||
|
run_for(p, wdt, 200)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
734
tool.py
Normal file
734
tool.py
Normal file
@@ -0,0 +1,734 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
LED Bar Configuration Web App
|
||||||
|
|
||||||
|
Flask-based web UI for downloading, editing, and uploading settings.json
|
||||||
|
to/from MicroPython devices via mpremote.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
render_template_string,
|
||||||
|
request,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
flash,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = "change-me-in-production"
|
||||||
|
|
||||||
|
|
||||||
|
SETTINGS_CONFIG = [
|
||||||
|
("led_pin", "LED Pin", "number"),
|
||||||
|
("num_leds", "Number of LEDs", "number"),
|
||||||
|
("color_order", "Color Order", "choice", ["rgb", "rbg", "grb", "gbr", "brg", "bgr"]),
|
||||||
|
("name", "Device Name", "text"),
|
||||||
|
("pattern", "Pattern", "text"),
|
||||||
|
("delay", "Delay (ms)", "number"),
|
||||||
|
("brightness", "Brightness", "number"),
|
||||||
|
("n1", "N1", "number"),
|
||||||
|
("n2", "N2", "number"),
|
||||||
|
("n3", "N3", "number"),
|
||||||
|
("n4", "N4", "number"),
|
||||||
|
("n5", "N5", "number"),
|
||||||
|
("n6", "N6", "number"),
|
||||||
|
("ap_password", "AP Password", "text"),
|
||||||
|
("id", "ID", "number"),
|
||||||
|
("debug", "Debug Mode", "choice", ["True", "False"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _run_mpremote_copy(from_device: bool, device: str, temp_path: str) -> None:
|
||||||
|
if from_device:
|
||||||
|
cmd = ["mpremote", "connect", device, "cp", ":/settings.json", temp_path]
|
||||||
|
else:
|
||||||
|
cmd = ["mpremote", "connect", device, "cp", temp_path, ":/settings.json"]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"mpremote error: {result.stderr.strip() or result.stdout.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def download_settings(device: str) -> dict:
|
||||||
|
"""Download settings.json from the device using mpremote."""
|
||||||
|
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
|
||||||
|
temp_path = temp_file.name
|
||||||
|
temp_file.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_run_mpremote_copy(from_device=True, device=device, temp_path=temp_path)
|
||||||
|
with open(temp_path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
try:
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def upload_settings(device: str, settings: dict) -> None:
|
||||||
|
"""Upload settings.json to the device using mpremote and reset device."""
|
||||||
|
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
|
||||||
|
temp_path = temp_file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
json.dump(settings, temp_file, indent=2)
|
||||||
|
temp_file.close()
|
||||||
|
|
||||||
|
_run_mpremote_copy(from_device=False, device=device, temp_path=temp_path)
|
||||||
|
|
||||||
|
# Reset device (best effort)
|
||||||
|
try:
|
||||||
|
import serial # type: ignore
|
||||||
|
|
||||||
|
with serial.Serial(device, baudrate=115200) as ser:
|
||||||
|
ser.write(b"\x03\x03\x04")
|
||||||
|
except Exception:
|
||||||
|
reset_cmd = [
|
||||||
|
"mpremote",
|
||||||
|
"connect",
|
||||||
|
device,
|
||||||
|
"exec",
|
||||||
|
"import machine; machine.reset()",
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
subprocess.run(reset_cmd, capture_output=True, text=True, timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
try:
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_settings_from_form(form) -> dict:
|
||||||
|
settings = {}
|
||||||
|
for cfg in SETTINGS_CONFIG:
|
||||||
|
key = cfg[0]
|
||||||
|
raw = (form.get(key) or "").strip()
|
||||||
|
if raw == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if key in ["led_pin", "num_leds", "delay", "brightness", "id", "n1", "n2", "n3", "n4", "n5", "n6"]:
|
||||||
|
try:
|
||||||
|
settings[key] = int(raw)
|
||||||
|
except ValueError:
|
||||||
|
settings[key] = raw
|
||||||
|
elif key == "debug":
|
||||||
|
settings[key] = raw == "True"
|
||||||
|
else:
|
||||||
|
settings[key] = raw
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATE = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>LED Bar Configuration</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #0b1020;
|
||||||
|
--bg-alt: #141b2f;
|
||||||
|
--accent: #3b82f6;
|
||||||
|
--accent-soft: rgba(59, 130, 246, 0.15);
|
||||||
|
--border: #1f2937;
|
||||||
|
--text: #e5e7eb;
|
||||||
|
--muted: #9ca3af;
|
||||||
|
--danger: #f97373;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
background: radial-gradient(circle at top, #1f2937 0, #020617 55%);
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
.shell {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 960px;
|
||||||
|
background: linear-gradient(145deg, #020617 0, #020617 40%, #030712 100%);
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(15, 23, 42, 0.9),
|
||||||
|
0 45px 80px rgba(15, 23, 42, 0.95),
|
||||||
|
0 0 80px rgba(37, 99, 235, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: radial-gradient(circle at top left, rgba(37, 99, 235, 0.4), transparent 55%);
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
header h1 span.badge {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(96, 165, 250, 0.6);
|
||||||
|
background: rgba(37, 99, 235, 0.15);
|
||||||
|
color: #bfdbfe;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.chip-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
padding: 0.15rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(55, 65, 81, 0.9);
|
||||||
|
background: rgba(15, 23, 42, 0.9);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 1.25rem 1.5rem 1.5rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 2.2fr) minmax(0, 1.2fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
main {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: radial-gradient(circle at top left, rgba(37, 99, 235, 0.18), transparent 55%);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid rgba(31, 41, 55, 0.95);
|
||||||
|
padding: 1rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 0 0, rgba(59, 130, 246, 0.4), transparent 55%),
|
||||||
|
radial-gradient(circle at 100% 0, rgba(236, 72, 153, 0.28), transparent 55%);
|
||||||
|
opacity: 0.55;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
.card > * { position: relative; z-index: 1; }
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.card-header h2 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #cbd5f5;
|
||||||
|
}
|
||||||
|
.card-header span.sub {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.field-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 0.6rem 0.75rem;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.4rem 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(31, 41, 55, 0.95);
|
||||||
|
background: rgba(15, 23, 42, 0.92);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.14s ease, box-shadow 0.14s ease, background 0.14s ease;
|
||||||
|
}
|
||||||
|
input:focus, select:focus {
|
||||||
|
border-color: rgba(59, 130, 246, 0.95);
|
||||||
|
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.65), 0 0 25px rgba(37, 99, 235, 0.6);
|
||||||
|
background: rgba(15, 23, 42, 0.98);
|
||||||
|
}
|
||||||
|
.device-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.55rem;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
.device-row input {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: linear-gradient(135deg, #2563eb, #4f46e5);
|
||||||
|
color: white;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 25px rgba(37, 99, 235, 0.55),
|
||||||
|
0 0 0 1px rgba(15, 23, 42, 0.95);
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease, opacity 0.1s ease;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.95), rgba(17, 24, 39, 0.98));
|
||||||
|
border-color: rgba(55, 65, 81, 0.9);
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.85);
|
||||||
|
}
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
border-color: rgba(55, 65, 81, 0.8);
|
||||||
|
color: var(--muted);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow:
|
||||||
|
0 20px 40px rgba(37, 99, 235, 0.75),
|
||||||
|
0 0 0 1px rgba(191, 219, 254, 0.45);
|
||||||
|
opacity: 0.97;
|
||||||
|
}
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 20px rgba(15, 23, 42, 0.9),
|
||||||
|
0 0 0 1px rgba(30, 64, 175, 0.9);
|
||||||
|
}
|
||||||
|
.btn-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin-top: 0.65rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #22c55e;
|
||||||
|
box-shadow: 0 0 14px rgba(34, 197, 94, 0.95);
|
||||||
|
}
|
||||||
|
.status.error .status-dot {
|
||||||
|
background: var(--danger);
|
||||||
|
box-shadow: 0 0 14px rgba(248, 113, 113, 0.95);
|
||||||
|
}
|
||||||
|
.flash-container {
|
||||||
|
position: fixed;
|
||||||
|
right: 1.4rem;
|
||||||
|
bottom: 1.4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
max-width: 320px;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
.flash {
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
background: radial-gradient(circle at top left, rgba(37, 99, 235, 0.8), rgba(15, 23, 42, 0.96));
|
||||||
|
border: 1px solid rgba(96, 165, 250, 0.8);
|
||||||
|
color: #e5f0ff;
|
||||||
|
box-shadow:
|
||||||
|
0 22px 40px rgba(15, 23, 42, 0.95),
|
||||||
|
0 0 30px rgba(37, 99, 235, 0.7);
|
||||||
|
}
|
||||||
|
.flash.error {
|
||||||
|
background: radial-gradient(circle at top left, rgba(248, 113, 113, 0.85), rgba(15, 23, 42, 0.96));
|
||||||
|
border-color: rgba(248, 113, 113, 0.8);
|
||||||
|
}
|
||||||
|
.flash small {
|
||||||
|
display: block;
|
||||||
|
color: rgba(226, 232, 240, 0.8);
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
.pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(55, 65, 81, 0.9);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
<span>LED Bar Configuration</span>
|
||||||
|
<span class="badge">Web Console</span>
|
||||||
|
</h1>
|
||||||
|
<div class="chip-row">
|
||||||
|
<span class="chip">
|
||||||
|
<span style="width: 6px; height: 6px; border-radius: 999px; background: #22c55e; box-shadow: 0 0 12px rgba(34, 197, 94, 0.95);"></span>
|
||||||
|
<span>Raspberry Pi · MicroPython</span>
|
||||||
|
</span>
|
||||||
|
<span class="chip">settings.json live editor</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chip-row">
|
||||||
|
<span class="chip">Device: {{ device or "/dev/ttyACM0" }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div>
|
||||||
|
<h2>Device Connection</h2>
|
||||||
|
<span class="sub">Connect to your MicroPython LED controller and sync configuration</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="{{ url_for('handle_action') }}">
|
||||||
|
<label for="device">Serial / mpremote device</label>
|
||||||
|
<div class="device-row">
|
||||||
|
<input
|
||||||
|
id="device"
|
||||||
|
name="device"
|
||||||
|
type="text"
|
||||||
|
value="{{ device or '/dev/ttyACM0' }}"
|
||||||
|
placeholder="/dev/ttyACM0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button class="btn" type="submit" name="action" value="download">
|
||||||
|
⬇ Download
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" type="submit" name="action" value="upload">
|
||||||
|
⬆ Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status {% if status_type == 'error' %}error{% endif %}">
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span>{{ status or "Ready" }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pill">
|
||||||
|
<span>Tip:</span>
|
||||||
|
<span>Download from device → tweak parameters → Upload and reboot.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style="border: none; border-top: 1px solid rgba(31, 41, 55, 0.9); margin: 0.9rem 0 0.7rem;" />
|
||||||
|
|
||||||
|
<div class="card-header">
|
||||||
|
<div>
|
||||||
|
<h2>LED Settings</h2>
|
||||||
|
<span class="sub">Edit all fields before uploading back to your controller</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-grid">
|
||||||
|
{% for field in settings_config %}
|
||||||
|
{% set key, label, field_type = field[0], field[1], field[2] %}
|
||||||
|
<div>
|
||||||
|
<label for="{{ key }}">{{ label }}</label>
|
||||||
|
{% if field_type == 'choice' %}
|
||||||
|
{% set choices = field[3] %}
|
||||||
|
<select id="{{ key }}" name="{{ key }}">
|
||||||
|
<option value=""></option>
|
||||||
|
{% for choice in choices %}
|
||||||
|
{% if key == 'debug' %}
|
||||||
|
{% set selected = 'selected' if (settings.get(key) is sameas true and choice == 'True') or (settings.get(key) is sameas false and choice == 'False') else '' %}
|
||||||
|
{% else %}
|
||||||
|
{% set selected = 'selected' if settings.get(key) == choice else '' %}
|
||||||
|
{% endif %}
|
||||||
|
<option value="{{ choice }}" {{ selected }}>{{ choice }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% else %}
|
||||||
|
<input
|
||||||
|
id="{{ key }}"
|
||||||
|
name="{{ key }}"
|
||||||
|
type="text"
|
||||||
|
value="{{ settings.get(key, '') }}"
|
||||||
|
/>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn btn-secondary" type="submit" name="action" value="clear">
|
||||||
|
Reset form
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost" type="submit" name="action" value="from_json">
|
||||||
|
Paste JSON…
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div>
|
||||||
|
<h2>Raw JSON</h2>
|
||||||
|
<span class="sub">For advanced editing, paste or copy the full settings.json</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="{{ url_for('handle_action') }}">
|
||||||
|
<input type="hidden" name="device" value="{{ device or '/dev/ttyACM0' }}" />
|
||||||
|
<label for="raw_json">settings.json</label>
|
||||||
|
<textarea
|
||||||
|
id="raw_json"
|
||||||
|
name="raw_json"
|
||||||
|
rows="16"
|
||||||
|
style="
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
padding: 0.65rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(31, 41, 55, 0.95);
|
||||||
|
background: rgba(15, 23, 42, 0.96);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||||
|
outline: none;
|
||||||
|
"
|
||||||
|
>{{ raw_json }}</textarea>
|
||||||
|
|
||||||
|
<div class="btn-row" style="margin-top: 0.75rem;">
|
||||||
|
<button class="btn btn-secondary" type="submit" name="action" value="to_form">
|
||||||
|
Use JSON for form
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost" type="submit" name="action" value="pretty">
|
||||||
|
Pretty-print
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flash-container">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash {% if category == 'error' %}error{% endif %}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/", methods=["GET"])
|
||||||
|
def index():
|
||||||
|
return render_template_string(
|
||||||
|
TEMPLATE,
|
||||||
|
device="/dev/ttyACM0",
|
||||||
|
settings={},
|
||||||
|
settings_config=SETTINGS_CONFIG,
|
||||||
|
status="Ready",
|
||||||
|
status_type="ok",
|
||||||
|
raw_json="{}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/", methods=["POST"])
|
||||||
|
def handle_action():
|
||||||
|
action = request.form.get("action") or ""
|
||||||
|
device = (request.form.get("device") or "/dev/ttyACM0").strip()
|
||||||
|
raw_json = (request.form.get("raw_json") or "").strip()
|
||||||
|
|
||||||
|
settings = {}
|
||||||
|
status = "Ready"
|
||||||
|
status_type = "ok"
|
||||||
|
|
||||||
|
if action == "download":
|
||||||
|
if not device:
|
||||||
|
flash("Please specify a device.", "error")
|
||||||
|
status, status_type = "Missing device.", "error"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
settings = download_settings(device)
|
||||||
|
raw_json = json.dumps(settings, indent=2)
|
||||||
|
flash(f"Settings downloaded from {device}.", "success")
|
||||||
|
status = f"Settings downloaded from {device}"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
flash("Connection timeout. Check device connection.", "error")
|
||||||
|
status, status_type = "Connection timeout.", "error"
|
||||||
|
except FileNotFoundError:
|
||||||
|
flash("mpremote not found. Install with: pip install mpremote", "error")
|
||||||
|
status, status_type = "mpremote not found.", "error"
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
flash(f"Failed to download settings: {exc}", "error")
|
||||||
|
status, status_type = "Download failed.", "error"
|
||||||
|
|
||||||
|
elif action == "upload":
|
||||||
|
if not device:
|
||||||
|
flash("Please specify a device.", "error")
|
||||||
|
status, status_type = "Missing device.", "error"
|
||||||
|
else:
|
||||||
|
# Take current form fields as source of truth, falling back to JSON if present
|
||||||
|
if raw_json:
|
||||||
|
try:
|
||||||
|
settings = json.loads(raw_json)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
flash("Raw JSON is invalid; using form values instead.", "error")
|
||||||
|
settings = {}
|
||||||
|
form_settings = parse_settings_from_form(request.form)
|
||||||
|
settings.update(form_settings)
|
||||||
|
|
||||||
|
if not settings:
|
||||||
|
flash("No settings to upload. Download or provide settings first.", "error")
|
||||||
|
status, status_type = "No settings to upload.", "error"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
upload_settings(device, settings)
|
||||||
|
raw_json = json.dumps(settings, indent=2)
|
||||||
|
flash(f"Settings uploaded and device reset on {device}.", "success")
|
||||||
|
status = f"Settings uploaded and device reset on {device}"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
flash("Connection timeout. Check device connection.", "error")
|
||||||
|
status, status_type = "Connection timeout.", "error"
|
||||||
|
except FileNotFoundError:
|
||||||
|
flash("mpremote not found. Install with: pip install mpremote", "error")
|
||||||
|
status, status_type = "mpremote not found.", "error"
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
flash(f"Failed to upload settings: {exc}", "error")
|
||||||
|
status, status_type = "Upload failed.", "error"
|
||||||
|
|
||||||
|
elif action == "from_json":
|
||||||
|
# No-op here, JSON is just edited in the side panel
|
||||||
|
form_settings = parse_settings_from_form(request.form)
|
||||||
|
settings.update(form_settings)
|
||||||
|
if raw_json:
|
||||||
|
try:
|
||||||
|
settings.update(json.loads(raw_json))
|
||||||
|
flash("JSON merged into form values.", "success")
|
||||||
|
status = "JSON merged into form."
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
flash("Invalid JSON; keeping previous form values.", "error")
|
||||||
|
status, status_type = "JSON parse error.", "error"
|
||||||
|
|
||||||
|
elif action == "to_form":
|
||||||
|
if raw_json:
|
||||||
|
try:
|
||||||
|
settings = json.loads(raw_json)
|
||||||
|
flash("Form fields updated from JSON.", "success")
|
||||||
|
status = "Form fields updated from JSON."
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
flash("Invalid JSON; could not update form fields.", "error")
|
||||||
|
status, status_type = "JSON parse error.", "error"
|
||||||
|
|
||||||
|
elif action == "pretty":
|
||||||
|
if raw_json:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(raw_json)
|
||||||
|
raw_json = json.dumps(parsed, indent=2)
|
||||||
|
settings = parsed if isinstance(parsed, dict) else {}
|
||||||
|
flash("JSON pretty-printed.", "success")
|
||||||
|
status = "JSON pretty-printed."
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
flash("Invalid JSON; cannot pretty-print.", "error")
|
||||||
|
status, status_type = "JSON parse error.", "error"
|
||||||
|
|
||||||
|
elif action == "clear":
|
||||||
|
settings = {}
|
||||||
|
raw_json = "{}"
|
||||||
|
flash("Form cleared.", "success")
|
||||||
|
status = "Form cleared."
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Unknown / initial action: just reflect form values back
|
||||||
|
settings = parse_settings_from_form(request.form)
|
||||||
|
if raw_json and not settings:
|
||||||
|
try:
|
||||||
|
settings = json.loads(raw_json)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return render_template_string(
|
||||||
|
TEMPLATE,
|
||||||
|
device=device,
|
||||||
|
settings=settings,
|
||||||
|
settings_config=SETTINGS_CONFIG,
|
||||||
|
status=status,
|
||||||
|
status_type=status_type,
|
||||||
|
raw_json=raw_json or json.dumps(settings or {}, indent=2),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# Bind to all interfaces so you can reach it from your LAN:
|
||||||
|
# python web_app.py
|
||||||
|
# Then open: http://<pi-ip>:5000/
|
||||||
|
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user