Files
NODEDC_MISSION_CORE/plugins/insta360-x4/runtime/native.py
T

194 lines
7.1 KiB
Python

"""Typed private C ABI. Imported safely; vendor code loads only after admission."""
import ctypes
import json
import math
import re
import threading
from .identity import verify_isolation
ACTIONS = {
"details": 1,
"settings.read": 2,
"settings.apply": 3,
"preview.start": 4,
"preview.stop": 5,
"record.start": 6,
"record.stop": 7,
"photo.capture": 8,
"files.list": 9,
}
MAX_PACKET = 4 * 1024 * 1024
class VideoHeader(ctypes.Structure):
_fields_ = [
("sequence", ctypes.c_uint64),
("generation", ctypes.c_uint64),
("timestamp", ctypes.c_int64),
("stream_index", ctypes.c_int32),
("codec", ctypes.c_int32),
("bytes", ctypes.c_uint32),
]
def parameters(action, value):
"""Validate before any SDK call; parameters never select a file or library."""
if action == "power.wake":
if not isinstance(value, dict) or value:
raise ValueError("Wake uses the enrolled camera identity only")
return 0, "", 0.0
if action == "recovery.configure":
if (
not isinstance(value, dict)
or set(value) != {"enabled", "wakeup_confirmed"}
or type(value["enabled"]) is not bool
or type(value["wakeup_confirmed"]) is not bool
or (value["enabled"] and not value["wakeup_confirmed"])
):
raise ValueError("Confirm camera Bluetooth Wakeup before enabling recovery")
return 0, "", 0.0
if action == "offer":
if (
not isinstance(value, dict)
or set(value) != {"sdp", "layer"}
or value["layer"] != "preview"
or not isinstance(value["sdp"], str)
or not 0 < len(value["sdp"]) <= 32768
):
raise ValueError("Invalid camera preview offer")
return 0, "", 0.0
if action == "close-peer":
if (
not isinstance(value, dict)
or set(value) != {"peer_id"}
or not isinstance(value["peer_id"], str)
or not re.fullmatch(r"peer_[0-9a-f]{32}", value["peer_id"])
):
raise ValueError("Invalid camera preview identity")
return 0, "", 0.0
if action == "verify":
if not isinstance(value, dict) or value:
raise ValueError("Image verification uses the installed fixed profile")
return 0, "", 0.0
if action not in ACTIONS or not isinstance(value, dict):
raise ValueError("Unsupported camera command")
if action in ("settings.read", "settings.apply"):
required = {"mode"} if action == "settings.read" else {"mode", "key", "value"}
if (
set(value) != required
or type(value["mode"]) is not int
or not 0 <= value["mode"] <= 255
):
raise ValueError("Invalid camera mode")
if action == "settings.read":
return value["mode"], "", 0.0
if value["key"] not in (
"function_mode",
"video_resolution",
"photo_size",
"white_balance",
"iso",
"exposure_mode",
):
raise ValueError("Unsupported camera setting")
number = value["value"]
if (
type(number) not in (int, float)
or not math.isfinite(number)
or not 0 <= number <= 65535
):
raise ValueError("Invalid camera setting")
return value["mode"], value["key"], float(number)
if action == "files.list":
if (
set(value) - {"offset"}
or type(value.get("offset", 0)) is not int
or not 0 <= value.get("offset", 0) <= 100000
):
raise ValueError("Invalid camera file page")
return 0, "", float(value.get("offset", 0))
if value:
raise ValueError("Unexpected camera parameters")
return 0, "", 0.0
class NativeCamera:
def __init__(self, binding, library_path, log_directory):
# No ctypes.CDLL anywhere above this check: loading a .so can execute
# constructors even before the first explicit SDK function call.
verify_isolation(binding)
self.lock = threading.RLock()
self.video_lock = threading.Lock()
self.api = ctypes.CDLL(str(library_path))
self.api.mc_x4_abi.restype = ctypes.c_int
if self.api.mc_x4_abi() != 1:
raise RuntimeError("Incompatible camera adapter")
self.api.mc_x4_open.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
self.api.mc_x4_open.restype = ctypes.c_void_p
self.api.mc_x4_call.argtypes = [
ctypes.c_void_p,
ctypes.c_int,
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_double,
]
self.api.mc_x4_call.restype = ctypes.c_char_p
self.api.mc_x4_read_video.argtypes = [
ctypes.c_void_p,
ctypes.POINTER(VideoHeader),
ctypes.POINTER(ctypes.c_uint8),
ctypes.c_size_t,
]
self.api.mc_x4_read_video.restype = ctypes.c_int
self.api.mc_x4_close.argtypes = [ctypes.c_void_p]
self.api.mc_x4_close.restype = None
self.handle = self.api.mc_x4_open(binding.serial.encode(), str(log_directory).encode())
if not self.handle:
raise RuntimeError("SDK could not open the selected X4")
self.buffer = (ctypes.c_uint8 * MAX_PACKET)()
def call(self, action, values):
mode, key, number = parameters(action, values)
with self.lock:
if not self.handle:
raise RuntimeError("Camera session is closed")
raw = self.api.mc_x4_call(self.handle, ACTIONS[action], mode, key.encode(), number)
if not raw or len(raw) > 65536:
raise RuntimeError("Camera returned an invalid response")
result = json.loads(raw)
if not isinstance(result, dict) or result.get("state") not in (
"complete",
"error",
"unknown",
):
raise RuntimeError("Camera returned an invalid response")
return result
def video(self):
# SDK control calls may block for seconds. The callback queue has its
# own native mutex and can be drained without taking the command lock.
with self.video_lock:
if not self.handle:
return None
header = VideoHeader()
state = self.api.mc_x4_read_video(
self.handle, ctypes.byref(header), self.buffer, MAX_PACKET
)
if state < 0 or header.bytes > MAX_PACKET:
raise RuntimeError("Invalid video packet")
if state == 0:
return None
if header.stream_index not in (0, 1) or header.codec not in (0, 1) or not header.bytes:
raise RuntimeError("Unsupported video packet")
return {name: getattr(header, name) for name, _ in VideoHeader._fields_}, bytes(
self.buffer[: header.bytes]
)
def close(self):
with self.lock, self.video_lock:
if self.handle:
self.api.mc_x4_close(self.handle)
self.handle = None