IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
145
iqpilot/system/hardware/tici/qr_decode.py
Normal file
145
iqpilot/system/hardware/tici/qr_decode.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import ctypes
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from pyzbar.pyzbar import decode as _pyzbar_decode
|
||||
except Exception:
|
||||
_pyzbar_decode = None
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
QUIRC_LIB_DIR = ROOT / "third_party" / "quirc" / "lib"
|
||||
HELPER_C = Path(__file__).with_name("qr_decode_quirc.c")
|
||||
BUILD_DIR = ROOT / ".run" / "cache" / "esim_qr"
|
||||
SO_PATH = BUILD_DIR / "libiqpilot_quirc_decode.so"
|
||||
|
||||
_LIB: ctypes.CDLL | None = None
|
||||
|
||||
|
||||
def _build_decoder() -> bool:
|
||||
BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
cmd = [
|
||||
os.environ.get("CC", "cc"),
|
||||
"-O2",
|
||||
"-shared",
|
||||
"-fPIC",
|
||||
str(HELPER_C),
|
||||
str(QUIRC_LIB_DIR / "quirc.c"),
|
||||
str(QUIRC_LIB_DIR / "identify.c"),
|
||||
str(QUIRC_LIB_DIR / "decode.c"),
|
||||
str(QUIRC_LIB_DIR / "version_db.c"),
|
||||
"-I",
|
||||
str(QUIRC_LIB_DIR),
|
||||
"-o",
|
||||
str(SO_PATH),
|
||||
]
|
||||
try:
|
||||
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _load_decoder() -> ctypes.CDLL | None:
|
||||
global _LIB
|
||||
if _LIB is not None:
|
||||
return _LIB
|
||||
|
||||
if not SO_PATH.exists():
|
||||
if not _build_decoder():
|
||||
return None
|
||||
|
||||
try:
|
||||
lib = ctypes.CDLL(str(SO_PATH))
|
||||
lib.iqpilot_decode_qr_gray.argtypes = [
|
||||
ctypes.POINTER(ctypes.c_uint8),
|
||||
ctypes.c_int,
|
||||
ctypes.c_int,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int,
|
||||
]
|
||||
lib.iqpilot_decode_qr_gray.restype = ctypes.c_int
|
||||
_LIB = lib
|
||||
return _LIB
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def decode_qr(image: bytes | np.ndarray, width: int | None = None, height: int | None = None) -> list[str]:
|
||||
"""
|
||||
Decode QR payloads from a grayscale image.
|
||||
Accepts:
|
||||
- ndarray shape (H, W), uint8
|
||||
- bytes + explicit width/height
|
||||
"""
|
||||
arr: np.ndarray
|
||||
if isinstance(image, np.ndarray):
|
||||
if image.ndim != 2:
|
||||
raise ValueError("decode_qr expects grayscale ndarray with shape (H, W)")
|
||||
arr = np.ascontiguousarray(image, dtype=np.uint8)
|
||||
h, w = arr.shape
|
||||
else:
|
||||
if width is None or height is None:
|
||||
raise ValueError("width and height are required when passing raw bytes")
|
||||
arr = np.frombuffer(image, dtype=np.uint8).reshape((height, width))
|
||||
arr = np.ascontiguousarray(arr)
|
||||
h, w = arr.shape
|
||||
|
||||
if _pyzbar_decode is not None:
|
||||
try:
|
||||
pyzbar_results = _pyzbar_decode(arr)
|
||||
payloads = []
|
||||
for result in pyzbar_results:
|
||||
payload = result.data.decode("utf-8", errors="ignore").strip()
|
||||
if payload:
|
||||
payloads.append(payload)
|
||||
if payloads:
|
||||
return payloads
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
lib = _load_decoder()
|
||||
if lib is None:
|
||||
return []
|
||||
|
||||
out_size = 8192
|
||||
out_buf = ctypes.create_string_buffer(out_size)
|
||||
count = lib.iqpilot_decode_qr_gray(
|
||||
arr.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
|
||||
int(w),
|
||||
int(h),
|
||||
out_buf,
|
||||
out_size,
|
||||
)
|
||||
if count <= 0:
|
||||
return []
|
||||
|
||||
raw = out_buf.value.decode("utf-8", errors="ignore")
|
||||
return [line.strip() for line in raw.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def validate_lpa_activation_code(payload: str) -> tuple[bool, str]:
|
||||
if not payload.startswith("LPA:"):
|
||||
return False, "QR does not contain an LPA activation code"
|
||||
|
||||
parts = payload[4:].split("$")
|
||||
if len(parts) != 3:
|
||||
return False, "Invalid LPA format"
|
||||
|
||||
version, smdp, matching = [p.strip() for p in parts]
|
||||
if version != "1":
|
||||
return False, "Unsupported LPA version"
|
||||
if len(smdp) == 0 or "." not in smdp:
|
||||
return False, "Invalid SM-DP+ address"
|
||||
if len(matching) == 0:
|
||||
return False, "Missing matching ID"
|
||||
return True, ""
|
||||
|
||||
|
||||
def stable_code_key(payload: str) -> str:
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
Reference in New Issue
Block a user