Files
2026-07-20 11:06:57 -05:00

146 lines
3.6 KiB
Python

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()