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

@@ -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"