feat(api): improve pattern deploy and device tcp handling
Made-with: Cursor
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
from microdot import Microdot
|
||||
from models.pattern import Pattern
|
||||
from models.device import Device
|
||||
from models.wifi_ws_clients import send_json_line_to_ip
|
||||
from util.driver_patterns import (
|
||||
driver_patterns_dir,
|
||||
is_firmware_builtin_pattern_module,
|
||||
@@ -9,8 +8,9 @@ from util.driver_patterns import (
|
||||
)
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
import socket
|
||||
from urllib.parse import quote
|
||||
|
||||
controller = Microdot()
|
||||
patterns = Pattern()
|
||||
@@ -48,6 +48,52 @@ def _normalize_pattern_key(raw):
|
||||
def _valid_pattern_key(key):
|
||||
return bool(key and _PATTERN_KEY_RE.match(key))
|
||||
|
||||
|
||||
def _http_post_pattern_source(ip, filename, code_text, reload_patterns=True, timeout_s=10.0):
|
||||
"""POST source to driver /patterns/upload?name=...&reload=...; return True on 2xx."""
|
||||
if not isinstance(ip, str) or not ip.strip():
|
||||
return False
|
||||
if not isinstance(filename, str) or not filename:
|
||||
return False
|
||||
if not isinstance(code_text, str):
|
||||
return False
|
||||
|
||||
name_q = quote(filename, safe="")
|
||||
reload_q = "1" if reload_patterns else "0"
|
||||
path = "/patterns/upload?name=%s&reload=%s" % (name_q, reload_q)
|
||||
body = code_text.encode("utf-8")
|
||||
req = (
|
||||
"POST %s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n" % (path, ip, len(body))
|
||||
).encode("utf-8") + body
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.settimeout(timeout_s)
|
||||
sock.connect((ip.strip(), 80))
|
||||
sock.sendall(req)
|
||||
data = b""
|
||||
while True:
|
||||
chunk = sock.recv(1024)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
except OSError:
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
first_line = data.split(b"\r\n", 1)[0] if data else b""
|
||||
# Accept any 2xx status.
|
||||
return b" 2" in first_line
|
||||
|
||||
def load_pattern_definitions():
|
||||
"""Load pattern definitions from pattern.json file."""
|
||||
try:
|
||||
@@ -170,7 +216,7 @@ async def ota_pattern_file(request, name):
|
||||
|
||||
@controller.post('/<name>/send')
|
||||
async def send_pattern_to_device(request, name):
|
||||
"""Tell Wi-Fi driver(s) to download one pattern source file over HTTP."""
|
||||
"""Push one pattern source file directly to Wi-Fi driver(s) over HTTP."""
|
||||
if not isinstance(name, str):
|
||||
return json.dumps({"error": "Invalid pattern name"}), 400, {
|
||||
"Content-Type": "application/json"
|
||||
@@ -183,7 +229,7 @@ async def send_pattern_to_device(request, name):
|
||||
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.",
|
||||
"error": "on and off are built into the driver firmware; send does not apply.",
|
||||
}
|
||||
), 400, {
|
||||
"Content-Type": "application/json"
|
||||
@@ -206,22 +252,11 @@ async def send_pattern_to_device(request, name):
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
file_url = "/patterns/ota/file/%s" % filename
|
||||
|
||||
msg = json.dumps(
|
||||
{
|
||||
"v": "1",
|
||||
"manifest": {
|
||||
"files": [
|
||||
{
|
||||
"name": filename,
|
||||
"url": file_url,
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
source = f.read()
|
||||
except OSError as e:
|
||||
return json.dumps({"error": str(e)}), 500, {"Content-Type": "application/json"}
|
||||
target_ids = []
|
||||
if requested_device_id:
|
||||
dev = devices.read(requested_device_id)
|
||||
@@ -250,12 +285,12 @@ async def send_pattern_to_device(request, name):
|
||||
ip = str(dev.get("address") or "").strip()
|
||||
if not ip:
|
||||
continue
|
||||
ok = await send_json_line_to_ip(ip, msg)
|
||||
ok = _http_post_pattern_source(ip, filename, source, reload_patterns=True, timeout_s=10.0)
|
||||
if ok:
|
||||
sent_ids.append(did)
|
||||
|
||||
if not sent_ids:
|
||||
return json.dumps({"error": "No Wi-Fi drivers connected"}), 503, {
|
||||
return json.dumps({"error": "No Wi-Fi drivers accepted pattern upload"}), 503, {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
return json.dumps({"message": "Pattern sent", "pattern": filename, "device_ids": sent_ids, "sent_count": len(sent_ids)}), 200, {
|
||||
|
||||
Reference in New Issue
Block a user