Add packaged per-camera X4 wake and preview recovery

This commit is contained in:
DCCONSTRUCTIONS
2026-09-10 14:20:52 +03:00
parent f164606828
commit d45b329de8
22 changed files with 1045 additions and 16 deletions
+8 -1
View File
@@ -3,6 +3,7 @@
import hashlib
import json
import re
import signal
import sys
from pathlib import Path
@@ -33,7 +34,9 @@ def wake_payload(target):
ident = "instax4_" + hashlib.sha256(serial.encode()).hexdigest()[:32]
if target["device_id"] != ident or target["wakeup_enabled"] is not True:
raise ValueError("USB identity and enabled camera wakeup must be confirmed")
if not re.fullmatch(r"(?:[0-9A-F]{2}:){5}[0-9A-F]{2}", target["bluetooth_address"]):
if target["bluetooth_address"] is not None and not re.fullmatch(
r"(?:[0-9A-F]{2}:){5}[0-9A-F]{2}", target["bluetooth_address"]
):
raise ValueError("Prior correlated Bluetooth address is required")
# Company ID 0x004c is passed separately to BlueZ. X4 remote spec section1.
return (
@@ -189,6 +192,10 @@ def run(target, report):
def main():
def cancel(*_):
raise RuntimeError("Wake cancelled")
signal.signal(signal.SIGTERM, cancel)
report = {
"schema": "missioncore.insta360.ble-wake/v1",
"started": timestamp(),
+2 -1
View File
@@ -16,7 +16,7 @@ sys.path.insert(0, str(REPOSITORY / "scripts/packaging"))
from debian import package # noqa: E402
from fetch_sdk import verify # noqa: E402
VERSION = "0.1.3-6"
VERSION = "0.1.3-7"
WHEELS = {
"aiohappyeyeballs",
"aiohttp",
@@ -160,6 +160,7 @@ def build(output):
"ble_diagnostic.py",
"ble_options.py",
"ble_wake.py",
"wake_service.py",
):
files.append(
("usr/lib/mission-core-node/insta360/" + name, (PACKAGING / name).read_bytes(), 0o644)
@@ -23,7 +23,7 @@ data = subprocess.check_output(["/usr/bin/dpkg-deb", "--fsys-tarfile", str(packa
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
payload = archive.extractfile("usr/share/mission-core-node/insta360/payload.zip").read()
bundle = json.load(archive.extractfile("usr/share/mission-core-node/insta360/bundle.json"))
for name in ("ble_diagnostic.py", "ble_options.py", "ble_wake.py"):
for name in ("ble_diagnostic.py", "ble_options.py", "ble_wake.py", "wake_service.py"):
packaged = archive.extractfile("usr/lib/mission-core-node/insta360/" + name).read()
if packaged != (ROOT / "packaging" / name).read_bytes():
raise ValueError("Packaged BLE module differs from the qualified source")
@@ -0,0 +1,35 @@
[Unit]
Description=Mission Core bounded X4 Bluetooth wake
After=bluetooth.service
Wants=bluetooth.service
ConditionPathExists=/var/lib/mission-core-insta360/active.path
[Service]
Type=simple
Group=mission-core-node
ExecStart=/usr/bin/python3 -I /usr/lib/mission-core-node/insta360/wake_service.py
RuntimeDirectory=mission-core-x4-recovery
RuntimeDirectoryMode=0750
StateDirectory=mission-core-x4-recovery
StateDirectoryMode=0700
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictAddressFamilies=AF_UNIX
CapabilityBoundingSet=
TasksMax=20
MemoryMax=128M
CPUQuota=30%
LimitCORE=0
TimeoutStopSec=8
KillMode=control-group
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+1
View File
@@ -184,6 +184,7 @@ def prepare_locked():
trusted(maintenance).unlink()
run("/usr/bin/systemctl", "daemon-reload")
for service in (
"mission-core-insta360-wake.service",
"mission-core-insta360.service",
"mission-core-insta360-supervisor.service",
):
+1
View File
@@ -5,6 +5,7 @@ case "$1" in
/usr/bin/python3 -I /usr/lib/mission-core-node/insta360/prepare.py --quiesce
if [ -d /run/systemd/system ]; then
systemctl stop mission-core-insta360-supervisor.service
systemctl stop mission-core-insta360-wake.service
systemctl stop 'mission-core-x4@*.service'
systemctl stop mission-core-insta360.service
fi
@@ -0,0 +1,174 @@
"""Fixed Bluetooth privilege boundary. Never accepts serials, paths or SDK calls."""
import json
import os
import pwd
import re
import subprocess
import sys
import tempfile
import threading
import time
from contextlib import suppress
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from layout import CODE, trusted, write # noqa: E402
from runtime.http import Server # noqa: E402
from runtime.identity import read_binding # noqa: E402
from runtime.lifecycle import acquisition # noqa: E402
ROOT = Path("/var/lib/mission-core-x4-recovery")
SOCKET = Path("/run/mission-core-x4-recovery/driver.sock")
IDENTIFIER = re.compile(r"instax4_[0-9a-f]{32}")
def connected():
values = []
for path in Path("/sys/bus/usb/devices").iterdir():
with suppress(OSError, ValueError):
values.append(read_binding(path.name))
return values
class WakeService:
def __init__(self, root=ROOT):
self.root = root
self.lock = threading.RLock()
self.advertiser = threading.Lock()
self.running = {}
def load(self, ident):
path = self.root / (ident + ".json")
if not path.exists():
return {"enabled": False, "attempts": []}
return json.loads(trusted(path).read_text())
def save(self, ident, value):
write(self.root / (ident + ".json"), json.dumps(value).encode(), 0o600)
def control(self, ident, enabled):
with self.lock:
value = self.load(ident)
if not enabled and "serial" not in value:
return {"enabled": False}
if enabled:
matches = [x for x in connected() if x.device_id == ident]
if len(matches) != 1 or not re.fullmatch(r"[A-Za-z0-9]{7,64}", matches[0].serial):
raise ValueError("Connect the exact X4 before enabling recovery")
if len(list(self.root.glob("instax4_*.json"))) >= 500 and "serial" not in value:
raise ValueError("Recovery inventory is full")
value["serial"] = matches[0].serial
value["enabled"] = enabled
self.save(ident, value)
if not enabled and ident in self.running:
self.running[ident].terminate()
return {"enabled": enabled}
def wake(self, ident, attempt):
if not self.advertiser.acquire(blocking=False):
return {"state": "busy"}
try:
# Installation cannot replace code/state while a wake is in flight.
with acquisition():
return self.wake_locked(ident, attempt)
finally:
self.advertiser.release()
def wake_locked(self, ident, attempt):
with self.lock:
value = self.load(ident)
if not value["enabled"] or "serial" not in value:
return {"state": "disabled"}
serial = value["serial"]
if any(x.device_id == ident for x in connected()):
return {"state": "connected"}
# A radio suffix is weaker than full USB identity. Refuse known collisions.
for path in self.root.glob("instax4_*.json"):
other = json.loads(trusted(path).read_text()).get("serial", "")
if other and other != serial and other[-6:] == serial[-6:]:
return {"state": "identity_conflict"}
now = time.time()
attempts = [x for x in value["attempts"] if now - x["at"] < 600]
if any(x["id"] == attempt for x in attempts):
return {"state": "already_attempted"}
if len(attempts) >= 3:
return {"state": "exhausted"}
value["attempts"] = attempts + [{"id": attempt, "at": now}]
# Persist before advertising; a crash cannot silently replay the attempt.
self.save(ident, value)
target = {
"device_id": ident,
"serial": serial,
"bluetooth_address": None,
"wakeup_enabled": True,
}
with tempfile.TemporaryFile(dir=self.root) as output:
process = subprocess.Popen(
["/usr/bin/python3", "-I", str(CODE / "ble_wake.py")],
stdin=subprocess.PIPE,
stdout=output,
stderr=subprocess.DEVNULL,
)
self.running[ident] = process
process.stdin.write(json.dumps(target).encode())
process.stdin.close()
# Release state lock during hardware wait so disable can cancel.
self.lock.release()
try:
try:
process.wait(timeout=45)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
output.seek(0)
raw = output.read(1024 * 1024 + 1)
finally:
self.lock.acquire()
self.running.pop(ident, None)
state = "unavailable"
try:
if len(raw) <= 1024 * 1024:
report = json.loads(raw)
write(self.root / (ident + ".last-wake"), raw, 0o600)
if process.returncode == 0 and report.get("sdk_usb_returned"):
state = "connected"
except (ValueError, OSError):
pass
return {"state": state if self.load(ident)["enabled"] else "disabled"}
def dispatch(self, method, route, value, _headers):
if method == "GET" and route == "/health":
return {"ready": True}
if method != "POST" or not isinstance(value, dict):
raise ValueError("Unsupported recovery request")
ident = value.get("device_id")
if not isinstance(ident, str) or not IDENTIFIER.fullmatch(ident):
raise ValueError("Invalid recovery identity")
if route == "/control" and set(value) == {"device_id", "enabled"}:
if type(value["enabled"]) is not bool:
raise ValueError("Recovery requires an explicit boolean")
return self.control(ident, value["enabled"])
if route == "/wake" and set(value) == {"device_id", "attempt"}:
if not isinstance(value["attempt"], str) or not re.fullmatch(
r"[0-9a-f]{32}", value["attempt"]
):
raise ValueError("Invalid wake attempt")
return self.wake(ident, value["attempt"])
raise ValueError("Unsupported recovery request")
def main():
if os.geteuid() or sys.argv[1:]:
raise RuntimeError("Use the installed fixed wake service")
trusted(ROOT, True)
if SOCKET.exists():
SOCKET.unlink()
allowed = {0, pwd.getpwnam("mission-core-insta360").pw_uid}
with Server(SOCKET, WakeService().dispatch, allowed) as server:
SOCKET.chmod(0o660)
server.serve_forever(poll_interval=0.5)
if __name__ == "__main__":
main()