Add bounded X4 Bluetooth wake and record Android recovery proof

This commit is contained in:
DCCONSTRUCTIONS
2026-09-10 12:48:09 +03:00
parent 556e376a62
commit f164606828
14 changed files with 789 additions and 43 deletions
@@ -283,6 +283,7 @@ class Inspector:
self.report["notifications"].append({"hex": raw.hex(), **timestamp()})
try:
received.extend(reader.feed(raw))
self.report["heartbeat_packets"] = reader.heartbeats
except ValueError as error:
errors.append(str(error))
+11 -1
View File
@@ -83,6 +83,7 @@ class PacketReader:
def __init__(self):
self.buffer = bytearray()
self.heartbeats = 0
def feed(self, chunk):
if not chunk or len(self.buffer) + len(chunk) > MAX_PACKET * 2:
@@ -91,12 +92,21 @@ class PacketReader:
result = []
while len(self.buffer) >= 4:
size = struct.unpack_from("<I", self.buffer)[0]
if not 16 <= size <= MAX_PACKET:
if size != 7 and not 16 <= size <= MAX_PACKET:
raise ValueError("Unknown packet framing")
if len(self.buffer) < size:
break
raw = bytes(self.buffer[:size])
del self.buffer[:size]
if size == 7:
# X4 BLE04 capture; SDK CameraSession::Start registers type5
# with OnHeartbeatHandler. This is not an Options response.
if raw != b"\x07\x00\x00\x00\x05\x00\x00":
raise ValueError("Unsupported short control packet")
self.heartbeats += 1
if self.heartbeats > 64:
raise ValueError("Heartbeat bound exceeded")
continue
_, kind, reserved, code, content, ident, tail = struct.unpack("<IBHHBIH", raw[:16])
if kind != 4 or reserved or tail or content != 2 or ident & 0xC0000000 != 0xC0000000:
raise ValueError("Unsupported message header/direction/fragmentation")
+216
View File
@@ -0,0 +1,216 @@
"""One bounded X4 wake advertisement; no camera GATT writes or power-off."""
import hashlib
import json
import re
import sys
from pathlib import Path
if "__file__" in globals():
sys.path.insert(0, str(Path(__file__).resolve().parent))
from ble_diagnostic import ( # noqa: E402
ADAPTER,
DEVICE,
PROPS,
Inspector,
assert_no_usb_owner,
plain,
timestamp,
usb_snapshot,
)
ADV = "org.bluez.LEAdvertisement1"
ADV_MANAGER = "org.bluez.LEAdvertisingManager1"
def wake_payload(target):
if set(target) != {"device_id", "serial", "bluetooth_address", "wakeup_enabled"}:
raise ValueError("Exact private target fields are required")
serial = target["serial"]
if not isinstance(serial, str) or not re.fullmatch(r"[A-Za-z0-9]{7,64}", serial):
raise ValueError("Full USB serial is required, never a suffix alone")
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"]):
raise ValueError("Prior correlated Bluetooth address is required")
# Company ID 0x004c is passed separately to BlueZ. X4 remote spec section1.
return (
bytes.fromhex("0215094f5242495409ff0f00")
+ serial[-6:].encode()
+ bytes.fromhex("00000000e401")
)
def matching_sdk(samples, expected):
return any(
d.get("idVendor") == "2e1a"
and d.get("idProduct") == "0002"
and "instax4_" + hashlib.sha256(d.get("serial", "").encode()).hexdigest()[:32] == expected
for d in samples["devices"]
)
def run(target, report):
payload = wake_payload(target)
assert_no_usb_owner(target["device_id"])
inspector = Inspector(report)
advertisement = None
manager = None
registered = False
attempted = False
own_path = "/org/missioncore/x4/wake"
initial = {}
cleanup = []
try:
objects = inspector.objects()
adapters = [
(p, x)
for p, x in objects.items()
if x.get(ADAPTER, {}).get("Powered") and ADV_MANAGER in x
]
if len(adapters) != 1:
raise RuntimeError("Exactly one powered advertising adapter is required")
adapter_path, adapter = adapters[0]
if (
adapter[ADAPTER].get("Pairable", True)
or adapter[ADAPTER].get("Discoverable", True)
or adapter[ADV_MANAGER].get("ActiveInstances", -1) != 0
):
raise RuntimeError(
"Require non-pairable/non-discoverable adapter with no advertisements"
)
if not adapter[ADV_MANAGER].get("SupportedInstances"):
raise RuntimeError("No advertising instance is available")
initial = {p: plain(x[DEVICE]) for p, x in objects.items() if DEVICE in x}
for device in initial.values():
if device.get("Address") == target["bluetooth_address"] and any(
device.get(k) for k in ("Connected", "Paired", "Bonded")
):
raise RuntimeError("Preserve existing target connection/bond")
report["adapter_before"] = plain(adapter)
manager = inspector.iface(adapter_path, ADV_MANAGER)
import dbus.service
class Advertisement(dbus.service.Object):
released = False
@dbus.service.method(PROPS, in_signature="s", out_signature="a{sv}")
def GetAll(self, interface):
if interface != ADV:
return dbus.Dictionary({}, signature="sv")
return dbus.Dictionary(
{
"Type": dbus.String("peripheral"),
"ManufacturerData": dbus.Dictionary(
{dbus.UInt16(0x004C): dbus.Array(payload, signature="y")},
signature="qv",
),
"Discoverable": dbus.Boolean(True),
"DiscoverableTimeout": dbus.UInt16(20),
"Timeout": dbus.UInt16(20),
# Honored only with BlueZ experimental features. Never enable
# that setting here; actual on-air interval is not asserted.
"MinInterval": dbus.UInt32(100),
"MaxInterval": dbus.UInt32(150),
},
signature="sv",
)
@dbus.service.method(ADV, in_signature="", out_signature="")
def Release(self):
self.released = True
report["advertisement_released"] = timestamp()
advertisement = Advertisement(inspector.bus, own_path)
result, errors = [], []
assert_no_usb_owner(target["device_id"])
report["advertisement_intent"] = {
"manufacturer_id": 0x004C,
"bytes": len(payload),
"timeout_seconds": 20,
**timestamp(),
}
attempted = True
manager.RegisterAdvertisement(
own_path,
dbus.Dictionary({}, signature="sv"),
reply_handler=lambda: result.append(True),
error_handler=lambda e: errors.append(str(e)),
timeout=5,
)
inspector.pump(6, lambda: bool(result or errors))
if errors or not result:
raise RuntimeError(errors[0] if errors else "Advertisement registration timed out")
registered = True
report["advertisement_registered"] = timestamp()
inspector.pump(20, lambda: matching_sdk(usb_snapshot(), target["device_id"]))
report["sdk_usb_returned"] = matching_sdk(usb_snapshot(), target["device_id"])
finally:
if attempted and advertisement and not advertisement.released:
done, errors = [], []
try:
manager.UnregisterAdvertisement(
own_path,
reply_handler=lambda: done.append(True),
error_handler=lambda e: errors.append(str(e)),
timeout=5,
)
inspector.pump(6, lambda: bool(done or errors or advertisement.released))
if (errors and registered) or not (done or errors or advertisement.released):
cleanup.append("Advertisement removal did not complete")
except Exception as error:
cleanup.append(str(error))
# A wake beacon is connectable; disconnect only the prior correlated
# X4 address when it acquired a connection during our advertisement.
try:
current = {p: plain(x[DEVICE]) for p, x in inspector.objects().items() if DEVICE in x}
for path, device in current.items():
if device.get("Connected") and not initial.get(path, {}).get("Connected"):
if attempted and device.get("Address") == target["bluetooth_address"]:
inspector.iface(path, DEVICE).Disconnect(timeout=5)
report["wake_connection_closed"] = True
else:
cleanup.append("New uncorrelated Bluetooth connection preserved")
if manager:
report["adapter_after"] = plain(inspector.objects().get(adapter_path, {}))
except Exception as error:
cleanup.append(str(error))
try:
if advertisement:
advertisement.remove_from_connection()
except Exception as error:
cleanup.append(str(error))
finally:
# Private bus owner disappearance also removes its advertisement.
inspector.close()
report["cleanup_errors"].extend(cleanup)
def main():
report = {
"schema": "missioncore.insta360.ble-wake/v1",
"started": timestamp(),
"vendor_writes": 0,
"pair_calls": 0,
"notify_subscriptions": 0,
"state": "running",
}
try:
raw = sys.stdin.buffer.read(4097)
if len(raw) > 4096:
raise ValueError("Private wake target exceeds limit")
run(json.loads(raw), report)
report["state"] = "complete" if report.get("sdk_usb_returned") else "no_sdk_return"
except Exception as error:
report.update(state="error", error=str(error))
if report.get("cleanup_errors"):
report["state"] = "error"
report["finished"] = timestamp()
print(json.dumps(report), flush=True)
return 0 if report["state"] == "complete" else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,53 +1,62 @@
"""Package the exact diagnostic entry point; never install host prerequisites."""
"""Package exact model diagnostics; never install host prerequisites."""
import argparse
import hashlib
import json
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ENTRY = """import hashlib, json, sys, zipfile
ENTRY = """import hashlib, json, sys, types, zipfile
from pathlib import Path
with zipfile.ZipFile(sys.argv[0]) as archive:
manifest = json.loads(archive.read("manifest.json"))
source = archive.read("ble_diagnostic.py")
codec = archive.read("ble_options.py")
if hashlib.sha256(source).hexdigest() != manifest["source_sha256"]:
raise RuntimeError("Diagnostic source hash mismatch")
if hashlib.sha256(codec).hexdigest() != manifest["codec_sha256"]:
raise RuntimeError("Diagnostic codec hash mismatch")
ident = hashlib.sha256(source + b"\\0" + codec).hexdigest()[:24]
if Path(sys.argv[0]).name != "mission-core-x4-ble-" + ident + ".pyz":
raw = archive.read("manifest.json")
manifest = json.loads(raw)
names = ("ble_options", "ble_diagnostic", "ble_wake")
modules = {name: archive.read(name + ".py") for name in names}
ident = hashlib.sha256(raw).hexdigest()[:24]
entry = manifest["entrypoint"]
if entry not in {"ble_diagnostic", "ble_wake"}:
raise RuntimeError("Unknown diagnostic entry point")
prefix = "mission-core-x4-wake-" if entry == "ble_wake" else "mission-core-x4-ble-"
if Path(sys.argv[0]).name != prefix + ident + ".pyz":
raise RuntimeError("Diagnostic artifact name mismatch")
import types
module = types.ModuleType("ble_options")
exec(compile(codec, "ble_options.py", "exec"), module.__dict__)
sys.modules["ble_options"] = module
exec(compile(source, "ble_diagnostic.py", "exec"), {"__name__": "__main__"})
for name, source in modules.items():
if hashlib.sha256(source).hexdigest() != manifest["modules"][name]:
raise RuntimeError("Diagnostic source hash mismatch")
for name, source in modules.items():
module = types.ModuleType(name)
sys.modules[name] = module
exec(compile(source, name + ".py", "exec"), module.__dict__)
raise SystemExit(sys.modules[entry].main())
"""
def build():
source = (ROOT / "packaging/ble_diagnostic.py").read_bytes()
codec = (ROOT / "packaging/ble_options.py").read_bytes()
sha = hashlib.sha256(source).hexdigest()
def build(wake=False):
modules = {
name: (ROOT / "packaging" / (name + ".py")).read_bytes()
for name in ("ble_options", "ble_diagnostic", "ble_wake")
}
manifest = {
"schema": "missioncore.insta360.ble-diagnostic-artifact/v1",
"source_sha256": sha,
"codec_sha256": hashlib.sha256(codec).hexdigest(),
"schema": "missioncore.insta360.ble-diagnostic-artifact/v2",
"entrypoint": "ble_wake" if wake else "ble_diagnostic",
"modules": {name: hashlib.sha256(data).hexdigest() for name, data in modules.items()},
"dependencies": ["bluez", "python3-dbus", "python3-gi"],
"persistent_system_changes": False,
"vendor_commands": [8],
"scope": "bounded-discovery-and-explicit-identity-options-read",
"vendor_commands": [] if wake else [8],
"scope": "single-addressed-wake-advertisement"
if wake
else "bounded-discovery-and-explicit-identity-options-read",
}
ident = hashlib.sha256(source + b"\0" + codec).hexdigest()[:24]
output = ROOT / "build" / ("mission-core-x4-ble-" + ident + ".pyz")
raw = json.dumps(manifest, sort_keys=True).encode()
ident = hashlib.sha256(raw).hexdigest()[:24]
prefix = "mission-core-x4-wake-" if wake else "mission-core-x4-ble-"
output = ROOT / "build" / (prefix + ident + ".pyz")
with output.open("xb") as stream, zipfile.ZipFile(stream, "w") as archive:
for name, data in {
"__main__.py": ENTRY.encode(),
"ble_diagnostic.py": source,
"ble_options.py": codec,
"manifest.json": json.dumps(manifest, sort_keys=True).encode(),
"manifest.json": raw,
**{name + ".py": data for name, data in modules.items()},
}.items():
info = zipfile.ZipInfo(name, date_time=(2026, 9, 10, 0, 0, 0))
info.external_attr = 0o600 << 16
@@ -56,10 +65,12 @@ def build():
return {
"artifact": str(output),
"sha256": hashlib.sha256(output.read_bytes()).hexdigest(),
"source_sha256": sha,
"bytes": output.stat().st_size,
"modules": manifest["modules"],
}
if __name__ == "__main__":
print(json.dumps(build()))
parser = argparse.ArgumentParser()
parser.add_argument("--wake", action="store_true")
print(json.dumps(build(parser.parse_args().wake)))
+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-4"
VERSION = "0.1.3-6"
WHEELS = {
"aiohappyeyeballs",
"aiohttp",
@@ -159,6 +159,7 @@ def build(output):
"prepare.py",
"ble_diagnostic.py",
"ble_options.py",
"ble_wake.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"):
for name in ("ble_diagnostic.py", "ble_options.py", "ble_wake.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")
@@ -53,6 +53,30 @@ class OptionsTests(unittest.TestCase):
{"acknowledged": True, "present": False, "value": None},
)
def test_captured_heartbeat_is_not_an_options_response(self):
heartbeat = bytes.fromhex("07000000050000")
raw = reply(field(1, 95) + field(2, field(95, 1)))
reader = codec.PacketReader()
self.assertEqual(reader.feed(heartbeat[:4]), [])
self.assertEqual(reader.feed(heartbeat[4:]), [])
self.assertEqual(reader.heartbeats, 1)
packets = reader.feed(heartbeat + raw[:9])
self.assertEqual(packets, [])
packets = reader.feed(raw[9:] + heartbeat)
self.assertEqual(len(packets), 1)
self.assertEqual(reader.heartbeats, 3)
self.assertEqual(codec.decode_options_response(packets[0], 1, [95])[95]["value"], 1)
def test_short_control_packets_are_exact_and_bounded(self):
for raw in ("07000000040000", "07000000050100", "0800000005000000"):
with self.assertRaises(ValueError):
codec.PacketReader().feed(bytes.fromhex(raw))
reader = codec.PacketReader()
for _ in range(64):
self.assertEqual(reader.feed(bytes.fromhex("07000000050000")), [])
with self.assertRaises(ValueError):
reader.feed(bytes.fromhex("07000000050000"))
def test_explicit_zero_and_packed_option_list(self):
packet = codec.PacketReader().feed(reply(field(1, b"\x5f") + field(2, field(95, 0))))[0]
self.assertEqual(codec.decode_options_response(packet, 1, [95])[95]["value"], 0)
@@ -0,0 +1,64 @@
"""Synthetic identity and exact wake payload; no host Bluetooth access."""
import hashlib
import importlib.util
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
directory = Path(__file__).resolve().parents[1] / "packaging"
sys.path.insert(0, str(directory))
spec = importlib.util.spec_from_file_location("wake_under_test", directory / "ble_wake.py")
wake = importlib.util.module_from_spec(spec)
spec.loader.exec_module(wake)
sys.path.pop(0)
def target(serial="SYNTHETIC34UQG5"):
return {
"serial": serial,
"device_id": "instax4_" + hashlib.sha256(serial.encode()).hexdigest()[:32],
"bluetooth_address": "02:00:00:00:00:01",
"wakeup_enabled": True,
}
class WakeTests(unittest.TestCase):
def test_spec_vector_has_one_company_id_and_fits_legacy_advertisement(self):
data = wake.wake_payload(target())
self.assertEqual(data.hex(), "0215094f5242495409ff0f0033345551473500000000e401")
self.assertEqual(len(data) + 7, 31)
def test_unknown_wakeup_suffix_only_and_wrong_identity_are_rejected(self):
for value in (
target("34UQG5"),
dict(target(), device_id="instax4_" + "0" * 32),
dict(target(), wakeup_enabled=False),
dict(target(), bluetooth_address="unknown"),
):
with self.assertRaises(ValueError):
wake.wake_payload(value)
def test_sdk_identity_requires_full_serial_and_correct_mode(self):
value = target()
device = {"idVendor": "2e1a", "idProduct": "0002", "serial": value["serial"]}
self.assertTrue(wake.matching_sdk({"devices": [device]}, value["device_id"]))
for changed in (
dict(device, serial="34UQG5"),
dict(device, idVendor="070a", idProduct="4026"),
):
self.assertFalse(wake.matching_sdk({"devices": [changed]}, value["device_id"]))
def test_present_sdk_owner_blocks_before_any_bluetooth_access(self):
with (
patch.object(wake, "assert_no_usb_owner", side_effect=RuntimeError("owned")),
patch.object(wake, "Inspector") as inspector,
):
with self.assertRaisesRegex(RuntimeError, "owned"):
wake.run(target(), {})
inspector.assert_not_called()
if __name__ == "__main__":
unittest.main()