diff --git a/local_service_win/.gitignore b/local_service_win/.gitignore new file mode 100644 index 0000000..b23a173 --- /dev/null +++ b/local_service_win/.gitignore @@ -0,0 +1,140 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff +instance/ +.webassets-cache + +# Scrapy stuff +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +#Pipfile.lock + +# poetry +#poetry.lock + +# pdm +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IDE +.vscode/ +.idea/ diff --git a/local_service_win/KneronPLUS-3.1.2-py3-none-any.whl b/local_service_win/KneronPLUS-3.1.2-py3-none-any.whl new file mode 100644 index 0000000..c8f5b22 Binary files /dev/null and b/local_service_win/KneronPLUS-3.1.2-py3-none-any.whl differ diff --git a/local_service_win/LocalAPI/__init__.py b/local_service_win/LocalAPI/__init__.py new file mode 100644 index 0000000..da5f2a6 --- /dev/null +++ b/local_service_win/LocalAPI/__init__.py @@ -0,0 +1 @@ +# LocalAPI package diff --git a/local_service_win/LocalAPI/legacy_plus121_runner.py b/local_service_win/LocalAPI/legacy_plus121_runner.py new file mode 100644 index 0000000..62f60a1 --- /dev/null +++ b/local_service_win/LocalAPI/legacy_plus121_runner.py @@ -0,0 +1,297 @@ +from __future__ import annotations + +import ctypes +import hashlib +import json +import os +import sys +import time +from pathlib import Path +from typing import Any, Dict + +KDP_MAGIC_CONNECTION_PASS = 536173391 +KP_SUCCESS = 0 +KP_RESET_REBOOT = 0 +USB_WAIT_CONNECT_DELAY_MS = 100 +USB_WAIT_AFTER_REBOOT_MS = 2000 +USB_WAIT_RETRY_CONNECT_MS = 10 +MAX_RETRY_CONNECT_TIMES = 10 + + +def _normalize_code(code: int) -> int: + # Some legacy paths may return int8-like unsigned values (e.g. 253 for -3). + if code > 127: + return code - 256 + return code + + +def _load_libkplus(dist_root: Path) -> ctypes.CDLL: + lib_dir = dist_root / "kp" / "lib" + dll_path = lib_dir / "libkplus.dll" + if not dll_path.is_file(): + raise RuntimeError(f"libkplus.dll not found: {dll_path}") + + if hasattr(os, "add_dll_directory"): + os.add_dll_directory(str(lib_dir)) + + lib = ctypes.CDLL(str(dll_path)) + lib.kp_connect_devices.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)] + lib.kp_connect_devices.restype = ctypes.c_void_p + lib.kp_set_timeout.argtypes = [ctypes.c_void_p, ctypes.c_int] + lib.kp_set_timeout.restype = None + lib.kp_reset_device.argtypes = [ctypes.c_void_p, ctypes.c_int] + lib.kp_reset_device.restype = ctypes.c_int + lib.kp_load_firmware_from_file.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] + lib.kp_load_firmware_from_file.restype = ctypes.c_int + lib.kp_update_kdp_firmware_from_files.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool] + lib.kp_update_kdp_firmware_from_files.restype = ctypes.c_int + lib.kp_disconnect_devices.argtypes = [ctypes.c_void_p] + lib.kp_disconnect_devices.restype = ctypes.c_int + lib.kp_scan_devices.argtypes = [] + lib.kp_scan_devices.restype = ctypes.c_void_p + if hasattr(lib, "kp_error_string"): + lib.kp_error_string.argtypes = [ctypes.c_int] + lib.kp_error_string.restype = ctypes.c_char_p + return lib + + +def _errstr(lib: ctypes.CDLL, code: int) -> str: + signed_code = _normalize_code(code) + if hasattr(lib, "kp_error_string"): + try: + msg = lib.kp_error_string(int(code)) + if not msg and signed_code != code: + msg = lib.kp_error_string(int(signed_code)) + if msg: + return msg.decode("utf-8", errors="replace") + except Exception: + pass + return str(code) + + +def _find_port_id_with_kp(dist_root: Path, port_id: int | None, scan_index: int | None) -> int: + if port_id is not None: + return int(port_id) + + sys.path.insert(0, str(dist_root)) + import kp + + device_list = kp.core.scan_devices() + if device_list.device_descriptor_number == 0: + raise RuntimeError("NO_DEVICE: no device found") + + idx = 0 if scan_index is None else int(scan_index) + if idx < 0 or idx >= device_list.device_descriptor_number: + raise RuntimeError(f"INVALID_SCAN_INDEX: {idx}") + return int(device_list.device_descriptor_list[idx].usb_port_id) + + +def _file_diag(path_str: str) -> Dict[str, Any]: + p = Path(path_str) + info: Dict[str, Any] = { + "path": str(p), + "name": p.name, + "exists": p.is_file(), + } + if not p.is_file(): + return info + data = p.read_bytes() + info["size_bytes"] = len(data) + info["sha256"] = hashlib.sha256(data).hexdigest() + return info + + +def _scan_diag_with_kp(dist_root: Path) -> Dict[str, Any]: + sys.path.insert(0, str(dist_root)) + import kp + + scanned = [] + dev_list = kp.core.scan_devices() + num = int(dev_list.device_descriptor_number) + for i in range(num): + d = dev_list.device_descriptor_list[i] + scanned.append( + { + "scan_index": i, + "usb_port_id": int(d.usb_port_id), + "vendor_id": int(d.vendor_id), + "product_id": f"0x{int(d.product_id):04X}", + "link_speed": str(d.link_speed), + "usb_port_path": str(d.usb_port_path), + "is_connectable": bool(d.is_connectable), + "firmware": str(d.firmware), + } + ) + return {"count": num, "devices": scanned} + + +def _firmware_from_scan(scan_diag: Dict[str, Any], port_id: int) -> str: + for d in scan_diag.get("devices", []): + if int(d.get("usb_port_id", -1)) == int(port_id): + return str(d.get("firmware", "")).upper() + return "" + + +def _product_id_from_scan(scan_diag: Dict[str, Any], port_id: int) -> int | None: + for d in scan_diag.get("devices", []): + if int(d.get("usb_port_id", -1)) != int(port_id): + continue + raw = d.get("product_id") + if raw is None: + return None + text = str(raw).strip() + try: + if text.lower().startswith("0x"): + return int(text, 16) + return int(text) + except Exception: + return None + return None + + +def _connect_with_magic(lib: ctypes.CDLL, port_id: int) -> ctypes.c_void_p: + port_ids = (ctypes.c_int * 1)(int(port_id)) + status = ctypes.c_int(KDP_MAGIC_CONNECTION_PASS) + device_group = lib.kp_connect_devices(1, port_ids, ctypes.byref(status)) + if not device_group or status.value != KP_SUCCESS: + signed = _normalize_code(status.value) + raise RuntimeError( + f"CONNECT_FAILED: raw_code={status.value}, signed_code={signed}, msg={_errstr(lib, status.value)}" + ) + return device_group + + +def _reboot_and_reconnect(lib: ctypes.CDLL, device_group: ctypes.c_void_p, port_id: int) -> ctypes.c_void_p: + time.sleep(USB_WAIT_CONNECT_DELAY_MS / 1000.0) + ret = lib.kp_reset_device(device_group, KP_RESET_REBOOT) + if ret != KP_SUCCESS: + raise RuntimeError( + f"RESET_FAILED: raw_code={ret}, signed_code={_normalize_code(ret)}, msg={_errstr(lib, ret)}" + ) + time.sleep(USB_WAIT_AFTER_REBOOT_MS / 1000.0) + lib.kp_disconnect_devices(device_group) + + retries = 0 + while retries <= MAX_RETRY_CONNECT_TIMES: + try: + return _connect_with_magic(lib, port_id) + except RuntimeError: + time.sleep(USB_WAIT_RETRY_CONNECT_MS / 1000.0) + retries += 1 + + raise RuntimeError("RECONNECT_FAILED: max retry exceeded") + + +def main() -> None: + stage = "init" + diag: Dict[str, Any] = {} + try: + if len(sys.argv) != 2: + raise RuntimeError("missing json payload argument") + + req: Dict[str, Any] = json.loads(sys.argv[1]) + dist_root = Path(req["legacy_dist_root"]) + lib = _load_libkplus(dist_root) + + stage = "resolve_port" + port_id = _find_port_id_with_kp(dist_root, req.get("port_id"), req.get("scan_index")) + timeout_ms = req.get("timeout_ms", 5000) + scpu_path = req["scpu_path"] + ncpu_path = req["ncpu_path"] + loader_path = req.get("loader_path") or str(Path(scpu_path).with_name("fw_loader.bin")) + scan_diag = _scan_diag_with_kp(dist_root) + detected_firmware = _firmware_from_scan(scan_diag, int(port_id)) + selected_product_id = _product_id_from_scan(scan_diag, int(port_id)) + diag = { + "selected_port_id": int(port_id), + "selected_product_id": ( + f"0x{int(selected_product_id):04X}" if selected_product_id is not None else None + ), + "timeout_ms": int(timeout_ms) if timeout_ms is not None else None, + "firmware_files": { + "loader": _file_diag(loader_path), + "scpu": _file_diag(scpu_path), + "ncpu": _file_diag(ncpu_path), + }, + "scan": scan_diag, + "detected_firmware": detected_firmware, + } + + stage = "connect" + device_group = _connect_with_magic(lib, port_id) + + stage = "set_timeout" + if timeout_ms is not None: + lib.kp_set_timeout(device_group, int(timeout_ms)) + + method = "" + if detected_firmware == "KDP": + if not Path(loader_path).is_file(): + raise RuntimeError(f"LOADER_NOT_FOUND: {loader_path}") + + stage = "fw_switch_to_usb_boot_loader" + ret = lib.kp_update_kdp_firmware_from_files( + device_group, + loader_path.encode("utf-8"), + None, + True, + ) + method = "kp_update_kdp_firmware_from_files(loader)->kp_load_firmware_from_file" + if ret != KP_SUCCESS: + stage = "disconnect_after_fw_fail" + lib.kp_disconnect_devices(device_group) + raise RuntimeError( + f"FW_LOAD_FAILED: method={method}, raw_code={ret}, msg={_errstr(lib, ret)}" + ) + + stage = "fw_load_kdp2_after_loader" + if timeout_ms is not None: + lib.kp_set_timeout(device_group, int(timeout_ms)) + ret = lib.kp_load_firmware_from_file( + device_group, + scpu_path.encode("utf-8"), + ncpu_path.encode("utf-8"), + ) + else: + stage = "fw_load_kdp2_direct" + method = "kp_load_firmware_from_file_direct" + ret = lib.kp_load_firmware_from_file( + device_group, + scpu_path.encode("utf-8"), + ncpu_path.encode("utf-8"), + ) + + if ret != KP_SUCCESS: + stage = "disconnect_after_fw_fail" + lib.kp_disconnect_devices(device_group) + raise RuntimeError( + f"FW_LOAD_FAILED: method={method}, raw_code={ret}, msg={_errstr(lib, ret)}" + ) + + stage = "disconnect_after_fw_success" + # After firmware update with auto_reboot, disconnect may fail due to USB re-enumeration. + disc = lib.kp_disconnect_devices(device_group) + if disc != KP_SUCCESS: + disc_info = f"disconnect_nonzero_raw={disc},signed={_normalize_code(disc)}" + else: + disc_info = "disconnect_ok" + + print( + json.dumps( + { + "ok": True, + "port_id": int(port_id), + "connect_mode": "kp_connect_devices_with_magic_pass", + "firmware_method": method, + "disconnect_info": disc_info, + "diag": diag, + } + ) + ) + except Exception as exc: + print(json.dumps({"ok": False, "stage": stage, "error": str(exc), "diag": diag})) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/local_service_win/LocalAPI/main.py b/local_service_win/LocalAPI/main.py new file mode 100644 index 0000000..076410f --- /dev/null +++ b/local_service_win/LocalAPI/main.py @@ -0,0 +1,1282 @@ +from __future__ import annotations + +import base64 +import json +import os +import tempfile +import subprocess +import sys +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional + +from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile +from fastapi.responses import FileResponse, JSONResponse, StreamingResponse +from pydantic import BaseModel, Field + +import kp + +try: + import cv2 # type: ignore +except Exception: + cv2 = None + + +SERVICE_VERSION = "0.1.0" +PROJECT_ROOT = Path(__file__).resolve().parent.parent +DFUT_ROOT = PROJECT_ROOT / "third_party" / "Kneron_DFUT" +DFUT_BIN = DFUT_ROOT / "bin" +DFUT_EXE = DFUT_BIN / "KneronDFUT.exe" +KP121_DIST = PROJECT_ROOT / "third_party" / "kneron_plus_1_2_1" / "dist" +KP121_RUNNER = Path(__file__).resolve().parent / "legacy_plus121_runner.py" +VIDEO_VIEWER_HTML = PROJECT_ROOT / "TestRes" / "Images" / "VideoInferenceWeb.html" + + +@dataclass +class DeviceState: + device_group: Optional[kp.DeviceGroup] = None + port_id: Optional[int] = None + model_desc: Optional[kp.ModelNefDescriptor] = None + + +STATE = DeviceState() +STATE_LOCK = threading.Lock() + + +app = FastAPI(title="Kneron LocalAPI", version=SERVICE_VERSION) + + +def _ok(data: Any) -> Dict[str, Any]: + return {"ok": True, "data": data, "error": None} + + +def _err(code: str, message: str) -> Dict[str, Any]: + return {"ok": False, "data": None, "error": {"code": code, "message": message}} + + +def _require_device() -> kp.DeviceGroup: + if STATE.device_group is None: + raise HTTPException(status_code=400, detail=_err("NO_DEVICE", "No connected device")) + return STATE.device_group + + +def _image_format_from_str(value: str) -> kp.ImageFormat: + value = value.upper() + # Build mapping from symbols that exist in the installed kp version. + candidate_names = { + "RGB565": "KP_IMAGE_FORMAT_RGB565", + "RGBA8888": "KP_IMAGE_FORMAT_RGBA8888", + "RAW8": "KP_IMAGE_FORMAT_RAW8", + "YUYV": "KP_IMAGE_FORMAT_YUYV", + "YUV420": "KP_IMAGE_FORMAT_YUV420", + } + mapping: Dict[str, kp.ImageFormat] = {} + for key, attr_name in candidate_names.items(): + enum_value = getattr(kp.ImageFormat, attr_name, None) + if enum_value is not None: + mapping[key] = enum_value + if value not in mapping: + supported = ", ".join(sorted(mapping.keys())) + raise HTTPException( + status_code=400, + detail=_err( + "INVALID_IMAGE_FORMAT", + f"Unsupported image_format: {value}. supported=[{supported}]", + ), + ) + return mapping[value] + + +def _channels_ordering_from_str(value: str) -> kp.ChannelOrdering: + value = value.upper() + candidate_names = { + "HCW": "KP_CHANNEL_ORDERING_HCW", + "CHW": "KP_CHANNEL_ORDERING_CHW", + "HWC": "KP_CHANNEL_ORDERING_HWC", + "DEFAULT": "KP_CHANNEL_ORDERING_DEFAULT", + } + mapping: Dict[str, kp.ChannelOrdering] = {} + for key, attr_name in candidate_names.items(): + enum_value = getattr(kp.ChannelOrdering, attr_name, None) + if enum_value is not None: + mapping[key] = enum_value + if "DEFAULT" not in mapping: + if "CHW" in mapping: + mapping["DEFAULT"] = mapping["CHW"] + elif "HCW" in mapping: + mapping["DEFAULT"] = mapping["HCW"] + if value not in mapping: + supported = ", ".join(sorted(mapping.keys())) + raise HTTPException( + status_code=400, + detail=_err( + "INVALID_CHANNEL_ORDERING", + f"Unsupported channels_ordering: {value}. supported=[{supported}]", + ), + ) + return mapping[value] + + +def _expected_image_size_bytes(image_format: str, width: int, height: int) -> Optional[int]: + fmt = image_format.upper() + if fmt == "RGB565": + return width * height * 2 + if fmt == "RGBA8888": + return width * height * 4 + if fmt == "RAW8": + return width * height + if fmt == "YUYV": + return width * height * 2 + if fmt == "YUV420": + # YUV420 requires even width/height; caller checks exact size only. + return (width * height * 3) // 2 + return None + + +def _product_name_from_id(product_id: int) -> str: + try: + return kp.ProductId(product_id).name.replace("KP_DEVICE_", "") + except ValueError: + return "UNKNOWN" + + +@app.exception_handler(HTTPException) +def http_exception_handler(_: Request, exc: HTTPException) -> JSONResponse: + if isinstance(exc.detail, dict) and "ok" in exc.detail: + return JSONResponse(status_code=exc.status_code, content=exc.detail) + return JSONResponse( + status_code=exc.status_code, + content=_err("HTTP_ERROR", str(exc.detail)), + ) + + +@app.exception_handler(Exception) +def unhandled_exception_handler(_: Request, exc: Exception) -> JSONResponse: + return JSONResponse( + status_code=500, + content=_err("INTERNAL_ERROR", str(exc)), + ) + + +class ConnectRequest(BaseModel): + port_id: Optional[int] = Field(default=None) + scan_index: Optional[int] = Field(default=None) + timeout_ms: Optional[int] = Field(default=5000) + + +class FirmwareLoadRequest(BaseModel): + scpu_path: str + ncpu_path: str + + +class LegacyPlusFirmwareLoadRequest(BaseModel): + port_id: Optional[int] = Field(default=None) + scan_index: Optional[int] = Field(default=None) + timeout_ms: Optional[int] = Field(default=5000) + loader_path: Optional[str] = Field(default=None) + scpu_path: str + ncpu_path: str + + +class ModelLoadRequest(BaseModel): + nef_path: str + + +class DriverInstallRequest(BaseModel): + target: str = "ALL" # ALL | KL520 | KL720 | KL630 | KL730 | KL830 + force: bool = False + + +class InferenceRunRequest(BaseModel): + model_id: int + image_format: str + width: int + height: int + image_base64: str + channels_ordering: str = "DEFAULT" + output_dtype: str = "float32" + + +def _resolve_port_id(req: ConnectRequest) -> int: + port_id = req.port_id + if port_id is not None: + return int(port_id) + + device_list = kp.core.scan_devices() + if device_list.device_descriptor_number == 0: + raise HTTPException(status_code=404, detail=_err("NO_DEVICE", "No device found")) + + scan_index = 0 if req.scan_index is None else req.scan_index + if scan_index < 0 or scan_index >= device_list.device_descriptor_number: + raise HTTPException( + status_code=400, + detail=_err("INVALID_SCAN_INDEX", f"Invalid scan_index: {scan_index}"), + ) + return int(device_list.device_descriptor_list[scan_index].usb_port_id) + + +def _scan_devices_snapshot() -> List[Dict[str, Any]]: + device_list = kp.core.scan_devices() + out: List[Dict[str, Any]] = [] + for i in range(int(device_list.device_descriptor_number)): + d = device_list.device_descriptor_list[i] + out.append( + { + "scan_index": i, + "usb_port_id": int(d.usb_port_id), + "product_id": int(d.product_id), + "firmware": str(d.firmware), + "usb_port_path": str(d.usb_port_path), + } + ) + return out + + +def _kl520_kdp_observed_after_timeout(target_port_id: int) -> Dict[str, Any]: + devices = _scan_devices_snapshot() + kl520 = [d for d in devices if int(d["product_id"]) == 0x0100] + exact = [d for d in kl520 if int(d["usb_port_id"]) == int(target_port_id)] + exact_kdp = [d for d in exact if "KDP" in str(d["firmware"]).upper()] + if exact_kdp: + return {"is_success": True, "reason": "exact_port_kdp", "devices": devices} + if not exact: + any_kdp = [d for d in kl520 if "KDP" in str(d["firmware"]).upper()] + if any_kdp: + return {"is_success": True, "reason": "reenumerated_port_kdp", "devices": devices} + return {"is_success": False, "reason": "kdp_not_observed", "devices": devices} + + +def _ensure_file_exists(path: Path, label: str) -> None: + if not path.is_file(): + raise HTTPException( + status_code=500, + detail=_err("FILE_NOT_FOUND", f"{label} not found: {path}"), + ) + + +def _run_dfut_command( + args: List[str], + timeout_sec: int = 180, + echo_console: bool = False, + echo_tag: str = "DFUT", +) -> Dict[str, Any]: + _ensure_file_exists(DFUT_EXE, "DFUT executable") + if echo_console: + print(f"[{echo_tag}] CMD: {' '.join(args)}", flush=True) + + stdout_lines: List[str] = [] + stderr_lines: List[str] = [] + + proc = subprocess.Popen( + args, + cwd=str(DFUT_BIN), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + bufsize=1, + ) + + def _read_stream(stream: Any, collector: List[str], stream_name: str) -> None: + if stream is None: + return + try: + for line in iter(stream.readline, ""): + text = line.rstrip("\r\n") + collector.append(text) + if echo_console: + print(f"[{echo_tag}][{stream_name}] {text}", flush=True) + finally: + try: + stream.close() + except Exception: + pass + + t_out = threading.Thread(target=_read_stream, args=(proc.stdout, stdout_lines, "STDOUT"), daemon=True) + t_err = threading.Thread(target=_read_stream, args=(proc.stderr, stderr_lines, "STDERR"), daemon=True) + t_out.start() + t_err.start() + + try: + proc.wait(timeout=timeout_sec) + except subprocess.TimeoutExpired as exc: + proc.kill() + proc.wait() + t_out.join(timeout=1.0) + t_err.join(timeout=1.0) + stdout_text = "\n".join(stdout_lines).strip() + stderr_text = "\n".join(stderr_lines).strip() + raise HTTPException( + status_code=504, + detail=_err( + "DFUT_TIMEOUT", + ( + f"DFUT timed out after {timeout_sec}s. " + f"stdout={stdout_text} " + f"stderr={stderr_text}" + ), + ), + ) + t_out.join(timeout=1.0) + t_err.join(timeout=1.0) + + stdout_text = "\n".join(stdout_lines).strip() + stderr_text = "\n".join(stderr_lines).strip() + if proc.returncode != 0: + signed_code = proc.returncode + if signed_code > 0x7FFFFFFF: + signed_code -= 0x100000000 + raise HTTPException( + status_code=500, + detail=_err( + "DFUT_FAILED", + ( + f"DFUT failed with return code {proc.returncode} (signed={signed_code}). " + f"command={args}. " + f"stdout={stdout_text} " + f"stderr={stderr_text}" + ), + ), + ) + return { + "return_code": proc.returncode, + "stdout": stdout_text, + "stderr": stderr_text, + "command": args, + } + + +def _run_legacy_plus121_load_firmware(req: LegacyPlusFirmwareLoadRequest) -> Dict[str, Any]: + _ensure_file_exists(KP121_RUNNER, "Legacy 1.2.1 runner") + if not KP121_DIST.is_dir(): + raise HTTPException( + status_code=500, + detail=_err( + "LEGACY_PLUS_NOT_FOUND", + f"Legacy KneronPLUS 1.2.1 package directory not found: {KP121_DIST}", + ), + ) + + payload = { + "port_id": req.port_id, + "scan_index": req.scan_index, + "timeout_ms": req.timeout_ms, + "loader_path": req.loader_path, + "scpu_path": req.scpu_path, + "ncpu_path": req.ncpu_path, + "legacy_dist_root": str(KP121_DIST), + } + + env = os.environ.copy() + existing_pythonpath = env.get("PYTHONPATH", "") + legacy_pythonpath = str(KP121_DIST) + env["PYTHONPATH"] = ( + f"{legacy_pythonpath}{os.pathsep}{existing_pythonpath}" + if existing_pythonpath + else legacy_pythonpath + ) + + proc = subprocess.run( + [sys.executable, str(KP121_RUNNER), json.dumps(payload)], + cwd=str(PROJECT_ROOT), + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + env=env, + ) + + if proc.returncode != 0: + runner_json = None + if proc.stdout.strip(): + try: + runner_json = json.loads(proc.stdout.strip().splitlines()[-1]) + except json.JSONDecodeError: + runner_json = None + debug_payload = { + "return_code": proc.returncode, + "stdout": proc.stdout.strip(), + "stderr": proc.stderr.strip(), + "runner_json": runner_json, + } + print("[legacy-plus121-runner-failed]", json.dumps(debug_payload, ensure_ascii=False)) + raise HTTPException( + status_code=500, + detail=_err( + "LEGACY_PLUS_FW_LOAD_FAILED", + json.dumps(debug_payload, ensure_ascii=False), + ), + ) + + parsed = {} + if proc.stdout.strip(): + try: + parsed = json.loads(proc.stdout.strip().splitlines()[-1]) + except json.JSONDecodeError: + parsed = {"raw_stdout": proc.stdout.strip()} + + return { + "return_code": proc.returncode, + "result": parsed, + } + + +def _target_product_ids(target: str) -> List[kp.ProductId]: + target = target.upper() + if target == "KL520": + return [kp.ProductId.KP_DEVICE_KL520] + if target == "KL720": + return [kp.ProductId.KP_DEVICE_KL720_LEGACY, kp.ProductId.KP_DEVICE_KL720] + if target == "KL630": + return [kp.ProductId.KP_DEVICE_KL630] + if target == "KL730": + return [kp.ProductId.KP_DEVICE_KL730] + if target == "KL830": + return [kp.ProductId.KP_DEVICE_KL830] + if target == "ALL": + return [ + kp.ProductId.KP_DEVICE_KL520, + kp.ProductId.KP_DEVICE_KL720_LEGACY, + kp.ProductId.KP_DEVICE_KL720, + kp.ProductId.KP_DEVICE_KL630, + kp.ProductId.KP_DEVICE_KL730, + kp.ProductId.KP_DEVICE_KL830, + ] + raise HTTPException(status_code=400, detail=_err("INVALID_TARGET", f"Unsupported target: {target}")) + + +def _pid_to_product_name(pid_value: int) -> str: + try: + return kp.ProductId(pid_value).name.replace("KP_DEVICE_", "") + except ValueError: + return f"UNKNOWN_0x{pid_value:04X}" + + +def _query_windows_driver_status() -> List[Dict[str, Any]]: + # Query current connected Kneron USB devices from Windows PnP layer. + ps = ( + "$items = Get-CimInstance Win32_PnPEntity | " + "Where-Object { $_.PNPDeviceID -like 'USB\\VID_3231&PID_*' } | " + "Select-Object PNPDeviceID,Name,Service,Status; " + "if ($null -eq $items) { '[]' } else { $items | ConvertTo-Json -Depth 3 }" + ) + proc = subprocess.run( + ["powershell", "-NoProfile", "-Command", ps], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + if proc.returncode != 0: + raise HTTPException( + status_code=500, + detail=_err("DRIVER_CHECK_FAILED", proc.stderr.strip() or "failed to query Windows PnP devices"), + ) + + out = proc.stdout.strip() + if not out: + return [] + try: + parsed = json.loads(out) + except json.JSONDecodeError: + return [] + if isinstance(parsed, dict): + parsed = [parsed] + + results = [] + for item in parsed: + pnp_id = item.get("PNPDeviceID", "") + pid_hex = "" + if "PID_" in pnp_id: + pid_hex = pnp_id.split("PID_")[1][:4] + pid_val = int(pid_hex, 16) if pid_hex else None + service = item.get("Service") or "" + results.append( + { + "pnp_device_id": pnp_id, + "name": item.get("Name"), + "status": item.get("Status"), + "service": service, + "pid_hex": f"0x{pid_hex}" if pid_hex else None, + "pid_value": pid_val, + "product_name": _pid_to_product_name(pid_val) if pid_val is not None else "UNKNOWN", + "is_winusb": service.lower() == "winusb", + } + ) + return results + + +def _open_camera_capture(camera_id: int) -> Any: + if cv2 is None: + raise HTTPException( + status_code=500, + detail=_err("OPENCV_NOT_AVAILABLE", "opencv-python is not installed"), + ) + cap = cv2.VideoCapture(camera_id, cv2.CAP_DSHOW) + if not cap.isOpened(): + cap.release() + cap = cv2.VideoCapture(camera_id) + if not cap.isOpened(): + cap.release() + raise HTTPException( + status_code=404, + detail=_err("CAMERA_NOT_FOUND", f"Cannot open camera id={camera_id}"), + ) + return cap + + +def _mjpeg_stream_generator(cap: Any, jpeg_quality: int, frame_interval_sec: float): + try: + while True: + ok, frame = cap.read() + if not ok: + time.sleep(0.03) + continue + + ok, encoded = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), jpeg_quality]) + if not ok: + continue + + jpg = encoded.tobytes() + header = ( + b"--frame\r\n" + b"Content-Type: image/jpeg\r\n" + b"Content-Length: " + str(len(jpg)).encode("ascii") + b"\r\n\r\n" + ) + yield header + jpg + b"\r\n" + + if frame_interval_sec > 0: + time.sleep(frame_interval_sec) + finally: + cap.release() + + +def _frame_to_input_bytes(frame_bgr: Any, image_format: str) -> bytes: + fmt = image_format.upper() + if fmt == "RGB565": + converted = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2BGR565) + return converted.tobytes() + if fmt == "RGBA8888": + converted = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGBA) + return converted.tobytes() + if fmt == "RAW8": + converted = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY) + return converted.tobytes() + raise HTTPException( + status_code=400, + detail=_err( + "UNSUPPORTED_STREAM_IMAGE_FORMAT", + "For /inference/run_video, supported image_format: RGB565, RGBA8888, RAW8", + ), + ) + + +def _run_inference_from_image_bytes( + image_bytes: bytes, + width: int, + height: int, + model_id: int, + image_format_text: str, + channels_ordering_text: str, + output_dtype_text: str, +) -> List[Dict[str, Any]]: + device_group = _require_device() + image_format = _image_format_from_str(image_format_text) + channels_ordering = _channels_ordering_from_str(channels_ordering_text) + if output_dtype_text.lower() != "float32": + raise HTTPException( + status_code=400, + detail=_err("INVALID_OUTPUT_DTYPE", "Only float32 output is supported in PoC"), + ) + + try: + if STATE.port_id is not None: + kp.core.get_model_info(device_group, STATE.port_id) + except kp.ApiKPException as exc: + if exc.api_return_code == kp.ApiReturnCode.KP_ERROR_MODEL_NOT_LOADED_35: + raise HTTPException( + status_code=500, + detail=_err( + "KP_ERROR_MODEL_NOT_LOADED_35", + str(kp.ApiReturnCode.KP_ERROR_MODEL_NOT_LOADED_35), + ), + ) + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + + expected_size = _expected_image_size_bytes(image_format_text, width, height) + if expected_size is not None and len(image_bytes) != expected_size: + raise HTTPException( + status_code=400, + detail=_err( + "INVALID_IMAGE_SIZE", + ( + f"image bytes size mismatch: expected={expected_size}, actual={len(image_bytes)}. " + "Send raw pixel bytes for selected image_format (not BMP/JPEG/PNG file bytes)." + ), + ), + ) + + input_image = kp.GenericInputNodeImage( + image=image_bytes, + width=width, + height=height, + image_format=image_format, + ) + + input_desc = kp.GenericImageInferenceDescriptor( + model_id=model_id, + input_node_image_list=[input_image], + ) + + try: + kp.inference.generic_image_inference_send(device_group, input_desc) + result = kp.inference.generic_image_inference_receive(device_group) + except kp.ApiKPException as exc: + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + + outputs = [] + for node_idx in range(result.header.num_output_node): + try: + node_output = kp.inference.generic_inference_retrieve_float_node( + node_idx, result, channels_ordering + ) + except kp.ApiKPException as exc: + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + + data_bytes = node_output.ndarray.astype("float32").tobytes() + outputs.append( + { + "node_idx": node_idx, + "name": node_output.name, + "dtype": "float32", + "shape": node_output.shape, + "data_base64": base64.b64encode(data_bytes).decode("ascii"), + "channels_ordering": channels_ordering.name, + } + ) + return outputs + + +@app.get("/health") +def health() -> Dict[str, Any]: + return _ok({"status": "up"}) + + +@app.get("/tools/video-inference") +def tools_video_inference() -> FileResponse: + if not VIDEO_VIEWER_HTML.is_file(): + raise HTTPException( + status_code=404, + detail=_err("TOOL_PAGE_NOT_FOUND", f"Tool page not found: {VIDEO_VIEWER_HTML}"), + ) + return FileResponse(str(VIDEO_VIEWER_HTML), media_type="text/html; charset=utf-8") + + +@app.get("/version") +def version() -> Dict[str, Any]: + return _ok( + { + "service_version": SERVICE_VERSION, + "kneronplus_version": kp.core.get_version(), + } + ) + + +@app.get("/camera/list") +def camera_list(max_probe: int = 5) -> Dict[str, Any]: + if max_probe < 1 or max_probe > 20: + raise HTTPException( + status_code=400, + detail=_err("INVALID_MAX_PROBE", "max_probe must be between 1 and 20"), + ) + if cv2 is None: + raise HTTPException( + status_code=500, + detail=_err("OPENCV_NOT_AVAILABLE", "opencv-python is not installed"), + ) + + cameras: List[Dict[str, Any]] = [] + for camera_id in range(max_probe): + cap = cv2.VideoCapture(camera_id, cv2.CAP_DSHOW) + opened = cap.isOpened() + if not opened: + cap.release() + cap = cv2.VideoCapture(camera_id) + opened = cap.isOpened() + if opened: + cameras.append( + { + "camera_id": camera_id, + "width": int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0), + "height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0), + "fps": float(cap.get(cv2.CAP_PROP_FPS) or 0.0), + } + ) + cap.release() + return _ok({"cameras": cameras}) + + +@app.get("/camera/stream") +def camera_stream( + camera_id: int = 0, + width: Optional[int] = None, + height: Optional[int] = None, + fps: Optional[float] = None, + jpeg_quality: int = 80, +) -> StreamingResponse: + if camera_id < 0: + raise HTTPException( + status_code=400, + detail=_err("INVALID_CAMERA_ID", "camera_id must be >= 0"), + ) + if width is not None and width <= 0: + raise HTTPException(status_code=400, detail=_err("INVALID_WIDTH", "width must be > 0")) + if height is not None and height <= 0: + raise HTTPException(status_code=400, detail=_err("INVALID_HEIGHT", "height must be > 0")) + if fps is not None and (fps <= 0 or fps > 60): + raise HTTPException(status_code=400, detail=_err("INVALID_FPS", "fps must be in range (0, 60]")) + if jpeg_quality < 1 or jpeg_quality > 100: + raise HTTPException( + status_code=400, + detail=_err("INVALID_JPEG_QUALITY", "jpeg_quality must be in range [1, 100]"), + ) + + cap = _open_camera_capture(camera_id) + if width is not None: + cap.set(cv2.CAP_PROP_FRAME_WIDTH, float(width)) + if height is not None: + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, float(height)) + if fps is not None: + cap.set(cv2.CAP_PROP_FPS, float(fps)) + + frame_interval_sec = (1.0 / float(fps)) if fps else 0.0 + stream = _mjpeg_stream_generator(cap, jpeg_quality=jpeg_quality, frame_interval_sec=frame_interval_sec) + + headers = { + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + } + return StreamingResponse( + stream, + media_type="multipart/x-mixed-replace; boundary=frame", + headers=headers, + ) + + +@app.get("/devices") +def devices() -> Dict[str, Any]: + device_list = kp.core.scan_devices() + devices_out = [] + for idx, device in enumerate(device_list.device_descriptor_list): + devices_out.append( + { + "scan_index": idx, + "usb_port_id": device.usb_port_id, + "vendor_id": device.vendor_id, + "product_id": f"0x{device.product_id:X}", + "product_name": _product_name_from_id(device.product_id), + "link_speed": device.link_speed.name, + "usb_port_path": device.usb_port_path, + "kn_number": device.kn_number, + "is_connectable": device.is_connectable, + "firmware": device.firmware, + } + ) + return _ok({"devices": devices_out}) + + +@app.post("/devices/connect") +def connect(req: ConnectRequest) -> Dict[str, Any]: + with STATE_LOCK: + if STATE.device_group is not None: + try: + kp.core.disconnect_devices(STATE.device_group) + except kp.ApiKPException: + pass + STATE.device_group = None + STATE.port_id = None + STATE.model_desc = None + + port_id = _resolve_port_id(req) + + try: + device_group = kp.core.connect_devices([int(port_id)]) + if req.timeout_ms is not None: + kp.core.set_timeout(device_group, int(req.timeout_ms)) + except kp.ApiKPException as exc: + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + + STATE.device_group = device_group + STATE.port_id = int(port_id) + return _ok({"connected": True, "port_id": STATE.port_id}) + + +@app.post("/devices/connect_force") +def connect_force(req: ConnectRequest) -> Dict[str, Any]: + with STATE_LOCK: + if STATE.device_group is not None: + try: + kp.core.disconnect_devices(STATE.device_group) + except kp.ApiKPException: + pass + STATE.device_group = None + STATE.port_id = None + STATE.model_desc = None + + port_id = _resolve_port_id(req) + + try: + device_group = kp.core.connect_devices_without_check([int(port_id)]) + if req.timeout_ms is not None: + kp.core.set_timeout(device_group, int(req.timeout_ms)) + except kp.ApiKPException as exc: + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + + STATE.device_group = device_group + STATE.port_id = int(port_id) + return _ok({"connected": True, "port_id": STATE.port_id, "forced": True}) + + +@app.post("/devices/disconnect") +def disconnect() -> Dict[str, Any]: + with STATE_LOCK: + if STATE.device_group is not None: + try: + kp.core.disconnect_devices(STATE.device_group) + except kp.ApiKPException as exc: + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + STATE.device_group = None + STATE.port_id = None + STATE.model_desc = None + return _ok({"connected": False}) + + +@app.get("/driver/check") +def driver_check() -> Dict[str, Any]: + entries = _query_windows_driver_status() + all_winusb = all(entry["is_winusb"] for entry in entries) if entries else False + return _ok( + { + "entries": entries, + "all_connected_kneron_are_winusb": all_winusb, + "count": len(entries), + "note": "Run service as Administrator if driver install/update fails.", + } + ) + + +@app.post("/driver/install") +def driver_install(req: DriverInstallRequest) -> Dict[str, Any]: + targets = _target_product_ids(req.target) + results = [] + for product_id in targets: + try: + kp.core.install_driver_for_windows(product_id=product_id) + results.append({"product_id": f"0x{product_id.value:04X}", "product_name": product_id.name, "installed": True}) + except kp.ApiKPException as exc: + results.append( + { + "product_id": f"0x{product_id.value:04X}", + "product_name": product_id.name, + "installed": False, + "error_code": str(exc.api_return_code), + "error_message": str(exc), + } + ) + return _ok({"target": req.target.upper(), "results": results}) + + +@app.post("/driver/ensure") +def driver_ensure(req: DriverInstallRequest) -> Dict[str, Any]: + target = req.target.upper() + target_ids = _target_product_ids(target) + target_pid_values = {pid.value for pid in target_ids} + + entries = _query_windows_driver_status() + needs_install = req.force + for entry in entries: + if entry["pid_value"] in target_pid_values and not entry["is_winusb"]: + needs_install = True + break + + install_result = None + if needs_install: + install_result = driver_install(req) + + after = _query_windows_driver_status() + return _ok( + { + "target": target, + "needed_install": needs_install, + "install_result": install_result["data"] if install_result else None, + "before": entries, + "after": after, + } + ) + + +@app.post("/firmware/load") +def firmware_load(req: FirmwareLoadRequest) -> Dict[str, Any]: + device_group = _require_device() + try: + kp.core.load_firmware_from_file(device_group, req.scpu_path, req.ncpu_path) + except kp.ApiKPException as exc: + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + return _ok({"loaded": True}) + + +@app.post("/firmware/legacy-plus121/load") +def firmware_legacy_plus121_load(req: LegacyPlusFirmwareLoadRequest) -> Dict[str, Any]: + # Ensure current process does not hold USB handles before legacy subprocess runs. + with STATE_LOCK: + if STATE.device_group is not None: + try: + kp.core.disconnect_devices(STATE.device_group) + except kp.ApiKPException: + pass + STATE.device_group = None + STATE.port_id = None + STATE.model_desc = None + + _ensure_file_exists(Path(req.scpu_path), "Legacy SCPU firmware") + _ensure_file_exists(Path(req.ncpu_path), "Legacy NCPU firmware") + if req.loader_path is not None: + _ensure_file_exists(Path(req.loader_path), "Legacy loader firmware") + result = _run_legacy_plus121_load_firmware(req) + return _ok({"loaded": True, "legacy_plus_version": "1.2.1", "runner": result}) + + +@app.post("/firmware/legacy-upgrade/kl520") +def firmware_legacy_upgrade_kl520(req: ConnectRequest) -> Dict[str, Any]: + print("[DFUT-KL520-UPGRADE] endpoint entered", flush=True) + port_id = _resolve_port_id(req) + print(f"[DFUT-KL520-UPGRADE] resolved port_id={port_id}", flush=True) + fw_scpu = PROJECT_ROOT / "firmware" / "KL520" / "fw_scpu.bin" + fw_ncpu = PROJECT_ROOT / "firmware" / "KL520" / "fw_ncpu.bin" + _ensure_file_exists(fw_scpu, "KL520 SCPU firmware") + _ensure_file_exists(fw_ncpu, "KL520 NCPU firmware") + + args = [ + str(DFUT_EXE), + "--kl520-flash-boot", + "--port", + str(port_id), + "--scpu", + str(fw_scpu), + "--ncpu", + str(fw_ncpu), + "--quiet", + ] + result = _run_dfut_command(args, echo_console=True, echo_tag="DFUT-KL520-UPGRADE") + return _ok( + { + "upgraded": True, + "target": "KL520", + "port_id": port_id, + "dfut": result, + } + ) + + +@app.post("/firmware/legacy-downgrade/kl520") +def firmware_legacy_downgrade_kl520(req: ConnectRequest) -> Dict[str, Any]: + port_id = _resolve_port_id(req) + fw_scpu = PROJECT_ROOT / "firmware" / "KL520_kdp" / "fw_scpu.bin" + fw_ncpu = PROJECT_ROOT / "firmware" / "KL520_kdp" / "fw_ncpu.bin" + _ensure_file_exists(fw_scpu, "KL520 KDP SCPU firmware") + _ensure_file_exists(fw_ncpu, "KL520 KDP NCPU firmware") + + args = [ + str(DFUT_EXE), + "--kl520-update", + "--port", + str(port_id), + "--scpu", + str(fw_scpu), + "--ncpu", + str(fw_ncpu), + ] + timeout_sec = 30 + try: + result = _run_dfut_command( + args, + timeout_sec=timeout_sec, + echo_console=False, + echo_tag="DFUT-KL520-DOWNGRADE", + ) + except HTTPException as exc: + detail = exc.detail if isinstance(exc.detail, dict) else None + err_code = ( + str(detail.get("error", {}).get("code", "")) if isinstance(detail, dict) else "" + ) + if exc.status_code == 504 and err_code == "DFUT_TIMEOUT": + post = _kl520_kdp_observed_after_timeout(port_id) + if post["is_success"]: + return _ok( + { + "downgraded": True, + "target": "KL520", + "port_id": port_id, + "dfut_timeout_but_verified": True, + "verification_reason": post["reason"], + "scan_after_timeout": post["devices"], + "warning": "DFUT timeout but KL520 KDP state observed after timeout.", + } + ) + raise + return _ok( + { + "downgraded": True, + "target": "KL520", + "port_id": port_id, + "dfut": result, + } + ) + + +@app.post("/firmware/legacy-upgrade/kl720") +def firmware_legacy_upgrade_kl720(req: ConnectRequest) -> Dict[str, Any]: + port_id = _resolve_port_id(req) + fw_scpu = PROJECT_ROOT / "firmware" / "KL720" / "fw_scpu.bin" + fw_ncpu = PROJECT_ROOT / "firmware" / "KL720" / "fw_ncpu.bin" + _ensure_file_exists(fw_scpu, "KL720 SCPU firmware") + _ensure_file_exists(fw_ncpu, "KL720 NCPU firmware") + + args = [ + str(DFUT_EXE), + "--kl720-update", + "--port", + str(port_id), + "--scpu", + str(fw_scpu), + "--ncpu", + str(fw_ncpu), + "--quiet", + ] + result = _run_dfut_command(args) + return _ok( + { + "upgraded": True, + "target": "KL720", + "port_id": port_id, + "dfut": result, + } + ) + + +@app.post("/models/load") +def models_load(req: ModelLoadRequest) -> Dict[str, Any]: + device_group = _require_device() + try: + model_desc = kp.core.load_model_from_file(device_group, req.nef_path) + except kp.ApiKPException as exc: + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + + with STATE_LOCK: + STATE.model_desc = model_desc + + models = [] + for model in model_desc.models: + models.append( + { + "id": model.id, + "input_nodes": len(model.input_nodes), + "output_nodes": len(model.output_nodes), + "max_raw_out_size": model.max_raw_out_size, + } + ) + + return _ok({"models": models}) + + +def _reset_device_and_clear_state(device_group: kp.DeviceGroup) -> None: + kp.core.reset_device(device_group, kp.ResetMode.KP_RESET_REBOOT) + kp.core.disconnect_devices(device_group) + + +@app.post("/models/clear") +def models_clear() -> Dict[str, Any]: + device_group = _require_device() + try: + _reset_device_and_clear_state(device_group) + except kp.ApiKPException as exc: + raise HTTPException( + status_code=500, + detail=_err(str(exc.api_return_code), str(exc)), + ) + + with STATE_LOCK: + STATE.device_group = None + STATE.port_id = None + STATE.model_desc = None + + return _ok({"cleared": True}) + + +@app.post("/models/reset") +def models_reset() -> Dict[str, Any]: + return models_clear() + + +@app.post("/inference/run") +def inference_run(req: InferenceRunRequest) -> Dict[str, Any]: + b64_text = req.image_base64.strip() + if b64_text.startswith("data:") and "," in b64_text: + b64_text = b64_text.split(",", 1)[1] + + try: + image_bytes = base64.b64decode(b64_text) + except (ValueError, TypeError): + raise HTTPException( + status_code=400, + detail=_err("INVALID_BASE64", "image_base64 is not valid base64 data"), + ) + outputs = _run_inference_from_image_bytes( + image_bytes=image_bytes, + width=req.width, + height=req.height, + model_id=req.model_id, + image_format_text=req.image_format, + channels_ordering_text=req.channels_ordering, + output_dtype_text=req.output_dtype, + ) + + return _ok({"outputs": outputs}) + + +@app.post("/inference/run_video") +async def inference_run_video( + file: UploadFile = File(...), + model_id: int = Form(...), + image_format: str = Form(...), + channels_ordering: str = Form("DEFAULT"), + output_dtype: str = Form("float32"), + sample_every_n: int = Form(1), + max_frames: Optional[int] = Form(default=None), +) -> StreamingResponse: + if cv2 is None: + raise HTTPException( + status_code=500, + detail=_err("OPENCV_NOT_AVAILABLE", "opencv-python is not installed"), + ) + if sample_every_n <= 0: + raise HTTPException( + status_code=400, + detail=_err("INVALID_SAMPLE_EVERY_N", "sample_every_n must be >= 1"), + ) + if max_frames is not None and max_frames <= 0: + raise HTTPException( + status_code=400, + detail=_err("INVALID_MAX_FRAMES", "max_frames must be >= 1 when provided"), + ) + + suffix = Path(file.filename or "upload.mp4").suffix or ".mp4" + tmp_path = Path(tempfile.gettempdir()) / f"inference_upload_{int(time.time() * 1000)}{suffix}" + with tmp_path.open("wb") as f: + while True: + chunk = await file.read(1024 * 1024) + if not chunk: + break + f.write(chunk) + await file.close() + + def _iter_results(): + cap = cv2.VideoCapture(str(tmp_path)) + if not cap.isOpened(): + cap.release() + if tmp_path.exists(): + tmp_path.unlink() + error_line = json.dumps( + _err("VIDEO_OPEN_FAILED", f"Cannot open uploaded video: {tmp_path.name}"), + ensure_ascii=False, + ) + yield (error_line + "\n").encode("utf-8") + return + + sent_count = 0 + frame_index = -1 + try: + while True: + ok, frame = cap.read() + if not ok: + break + frame_index += 1 + if frame_index % sample_every_n != 0: + continue + + height, width = int(frame.shape[0]), int(frame.shape[1]) + image_bytes = _frame_to_input_bytes(frame, image_format) + outputs = _run_inference_from_image_bytes( + image_bytes=image_bytes, + width=width, + height=height, + model_id=model_id, + image_format_text=image_format, + channels_ordering_text=channels_ordering, + output_dtype_text=output_dtype, + ) + payload = _ok( + { + "frame_index": frame_index, + "width": width, + "height": height, + "outputs": outputs, + } + ) + yield (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8") + + sent_count += 1 + if max_frames is not None and sent_count >= max_frames: + break + finally: + cap.release() + if tmp_path.exists(): + tmp_path.unlink() + + headers = { + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + "Connection": "keep-alive", + } + return StreamingResponse( + _iter_results(), + media_type="application/x-ndjson", + headers=headers, + ) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="127.0.0.1", port=4398) diff --git a/local_service_win/LocalAPI/postprocess_core.py b/local_service_win/LocalAPI/postprocess_core.py new file mode 100644 index 0000000..8021e75 --- /dev/null +++ b/local_service_win/LocalAPI/postprocess_core.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +import base64 +import math +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Sequence, Tuple + +import numpy as np + + +YOLO_DEFAULT_ANCHORS: List[List[Tuple[float, float]]] = [ + [(10.0, 14.0), (23.0, 27.0), (37.0, 58.0)], + [(81.0, 82.0), (135.0, 169.0), (344.0, 319.0)], +] + + +@dataclass +class Box: + cls: int + score: float + x1: float + y1: float + x2: float + y2: float + + +def _sigmoid(v: np.ndarray | float) -> np.ndarray | float: + return 1.0 / (1.0 + np.exp(-v)) + + +def decode_outputs(raw_outputs: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]: + decoded: List[Dict[str, Any]] = [] + for idx, o in enumerate(raw_outputs): + shape = list(o.get("shape") or []) + data_b64 = str(o.get("data_base64") or "") + raw = base64.b64decode(data_b64) + arr = np.frombuffer(raw, dtype=" List[Dict[str, Any]]: + picked: List[Dict[str, Any]] = [] + for o in all_nodes: + shape = o["shape"] + if len(shape) != 4 or shape[0] != 1: + continue + ch = int(shape[1]) + if ch % (5 + num_classes) != 0: + continue + picked.append(o) + picked.sort(key=lambda n: int(n["shape"][2]), reverse=True) + return picked + + +def decode_yolo_common( + all_nodes: Sequence[Dict[str, Any]], + mode: str, + num_classes: int, + input_w: int, + input_h: int, + conf_th: float, + use_sigmoid: bool = True, + use_xy_sigmoid: bool = True, + score_mode: str = "obj_cls", + anchors_by_level: Optional[List[List[Tuple[float, float]]]] = None, +) -> List[Box]: + nodes = _pick_yolo_nodes(all_nodes, num_classes) + if not nodes: + raise RuntimeError("No YOLO-like [1,C,H,W] output nodes found") + anchors_levels = anchors_by_level or YOLO_DEFAULT_ANCHORS + + boxes: List[Box] = [] + attrs = 5 + num_classes + + for lv, o in enumerate(nodes): + _, ch, gh, gw = o["shape"] + na = int(ch // attrs) + data: np.ndarray = o["data"] + anchors = anchors_levels[min(lv, len(anchors_levels) - 1)] + + def at(channel_idx: int, y: int, x: int) -> float: + return float(data[channel_idx * gh * gw + y * gw + x]) + + for a in range(na): + aw, ah = anchors[min(a, len(anchors) - 1)] + base = a * attrs + for y in range(gh): + for x in range(gw): + tx = at(base + 0, y, x) + ty = at(base + 1, y, x) + tw = at(base + 2, y, x) + th = at(base + 3, y, x) + to = at(base + 4, y, x) + + obj = float(_sigmoid(to) if use_sigmoid else to) + best_cls = -1 + best_prob = -1e9 + for k in range(num_classes): + p = at(base + 5 + k, y, x) + p = float(_sigmoid(p) if use_sigmoid else p) + if p > best_prob: + best_prob = p + best_cls = k + + if score_mode == "obj": + score = obj + elif score_mode == "cls": + score = best_prob + else: + score = obj * best_prob + if score < conf_th: + continue + + if mode == "yolov5": + sx = input_w / gw + sy = input_h / gh + txv = float(_sigmoid(tx) if use_xy_sigmoid else tx) + tyv = float(_sigmoid(ty) if use_xy_sigmoid else ty) + bx = (txv * 2.0 - 0.5 + x) * sx + by = (tyv * 2.0 - 0.5 + y) * sy + bw = (float(_sigmoid(tw)) * 2.0) ** 2 * aw + bh = (float(_sigmoid(th)) * 2.0) ** 2 * ah + else: + txv = float(_sigmoid(tx) if use_xy_sigmoid else tx) + tyv = float(_sigmoid(ty) if use_xy_sigmoid else ty) + bx = (txv + x) / gw * input_w + by = (tyv + y) / gh * input_h + bw = aw * math.exp(tw) + bh = ah * math.exp(th) + + boxes.append( + Box( + cls=best_cls, + score=score, + x1=bx - bw / 2.0, + y1=by - bh / 2.0, + x2=bx + bw / 2.0, + y2=by + bh / 2.0, + ) + ) + return boxes + + +def _auto_fcos_indices(all_nodes: Sequence[Dict[str, Any]], num_classes: int) -> List[Tuple[int, int, int, int]]: + valid = [o for o in all_nodes if len(o["shape"]) == 4 and o["shape"][0] == 1] + cls_nodes = [o for o in valid if int(o["shape"][1]) == num_classes] + reg_nodes = [o for o in valid if int(o["shape"][1]) == 4] + ctr_nodes = [o for o in valid if int(o["shape"][1]) == 1] + + by_hw: Dict[Tuple[int, int], Dict[str, Dict[str, Any]]] = {} + for n in cls_nodes: + by_hw.setdefault((int(n["shape"][2]), int(n["shape"][3])), {})["cls"] = n + for n in reg_nodes: + by_hw.setdefault((int(n["shape"][2]), int(n["shape"][3])), {})["reg"] = n + for n in ctr_nodes: + by_hw.setdefault((int(n["shape"][2]), int(n["shape"][3])), {})["ctr"] = n + + levels: List[Tuple[int, int, int, int]] = [] + for (h, _w), items in by_hw.items(): + if not {"cls", "reg", "ctr"}.issubset(items.keys()): + continue + levels.append( + ( + h, + int(items["cls"]["node_idx"]), + int(items["reg"]["node_idx"]), + int(items["ctr"]["node_idx"]), + ) + ) + levels.sort(key=lambda x: x[0], reverse=True) + strides = [8, 16, 32, 64, 128] + return [ + (cls_i, reg_i, ctr_i, strides[min(i, len(strides) - 1)]) + for i, (_h, cls_i, reg_i, ctr_i) in enumerate(levels) + ] + + +def decode_fcos( + all_nodes: Sequence[Dict[str, Any]], + num_classes: int, + input_w: int, + input_h: int, + conf_th: float, + use_sigmoid: bool = True, + score_mode: str = "obj_cls", +) -> List[Box]: + levels = _auto_fcos_indices(all_nodes, num_classes) + if not levels: + raise RuntimeError("Cannot auto match FCOS cls/reg/ctr nodes") + + boxes: List[Box] = [] + by_idx = {int(n["node_idx"]): n for n in all_nodes} + + for cls_idx, reg_idx, ctr_idx, stride in levels: + cls_node = by_idx.get(cls_idx) + reg_node = by_idx.get(reg_idx) + ctr_node = by_idx.get(ctr_idx) + if not cls_node or not reg_node or not ctr_node: + continue + + gh = int(cls_node["shape"][2]) + gw = int(cls_node["shape"][3]) + cls_data: np.ndarray = cls_node["data"] + reg_data: np.ndarray = reg_node["data"] + ctr_data: np.ndarray = ctr_node["data"] + + def at(node_data: np.ndarray, channel_idx: int, y: int, x: int) -> float: + return float(node_data[channel_idx * gh * gw + y * gw + x]) + + cls_channels = int(cls_node["shape"][1]) + for y in range(gh): + for x in range(gw): + ctr = at(ctr_data, 0, y, x) + ctr = float(_sigmoid(ctr) if use_sigmoid else ctr) + + best_cls = -1 + best_prob = -1e9 + for k in range(min(num_classes, cls_channels)): + p = at(cls_data, k, y, x) + p = float(_sigmoid(p) if use_sigmoid else p) + if p > best_prob: + best_prob = p + best_cls = k + + if score_mode == "obj": + score = ctr + elif score_mode == "cls": + score = best_prob + else: + score = math.sqrt(max(0.0, best_prob * ctr)) + if score < conf_th: + continue + + l = max(0.0, at(reg_data, 0, y, x)) + t = max(0.0, at(reg_data, 1, y, x)) + r = max(0.0, at(reg_data, 2, y, x)) + b = max(0.0, at(reg_data, 3, y, x)) + cx = (x + 0.5) * stride + cy = (y + 0.5) * stride + + x1 = max(0.0, min(input_w, cx - l)) + y1 = max(0.0, min(input_h, cy - t)) + x2 = max(0.0, min(input_w, cx + r)) + y2 = max(0.0, min(input_h, cy + b)) + if x2 <= x1 or y2 <= y1: + continue + boxes.append(Box(cls=best_cls, score=score, x1=x1, y1=y1, x2=x2, y2=y2)) + return boxes + + +def _iou(a: Box, b: Box) -> float: + xx1 = max(a.x1, b.x1) + yy1 = max(a.y1, b.y1) + xx2 = min(a.x2, b.x2) + yy2 = min(a.y2, b.y2) + w = max(0.0, xx2 - xx1) + h = max(0.0, yy2 - yy1) + inter = w * h + if inter <= 0: + return 0.0 + area_a = max(0.0, a.x2 - a.x1) * max(0.0, a.y2 - a.y1) + area_b = max(0.0, b.x2 - b.x1) * max(0.0, b.y2 - b.y1) + return inter / max(1e-9, area_a + area_b - inter) + + +def nms(boxes: Sequence[Box], iou_th: float, max_out: int) -> List[Box]: + by_cls: Dict[int, List[Box]] = {} + for b in boxes: + by_cls.setdefault(b.cls, []).append(b) + + kept: List[Box] = [] + for cls_boxes in by_cls.values(): + cls_boxes = sorted(cls_boxes, key=lambda b: b.score, reverse=True) + picked: List[Box] = [] + while cls_boxes: + cur = cls_boxes.pop(0) + picked.append(cur) + cls_boxes = [b for b in cls_boxes if _iou(cur, b) <= iou_th] + kept.extend(picked) + + kept.sort(key=lambda b: b.score, reverse=True) + return kept[:max_out] diff --git a/local_service_win/LocalAPI/win_driver/amd64/WdfCoInstaller01011.dll b/local_service_win/LocalAPI/win_driver/amd64/WdfCoInstaller01011.dll new file mode 100644 index 0000000..d49d291 Binary files /dev/null and b/local_service_win/LocalAPI/win_driver/amd64/WdfCoInstaller01011.dll differ diff --git a/local_service_win/LocalAPI/win_driver/amd64/winusbcoinstaller2.dll b/local_service_win/LocalAPI/win_driver/amd64/winusbcoinstaller2.dll new file mode 100644 index 0000000..30e5502 Binary files /dev/null and b/local_service_win/LocalAPI/win_driver/amd64/winusbcoinstaller2.dll differ diff --git a/local_service_win/LocalAPI/win_driver/installer_x64.exe b/local_service_win/LocalAPI/win_driver/installer_x64.exe new file mode 100644 index 0000000..0373edc Binary files /dev/null and b/local_service_win/LocalAPI/win_driver/installer_x64.exe differ diff --git a/local_service_win/LocalAPI/win_driver/kneron_kl520.inf b/local_service_win/LocalAPI/win_driver/kneron_kl520.inf new file mode 100644 index 0000000..38deac1 Binary files /dev/null and b/local_service_win/LocalAPI/win_driver/kneron_kl520.inf differ diff --git a/local_service_win/STRATEGY.md b/local_service_win/STRATEGY.md new file mode 100644 index 0000000..43881a1 --- /dev/null +++ b/local_service_win/STRATEGY.md @@ -0,0 +1,544 @@ +# Kneron Dongle PoC (Windows) - Strategy + +## Scope (PoC) +- OS: Windows only. +- Devices: KL520, KL720. +- Control path: Browser -> localhost HTTP service -> KneronPLUS (kp wrapper + DLL). +- Non-goals: macOS/Linux support, production hardening, installer automation for all platforms. + +## Required Installation (Windows) +Before running the local service, install Python dependencies and the KneronPLUS wheel. + +### 1. Install dependencies from requirements +```powershell +cd local_service_win +python -m pip install -r requirements.txt +``` + +### 2. Install KneronPLUS wheel +```powershell +cd local_service_win +python -m pip install .\KneronPLUS-3.1.2-py3-none-any.whl +``` + +### 3. (Optional) Force reinstall KneronPLUS wheel +Use this when switching versions or seeing package mismatch issues. +```powershell +cd local_service_win +python -m pip install --force-reinstall .\KneronPLUS-3.1.2-py3-none-any.whl +``` + +## Dependency Strategy +- Open-source packages installed by pip: + - `fastapi`, `uvicorn`, `numpy`, `PyQt5`, `opencv-python`, `pyinstaller`, `pyarmor` +- Non-pip dependency: + - `KneronPLUS` (installed from local wheel) +- Bundled runtime (not pip): + - `third_party/Kneron_DFUT` is copied into this repo and used by LocalAPI to recover old firmware in one tool. + - `third_party/kneron_plus_1_2_1/dist` is extracted from `KneronPLUS-1.2.1` wheel and used by a subprocess runner for old-device firmware update experiments. + +## Cross-Project Workflow +This repo is the main PoC implementation. If additional references are required, we can switch to +other repos during the same conversation and return here as needed. This is workable. + +## High-Level Architecture +- Browser UI + - Talks to localhost HTTP service for control APIs. + - Uses WebSocket for streaming inference. + - No direct USB access from browser. +- Local Service (Windows) + - Owns Kneron device lifecycle and IO. + - Uses Python `kp` high-level API (backed by `libkplus.dll`). + - Exposes HTTP endpoints for scan/connect/model/firmware/inference. +- KneronPLUS Runtime + - `kp` Python wrapper + DLLs + required USB driver. + - Version pinned inside installer to avoid mismatches. + +## API Spec (PoC) +### Conventions +- Base URL: `http://127.0.0.1:4398` +- WebSocket URL: `ws://127.0.0.1:4398/ws` +- Response envelope: + ```json + { + "ok": true, + "data": {}, + "error": null + } + ``` + ```json + { + "ok": false, + "data": null, + "error": { "code": "KP_ERROR_CONNECT_FAILED", "message": "..." } + } + ``` + +### `GET /health` +Response +```json +{ "ok": true, "data": { "status": "up" }, "error": null } +``` + +### `GET /version` +Response +```json +{ + "ok": true, + "data": { + "service_version": "0.1.0", + "kneronplus_version": "3.0.0" + }, + "error": null +} +``` + +### `GET /devices` +Response +```json +{ + "ok": true, + "data": { + "devices": [ + { + "scan_index": 0, + "usb_port_id": 32, + "product_id": 0x520, + "link_speed": "High-Speed", + "usb_port_path": "1-3", + "kn_number": 12345, + "is_connectable": true, + "firmware": "KDP2" + } + ] + }, + "error": null +} +``` + +### `POST /devices/connect` +Request +```json +{ "port_id": 32 } +``` +Response +```json +{ + "ok": true, + "data": { + "connected": true, + "port_id": 32 + }, + "error": null +} +``` + +### `POST /devices/connect_force` +Notes +- Force connection without firmware validation. +- Use this when firmware is incompatible and you need to call `/firmware/load` first. +Request +```json +{ "port_id": 32 } +``` +Response +```json +{ + "ok": true, + "data": { + "connected": true, + "port_id": 32, + "forced": true + }, + "error": null +} +``` + +### `POST /devices/disconnect` +Response +```json +{ "ok": true, "data": { "connected": false }, "error": null } +``` + +### `GET /driver/check` +Notes +- Query currently connected Kneron USB devices from Windows PnP. +- Reports whether each entry is bound to WinUSB. +Response +```json +{ + "ok": true, + "data": { + "entries": [ + { + "pnp_device_id": "USB\\VID_3231&PID_0720\\...", + "service": "WinUSB", + "pid_hex": "0x0720", + "product_name": "KL720", + "is_winusb": true + } + ], + "all_connected_kneron_are_winusb": true + }, + "error": null +} +``` + +### `POST /driver/install` +Notes +- Install/replace driver using `kp.core.install_driver_for_windows`. +- Requires Administrator privilege on Windows. +Request +```json +{ "target": "KL720", "force": false } +``` + +### `POST /driver/ensure` +Notes +- Check connected device driver binding, auto install if not WinUSB (or `force=true`). +- `target`: `ALL` | `KL520` | `KL720` | `KL630` | `KL730` | `KL830` +Request +```json +{ "target": "ALL", "force": false } +``` + +### `POST /firmware/load` +Request +```json +{ + "scpu_path": "C:\\path\\fw_scpu.bin", + "ncpu_path": "C:\\path\\fw_ncpu.bin" +} +``` +Response +```json +{ "ok": true, "data": { "loaded": true }, "error": null } +``` + +### `POST /firmware/legacy-plus121/load` +Notes +- Experimental endpoint for old hardware/firmware path. +- Independent route from DFUT. +- Runs a subprocess with bundled `KneronPLUS 1.2.1` package and calls `libkplus.dll` directly by `ctypes`. +- Single-endpoint auto flow: + - Scan target device firmware state. + - If firmware is `KDP`: first call loader (`fw_loader.bin`) to switch USB-boot, then call `kp_load_firmware_from_file(scpu,ncpu)`. + - If firmware is not `KDP`: call `kp_load_firmware_from_file(scpu,ncpu)` directly. + - Finally disconnect. +- Diagnostics include selected port, detected firmware, scan snapshot, and firmware file metadata (path/size/sha256). +Request +```json +{ + "port_id": 32, + "loader_path": "C:\\path\\fw_loader.bin", + "scpu_path": "C:\\path\\fw_scpu.bin", + "ncpu_path": "C:\\path\\fw_ncpu.bin" +} +``` +Response +```json +{ + "ok": true, + "data": { + "loaded": true, + "legacy_plus_version": "1.2.1" + }, + "error": null +} +``` + +### `POST /firmware/legacy-upgrade/kl520` +Notes +- Used for old KL520 firmware recovery path. +- Runs bundled DFUT console with `--kl520-update`. +Request +```json +{ "port_id": 32 } +``` +Response +```json +{ + "ok": true, + "data": { + "upgraded": true, + "target": "KL520", + "port_id": 32 + }, + "error": null +} +``` + +### `POST /firmware/legacy-upgrade/kl720` +Notes +- Used for old KL720 / KL720 legacy recovery path. +- Runs bundled DFUT console with `--kl720-update`. +Request +```json +{ "port_id": 32 } +``` +Response +```json +{ + "ok": true, + "data": { + "upgraded": true, + "target": "KL720", + "port_id": 32 + }, + "error": null +} +``` + +### `POST /models/load` +Request +```json +{ "nef_path": "C:\\path\\model.nef" } +``` +Response +```json +{ + "ok": true, + "data": { + "model_id": 1, + "input_tensor_count": 1, + "output_tensor_count": 1 + }, + "error": null +} +``` + +### `POST /models/clear` +Notes +- PoC uses device reset to clear RAM model. +Response +```json +{ "ok": true, "data": { "cleared": true }, "error": null } +``` + +### `POST /models/reset` +Notes +- Alias of `/models/clear`, uses device reset to clear RAM model. +Response +```json +{ "ok": true, "data": { "reset": true }, "error": null } +``` + +### `POST /inference/run` +Request (image inference, single image) +```json +{ + "model_id": 1, + "image_format": "RGB888", + "width": 224, + "height": 224, + "image_base64": "..." +} +``` +Response +```json +{ + "ok": true, + "data": { + "outputs": [ + { "node_idx": 0, "dtype": "float", "shape": [1, 1000], "data_base64": "..." } + ] + }, + "error": null +} +``` + +### `POST /inference/run_video` +Notes +- Video file upload endpoint for continuous inference in PoC. +- Response is NDJSON stream (`application/x-ndjson`), one JSON object per processed frame. +- ByteTrack-specific tracking output is out of scope for current PoC; this endpoint returns raw model outputs per frame. +Request (`multipart/form-data`) +- `file`: video file (`.mp4/.avi/...`) +- `model_id`: integer +- `image_format`: `RGB565` | `RGBA8888` | `RAW8` +- `channels_ordering`: optional, default `DEFAULT` +- `output_dtype`: optional, default `float32` +- `sample_every_n`: optional, default `1` +- `max_frames`: optional + +Response line example (NDJSON) +```json +{ + "ok": true, + "data": { + "frame_index": 0, + "width": 640, + "height": 640, + "outputs": [ + { "node_idx": 0, "dtype": "float32", "shape": [1, 255, 80, 80], "data_base64": "..." } + ] + }, + "error": null +} +``` + +### `GET /tools/video-inference` +Notes +- Serves a single-page visual test tool from LocalAPI. +- Supports two input sources: + - Video file + - Webcam (browser `getUserMedia`) +- Frontend calls `POST /inference/run` frame-by-frame and draws decoded boxes on canvas. +- Purpose: PoC visual validation for YOLOv5/FCOS/TinyYOLO style models. +- ByteTrack visualization/tracking is intentionally excluded in current phase. + +### `WS /ws` (streaming inference) +Notes +- For camera/video stream, use WebSocket for low-latency send/receive. +- HTTP endpoints remain for control operations during PoC. +Message (client -> server) +```json +{ + "type": "inference_frame", + "model_id": 1, + "image_format": "RGB888", + "width": 224, + "height": 224, + "image_base64": "..." +} +``` +Message (server -> client) +```json +{ + "type": "inference_result", + "outputs": [ + { "node_idx": 0, "dtype": "float", "shape": [1, 1000], "data_base64": "..." } + ] +} +``` + +### `POST /firmware/update` +- Reserved for flash update (later; may need C wrapper). + +## Packaging (PoC) +- Single Windows installer: + - Includes driver, `kp` wrapper, DLLs, and service. + - Ensures fixed versions (no external Kneron tools required). + - Reference from `C:\Users\user\Documents\KNEOX\README.md`: + - Install KneronPLUS wheel from `external/kneron_plus_{version}/package/{platform}/` + - `pip install KneronPLUS-{version}-py3-none-any.whl` (use `--force-reinstall` if needed) + - PyInstaller must bundle `kp\lib` with the app. + - Example: + ```shell + pyinstaller --onefile --windowed main.py --additional-hooks-dir=hooks --add-data "uxui;uxui" --add-data "src;src" --add-data "C:\path\to\venv\Lib\site-packages\kp\lib;kp\lib" + ``` + +## Risks / Constraints +- Flash model update / flash firmware update may not be exposed in Python. + - Use C library or request Kneron to expose in wrapper if required. +- Browser security model prevents direct USB access; local service is required. +- Driver install/update on Windows may require Administrator privileges (`install_driver_for_windows` can fail without elevation). + - MEMO: define production approach for privilege handling (installer-time elevation, helper process with UAC prompt, or enterprise pre-install policy) so end-user flow does not get blocked. + +## API Test Progress (Windows PoC) +Updated: 2026-03-04 + +### Completed +- `GET /health` +- `GET /version` +- `GET /devices` +- `POST /devices/connect` +- `POST /devices/connect_force` +- `POST /devices/disconnect` +- `GET /driver/check` +- `POST /driver/install` +- `POST /driver/ensure` +- `POST /firmware/load` +- `POST /firmware/legacy-plus121/load` +- `POST /firmware/legacy-upgrade/kl520` +- `POST /firmware/legacy-downgrade/kl520` +- `POST /firmware/legacy-upgrade/kl720` +- `POST /models/load` +- `POST /models/clear` +- `POST /models/reset` +- `POST /inference/run` +- `POST /inference/run_video` +- `GET /tools/video-inference` + +### Pending +- None (for currently implemented HTTP endpoints). + +### Not Implemented Yet (API spec) +- `WS /ws` +- `POST /firmware/update` + +### Paired Test Requirement +- `POST /models/load` and `POST /inference/run` must be tested as a pair in the same flow. +- Test pairs are defined in `local_service_win/TestRes/TEST_PAIRS.md`. + +### Video/Webcam PoC Test Flow +1. Start LocalAPI service. +2. Connect device and load model: + - `POST /devices/connect` + - `POST /models/load` +3. Visual tool path: + - Open `http://127.0.0.1:4398/tools/video-inference` + - Select source (`Video File` or `Webcam`) + - Use default model presets (YOLOv5=20005, FCOS=20004, TinyYOLO=19), then click `Start` +4. API-only path: + - Use `POST /inference/run_video` with `multipart/form-data` + - Start with small values: `sample_every_n=3`, `max_frames=30` +5. Expected: + - Continuous frame-wise inference results are returned. + - Visual page overlays detection boxes on displayed frames. +6. Current scope note: + - ByteTrack tracking output (`track_id` continuity) is not covered in this PoC phase. + +### Model/Inference Test Pairs +#### KL520 +1. YOLOv5 (model zoo) + - Model: `kl520_20005_yolov5-noupsample_w640h640.nef` + - Image: `one_bike_many_cars_800x800` (Base64) +2. FCOS (model zoo) + - Model: `kl520_20004_fcos-drk53s_w512h512.nef` + - Image: `one_bike_many_cars_800x800` (Base64) +3. Tiny YOLO v3 (generic demo) + - Model: `models_520.nef` + - Image: `bike_cars_street_224x224` (Base64) +4. Tiny YOLO v3 (multithread demo) + - Model: `models_520.nef` + - Image: `bike_cars_street_224x224` (Base64) + +#### KL720 +1. YOLOv5 (model zoo) + - Model: `kl720_20005_yolov5-noupsample_w640h640.nef` + - Image: `one_bike_many_cars_800x800` (Base64) +2. FCOS (model zoo) + - Model: `kl720_20004_fcos-drk53s_w512h512.nef` + - Image: `one_bike_many_cars_800x800` (Base64) + +## Next Steps (After Strategy) +- Confirm endpoint payloads (JSON schema). +- Decide service framework (FastAPI/Flask). +- Define error model and device state machine. +- Plan installer workflow (driver + service). + +## Legacy Firmware Story And Recommended Handling +- Background: + - Many shipped devices are still on old KDP firmware or KL720 legacy states. + - In that state, `kp.core.connect_devices` and even `connect_devices_without_check` may still return `KP_ERROR_INVALID_FIRMWARE_24`. +- Goal: + - Keep user operations in one tool without requiring a separate DFUT install flow. +- Recommended handling: + 1. User scans devices via `GET /devices`. + 2. If normal connect fails with `KP_ERROR_INVALID_FIRMWARE_24`, call: + - `POST /firmware/legacy-upgrade/kl520` or + - `POST /firmware/legacy-upgrade/kl720` + 3. Re-scan and reconnect using `POST /devices/connect`. + 4. Continue with `POST /firmware/load` (if needed), `POST /models/load`, and inference. +- Experimental alternative: + - If DFUT route is blocked on specific old-device states, test `POST /firmware/legacy-plus121/load` as an independent non-DFUT legacy SDK compatibility path. +- Notes: + - Recovery endpoints use bundled `third_party/Kneron_DFUT/bin/KneronDFUT.exe`. + - This keeps firmware recovery and inference service in the same product boundary. + +## Validation Memo (Next) +- Record and verify on newer KneronPLUS versions: + - For KL520 old KDP state, `loader -> load_firmware_from_file(scpu,ncpu)` sequence works in legacy-plus121 path. + - Hypothesis: the same sequence may also work on newer PLUS runtime. + - Action later: add an explicit validation task on current PLUS branch and capture pass/fail matrix by device FW state. diff --git a/local_service_win/TestRes/Images/BmpToRGB565.html b/local_service_win/TestRes/Images/BmpToRGB565.html new file mode 100644 index 0000000..fd0a45f --- /dev/null +++ b/local_service_win/TestRes/Images/BmpToRGB565.html @@ -0,0 +1,271 @@ + + + + + + BMP to RGB565 + + + +
+

BMP to RGB565 (Raw)

+

+ Select a BMP file, convert pixels to RGB565 raw bytes (little-endian), then copy Base64 for + /inference/run. +

+ +
+ + + + + + + + + +
+ + Preview will appear here +
No file loaded.
+ +
+ + +
+ +
+ + + + +
+ +
+ + +
+
+ + + + diff --git a/local_service_win/TestRes/Images/MOT16-03_trim.mp4 b/local_service_win/TestRes/Images/MOT16-03_trim.mp4 new file mode 100644 index 0000000..6124631 Binary files /dev/null and b/local_service_win/TestRes/Images/MOT16-03_trim.mp4 differ diff --git a/local_service_win/TestRes/Images/PayloadDetectionView.html b/local_service_win/TestRes/Images/PayloadDetectionView.html new file mode 100644 index 0000000..caf0f0f --- /dev/null +++ b/local_service_win/TestRes/Images/PayloadDetectionView.html @@ -0,0 +1,955 @@ + + + + + + Payload Detection Viewer (YOLO/TinyYOLO/FCOS) + + + +
+

Payload Detection Viewer

+

+ 專為 POC:手動選擇模型 (YOLOv5 / TinyYOLOv3 / FCOS) 後處理,將 payload 推論結果畫成框 + 類別 + 分數。 +

+ +
+
+ + + + + +
+ +
+ + + +
+
+ + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + + + +
+ +
+ + + + +
+
+ +
+
+
Detection Overlay
+ +
+
+
+
Top Boxes
+
+ + + + + + + +
#clsscorex1y1x2y2
+
+
+
+
+
+
+ + + + diff --git a/local_service_win/TestRes/Images/PayloadTensorView.html b/local_service_win/TestRes/Images/PayloadTensorView.html new file mode 100644 index 0000000..f91959f --- /dev/null +++ b/local_service_win/TestRes/Images/PayloadTensorView.html @@ -0,0 +1,624 @@ + + + + + + Payload Tensor Viewer + + + +
+

Payload Tensor Viewer

+

+ Paste full JSON payload and click Parse & Render. Supports base64 float32 tensors in NCHW shape (e.g. [1,255,7,7]). +

+ + +
+ + +
+ + + +
+

Overlay Viewer

+

+ Upload original image, pick output/channel, then overlay activation heatmap on top. +

+ +
+ + + +
+ +
+ + + + + + + + + + + 0.45 + +
+ +
+
+
Overlay
+ +
+
+
+
Heatmap Only
+ +
+
+
+
+ +
+
+ + + + diff --git a/local_service_win/TestRes/Images/Pic64View.html b/local_service_win/TestRes/Images/Pic64View.html new file mode 100644 index 0000000..a972019 --- /dev/null +++ b/local_service_win/TestRes/Images/Pic64View.html @@ -0,0 +1,83 @@ + + + + + + Pic64View + + + +
+

Pic64View

+

+ Paste a Base64 image string (with or without data URL prefix) and click "Render". +

+ +
+ + +
+ Preview will appear here +
+ + + + diff --git a/local_service_win/TestRes/Images/VideoInferenceWeb.html b/local_service_win/TestRes/Images/VideoInferenceWeb.html new file mode 100644 index 0000000..b05a64d --- /dev/null +++ b/local_service_win/TestRes/Images/VideoInferenceWeb.html @@ -0,0 +1,627 @@ + + + + + + Video Inference Viewer + + + +
+
+

Video Inference (API)

+
+
+ + +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
Ready.
+
預設值可直接測 YOLOv5。先確認 LocalAPI 已啟動,並完成 connect + load model。
+
+ +
+
+ +
+ + +
+
+ + + + diff --git a/local_service_win/TestRes/Images/bike_cars_street_224x224.bmp b/local_service_win/TestRes/Images/bike_cars_street_224x224.bmp new file mode 100644 index 0000000..987b217 Binary files /dev/null and b/local_service_win/TestRes/Images/bike_cars_street_224x224.bmp differ diff --git a/local_service_win/TestRes/Images/bike_cars_street_224x224.html b/local_service_win/TestRes/Images/bike_cars_street_224x224.html new file mode 100644 index 0000000..a9e9c3e --- /dev/null +++ b/local_service_win/TestRes/Images/bike_cars_street_224x224.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/local_service_win/TestRes/Images/one_bike_many_cars_800x800.bmp b/local_service_win/TestRes/Images/one_bike_many_cars_800x800.bmp new file mode 100644 index 0000000..2893d85 Binary files /dev/null and b/local_service_win/TestRes/Images/one_bike_many_cars_800x800.bmp differ diff --git a/local_service_win/TestRes/Images/one_bike_many_cars_800x800.html b/local_service_win/TestRes/Images/one_bike_many_cars_800x800.html new file mode 100644 index 0000000..ff12d84 --- /dev/null +++ b/local_service_win/TestRes/Images/one_bike_many_cars_800x800.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/local_service_win/TestRes/Models/kl520_20004_fcos-drk53s_w512h512.nef b/local_service_win/TestRes/Models/kl520_20004_fcos-drk53s_w512h512.nef new file mode 100644 index 0000000..8d9138a Binary files /dev/null and b/local_service_win/TestRes/Models/kl520_20004_fcos-drk53s_w512h512.nef differ diff --git a/local_service_win/TestRes/Models/kl520_20005_yolov5-noupsample_w640h640.nef b/local_service_win/TestRes/Models/kl520_20005_yolov5-noupsample_w640h640.nef new file mode 100644 index 0000000..3ebaf01 Binary files /dev/null and b/local_service_win/TestRes/Models/kl520_20005_yolov5-noupsample_w640h640.nef differ diff --git a/local_service_win/TestRes/Models/kl720_20004_fcos-drk53s_w512h512.nef b/local_service_win/TestRes/Models/kl720_20004_fcos-drk53s_w512h512.nef new file mode 100644 index 0000000..e540bff Binary files /dev/null and b/local_service_win/TestRes/Models/kl720_20004_fcos-drk53s_w512h512.nef differ diff --git a/local_service_win/TestRes/Models/kl720_20005_yolov5-noupsample_w640h640.nef b/local_service_win/TestRes/Models/kl720_20005_yolov5-noupsample_w640h640.nef new file mode 100644 index 0000000..64a0bec Binary files /dev/null and b/local_service_win/TestRes/Models/kl720_20005_yolov5-noupsample_w640h640.nef differ diff --git a/local_service_win/TestRes/Models/models_520.nef b/local_service_win/TestRes/Models/models_520.nef new file mode 100644 index 0000000..adefdaa Binary files /dev/null and b/local_service_win/TestRes/Models/models_520.nef differ diff --git a/local_service_win/TestRes/TEST_PAIRS.md b/local_service_win/TestRes/TEST_PAIRS.md new file mode 100644 index 0000000..62cebdb --- /dev/null +++ b/local_service_win/TestRes/TEST_PAIRS.md @@ -0,0 +1,29 @@ +# Model/Image Test Pairs (from kneron_plus examples) + +## KL520 +- YOLOv5 (model zoo) + - Model: `res/models/KL520/yolov5-noupsample_w640h640_kn-model-zoo/kl520_20005_yolov5-noupsample_w640h640.nef` + - Image: `res/images/one_bike_many_cars_800x800.bmp` + - Source: `examples_model_zoo/kl520_kn-model-zoo_generic_inference_post_yolov5/kl520_kn-model-zoo_generic_inference_post_yolov5.c` +- FCOS (model zoo) + - Model: `res/models/KL520/fcos-drk53s_w512h512_kn-model-zoo/kl520_20004_fcos-drk53s_w512h512.nef` + - Image: `res/images/one_bike_many_cars_800x800.bmp` + - Source: `examples_model_zoo/kl520_kn-model-zoo_generic_inference_post_fcos/kl520_kn-model-zoo_generic_inference_post_fcos.c` +- Tiny YOLO v3 (generic demo) + - Model: `res/models/KL520/tiny_yolo_v3/models_520.nef` + - Image: `res/images/bike_cars_street_224x224.bmp` + - Source: `examples/kl520_demo_app_yolo_inference/kl520_demo_app_yolo_inference.c` +- Tiny YOLO v3 (multithread demo) + - Model: `res/models/KL520/tiny_yolo_v3/models_520.nef` + - Image: `res/images/bike_cars_street_224x224.bmp` + - Source: `examples/kl520_demo_app_yolo_inference_multithread/kl520_demo_app_yolo_inference_multithread.c` + +## KL720 +- YOLOv5 (model zoo) + - Model: `res/models/KL720/yolov5-noupsample_w640h640_kn-model-zoo/kl720_20005_yolov5-noupsample_w640h640.nef` + - Image: `res/images/one_bike_many_cars_800x800.bmp` + - Source: `examples_model_zoo/kl720_kn-model-zoo_generic_inference_post_yolov5/kl720_kn-model-zoo_generic_inference_post_yolov5.c` +- FCOS (model zoo) + - Model: `res/models/KL720/fcos-drk53s_w512h512_kn-model-zoo/kl720_20004_fcos-drk53s_w512h512.nef` + - Image: `res/images/one_bike_many_cars_800x800.bmp` + - Source: `examples_model_zoo/kl720_kn-model-zoo_generic_inference_post_fcos/kl720_kn-model-zoo_generic_inference_post_fcos.c` diff --git a/local_service_win/TestRes/video_inference_viewer.py b/local_service_win/TestRes/video_inference_viewer.py new file mode 100644 index 0000000..ae714ee --- /dev/null +++ b/local_service_win/TestRes/video_inference_viewer.py @@ -0,0 +1,514 @@ +from __future__ import annotations + +import argparse +import base64 +import json +import math +import sys +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence, Tuple + +import cv2 +import numpy as np + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from LocalAPI import postprocess_core as core + + +YOLO_DEFAULT_ANCHORS: List[List[Tuple[float, float]]] = [ + [(10.0, 14.0), (23.0, 27.0), (37.0, 58.0)], + [(81.0, 82.0), (135.0, 169.0), (344.0, 319.0)], +] + + +def _sigmoid(v: np.ndarray | float) -> np.ndarray | float: + return 1.0 / (1.0 + np.exp(-v)) + + +def _encode_frame(frame_bgr: np.ndarray, image_format: str) -> bytes: + fmt = image_format.upper() + if fmt == "RGBA8888": + rgba = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGBA) + return rgba.tobytes() + if fmt == "RAW8": + gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY) + return gray.tobytes() + if fmt == "RGB565": + bgr565 = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2BGR565) + return bgr565.tobytes() + raise ValueError(f"Unsupported image_format: {image_format}") + + +def _call_inference_run( + base_url: str, + model_id: int, + image_format: str, + width: int, + height: int, + image_bytes: bytes, + channels_ordering: str = "DEFAULT", + output_dtype: str = "float32", + timeout_sec: float = 20.0, +) -> Dict[str, Any]: + body = { + "model_id": model_id, + "image_format": image_format, + "width": width, + "height": height, + "image_base64": base64.b64encode(image_bytes).decode("ascii"), + "channels_ordering": channels_ordering, + "output_dtype": output_dtype, + } + req = urllib.request.Request( + url=f"{base_url.rstrip('/')}/inference/run", + data=json.dumps(body).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=timeout_sec) as resp: + content = resp.read().decode("utf-8", errors="replace") + except urllib.error.HTTPError as exc: + msg = exc.read().decode("utf-8", errors="replace") + raise RuntimeError(f"HTTP {exc.code}: {msg}") from exc + except urllib.error.URLError as exc: + raise RuntimeError(f"Request failed: {exc}") from exc + + parsed = json.loads(content) + if not parsed.get("ok"): + raise RuntimeError(json.dumps(parsed.get("error"), ensure_ascii=False)) + return parsed["data"] + + +def _decode_outputs(raw_outputs: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]: + decoded: List[Dict[str, Any]] = [] + for idx, o in enumerate(raw_outputs): + shape = list(o.get("shape") or []) + data_b64 = str(o.get("data_base64") or "") + raw = base64.b64decode(data_b64) + arr = np.frombuffer(raw, dtype=" List[Dict[str, Any]]: + picked: List[Dict[str, Any]] = [] + for o in all_nodes: + shape = o["shape"] + if len(shape) != 4 or shape[0] != 1: + continue + ch = int(shape[1]) + if ch % (5 + num_classes) != 0: + continue + picked.append(o) + picked.sort(key=lambda n: int(n["shape"][2]), reverse=True) + return picked + + +def _decode_yolo_common( + all_nodes: Sequence[Dict[str, Any]], + mode: str, + num_classes: int, + input_w: int, + input_h: int, + conf_th: float, + use_sigmoid: bool = True, + use_xy_sigmoid: bool = True, + score_mode: str = "obj_cls", + anchors_by_level: Optional[List[List[Tuple[float, float]]]] = None, +) -> List[Box]: + nodes = _pick_yolo_nodes(all_nodes, num_classes) + if not nodes: + raise RuntimeError("No YOLO-like [1,C,H,W] output nodes found") + anchors_levels = anchors_by_level or YOLO_DEFAULT_ANCHORS + + boxes: List[Box] = [] + attrs = 5 + num_classes + + for lv, o in enumerate(nodes): + _, ch, gh, gw = o["shape"] + na = int(ch // attrs) + data: np.ndarray = o["data"] + anchors = anchors_levels[min(lv, len(anchors_levels) - 1)] + + def at(channel_idx: int, y: int, x: int) -> float: + return float(data[channel_idx * gh * gw + y * gw + x]) + + for a in range(na): + aw, ah = anchors[min(a, len(anchors) - 1)] + base = a * attrs + + for y in range(gh): + for x in range(gw): + tx = at(base + 0, y, x) + ty = at(base + 1, y, x) + tw = at(base + 2, y, x) + th = at(base + 3, y, x) + to = at(base + 4, y, x) + + obj = float(_sigmoid(to) if use_sigmoid else to) + best_cls = -1 + best_prob = -1e9 + for k in range(num_classes): + p = at(base + 5 + k, y, x) + p = float(_sigmoid(p) if use_sigmoid else p) + if p > best_prob: + best_prob = p + best_cls = k + + if score_mode == "obj": + score = obj + elif score_mode == "cls": + score = best_prob + else: + score = obj * best_prob + + if score < conf_th: + continue + + if mode == "yolov5": + sx = input_w / gw + sy = input_h / gh + txv = float(_sigmoid(tx) if use_xy_sigmoid else tx) + tyv = float(_sigmoid(ty) if use_xy_sigmoid else ty) + bx = (txv * 2.0 - 0.5 + x) * sx + by = (tyv * 2.0 - 0.5 + y) * sy + bw = (float(_sigmoid(tw)) * 2.0) ** 2 * aw + bh = (float(_sigmoid(th)) * 2.0) ** 2 * ah + else: + txv = float(_sigmoid(tx) if use_xy_sigmoid else tx) + tyv = float(_sigmoid(ty) if use_xy_sigmoid else ty) + bx = (txv + x) / gw * input_w + by = (tyv + y) / gh * input_h + bw = aw * math.exp(tw) + bh = ah * math.exp(th) + + boxes.append( + Box( + cls=best_cls, + score=score, + x1=bx - bw / 2.0, + y1=by - bh / 2.0, + x2=bx + bw / 2.0, + y2=by + bh / 2.0, + ) + ) + + return boxes + + +def _auto_fcos_indices(all_nodes: Sequence[Dict[str, Any]], num_classes: int) -> List[Tuple[int, int, int, int]]: + valid = [o for o in all_nodes if len(o["shape"]) == 4 and o["shape"][0] == 1] + cls_nodes = [o for o in valid if int(o["shape"][1]) == num_classes] + reg_nodes = [o for o in valid if int(o["shape"][1]) == 4] + ctr_nodes = [o for o in valid if int(o["shape"][1]) == 1] + + by_hw: Dict[Tuple[int, int], Dict[str, Dict[str, Any]]] = {} + for n in cls_nodes: + by_hw.setdefault((int(n["shape"][2]), int(n["shape"][3])), {})["cls"] = n + for n in reg_nodes: + by_hw.setdefault((int(n["shape"][2]), int(n["shape"][3])), {})["reg"] = n + for n in ctr_nodes: + by_hw.setdefault((int(n["shape"][2]), int(n["shape"][3])), {})["ctr"] = n + + levels: List[Tuple[int, int, int, int]] = [] + for (h, w), items in by_hw.items(): + if not {"cls", "reg", "ctr"}.issubset(items.keys()): + continue + levels.append( + ( + h, + int(items["cls"]["node_idx"]), + int(items["reg"]["node_idx"]), + int(items["ctr"]["node_idx"]), + ) + ) + levels.sort(key=lambda x: x[0], reverse=True) + strides = [8, 16, 32, 64, 128] + return [(cls_i, reg_i, ctr_i, strides[min(i, len(strides) - 1)]) for i, (_, cls_i, reg_i, ctr_i) in enumerate(levels)] + + +def _decode_fcos( + all_nodes: Sequence[Dict[str, Any]], + num_classes: int, + input_w: int, + input_h: int, + conf_th: float, + use_sigmoid: bool = True, + score_mode: str = "obj_cls", +) -> List[Box]: + levels = _auto_fcos_indices(all_nodes, num_classes) + if not levels: + raise RuntimeError("Cannot auto match FCOS cls/reg/ctr nodes") + + boxes: List[Box] = [] + by_idx = {int(n["node_idx"]): n for n in all_nodes} + + for cls_idx, reg_idx, ctr_idx, stride in levels: + cls_node = by_idx.get(cls_idx) + reg_node = by_idx.get(reg_idx) + ctr_node = by_idx.get(ctr_idx) + if not cls_node or not reg_node or not ctr_node: + continue + + gh = int(cls_node["shape"][2]) + gw = int(cls_node["shape"][3]) + cls_data: np.ndarray = cls_node["data"] + reg_data: np.ndarray = reg_node["data"] + ctr_data: np.ndarray = ctr_node["data"] + + def at(node_data: np.ndarray, channel_idx: int, y: int, x: int) -> float: + return float(node_data[channel_idx * gh * gw + y * gw + x]) + + cls_channels = int(cls_node["shape"][1]) + for y in range(gh): + for x in range(gw): + ctr = at(ctr_data, 0, y, x) + ctr = float(_sigmoid(ctr) if use_sigmoid else ctr) + + best_cls = -1 + best_prob = -1e9 + for k in range(min(num_classes, cls_channels)): + p = at(cls_data, k, y, x) + p = float(_sigmoid(p) if use_sigmoid else p) + if p > best_prob: + best_prob = p + best_cls = k + + if score_mode == "obj": + score = ctr + elif score_mode == "cls": + score = best_prob + else: + score = math.sqrt(max(0.0, best_prob * ctr)) + if score < conf_th: + continue + + l = max(0.0, at(reg_data, 0, y, x)) + t = max(0.0, at(reg_data, 1, y, x)) + r = max(0.0, at(reg_data, 2, y, x)) + b = max(0.0, at(reg_data, 3, y, x)) + cx = (x + 0.5) * stride + cy = (y + 0.5) * stride + + x1 = max(0.0, min(input_w, cx - l)) + y1 = max(0.0, min(input_h, cy - t)) + x2 = max(0.0, min(input_w, cx + r)) + y2 = max(0.0, min(input_h, cy + b)) + if x2 <= x1 or y2 <= y1: + continue + boxes.append(Box(cls=best_cls, score=score, x1=x1, y1=y1, x2=x2, y2=y2)) + + return boxes + + +def _iou(a: Box, b: Box) -> float: + xx1 = max(a.x1, b.x1) + yy1 = max(a.y1, b.y1) + xx2 = min(a.x2, b.x2) + yy2 = min(a.y2, b.y2) + w = max(0.0, xx2 - xx1) + h = max(0.0, yy2 - yy1) + inter = w * h + if inter <= 0: + return 0.0 + area_a = max(0.0, a.x2 - a.x1) * max(0.0, a.y2 - a.y1) + area_b = max(0.0, b.x2 - b.x1) * max(0.0, b.y2 - b.y1) + return inter / max(1e-9, area_a + area_b - inter) + + +def _nms(boxes: Sequence[Box], iou_th: float, max_out: int) -> List[Box]: + by_cls: Dict[int, List[Box]] = {} + for b in boxes: + by_cls.setdefault(b.cls, []).append(b) + + kept: List[Box] = [] + for cls_boxes in by_cls.values(): + cls_boxes = sorted(cls_boxes, key=lambda b: b.score, reverse=True) + picked: List[Box] = [] + while cls_boxes: + cur = cls_boxes.pop(0) + picked.append(cur) + cls_boxes = [b for b in cls_boxes if _iou(cur, b) <= iou_th] + kept.extend(picked) + + kept.sort(key=lambda b: b.score, reverse=True) + return kept[:max_out] + + +def _draw_boxes(frame: np.ndarray, boxes: Sequence[core.Box], input_w: int, input_h: int) -> np.ndarray: + out = frame.copy() + h, w = out.shape[:2] + sx = w / float(input_w) + sy = h / float(input_h) + + for b in boxes: + x1 = int(max(0, min(w - 1, round(b.x1 * sx)))) + y1 = int(max(0, min(h - 1, round(b.y1 * sy)))) + x2 = int(max(0, min(w - 1, round(b.x2 * sx)))) + y2 = int(max(0, min(h - 1, round(b.y2 * sy)))) + if x2 <= x1 or y2 <= y1: + continue + color = tuple(int(c) for c in cv2.cvtColor(np.uint8([[[b.cls * 47 % 180, 255, 220]]]), cv2.COLOR_HSV2BGR)[0][0]) + cv2.rectangle(out, (x1, y1), (x2, y2), color, 2) + text = f"{b.cls}:{b.score:.3f}" + cv2.putText(out, text, (x1, max(14, y1 - 4)), cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 2, cv2.LINE_AA) + return out + + +def _pick_video_via_dialog() -> Optional[str]: + try: + import tkinter as tk + from tkinter import filedialog + except Exception: + return None + root = tk.Tk() + root.withdraw() + path = filedialog.askopenfilename( + title="Select video file", + filetypes=[("Video files", "*.mp4 *.avi *.mov *.mkv *.wmv"), ("All files", "*.*")], + ) + root.destroy() + return path or None + + +def _defaults_for_model(model_type: str) -> Tuple[int, int]: + mt = model_type.lower() + if mt == "fcos": + return 512, 512 + if mt == "tinyyolo": + return 224, 224 + return 640, 640 + + +def main() -> None: + parser = argparse.ArgumentParser(description="Video -> /inference/run -> draw detection boxes") + parser.add_argument("--base-url", default="http://127.0.0.1:4398") + parser.add_argument("--video", default="") + parser.add_argument("--model-id", type=int, required=True) + parser.add_argument("--model-type", choices=["yolov5", "fcos", "tinyyolo"], default="yolov5") + parser.add_argument("--input-width", type=int, default=0) + parser.add_argument("--input-height", type=int, default=0) + parser.add_argument("--image-format", default="RGBA8888") + parser.add_argument("--num-classes", type=int, default=80) + parser.add_argument("--score-th", type=float, default=0.25) + parser.add_argument("--iou-th", type=float, default=0.45) + parser.add_argument("--max-boxes", type=int, default=200) + parser.add_argument("--sample-every-n", type=int, default=3) + parser.add_argument("--save-output", default="") + args = parser.parse_args() + + video_path = args.video.strip() or _pick_video_via_dialog() + if not video_path: + raise SystemExit("No video selected") + if not Path(video_path).is_file(): + raise SystemExit(f"Video not found: {video_path}") + + default_w, default_h = _defaults_for_model(args.model_type) + in_w = int(args.input_width or default_w) + in_h = int(args.input_height or default_h) + + cap = cv2.VideoCapture(video_path) + if not cap.isOpened(): + raise SystemExit(f"Cannot open video: {video_path}") + + writer: Optional[cv2.VideoWriter] = None + if args.save_output: + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + fps = float(cap.get(cv2.CAP_PROP_FPS) or 20.0) + frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or in_w) + frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or in_h) + writer = cv2.VideoWriter(args.save_output, fourcc, fps, (frame_w, frame_h)) + + print("Press 'q' to quit.") + frame_idx = -1 + infer_count = 0 + last_boxes: List[core.Box] = [] + t0 = time.time() + + try: + while True: + ok, frame = cap.read() + if not ok: + break + frame_idx += 1 + + if frame_idx % max(1, args.sample_every_n) == 0: + infer_count += 1 + resized = cv2.resize(frame, (in_w, in_h), interpolation=cv2.INTER_AREA) + image_bytes = _encode_frame(resized, args.image_format) + try: + result = _call_inference_run( + base_url=args.base_url, + model_id=args.model_id, + image_format=args.image_format, + width=in_w, + height=in_h, + image_bytes=image_bytes, + ) + raw_outputs = result.get("outputs") or [] + outputs = core.decode_outputs(raw_outputs) + if args.model_type == "fcos": + raw_boxes = core.decode_fcos( + outputs, + num_classes=args.num_classes, + input_w=in_w, + input_h=in_h, + conf_th=args.score_th, + ) + else: + raw_boxes = core.decode_yolo_common( + outputs, + mode="yolov5" if args.model_type == "yolov5" else "tinyyolo", + num_classes=args.num_classes, + input_w=in_w, + input_h=in_h, + conf_th=args.score_th, + ) + last_boxes = core.nms(raw_boxes, iou_th=args.iou_th, max_out=args.max_boxes) + except Exception as exc: + print(f"[frame {frame_idx}] inference failed: {exc}") + + vis = _draw_boxes(frame, last_boxes, in_w, in_h) + elapsed = max(1e-6, time.time() - t0) + api_fps = infer_count / elapsed + cv2.putText( + vis, + f"frame={frame_idx} infer={infer_count} api_fps={api_fps:.2f} boxes={len(last_boxes)}", + (10, 24), + cv2.FONT_HERSHEY_SIMPLEX, + 0.65, + (0, 255, 0), + 2, + cv2.LINE_AA, + ) + + cv2.imshow("Kneron Video Inference Viewer", vis) + if writer is not None: + writer.write(vis) + + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break + finally: + cap.release() + if writer is not None: + writer.release() + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() diff --git a/local_service_win/firmware/KL520/VERSION b/local_service_win/firmware/KL520/VERSION new file mode 100644 index 0000000..ccbccc3 --- /dev/null +++ b/local_service_win/firmware/KL520/VERSION @@ -0,0 +1 @@ +2.2.0 diff --git a/local_service_win/firmware/KL520/dfw/minions.bin b/local_service_win/firmware/KL520/dfw/minions.bin new file mode 100644 index 0000000..7463ab8 Binary files /dev/null and b/local_service_win/firmware/KL520/dfw/minions.bin differ diff --git a/local_service_win/firmware/KL520/fw_loader.bin b/local_service_win/firmware/KL520/fw_loader.bin new file mode 100644 index 0000000..51ca844 Binary files /dev/null and b/local_service_win/firmware/KL520/fw_loader.bin differ diff --git a/local_service_win/firmware/KL520/fw_ncpu.bin b/local_service_win/firmware/KL520/fw_ncpu.bin new file mode 100644 index 0000000..10cd08b Binary files /dev/null and b/local_service_win/firmware/KL520/fw_ncpu.bin differ diff --git a/local_service_win/firmware/KL520/fw_scpu.bin b/local_service_win/firmware/KL520/fw_scpu.bin new file mode 100644 index 0000000..482315a Binary files /dev/null and b/local_service_win/firmware/KL520/fw_scpu.bin differ diff --git a/local_service_win/firmware/KL520_kdp/fw_ncpu.bin b/local_service_win/firmware/KL520_kdp/fw_ncpu.bin new file mode 100644 index 0000000..0ed154f Binary files /dev/null and b/local_service_win/firmware/KL520_kdp/fw_ncpu.bin differ diff --git a/local_service_win/firmware/KL520_kdp/fw_scpu.bin b/local_service_win/firmware/KL520_kdp/fw_scpu.bin new file mode 100644 index 0000000..5101c06 Binary files /dev/null and b/local_service_win/firmware/KL520_kdp/fw_scpu.bin differ diff --git a/local_service_win/firmware/KL630/VERSION b/local_service_win/firmware/KL630/VERSION new file mode 100644 index 0000000..aefb325 --- /dev/null +++ b/local_service_win/firmware/KL630/VERSION @@ -0,0 +1 @@ +SDK-v2.5.7 diff --git a/local_service_win/firmware/KL630/kp_firmware.tar b/local_service_win/firmware/KL630/kp_firmware.tar new file mode 100644 index 0000000..822b319 Binary files /dev/null and b/local_service_win/firmware/KL630/kp_firmware.tar differ diff --git a/local_service_win/firmware/KL630/kp_loader.tar b/local_service_win/firmware/KL630/kp_loader.tar new file mode 100644 index 0000000..45f6cd4 Binary files /dev/null and b/local_service_win/firmware/KL630/kp_loader.tar differ diff --git a/local_service_win/firmware/KL720/VERSION b/local_service_win/firmware/KL720/VERSION new file mode 100644 index 0000000..ccbccc3 --- /dev/null +++ b/local_service_win/firmware/KL720/VERSION @@ -0,0 +1 @@ +2.2.0 diff --git a/local_service_win/firmware/KL720/fw_ncpu.bin b/local_service_win/firmware/KL720/fw_ncpu.bin new file mode 100644 index 0000000..815530e Binary files /dev/null and b/local_service_win/firmware/KL720/fw_ncpu.bin differ diff --git a/local_service_win/firmware/KL720/fw_scpu.bin b/local_service_win/firmware/KL720/fw_scpu.bin new file mode 100644 index 0000000..82f23e2 Binary files /dev/null and b/local_service_win/firmware/KL720/fw_scpu.bin differ diff --git a/local_service_win/firmware/KL730/VERSION b/local_service_win/firmware/KL730/VERSION new file mode 100644 index 0000000..5332ca0 --- /dev/null +++ b/local_service_win/firmware/KL730/VERSION @@ -0,0 +1 @@ +SDK-v1.3.0 diff --git a/local_service_win/firmware/KL730/kp_firmware.tar b/local_service_win/firmware/KL730/kp_firmware.tar new file mode 100644 index 0000000..a688586 Binary files /dev/null and b/local_service_win/firmware/KL730/kp_firmware.tar differ diff --git a/local_service_win/firmware/KL730/kp_loader.tar b/local_service_win/firmware/KL730/kp_loader.tar new file mode 100644 index 0000000..3e799e2 Binary files /dev/null and b/local_service_win/firmware/KL730/kp_loader.tar differ diff --git a/local_service_win/requirements.txt b/local_service_win/requirements.txt new file mode 100644 index 0000000..55f981e --- /dev/null +++ b/local_service_win/requirements.txt @@ -0,0 +1,16 @@ +# Core SDK (installed via local wheel; see STRATEGY.md) +# KneronPLUS==3.0.0 + +# HTTP service +fastapi +uvicorn +python-multipart + +# Reference packages from C:\Users\user\Documents\KNEOX\README.md +PyQt5 +opencv-python +pyinstaller +pyarmor + +# Common dependency for kp data handling +numpy diff --git a/local_service_win/third_party/Kneron_DFUT/VERSION b/local_service_win/third_party/Kneron_DFUT/VERSION new file mode 100644 index 0000000..56fea8a --- /dev/null +++ b/local_service_win/third_party/Kneron_DFUT/VERSION @@ -0,0 +1 @@ +3.0.0 \ No newline at end of file diff --git a/local_service_win/third_party/Kneron_DFUT/bin/D3Dcompiler_47.dll b/local_service_win/third_party/Kneron_DFUT/bin/D3Dcompiler_47.dll new file mode 100644 index 0000000..56512f5 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/D3Dcompiler_47.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/KneronDFUT.exe b/local_service_win/third_party/Kneron_DFUT/bin/KneronDFUT.exe new file mode 100644 index 0000000..d1a231d Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/KneronDFUT.exe differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/Qt5Core.dll b/local_service_win/third_party/Kneron_DFUT/bin/Qt5Core.dll new file mode 100644 index 0000000..4e0ee9a Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/Qt5Core.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/Qt5Gui.dll b/local_service_win/third_party/Kneron_DFUT/bin/Qt5Gui.dll new file mode 100644 index 0000000..bba96c8 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/Qt5Gui.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/Qt5Svg.dll b/local_service_win/third_party/Kneron_DFUT/bin/Qt5Svg.dll new file mode 100644 index 0000000..b006006 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/Qt5Svg.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/Qt5Widgets.dll b/local_service_win/third_party/Kneron_DFUT/bin/Qt5Widgets.dll new file mode 100644 index 0000000..e6ff156 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/Qt5Widgets.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/iconengines/qsvgicon.dll b/local_service_win/third_party/Kneron_DFUT/bin/iconengines/qsvgicon.dll new file mode 100644 index 0000000..b10761c Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/iconengines/qsvgicon.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qgif.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qgif.dll new file mode 100644 index 0000000..97beb57 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qgif.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qicns.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qicns.dll new file mode 100644 index 0000000..a64309b Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qicns.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qico.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qico.dll new file mode 100644 index 0000000..5c744ae Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qico.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qjpeg.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qjpeg.dll new file mode 100644 index 0000000..f2bb6be Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qjpeg.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qsvg.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qsvg.dll new file mode 100644 index 0000000..921f25f Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qsvg.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtga.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtga.dll new file mode 100644 index 0000000..b68d35d Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtga.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtiff.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtiff.dll new file mode 100644 index 0000000..31a8ce3 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtiff.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwbmp.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwbmp.dll new file mode 100644 index 0000000..a6751ab Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwbmp.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwebp.dll b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwebp.dll new file mode 100644 index 0000000..49862fa Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwebp.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/libEGL.dll b/local_service_win/third_party/Kneron_DFUT/bin/libEGL.dll new file mode 100644 index 0000000..d3247d2 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/libEGL.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/libGLESV2.dll b/local_service_win/third_party/Kneron_DFUT/bin/libGLESV2.dll new file mode 100644 index 0000000..7825e28 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/libGLESV2.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/libgcc_s_seh-1.dll b/local_service_win/third_party/Kneron_DFUT/bin/libgcc_s_seh-1.dll new file mode 100644 index 0000000..4ec945b Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/libgcc_s_seh-1.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/libkplus.dll b/local_service_win/third_party/Kneron_DFUT/bin/libkplus.dll new file mode 100644 index 0000000..0a9ff70 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/libkplus.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/libstdc++-6.dll b/local_service_win/third_party/Kneron_DFUT/bin/libstdc++-6.dll new file mode 100644 index 0000000..8e55acc Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/libstdc++-6.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/libusb-1.0.dll b/local_service_win/third_party/Kneron_DFUT/bin/libusb-1.0.dll new file mode 100644 index 0000000..bec7169 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/libusb-1.0.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/libwdi.dll b/local_service_win/third_party/Kneron_DFUT/bin/libwdi.dll new file mode 100644 index 0000000..354b339 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/libwdi.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/libwinpthread-1.dll b/local_service_win/third_party/Kneron_DFUT/bin/libwinpthread-1.dll new file mode 100644 index 0000000..d9f4e1a Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/libwinpthread-1.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/opengl32sw.dll b/local_service_win/third_party/Kneron_DFUT/bin/opengl32sw.dll new file mode 100644 index 0000000..475e82a Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/opengl32sw.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/platforms/qwindows.dll b/local_service_win/third_party/Kneron_DFUT/bin/platforms/qwindows.dll new file mode 100644 index 0000000..f9874fc Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/platforms/qwindows.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/styles/qwindowsvistastyle.dll b/local_service_win/third_party/Kneron_DFUT/bin/styles/qwindowsvistastyle.dll new file mode 100644 index 0000000..a3c0e52 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/styles/qwindowsvistastyle.dll differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ar.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ar.qm new file mode 100644 index 0000000..1e9227a Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ar.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_bg.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_bg.qm new file mode 100644 index 0000000..dcec255 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_bg.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ca.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ca.qm new file mode 100644 index 0000000..0b798e5 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ca.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_cs.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_cs.qm new file mode 100644 index 0000000..3ab5ca7 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_cs.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_da.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_da.qm new file mode 100644 index 0000000..6756496 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_da.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_de.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_de.qm new file mode 100644 index 0000000..86ae7fa Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_de.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_en.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_en.qm new file mode 100644 index 0000000..9dad8df Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_en.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_es.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_es.qm new file mode 100644 index 0000000..82012da Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_es.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fi.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fi.qm new file mode 100644 index 0000000..2548cca Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fi.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fr.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fr.qm new file mode 100644 index 0000000..8353f0a Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fr.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_gd.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_gd.qm new file mode 100644 index 0000000..fd7eecd Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_gd.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_he.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_he.qm new file mode 100644 index 0000000..e15d45e Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_he.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_hu.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_hu.qm new file mode 100644 index 0000000..b51bd1a Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_hu.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_it.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_it.qm new file mode 100644 index 0000000..a2433c6 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_it.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ja.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ja.qm new file mode 100644 index 0000000..74409b1 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ja.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ko.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ko.qm new file mode 100644 index 0000000..a46b8a0 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ko.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_lv.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_lv.qm new file mode 100644 index 0000000..c1dbfbd Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_lv.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_pl.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_pl.qm new file mode 100644 index 0000000..0909204 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_pl.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ru.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ru.qm new file mode 100644 index 0000000..b77ce55 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ru.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_sk.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_sk.qm new file mode 100644 index 0000000..215d234 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_sk.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_uk.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_uk.qm new file mode 100644 index 0000000..88c4362 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_uk.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_zh_TW.qm b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_zh_TW.qm new file mode 100644 index 0000000..21c4190 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/bin/translations/qt_zh_TW.qm differ diff --git a/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/flash_helper.bin b/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/flash_helper.bin new file mode 100644 index 0000000..e746210 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/flash_helper.bin differ diff --git a/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/fw_loader.bin b/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/fw_loader.bin new file mode 100644 index 0000000..51ca844 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/fw_loader.bin differ diff --git a/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL630/flash_helper.tar b/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL630/flash_helper.tar new file mode 100644 index 0000000..eb94b5a Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL630/flash_helper.tar differ diff --git a/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL730/flash_helper.tar b/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL730/flash_helper.tar new file mode 100644 index 0000000..b042714 Binary files /dev/null and b/local_service_win/third_party/Kneron_DFUT/resource/firmware/KL730/flash_helper.tar differ diff --git a/local_service_win/third_party/kneron_plus_1_2_1/KneronPLUS-1.2.1.zip b/local_service_win/third_party/kneron_plus_1_2_1/KneronPLUS-1.2.1.zip new file mode 100644 index 0000000..e2aee1c Binary files /dev/null and b/local_service_win/third_party/kneron_plus_1_2_1/KneronPLUS-1.2.1.zip differ