feat: add seat firmware workbench and OTA delivery

This commit is contained in:
vee
2026-09-16 17:55:30 +08:00
parent 3eeca2ea75
commit ab8e43050c
70 changed files with 22594 additions and 316 deletions
+65 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Start N7Web and its read-only LAN mode-card service together."""
"""Start N7Web, the read-only OTA endpoint, and the LAN mode-card service."""
from __future__ import annotations
@@ -13,6 +13,8 @@ import time
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from urllib import error as urlerror
from urllib import request as urlrequest
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
@@ -49,9 +51,20 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument(
"--host",
default="127.0.0.1",
help="main N7Web bind address (use 0.0.0.0 for trusted-LAN access)",
help="main N7Web bind address (defaults to local management only)",
)
parser.add_argument("--port", type=_port, default=8000, help="main N7Web port")
parser.add_argument(
"--ota-host",
default="127.0.0.1",
help="read-only firmware OTA bind address (designed for an HTTPS tunnel)",
)
parser.add_argument(
"--ota-port",
type=_port,
default=8012,
help="read-only firmware OTA port",
)
parser.add_argument(
"--scene-bind",
default="0.0.0.0",
@@ -140,7 +153,7 @@ def _commands(
args: argparse.Namespace,
python: Path,
catalog: Path,
) -> tuple[list[str], list[str]]:
) -> tuple[list[str], list[str], list[str]]:
main_command = [
str(python),
"-m",
@@ -167,7 +180,16 @@ def _commands(
"--server-id",
args.server_id,
]
return main_command, scene_command
ota_command = [
str(python),
"-m",
"cognitive_agent.firmware_ota_server",
"--host",
args.ota_host,
"--port",
str(args.ota_port),
]
return main_command, scene_command, ota_command
def _display_command(command: Sequence[str]) -> str:
@@ -189,6 +211,28 @@ def _catalog_is_ready(path: Path) -> bool:
)
def _ota_service_is_ready(host: str, port: int) -> bool:
probe_host = "127.0.0.1" if host in {"0.0.0.0", "::", "[::]"} else host
if ":" in probe_host and not probe_host.startswith("["):
probe_host = f"[{probe_host}]"
request = urlrequest.Request(
f"http://{probe_host}:{port}/healthz",
headers={"Accept": "application/json"},
)
opener = urlrequest.build_opener(urlrequest.ProxyHandler({}))
try:
with opener.open(request, timeout=1.0) as response:
document = json.loads(response.read().decode("utf-8"))
except (OSError, ValueError, urlerror.URLError):
return False
return bool(
response.status == 200
and isinstance(document, dict)
and document.get("status") == "ok"
and document.get("service") == "n7-firmware-ota"
)
def _wait_for_catalog(
path: Path,
main_process: subprocess.Popen[bytes],
@@ -264,10 +308,11 @@ def run(argv: Sequence[str] | None = None) -> int:
return 2
environment = _child_environment()
main_command, scene_command = _commands(args, python, catalog)
main_command, scene_command, ota_command = _commands(args, python, catalog)
print(f"Repository: {REPOSITORY_ROOT}", flush=True)
print(f"Catalog: {catalog}", flush=True)
print(f"Main: {_display_command(main_command)}", flush=True)
print(f"OTA: {_display_command(ota_command)}", flush=True)
print(f"Cards: {_display_command(scene_command)}", flush=True)
if args.dry_run:
return 0
@@ -283,6 +328,20 @@ def run(argv: Sequence[str] | None = None) -> int:
children.append(ChildProcess("main N7Web service", main_process))
_wait_for_catalog(catalog, main_process, args.catalog_ready_timeout)
if _ota_service_is_ready(args.ota_host, args.ota_port):
print(
f"OTA service already available at "
f"http://{args.ota_host}:{args.ota_port}; reusing it.",
flush=True,
)
else:
ota_process = subprocess.Popen(
ota_command,
cwd=REPOSITORY_ROOT,
env=environment,
)
children.append(ChildProcess("firmware OTA service", ota_process))
scene_process = subprocess.Popen(
scene_command,
cwd=REPOSITORY_ROOT,
@@ -290,7 +349,7 @@ def run(argv: Sequence[str] | None = None) -> int:
)
children.append(ChildProcess("LAN mode-card service", scene_process))
print(
"Both services are running; press Ctrl+C once to stop both.",
"All services are running; press Ctrl+C once to stop all of them.",
flush=True,
)
name, exit_code = _wait_for_exit(children)