"""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 == "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