feat: add seat firmware workbench and OTA delivery
This commit is contained in:
+65
-6
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user