feat(patterns): driver_patterns helper, on/off ota guard, drop duplicate py tree

Made-with: Cursor
This commit is contained in:
pi
2026-04-12 00:13:56 +12:00
parent 28b19b5219
commit 7bdb324ebc
10 changed files with 114 additions and 453 deletions

View File

@@ -11,6 +11,7 @@ from models.tcp_clients import (
send_json_line_to_ip,
tcp_client_connected,
)
from util.driver_patterns import driver_patterns_dir
from util.espnow_message import build_message
import asyncio
import json
@@ -73,11 +74,6 @@ def _device_json_with_live_status(dev_dict):
return row
def _driver_patterns_dir():
here = os.path.dirname(__file__)
return os.path.abspath(os.path.join(here, "../../led-driver/src/patterns"))
def _safe_pattern_filename(name):
if not isinstance(name, str):
return False
@@ -89,7 +85,7 @@ def _safe_pattern_filename(name):
def _build_patterns_manifest(host):
base_dir = _driver_patterns_dir()
base_dir = driver_patterns_dir()
names = sorted(os.listdir(base_dir))
files = []
for name in names:

View File

@@ -2,6 +2,11 @@ from microdot import Microdot
from models.pattern import Pattern
from models.device import Device
from models.tcp_clients import send_json_line_to_ip
from util.driver_patterns import (
driver_patterns_dir,
is_firmware_builtin_pattern_module,
normalize_pattern_py_filename,
)
import json
import re
import sys
@@ -17,11 +22,6 @@ def _project_root():
return os.path.abspath(os.path.join(here, "..", ".."))
def _driver_patterns_dir():
here = os.path.dirname(__file__)
return os.path.abspath(os.path.join(here, "../../led-driver/src/patterns"))
def _safe_pattern_filename(name):
if not isinstance(name, str):
return False
@@ -75,7 +75,7 @@ def load_driver_pattern_names():
"""List available pattern module names from led-driver/src/patterns."""
try:
names = []
for filename in os.listdir(_driver_patterns_dir()):
for filename in os.listdir(driver_patterns_dir()):
if not _safe_pattern_filename(filename) or filename == "__init__.py":
continue
names.append(filename[:-3])
@@ -111,7 +111,7 @@ async def get_pattern_definitions(request):
@controller.get('/ota/manifest')
async def ota_manifest(request):
"""Manifest of driver pattern source files for OTA pulls."""
base_dir = _driver_patterns_dir()
base_dir = driver_patterns_dir()
host = request.headers.get("Host", "")
if not host:
return json.dumps({"error": "Missing Host header"}), 400, {
@@ -137,16 +137,32 @@ async def ota_manifest(request):
@controller.get('/ota/file/<name>')
async def ota_pattern_file(request, name):
"""Serve one driver pattern source file for OTA pulls."""
if not _safe_pattern_filename(name) or name == "__init__.py":
fname = normalize_pattern_py_filename(name)
if not fname or not _safe_pattern_filename(fname) or fname == "__init__.py":
return json.dumps({"error": "Invalid filename"}), 400, {
"Content-Type": "application/json"
}
path = os.path.join(_driver_patterns_dir(), name)
if is_firmware_builtin_pattern_module(fname):
return json.dumps(
{
"error": "on and off are built into the driver firmware; there is no module file to serve.",
}
), 400, {
"Content-Type": "application/json"
}
base = driver_patterns_dir()
path = os.path.join(base, fname)
try:
with open(path, "r") as f:
content = f.read()
except OSError:
return json.dumps({"error": "Pattern file not found"}), 404, {
return json.dumps(
{
"error": "Pattern file not found",
"path": path,
"hint": "Ensure led-driver is present or set LED_CONTROLLER_PATTERNS_DIR.",
}
), 404, {
"Content-Type": "application/json"
}
return content, 200, {"Content-Type": "text/plain; charset=utf-8"}
@@ -159,19 +175,34 @@ async def send_pattern_to_device(request, name):
return json.dumps({"error": "Invalid pattern name"}), 400, {
"Content-Type": "application/json"
}
filename = name if name.endswith(".py") else (name + ".py")
if not _safe_pattern_filename(filename) or filename == "__init__.py":
filename = normalize_pattern_py_filename(name)
if not filename or not _safe_pattern_filename(filename) or filename == "__init__.py":
return json.dumps({"error": "Invalid pattern filename"}), 400, {
"Content-Type": "application/json"
}
if is_firmware_builtin_pattern_module(filename):
return json.dumps(
{
"error": "on and off are built into the driver firmware; OTA send does not apply.",
}
), 400, {
"Content-Type": "application/json"
}
devices = Device()
body = request.json or {}
requested_device_id = str(body.get("device_id") or "").strip()
path = os.path.join(_driver_patterns_dir(), filename)
base = driver_patterns_dir()
path = os.path.join(base, filename)
if not os.path.exists(path):
return json.dumps({"error": "Pattern file not found"}), 404, {
return json.dumps(
{
"error": "Pattern file not found",
"path": path,
"hint": "Ensure led-driver is present or set LED_CONTROLLER_PATTERNS_DIR.",
}
), 404, {
"Content-Type": "application/json"
}
@@ -261,12 +292,18 @@ async def upload_pattern_file(request):
return json.dumps({"error": "invalid pattern filename"}), 400, {
"Content-Type": "application/json"
}
if is_firmware_builtin_pattern_module(filename):
return json.dumps(
{"error": "on and off are built into the driver firmware; use a different pattern name."}
), 400, {
"Content-Type": "application/json"
}
if not isinstance(code, str) or not code.strip():
return json.dumps({"error": "code is required"}), 400, {
"Content-Type": "application/json"
}
path = os.path.join(_driver_patterns_dir(), filename)
path = os.path.join(driver_patterns_dir(), filename)
exists = os.path.exists(path)
if exists and not overwrite:
return json.dumps({"error": "pattern file already exists", "name": filename}), 409, {
@@ -304,6 +341,12 @@ async def create_driver_pattern(request):
return json.dumps({
"error": "name must be a valid Python identifier (e.g. sparkle, my_pattern)",
}), 400, {"Content-Type": "application/json"}
if is_firmware_builtin_pattern_module(key):
return json.dumps(
{"error": "on and off are built into the driver firmware; use a different pattern name."}
), 400, {
"Content-Type": "application/json"
}
code = data.get("code")
if not isinstance(code, str) or not code.strip():
@@ -314,7 +357,7 @@ async def create_driver_pattern(request):
overwrite = bool(data.get("overwrite", True))
filename = key + ".py"
py_path = os.path.join(_driver_patterns_dir(), filename)
py_path = os.path.join(driver_patterns_dir(), filename)
if os.path.exists(py_path) and not overwrite:
return json.dumps({"error": "pattern file already exists", "name": filename}), 409, {
"Content-Type": "application/json"

View File

@@ -0,0 +1,53 @@
import os
_ENV_PATTERNS_DIR = "LED_CONTROLLER_PATTERNS_DIR"
def driver_patterns_dir():
"""Absolute path to driver pattern ``.py`` modules.
If ``LED_CONTROLLER_PATTERNS_DIR`` is set to an existing directory, that wins
(for installs where ``led-driver`` is not next to this repo). Otherwise uses
``<project-root>/led-driver/src/patterns``.
"""
env = (os.environ.get(_ENV_PATTERNS_DIR) or "").strip()
if env and os.path.isdir(env):
return os.path.abspath(env)
here = os.path.dirname(os.path.abspath(__file__))
root = os.path.abspath(os.path.join(here, "..", ".."))
return os.path.join(root, "led-driver", "src", "patterns")
def normalize_pattern_py_filename(name):
"""Return a single ``*.py`` basename (no paths), or ``\"\"`` if invalid.
Strips repeated ``.py`` suffixes so ``blink.py.py`` becomes ``blink.py``.
"""
if not isinstance(name, str):
return ""
s = name.strip()
if not s:
return ""
lower = s.lower()
while lower.endswith(".py"):
s = s[:-3]
s = s.strip()
lower = s.lower()
if not s:
return ""
if "/" in s or "\\" in s or ".." in s:
return ""
return s + ".py"
# Implemented in led-driver ``presets.py`` only — no separate ``patterns/*.py``.
FIRMWARE_BUILTIN_PATTERN_IDS = frozenset({"on", "off"})
def is_firmware_builtin_pattern_module(name):
"""True for ``on`` / ``off``, with or without a ``.py`` suffix."""
if not isinstance(name, str):
return False
s = name.strip().lower()
while s.endswith(".py"):
s = s[:-3].strip()
return s in FIRMWARE_BUILTIN_PATTERN_IDS