Compare commits
2 Commits
0103a483b8
...
11e779bb40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11e779bb40 | ||
|
|
f85613b8a6 |
297
local_service_win/LocalAPI/legacy_plus121_runner.py
Normal file
297
local_service_win/LocalAPI/legacy_plus121_runner.py
Normal file
@ -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()
|
||||
@ -1,8 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
@ -13,6 +18,12 @@ import kp
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -45,37 +56,77 @@ def _require_device() -> kp.DeviceGroup:
|
||||
|
||||
def _image_format_from_str(value: str) -> kp.ImageFormat:
|
||||
value = value.upper()
|
||||
mapping = {
|
||||
"RGB565": kp.ImageFormat.KP_IMAGE_FORMAT_RGB565,
|
||||
"RGBA8888": kp.ImageFormat.KP_IMAGE_FORMAT_RGBA8888,
|
||||
"RAW8": kp.ImageFormat.KP_IMAGE_FORMAT_RAW8,
|
||||
"YUYV": kp.ImageFormat.KP_IMAGE_FORMAT_YUYV,
|
||||
"YUV420": kp.ImageFormat.KP_IMAGE_FORMAT_YUV420,
|
||||
# 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}"),
|
||||
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()
|
||||
mapping = {
|
||||
"HCW": kp.ChannelOrdering.KP_CHANNEL_ORDERING_HCW,
|
||||
"CHW": kp.ChannelOrdering.KP_CHANNEL_ORDERING_CHW,
|
||||
"HWC": kp.ChannelOrdering.KP_CHANNEL_ORDERING_HWC,
|
||||
"DEFAULT": kp.ChannelOrdering.KP_CHANNEL_ORDERING_DEFAULT,
|
||||
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}"),
|
||||
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_", "")
|
||||
@ -112,10 +163,24 @@ class FirmwareLoadRequest(BaseModel):
|
||||
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
|
||||
@ -126,6 +191,316 @@ class InferenceRunRequest(BaseModel):
|
||||
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
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> Dict[str, Any]:
|
||||
return _ok({"status": "up"})
|
||||
@ -175,21 +550,7 @@ def connect(req: ConnectRequest) -> Dict[str, Any]:
|
||||
STATE.port_id = None
|
||||
STATE.model_desc = None
|
||||
|
||||
port_id = req.port_id
|
||||
if port_id is None:
|
||||
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"))
|
||||
if req.scan_index is None:
|
||||
scan_index = 0
|
||||
else:
|
||||
scan_index = 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}"),
|
||||
)
|
||||
port_id = device_list.device_descriptor_list[scan_index].usb_port_id
|
||||
port_id = _resolve_port_id(req)
|
||||
|
||||
try:
|
||||
device_group = kp.core.connect_devices([int(port_id)])
|
||||
@ -206,6 +567,35 @@ def connect(req: ConnectRequest) -> Dict[str, Any]:
|
||||
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:
|
||||
@ -223,6 +613,70 @@ def disconnect() -> Dict[str, Any]:
|
||||
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()
|
||||
@ -236,6 +690,145 @@ def firmware_load(req: FirmwareLoadRequest) -> Dict[str, Any]:
|
||||
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()
|
||||
@ -321,14 +914,31 @@ def inference_run(req: InferenceRunRequest) -> Dict[str, Any]:
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
|
||||
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(req.image_base64)
|
||||
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"),
|
||||
)
|
||||
|
||||
expected_size = _expected_image_size_bytes(req.image_format, req.width, req.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=req.width,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
local_service_win/LocalAPI/win_driver/installer_x64.exe
Normal file
BIN
local_service_win/LocalAPI/win_driver/installer_x64.exe
Normal file
Binary file not shown.
BIN
local_service_win/LocalAPI/win_driver/kneron_kl520.inf
Normal file
BIN
local_service_win/LocalAPI/win_driver/kneron_kl520.inf
Normal file
Binary file not shown.
@ -28,6 +28,15 @@ 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.
|
||||
@ -124,12 +133,75 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
### `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
|
||||
@ -143,6 +215,80 @@ Response
|
||||
{ "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
|
||||
@ -246,21 +392,36 @@ Message (server -> client)
|
||||
- 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-03
|
||||
|
||||
### Completed
|
||||
- `GET /health`
|
||||
- `GET /version`
|
||||
- `GET /devices`
|
||||
- `POST /devices/connect`
|
||||
- `POST /devices/connect_force`
|
||||
- `POST /devices/disconnect`
|
||||
|
||||
### Pending
|
||||
- `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`
|
||||
|
||||
### Pending
|
||||
- None (for currently implemented HTTP endpoints).
|
||||
|
||||
### Not Implemented Yet (API spec)
|
||||
- `WS /ws`
|
||||
- `POST /firmware/update`
|
||||
|
||||
@ -296,3 +457,28 @@ Message (server -> client)
|
||||
- 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.
|
||||
|
||||
271
local_service_win/TestRes/Images/BmpToRGB565.html
Normal file
271
local_service_win/TestRes/Images/BmpToRGB565.html
Normal file
@ -0,0 +1,271 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>BMP to RGB565</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 16px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.controls {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 140px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.hint {
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
}
|
||||
.meta {
|
||||
margin: 8px 0;
|
||||
font-family: Consolas, monospace;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
.row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
button {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
label {
|
||||
font-size: 13px;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 120px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>BMP to RGB565 (Raw)</h1>
|
||||
<p class="hint">
|
||||
Select a BMP file, convert pixels to RGB565 raw bytes (little-endian), then copy Base64 for
|
||||
<code>/inference/run</code>.
|
||||
</p>
|
||||
|
||||
<div class="controls">
|
||||
<input id="fileInput" type="file" accept=".bmp,image/bmp" />
|
||||
<label>Model Preset:</label>
|
||||
<select id="modelPreset">
|
||||
<option value="tiny_yolo">TinyYOLO (KL520)</option>
|
||||
<option value="yolov5">YOLOv5 (KL520/KL720)</option>
|
||||
<option value="fcos">FCOS (KL520/KL720)</option>
|
||||
</select>
|
||||
<label>Target W:</label>
|
||||
<input id="targetWidth" type="number" min="1" placeholder="original" />
|
||||
<label>Target H:</label>
|
||||
<input id="targetHeight" type="number" min="1" placeholder="original" />
|
||||
<button id="convertBtn">Convert</button>
|
||||
<button id="clearBtn">Clear</button>
|
||||
</div>
|
||||
|
||||
<img id="preview" alt="Preview will appear here" />
|
||||
<div class="meta" id="meta">No file loaded.</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="base64Output">RGB565 Base64 (raw bytes)</label>
|
||||
<textarea id="base64Output" placeholder="RGB565 base64 output..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<label>model_id:</label>
|
||||
<input id="modelId" type="number" value="19" />
|
||||
<button id="copyB64Btn">Copy Base64</button>
|
||||
<button id="copyPayloadBtn">Copy Payload JSON</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="payloadOutput">Payload sample</label>
|
||||
<textarea id="payloadOutput" placeholder="Payload JSON..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.getElementById("fileInput");
|
||||
const convertBtn = document.getElementById("convertBtn");
|
||||
const clearBtn = document.getElementById("clearBtn");
|
||||
const copyB64Btn = document.getElementById("copyB64Btn");
|
||||
const copyPayloadBtn = document.getElementById("copyPayloadBtn");
|
||||
const modelIdInput = document.getElementById("modelId");
|
||||
const modelPresetSelect = document.getElementById("modelPreset");
|
||||
const targetWidthInput = document.getElementById("targetWidth");
|
||||
const targetHeightInput = document.getElementById("targetHeight");
|
||||
const preview = document.getElementById("preview");
|
||||
const meta = document.getElementById("meta");
|
||||
const base64Output = document.getElementById("base64Output");
|
||||
const payloadOutput = document.getElementById("payloadOutput");
|
||||
const modelPresets = {
|
||||
tiny_yolo: { modelId: 19, width: 224, height: 224, label: "TinyYOLO (KL520)" },
|
||||
yolov5: { modelId: 20005, width: 640, height: 640, label: "YOLOv5" },
|
||||
fcos: { modelId: 20004, width: 512, height: 512, label: "FCOS" }
|
||||
};
|
||||
|
||||
let currentWidth = 0;
|
||||
let currentHeight = 0;
|
||||
|
||||
function toBase64(uint8Array) {
|
||||
let binary = "";
|
||||
const chunkSize = 0x8000;
|
||||
for (let i = 0; i < uint8Array.length; i += chunkSize) {
|
||||
const chunk = uint8Array.subarray(i, i + chunkSize);
|
||||
binary += String.fromCharCode.apply(null, chunk);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function rgbTo565(r, g, b) {
|
||||
// 5 bits red, 6 bits green, 5 bits blue
|
||||
const r5 = (r >> 3) & 0x1f;
|
||||
const g6 = (g >> 2) & 0x3f;
|
||||
const b5 = (b >> 3) & 0x1f;
|
||||
return (r5 << 11) | (g6 << 5) | b5;
|
||||
}
|
||||
|
||||
function buildPayload(base64Value, width, height) {
|
||||
const payload = {
|
||||
model_id: Number(modelIdInput.value || 0),
|
||||
image_format: "RGB565",
|
||||
width: width,
|
||||
height: height,
|
||||
image_base64: base64Value,
|
||||
channels_ordering: "DEFAULT",
|
||||
output_dtype: "float32"
|
||||
};
|
||||
payloadOutput.value = JSON.stringify(payload, null, 2);
|
||||
}
|
||||
|
||||
function setMeta(message) {
|
||||
meta.textContent = message;
|
||||
}
|
||||
|
||||
function applyModelPreset(presetKey) {
|
||||
const preset = modelPresets[presetKey];
|
||||
if (!preset) return;
|
||||
modelIdInput.value = String(preset.modelId);
|
||||
targetWidthInput.value = String(preset.width);
|
||||
targetHeightInput.value = String(preset.height);
|
||||
setMeta(
|
||||
`preset=${preset.label}, model_id=${preset.modelId}, target=${preset.width}x${preset.height}`
|
||||
);
|
||||
if (base64Output.value && currentWidth > 0 && currentHeight > 0) {
|
||||
buildPayload(base64Output.value, currentWidth, currentHeight);
|
||||
}
|
||||
}
|
||||
|
||||
async function convert() {
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) {
|
||||
setMeta("Please select a BMP file first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const srcWidth = img.width;
|
||||
const srcHeight = img.height;
|
||||
|
||||
const tw = Number(targetWidthInput.value);
|
||||
const th = Number(targetHeightInput.value);
|
||||
const hasTarget = Number.isFinite(tw) && Number.isFinite(th) && tw > 0 && th > 0;
|
||||
|
||||
currentWidth = hasTarget ? Math.floor(tw) : srcWidth;
|
||||
currentHeight = hasTarget ? Math.floor(th) : srcHeight;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = currentWidth;
|
||||
canvas.height = currentHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, currentWidth, currentHeight);
|
||||
const imageData = ctx.getImageData(0, 0, currentWidth, currentHeight).data;
|
||||
|
||||
const out = new Uint8Array(currentWidth * currentHeight * 2);
|
||||
let p = 0;
|
||||
for (let i = 0; i < imageData.length; i += 4) {
|
||||
const r = imageData[i];
|
||||
const g = imageData[i + 1];
|
||||
const b = imageData[i + 2];
|
||||
const v565 = rgbTo565(r, g, b);
|
||||
// little-endian bytes
|
||||
out[p++] = v565 & 0xff;
|
||||
out[p++] = (v565 >> 8) & 0xff;
|
||||
}
|
||||
|
||||
const b64 = toBase64(out);
|
||||
base64Output.value = b64;
|
||||
buildPayload(b64, currentWidth, currentHeight);
|
||||
|
||||
preview.src = objectUrl;
|
||||
setMeta(
|
||||
`file=${file.name}, src=${srcWidth}x${srcHeight}, out=${currentWidth}x${currentHeight}, rgb565_bytes=${out.length}, expected=${
|
||||
currentWidth * currentHeight * 2
|
||||
}`
|
||||
);
|
||||
};
|
||||
img.onerror = () => {
|
||||
setMeta("Failed to decode image. Ensure the file is a valid BMP.");
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
img.src = objectUrl;
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
fileInput.value = "";
|
||||
base64Output.value = "";
|
||||
payloadOutput.value = "";
|
||||
preview.removeAttribute("src");
|
||||
currentWidth = 0;
|
||||
currentHeight = 0;
|
||||
setMeta("No file loaded.");
|
||||
}
|
||||
|
||||
convertBtn.addEventListener("click", convert);
|
||||
clearBtn.addEventListener("click", clearAll);
|
||||
modelPresetSelect.addEventListener("change", () => {
|
||||
applyModelPreset(modelPresetSelect.value);
|
||||
});
|
||||
modelIdInput.addEventListener("change", () => {
|
||||
if (base64Output.value && currentWidth > 0 && currentHeight > 0) {
|
||||
buildPayload(base64Output.value, currentWidth, currentHeight);
|
||||
}
|
||||
});
|
||||
|
||||
copyB64Btn.addEventListener("click", async () => {
|
||||
if (!base64Output.value) return;
|
||||
await navigator.clipboard.writeText(base64Output.value);
|
||||
});
|
||||
|
||||
copyPayloadBtn.addEventListener("click", async () => {
|
||||
if (!payloadOutput.value) return;
|
||||
await navigator.clipboard.writeText(payloadOutput.value);
|
||||
});
|
||||
|
||||
applyModelPreset(modelPresetSelect.value);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
955
local_service_win/TestRes/Images/PayloadDetectionView.html
Normal file
955
local_service_win/TestRes/Images/PayloadDetectionView.html
Normal file
@ -0,0 +1,955 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Payload Detection Viewer (YOLO/TinyYOLO/FCOS)</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f6f8;
|
||||
--card: #ffffff;
|
||||
--text: #1f2937;
|
||||
--muted: #6b7280;
|
||||
--line: #d1d5db;
|
||||
--accent: #2563eb;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1300px;
|
||||
margin: 20px auto;
|
||||
padding: 0 12px 24px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.hint {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.panel {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.row label {
|
||||
font-size: 13px;
|
||||
}
|
||||
input[type="number"], input[type="text"], select {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 95px;
|
||||
}
|
||||
input.wide {
|
||||
width: 360px;
|
||||
}
|
||||
.btn {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.error {
|
||||
color: #b91c1c;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.subcard {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
}
|
||||
.subcard-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
background: #fff;
|
||||
}
|
||||
.stats {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 4px 6px;
|
||||
text-align: left;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Payload Detection Viewer</h1>
|
||||
<p class="hint">
|
||||
專為 POC:手動選擇模型 (YOLOv5 / TinyYOLOv3 / FCOS) 後處理,將 payload 推論結果畫成框 + 類別 + 分數。
|
||||
</p>
|
||||
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<button id="parseBtn" class="btn primary">Parse Payload</button>
|
||||
<button id="clearBtn" class="btn">Clear</button>
|
||||
<label>原圖:</label>
|
||||
<input id="imgInput" type="file" accept="image/*" />
|
||||
<button id="clearImgBtn" class="btn">Clear Image</button>
|
||||
</div>
|
||||
<textarea id="payloadInput" placeholder="貼上完整 payload JSON..."></textarea>
|
||||
</div>
|
||||
|
||||
<div id="errorBox" class="error" style="display:none"></div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<label>模型類型:</label>
|
||||
<select id="modelType">
|
||||
<option value="tinyyolo">TinyYOLOv3</option>
|
||||
<option value="yolov5">YOLOv5</option>
|
||||
<option value="fcos">FCOS</option>
|
||||
</select>
|
||||
|
||||
<label>class 數:</label>
|
||||
<input id="numClasses" type="number" min="1" value="80" />
|
||||
|
||||
<label>score 閾值:</label>
|
||||
<input id="scoreTh" type="number" min="0" max="1" step="0.01" value="0.25" />
|
||||
|
||||
<label>NMS IoU:</label>
|
||||
<input id="nmsTh" type="number" min="0" max="1" step="0.01" value="0.45" />
|
||||
|
||||
<label>max boxes:</label>
|
||||
<input id="maxBoxes" type="number" min="1" value="200" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>score mode:</label>
|
||||
<select id="scoreMode">
|
||||
<option value="obj_cls">obj * cls</option>
|
||||
<option value="obj">obj only</option>
|
||||
<option value="cls">cls only</option>
|
||||
</select>
|
||||
|
||||
<label><input id="objClsSigmoid" type="checkbox" checked /> obj/cls sigmoid</label>
|
||||
<label><input id="yoloXySigmoid" type="checkbox" checked /> YOLO x/y sigmoid</label>
|
||||
|
||||
<button id="presetTinyBtn" class="btn">Preset TinyYOLO</button>
|
||||
<button id="presetYolo5Btn" class="btn">Preset YOLOv5</button>
|
||||
<button id="presetFcosBtn" class="btn">Preset FCOS</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>模型輸入寬:</label>
|
||||
<input id="inW" type="number" min="1" value="224" />
|
||||
|
||||
<label>模型輸入高:</label>
|
||||
<input id="inH" type="number" min="1" value="224" />
|
||||
|
||||
<label>YOLO anchors:</label>
|
||||
<input id="anchors" class="wide" type="text" value="10,14|23,27|37,58;81,82|135,169|344,319" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>FCOS class node idx(逗號):</label>
|
||||
<input id="fcosClsIdx" class="wide" type="text" value="" placeholder="例如: 0,3,6" />
|
||||
|
||||
<label>FCOS reg node idx(逗號):</label>
|
||||
<input id="fcosRegIdx" class="wide" type="text" value="" placeholder="例如: 1,4,7" />
|
||||
|
||||
<label>FCOS ctr node idx(逗號):</label>
|
||||
<input id="fcosCtrIdx" class="wide" type="text" value="" placeholder="例如: 2,5,8" />
|
||||
|
||||
<button id="autoFcosIdxBtn" class="btn">Auto Fill FCOS idx</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>FCOS strides:</label>
|
||||
<input id="fcosStrides" class="wide" type="text" value="8,16,32,64,128" />
|
||||
|
||||
<button id="runBtn" class="btn primary">Decode + Draw</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="subcard">
|
||||
<div class="subcard-title">Detection Overlay</div>
|
||||
<canvas id="overlayCanvas" width="1" height="1"></canvas>
|
||||
<div id="overlayStats" class="stats"></div>
|
||||
</div>
|
||||
<div class="subcard">
|
||||
<div class="subcard-title">Top Boxes</div>
|
||||
<div style="max-height:420px; overflow:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th><th>cls</th><th>score</th><th>x1</th><th>y1</th><th>x2</th><th>y2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="boxTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="decodeStats" class="stats"></div>
|
||||
<div id="debugStats" class="stats"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const parseBtn = document.getElementById("parseBtn");
|
||||
const clearBtn = document.getElementById("clearBtn");
|
||||
const runBtn = document.getElementById("runBtn");
|
||||
const clearImgBtn = document.getElementById("clearImgBtn");
|
||||
const payloadInput = document.getElementById("payloadInput");
|
||||
const imgInput = document.getElementById("imgInput");
|
||||
const errorBox = document.getElementById("errorBox");
|
||||
const overlayCanvas = document.getElementById("overlayCanvas");
|
||||
const overlayStats = document.getElementById("overlayStats");
|
||||
const decodeStats = document.getElementById("decodeStats");
|
||||
const debugStats = document.getElementById("debugStats");
|
||||
const boxTableBody = document.getElementById("boxTableBody");
|
||||
|
||||
const modelType = document.getElementById("modelType");
|
||||
const numClasses = document.getElementById("numClasses");
|
||||
const scoreTh = document.getElementById("scoreTh");
|
||||
const nmsTh = document.getElementById("nmsTh");
|
||||
const maxBoxes = document.getElementById("maxBoxes");
|
||||
const inW = document.getElementById("inW");
|
||||
const inH = document.getElementById("inH");
|
||||
const anchorsInput = document.getElementById("anchors");
|
||||
const fcosClsIdx = document.getElementById("fcosClsIdx");
|
||||
const fcosRegIdx = document.getElementById("fcosRegIdx");
|
||||
const fcosCtrIdx = document.getElementById("fcosCtrIdx");
|
||||
const fcosStrides = document.getElementById("fcosStrides");
|
||||
const autoFcosIdxBtn = document.getElementById("autoFcosIdxBtn");
|
||||
const scoreMode = document.getElementById("scoreMode");
|
||||
const objClsSigmoid = document.getElementById("objClsSigmoid");
|
||||
const yoloXySigmoid = document.getElementById("yoloXySigmoid");
|
||||
const presetTinyBtn = document.getElementById("presetTinyBtn");
|
||||
const presetYolo5Btn = document.getElementById("presetYolo5Btn");
|
||||
const presetFcosBtn = document.getElementById("presetFcosBtn");
|
||||
|
||||
let outputs = [];
|
||||
let srcImg = null;
|
||||
|
||||
function showError(msg) {
|
||||
errorBox.style.display = "block";
|
||||
errorBox.textContent = msg;
|
||||
}
|
||||
function clearError() {
|
||||
errorBox.style.display = "none";
|
||||
errorBox.textContent = "";
|
||||
}
|
||||
|
||||
function sigmoid(x) {
|
||||
return 1 / (1 + Math.exp(-x));
|
||||
}
|
||||
|
||||
function maybeSigmoid(v, on) {
|
||||
return on ? sigmoid(v) : v;
|
||||
}
|
||||
|
||||
function fmt(v) {
|
||||
if (!Number.isFinite(v)) return "nan";
|
||||
return Number(v).toFixed(4);
|
||||
}
|
||||
|
||||
function decodeBase64Float32(base64String) {
|
||||
const binary = atob(String(base64String || "").trim());
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
const len = Math.floor(bytes.byteLength / 4);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const out = new Float32Array(len);
|
||||
for (let i = 0; i < len; i++) out[i] = view.getFloat32(i * 4, true);
|
||||
return out;
|
||||
}
|
||||
|
||||
function arrProduct(a) {
|
||||
let p = 1;
|
||||
for (let i = 0; i < a.length; i++) p *= a[i];
|
||||
return p;
|
||||
}
|
||||
|
||||
function parsePayloadText() {
|
||||
clearError();
|
||||
const text = payloadInput.value.trim();
|
||||
if (!text) throw new Error("請先貼 payload JSON");
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(text);
|
||||
} catch (e) {
|
||||
throw new Error("JSON parse failed: " + e.message);
|
||||
}
|
||||
const arr = obj?.data?.outputs || obj?.outputs;
|
||||
if (!Array.isArray(arr) || arr.length === 0) {
|
||||
throw new Error("找不到 outputs,預期 payload.data.outputs");
|
||||
}
|
||||
const parsed = arr.map((o, i) => {
|
||||
const shape = Array.isArray(o.shape) ? o.shape : [];
|
||||
const data = decodeBase64Float32(o.data_base64);
|
||||
const expected = shape.length ? arrProduct(shape) : data.length;
|
||||
return {
|
||||
idx: i,
|
||||
node_idx: Number(o.node_idx ?? i),
|
||||
shape,
|
||||
data,
|
||||
expected
|
||||
};
|
||||
});
|
||||
outputs = parsed;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseAnchors(text) {
|
||||
const levels = String(text || "").split(";").map(s => s.trim()).filter(Boolean);
|
||||
return levels.map(level => level.split("|").map(pair => {
|
||||
const [w, h] = pair.split(",").map(v => Number(v.trim()));
|
||||
return [w, h];
|
||||
}));
|
||||
}
|
||||
|
||||
function pickYoloNodes(all) {
|
||||
const c = Number(numClasses.value) || 80;
|
||||
const picked = [];
|
||||
for (const o of all) {
|
||||
if (o.shape.length !== 4) continue;
|
||||
if (o.shape[0] !== 1) continue;
|
||||
const ch = o.shape[1];
|
||||
if (ch % (5 + c) !== 0) continue;
|
||||
picked.push(o);
|
||||
}
|
||||
picked.sort((a, b) => b.shape[2] - a.shape[2]);
|
||||
return picked;
|
||||
}
|
||||
function decodeYoloCommon(all, mode) {
|
||||
const c = Number(numClasses.value) || 80;
|
||||
const confTh = Number(scoreTh.value) || 0.25;
|
||||
const inpW = Number(inW.value) || 224;
|
||||
const inpH = Number(inH.value) || 224;
|
||||
const useSig = objClsSigmoid.checked;
|
||||
const useXySig = yoloXySigmoid.checked;
|
||||
const scoreModeValue = scoreMode.value;
|
||||
const nodes = pickYoloNodes(all);
|
||||
if (nodes.length === 0) throw new Error("??? YOLO ?? node????? [1,C,H,W] ? C ?? 5+classes ???");
|
||||
|
||||
const anchorLv = parseAnchors(anchorsInput.value);
|
||||
const boxes = [];
|
||||
const perLevel = [];
|
||||
|
||||
for (let lv = 0; lv < nodes.length; lv++) {
|
||||
const o = nodes[lv];
|
||||
const [, ch, gh, gw] = o.shape;
|
||||
const attrs = 5 + c;
|
||||
const na = Math.floor(ch / attrs);
|
||||
const anchors = anchorLv[lv] || anchorLv[anchorLv.length - 1] || [];
|
||||
|
||||
let maxObj = -Infinity;
|
||||
let maxCls = -Infinity;
|
||||
let maxScore = -Infinity;
|
||||
|
||||
for (let a = 0; a < na; a++) {
|
||||
const anchor = anchors[a] || [10, 10];
|
||||
const aw = anchor[0];
|
||||
const ah = anchor[1];
|
||||
const baseA = a * attrs;
|
||||
|
||||
for (let y = 0; y < gh; y++) {
|
||||
for (let x = 0; x < gw; x++) {
|
||||
const idx = (ci) => {
|
||||
const cidx = baseA + ci;
|
||||
return cidx * gh * gw + y * gw + x;
|
||||
};
|
||||
|
||||
const tx = o.data[idx(0)];
|
||||
const ty = o.data[idx(1)];
|
||||
const tw = o.data[idx(2)];
|
||||
const th = o.data[idx(3)];
|
||||
const to = o.data[idx(4)];
|
||||
|
||||
const obj = maybeSigmoid(to, useSig);
|
||||
if (obj > maxObj) maxObj = obj;
|
||||
|
||||
let bestCls = -1;
|
||||
let bestProb = -Infinity;
|
||||
for (let k = 0; k < c; k++) {
|
||||
const p = maybeSigmoid(o.data[idx(5 + k)], useSig);
|
||||
if (p > bestProb) {
|
||||
bestProb = p;
|
||||
bestCls = k;
|
||||
}
|
||||
}
|
||||
if (bestProb > maxCls) maxCls = bestProb;
|
||||
|
||||
let score;
|
||||
if (scoreModeValue === "obj") score = obj;
|
||||
else if (scoreModeValue === "cls") score = bestProb;
|
||||
else score = obj * bestProb;
|
||||
|
||||
if (score > maxScore) maxScore = score;
|
||||
if (score < confTh) continue;
|
||||
|
||||
let bx, by, bw, bh;
|
||||
if (mode === "yolov5") {
|
||||
const sx = inpW / gw;
|
||||
const sy = inpH / gh;
|
||||
const txv = useXySig ? sigmoid(tx) : tx;
|
||||
const tyv = useXySig ? sigmoid(ty) : ty;
|
||||
bx = (txv * 2 - 0.5 + x) * sx;
|
||||
by = (tyv * 2 - 0.5 + y) * sy;
|
||||
bw = Math.pow(sigmoid(tw) * 2, 2) * aw;
|
||||
bh = Math.pow(sigmoid(th) * 2, 2) * ah;
|
||||
} else {
|
||||
const txv = useXySig ? sigmoid(tx) : tx;
|
||||
const tyv = useXySig ? sigmoid(ty) : ty;
|
||||
bx = (txv + x) / gw * inpW;
|
||||
by = (tyv + y) / gh * inpH;
|
||||
bw = aw * Math.exp(tw);
|
||||
bh = ah * Math.exp(th);
|
||||
}
|
||||
|
||||
boxes.push({
|
||||
cls: bestCls,
|
||||
score,
|
||||
x1: bx - bw / 2,
|
||||
y1: by - bh / 2,
|
||||
x2: bx + bw / 2,
|
||||
y2: by + bh / 2
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
perLevel.push({
|
||||
lv,
|
||||
shape: `[${o.shape.join(",")}]`,
|
||||
maxObj,
|
||||
maxCls,
|
||||
maxScore
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
boxes,
|
||||
debug: {
|
||||
type: mode,
|
||||
scoreMode: scoreModeValue,
|
||||
useSigmoid: useSig,
|
||||
useXySigmoid: useXySig,
|
||||
levels: perLevel
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function parseIntList(text) {
|
||||
return String(text || "")
|
||||
.split(",")
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
.map(v => Number(v));
|
||||
}
|
||||
|
||||
function argmax(arr, start, len) {
|
||||
let best = -1;
|
||||
let bestVal = -Infinity;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const v = arr[start + i];
|
||||
if (v > bestVal) {
|
||||
bestVal = v;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return [best, bestVal];
|
||||
}
|
||||
function decodeFCOS(all) {
|
||||
const c = Number(numClasses.value) || 80;
|
||||
const confTh = Number(scoreTh.value) || 0.25;
|
||||
const inpW = Number(inW.value) || 512;
|
||||
const inpH = Number(inH.value) || 512;
|
||||
const useSig = objClsSigmoid.checked;
|
||||
const scoreModeValue = scoreMode.value;
|
||||
|
||||
const clsIdx = parseIntList(fcosClsIdx.value);
|
||||
const regIdx = parseIntList(fcosRegIdx.value);
|
||||
const ctrIdx = parseIntList(fcosCtrIdx.value);
|
||||
const strides = parseIntList(fcosStrides.value);
|
||||
|
||||
if (clsIdx.length === 0 || regIdx.length === 0 || ctrIdx.length === 0) {
|
||||
throw new Error("FCOS ?????? class/reg/centerness node idx??????");
|
||||
}
|
||||
if (!(clsIdx.length === regIdx.length && regIdx.length === ctrIdx.length)) {
|
||||
throw new Error("FCOS node idx ???????? level ?? 1 ? class/reg/ctr?");
|
||||
}
|
||||
|
||||
const boxes = [];
|
||||
const perLevel = [];
|
||||
|
||||
for (let lv = 0; lv < clsIdx.length; lv++) {
|
||||
const clsNode = all.find(o => o.node_idx === clsIdx[lv]);
|
||||
const regNode = all.find(o => o.node_idx === regIdx[lv]);
|
||||
const ctrNode = all.find(o => o.node_idx === ctrIdx[lv]);
|
||||
const stride = strides[lv] || strides[strides.length - 1] || 8;
|
||||
|
||||
if (!clsNode || !regNode || !ctrNode) {
|
||||
throw new Error(`FCOS level ${lv} node_idx ??????`);
|
||||
}
|
||||
|
||||
if (clsNode.shape.length !== 4 || regNode.shape.length !== 4 || ctrNode.shape.length !== 4) {
|
||||
throw new Error(`FCOS level ${lv} shape ??? [1,C,H,W]`);
|
||||
}
|
||||
|
||||
const gh = clsNode.shape[2];
|
||||
const gw = clsNode.shape[3];
|
||||
const clsC = clsNode.shape[1];
|
||||
const regC = regNode.shape[1];
|
||||
const ctrC = ctrNode.shape[1];
|
||||
|
||||
if (regC < 4 || ctrC < 1) {
|
||||
throw new Error(`FCOS level ${lv} reg/ctr ?????`);
|
||||
}
|
||||
|
||||
let maxCtr = -Infinity;
|
||||
let maxCls = -Infinity;
|
||||
let maxScore = -Infinity;
|
||||
|
||||
for (let y = 0; y < gh; y++) {
|
||||
for (let x = 0; x < gw; x++) {
|
||||
const at = (node, ch) => node.data[ch * gh * gw + y * gw + x];
|
||||
|
||||
const ctrRaw = at(ctrNode, 0);
|
||||
const center = maybeSigmoid(ctrRaw, useSig);
|
||||
if (center > maxCtr) maxCtr = center;
|
||||
|
||||
let bestCls = -1;
|
||||
let bestProb = -Infinity;
|
||||
for (let k = 0; k < Math.min(c, clsC); k++) {
|
||||
const p = maybeSigmoid(at(clsNode, k), useSig);
|
||||
if (p > bestProb) {
|
||||
bestProb = p;
|
||||
bestCls = k;
|
||||
}
|
||||
}
|
||||
if (bestProb > maxCls) maxCls = bestProb;
|
||||
|
||||
let score;
|
||||
if (scoreModeValue === "obj") score = center;
|
||||
else if (scoreModeValue === "cls") score = bestProb;
|
||||
else score = Math.sqrt(Math.max(0, bestProb * center));
|
||||
|
||||
if (score > maxScore) maxScore = score;
|
||||
if (score < confTh) continue;
|
||||
|
||||
const l = Math.max(0, at(regNode, 0));
|
||||
const t = Math.max(0, at(regNode, 1));
|
||||
const r = Math.max(0, at(regNode, 2));
|
||||
const b = Math.max(0, at(regNode, 3));
|
||||
|
||||
const cx = (x + 0.5) * stride;
|
||||
const cy = (y + 0.5) * stride;
|
||||
|
||||
const x1 = cx - l;
|
||||
const y1 = cy - t;
|
||||
const x2 = cx + r;
|
||||
const y2 = cy + b;
|
||||
|
||||
if (x2 <= x1 || y2 <= y1) continue;
|
||||
boxes.push({ cls: bestCls, score, x1, y1, x2, y2 });
|
||||
}
|
||||
}
|
||||
|
||||
perLevel.push({
|
||||
lv,
|
||||
clsShape: `[${clsNode.shape.join(",")}]`,
|
||||
regShape: `[${regNode.shape.join(",")}]`,
|
||||
ctrShape: `[${ctrNode.shape.join(",")}]`,
|
||||
stride,
|
||||
maxCtr,
|
||||
maxCls,
|
||||
maxScore
|
||||
});
|
||||
}
|
||||
|
||||
const clipped = boxes.map(b => ({
|
||||
cls: b.cls,
|
||||
score: b.score,
|
||||
x1: Math.max(0, Math.min(inpW, b.x1)),
|
||||
y1: Math.max(0, Math.min(inpH, b.y1)),
|
||||
x2: Math.max(0, Math.min(inpW, b.x2)),
|
||||
y2: Math.max(0, Math.min(inpH, b.y2))
|
||||
}));
|
||||
|
||||
return {
|
||||
boxes: clipped,
|
||||
debug: {
|
||||
type: "fcos",
|
||||
scoreMode: scoreModeValue,
|
||||
useSigmoid: useSig,
|
||||
levels: perLevel
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function iou(a, b) {
|
||||
const xx1 = Math.max(a.x1, b.x1);
|
||||
const yy1 = Math.max(a.y1, b.y1);
|
||||
const xx2 = Math.min(a.x2, b.x2);
|
||||
const yy2 = Math.min(a.y2, b.y2);
|
||||
const w = Math.max(0, xx2 - xx1);
|
||||
const h = Math.max(0, yy2 - yy1);
|
||||
const inter = w * h;
|
||||
if (inter <= 0) return 0;
|
||||
const areaA = Math.max(0, a.x2 - a.x1) * Math.max(0, a.y2 - a.y1);
|
||||
const areaB = Math.max(0, b.x2 - b.x1) * Math.max(0, b.y2 - b.y1);
|
||||
return inter / Math.max(1e-9, areaA + areaB - inter);
|
||||
}
|
||||
|
||||
function nms(boxes, iouTh, maxOut) {
|
||||
const byCls = new Map();
|
||||
for (const b of boxes) {
|
||||
if (!byCls.has(b.cls)) byCls.set(b.cls, []);
|
||||
byCls.get(b.cls).push(b);
|
||||
}
|
||||
|
||||
const kept = [];
|
||||
for (const [, arr] of byCls) {
|
||||
arr.sort((a, b) => b.score - a.score);
|
||||
const picked = [];
|
||||
while (arr.length > 0) {
|
||||
const cur = arr.shift();
|
||||
picked.push(cur);
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (iou(cur, arr[i]) > iouTh) arr.splice(i, 1);
|
||||
}
|
||||
}
|
||||
kept.push(...picked);
|
||||
}
|
||||
|
||||
kept.sort((a, b) => b.score - a.score);
|
||||
return kept.slice(0, maxOut);
|
||||
}
|
||||
|
||||
function drawDetections(boxes) {
|
||||
const ctx = overlayCanvas.getContext("2d");
|
||||
const iw = Number(inW.value) || 224;
|
||||
const ih = Number(inH.value) || 224;
|
||||
|
||||
const drawW = srcImg ? (srcImg.naturalWidth || srcImg.width) : iw;
|
||||
const drawH = srcImg ? (srcImg.naturalHeight || srcImg.height) : ih;
|
||||
|
||||
overlayCanvas.width = drawW;
|
||||
overlayCanvas.height = drawH;
|
||||
ctx.clearRect(0, 0, drawW, drawH);
|
||||
|
||||
if (srcImg) {
|
||||
ctx.drawImage(srcImg, 0, 0, drawW, drawH);
|
||||
} else {
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, drawW, drawH);
|
||||
}
|
||||
|
||||
const sx = drawW / iw;
|
||||
const sy = drawH / ih;
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.font = "12px Arial";
|
||||
|
||||
for (const b of boxes) {
|
||||
const x1 = b.x1 * sx;
|
||||
const y1 = b.y1 * sy;
|
||||
const x2 = b.x2 * sx;
|
||||
const y2 = b.y2 * sy;
|
||||
const w = Math.max(1, x2 - x1);
|
||||
const h = Math.max(1, y2 - y1);
|
||||
|
||||
const hue = (b.cls * 47) % 360;
|
||||
const color = `hsl(${hue} 90% 45%)`;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.fillStyle = color;
|
||||
ctx.strokeRect(x1, y1, w, h);
|
||||
|
||||
const tag = `${b.cls}:${b.score.toFixed(3)}`;
|
||||
const tw = ctx.measureText(tag).width + 6;
|
||||
const ty = Math.max(12, y1 - 3);
|
||||
ctx.fillRect(x1, ty - 12, tw, 12);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillText(tag, x1 + 3, ty - 2);
|
||||
}
|
||||
}
|
||||
|
||||
function fillTable(boxes) {
|
||||
boxTableBody.innerHTML = "";
|
||||
const top = boxes.slice(0, 200);
|
||||
for (let i = 0; i < top.length; i++) {
|
||||
const b = top[i];
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `<td>${i + 1}</td><td>${b.cls}</td><td>${b.score.toFixed(4)}</td><td>${b.x1.toFixed(1)}</td><td>${b.y1.toFixed(1)}</td><td>${b.x2.toFixed(1)}</td><td>${b.y2.toFixed(1)}</td>`;
|
||||
boxTableBody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
function runDecode() {
|
||||
clearError();
|
||||
if (outputs.length === 0) {
|
||||
showError("?? Parse Payload");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const type = modelType.value;
|
||||
let result;
|
||||
if (type === "yolov5") {
|
||||
result = decodeYoloCommon(outputs, "yolov5");
|
||||
} else if (type === "tinyyolo") {
|
||||
result = decodeYoloCommon(outputs, "tinyyolo");
|
||||
} else {
|
||||
result = decodeFCOS(outputs);
|
||||
}
|
||||
|
||||
const raw = result.boxes;
|
||||
const iouTh = Number(nmsTh.value) || 0.45;
|
||||
const maxOut = Number(maxBoxes.value) || 200;
|
||||
const finalBoxes = nms(raw, iouTh, maxOut);
|
||||
|
||||
drawDetections(finalBoxes);
|
||||
fillTable(finalBoxes);
|
||||
|
||||
overlayStats.textContent = `draw size: ${overlayCanvas.width}x${overlayCanvas.height}`;
|
||||
decodeStats.textContent = `decoded raw: ${raw.length}
|
||||
final after NMS: ${finalBoxes.length}`;
|
||||
|
||||
const lvLines = (result.debug?.levels || []).map((lv) => {
|
||||
if (result.debug.type === "fcos") {
|
||||
return `L${lv.lv} stride=${lv.stride} cls=${lv.clsShape} reg=${lv.regShape} ctr=${lv.ctrShape} maxCtr=${fmt(lv.maxCtr)} maxCls=${fmt(lv.maxCls)} maxScore=${fmt(lv.maxScore)}`;
|
||||
}
|
||||
return `L${lv.lv} shape=${lv.shape} maxObj=${fmt(lv.maxObj)} maxCls=${fmt(lv.maxCls)} maxScore=${fmt(lv.maxScore)}`;
|
||||
});
|
||||
|
||||
debugStats.textContent = [
|
||||
`decoder: ${result.debug?.type || type}`,
|
||||
`scoreMode: ${result.debug?.scoreMode || scoreMode.value}`,
|
||||
`obj/cls sigmoid: ${result.debug?.useSigmoid ?? objClsSigmoid.checked}`,
|
||||
`yolo x/y sigmoid: ${result.debug?.useXySigmoid ?? yoloXySigmoid.checked}`,
|
||||
...lvLines
|
||||
].join("\n");
|
||||
} catch (e) {
|
||||
showError("Decode failed: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function applyTinyPreset() {
|
||||
modelType.value = "tinyyolo";
|
||||
numClasses.value = 80;
|
||||
scoreTh.value = 0.25;
|
||||
nmsTh.value = 0.45;
|
||||
maxBoxes.value = 200;
|
||||
inW.value = 224;
|
||||
inH.value = 224;
|
||||
anchorsInput.value = "10,14|23,27|37,58;81,82|135,169|344,319";
|
||||
scoreMode.value = "obj_cls";
|
||||
objClsSigmoid.checked = true;
|
||||
yoloXySigmoid.checked = true;
|
||||
}
|
||||
|
||||
function applyYolo5Preset() {
|
||||
modelType.value = "yolov5";
|
||||
numClasses.value = 80;
|
||||
scoreTh.value = 0.25;
|
||||
nmsTh.value = 0.45;
|
||||
maxBoxes.value = 200;
|
||||
inW.value = 640;
|
||||
inH.value = 640;
|
||||
anchorsInput.value = "10,14|23,27|37,58;81,82|135,169|344,319";
|
||||
scoreMode.value = "obj_cls";
|
||||
objClsSigmoid.checked = true;
|
||||
yoloXySigmoid.checked = true;
|
||||
}
|
||||
|
||||
function applyFcosPreset() {
|
||||
modelType.value = "fcos";
|
||||
numClasses.value = 80;
|
||||
scoreTh.value = 0.25;
|
||||
nmsTh.value = 0.45;
|
||||
maxBoxes.value = 200;
|
||||
inW.value = 512;
|
||||
inH.value = 512;
|
||||
fcosStrides.value = "8,16,32,64,128";
|
||||
scoreMode.value = "obj_cls";
|
||||
objClsSigmoid.checked = true;
|
||||
if (outputs.length > 0) {
|
||||
autoFillFcosIndices();
|
||||
}
|
||||
}
|
||||
|
||||
function autoFillFcosIndices() {
|
||||
clearError();
|
||||
if (!Array.isArray(outputs) || outputs.length === 0) {
|
||||
showError("請先 Parse Payload,才能自動填 FCOS idx");
|
||||
return;
|
||||
}
|
||||
|
||||
const c = Number(numClasses.value) || 80;
|
||||
const valid = outputs.filter(o => o.shape.length === 4 && o.shape[0] === 1);
|
||||
const clsNodes = valid.filter(o => o.shape[1] === c);
|
||||
const regNodes = valid.filter(o => o.shape[1] === 4);
|
||||
const ctrNodes = valid.filter(o => o.shape[1] === 1);
|
||||
|
||||
const byHW = (arr) => {
|
||||
const m = new Map();
|
||||
for (const o of arr) {
|
||||
const key = `${o.shape[2]}x${o.shape[3]}`;
|
||||
if (!m.has(key)) m.set(key, []);
|
||||
m.get(key).push(o);
|
||||
}
|
||||
return m;
|
||||
};
|
||||
|
||||
const clsMap = byHW(clsNodes);
|
||||
const regMap = byHW(regNodes);
|
||||
const ctrMap = byHW(ctrNodes);
|
||||
|
||||
const keys = [];
|
||||
for (const key of clsMap.keys()) {
|
||||
if (regMap.has(key) && ctrMap.has(key)) keys.push(key);
|
||||
}
|
||||
keys.sort((a, b) => {
|
||||
const ah = Number(a.split("x")[0]);
|
||||
const bh = Number(b.split("x")[0]);
|
||||
return bh - ah;
|
||||
});
|
||||
|
||||
const levels = keys.map((key) => {
|
||||
const pickMinNode = (arr) => arr.slice().sort((x, y) => x.node_idx - y.node_idx)[0];
|
||||
return {
|
||||
key,
|
||||
cls: pickMinNode(clsMap.get(key)),
|
||||
reg: pickMinNode(regMap.get(key)),
|
||||
ctr: pickMinNode(ctrMap.get(key))
|
||||
};
|
||||
});
|
||||
|
||||
if (levels.length === 0) {
|
||||
showError("找不到可配對的 FCOS cls/reg/ctr node(同 HxW)");
|
||||
return;
|
||||
}
|
||||
|
||||
fcosClsIdx.value = levels.map(l => l.cls.node_idx).join(",");
|
||||
fcosRegIdx.value = levels.map(l => l.reg.node_idx).join(",");
|
||||
fcosCtrIdx.value = levels.map(l => l.ctr.node_idx).join(",");
|
||||
|
||||
const defaultStrides = [8, 16, 32, 64, 128];
|
||||
fcosStrides.value = defaultStrides.slice(0, levels.length).join(",");
|
||||
|
||||
debugStats.textContent = [
|
||||
"auto filled FCOS idx:",
|
||||
`class: ${fcosClsIdx.value}`,
|
||||
`reg: ${fcosRegIdx.value}`,
|
||||
`ctr: ${fcosCtrIdx.value}`,
|
||||
`levels(HxW): ${levels.map(l => l.key).join(",")}`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
parseBtn.addEventListener("click", () => {
|
||||
clearError();
|
||||
try {
|
||||
const arr = parsePayloadText();
|
||||
decodeStats.textContent = `parsed outputs: ${arr.length}`;
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
clearBtn.addEventListener("click", () => {
|
||||
payloadInput.value = "";
|
||||
outputs = [];
|
||||
boxTableBody.innerHTML = "";
|
||||
decodeStats.textContent = "";
|
||||
debugStats.textContent = "";
|
||||
overlayStats.textContent = "";
|
||||
clearError();
|
||||
});
|
||||
|
||||
imgInput.addEventListener("change", (evt) => {
|
||||
const file = evt.target.files && evt.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
srcImg = img;
|
||||
if (outputs.length > 0) runDecode();
|
||||
};
|
||||
img.src = String(reader.result || "");
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
clearImgBtn.addEventListener("click", () => {
|
||||
srcImg = null;
|
||||
imgInput.value = "";
|
||||
if (outputs.length > 0) runDecode();
|
||||
});
|
||||
|
||||
runBtn.addEventListener("click", runDecode);
|
||||
presetTinyBtn.addEventListener("click", applyTinyPreset);
|
||||
presetYolo5Btn.addEventListener("click", applyYolo5Preset);
|
||||
presetFcosBtn.addEventListener("click", applyFcosPreset);
|
||||
autoFcosIdxBtn.addEventListener("click", autoFillFcosIndices);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
624
local_service_win/TestRes/Images/PayloadTensorView.html
Normal file
624
local_service_win/TestRes/Images/PayloadTensorView.html
Normal file
@ -0,0 +1,624 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Payload Tensor Viewer</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f6f8;
|
||||
--card: #ffffff;
|
||||
--text: #1f2937;
|
||||
--muted: #6b7280;
|
||||
--line: #d1d5db;
|
||||
--accent: #2563eb;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
padding: 0 12px 24px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.hint {
|
||||
margin: 0 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.panel {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.panel h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.panel .hint {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 220px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
font-family: Consolas, monospace;
|
||||
background: #fff;
|
||||
}
|
||||
.controls {
|
||||
margin: 10px 0 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
button {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.error {
|
||||
color: #b91c1c;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
.title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.meta {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.row label {
|
||||
font-size: 13px;
|
||||
}
|
||||
.row input[type="number"] {
|
||||
width: 90px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.row input[type="range"] {
|
||||
width: 160px;
|
||||
}
|
||||
.row select {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.subcard {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
.subcard-title {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.stats {
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Payload Tensor Viewer</h1>
|
||||
<p class="hint">
|
||||
Paste full JSON payload and click <b>Parse & Render</b>. Supports base64 float32 tensors in NCHW shape (e.g. [1,255,7,7]).
|
||||
</p>
|
||||
|
||||
<textarea id="payloadInput" placeholder="Paste full payload JSON here..."></textarea>
|
||||
<div class="controls">
|
||||
<button id="parseBtn" class="primary">Parse & Render</button>
|
||||
<button id="clearBtn">Clear</button>
|
||||
</div>
|
||||
|
||||
<div id="errorBox" class="error" style="display:none"></div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Overlay Viewer</h2>
|
||||
<p class="hint">
|
||||
Upload original image, pick output/channel, then overlay activation heatmap on top.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<label>Image:</label>
|
||||
<input id="imageInput" type="file" accept="image/*" />
|
||||
<button id="clearImageBtn">Clear Image</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>Output:</label>
|
||||
<select id="overlayOutputSelect"></select>
|
||||
<label>batch:</label>
|
||||
<input id="overlayBatchInput" type="number" min="0" value="0" />
|
||||
<label>channel:</label>
|
||||
<input id="overlayChannelInput" type="number" min="0" value="0" />
|
||||
<label>colormap:</label>
|
||||
<select id="overlayMapSelect">
|
||||
<option value="jet">jet</option>
|
||||
<option value="gray">gray</option>
|
||||
</select>
|
||||
<label>alpha:</label>
|
||||
<input id="overlayAlphaInput" type="range" min="0" max="1" step="0.01" value="0.45" />
|
||||
<span id="overlayAlphaText">0.45</span>
|
||||
<button id="overlayRenderBtn" class="primary">Render Overlay</button>
|
||||
</div>
|
||||
|
||||
<div class="preview-grid">
|
||||
<div class="subcard">
|
||||
<div class="subcard-title">Overlay</div>
|
||||
<canvas id="overlayCanvas" width="1" height="1"></canvas>
|
||||
<div id="overlayStats" class="stats"></div>
|
||||
</div>
|
||||
<div class="subcard">
|
||||
<div class="subcard-title">Heatmap Only</div>
|
||||
<canvas id="heatmapCanvas" width="1" height="1"></canvas>
|
||||
<div id="heatmapStats" class="stats"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="outputGrid" class="grid"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const payloadInput = document.getElementById("payloadInput");
|
||||
const parseBtn = document.getElementById("parseBtn");
|
||||
const clearBtn = document.getElementById("clearBtn");
|
||||
const errorBox = document.getElementById("errorBox");
|
||||
const outputGrid = document.getElementById("outputGrid");
|
||||
const imageInput = document.getElementById("imageInput");
|
||||
const clearImageBtn = document.getElementById("clearImageBtn");
|
||||
const overlayOutputSelect = document.getElementById("overlayOutputSelect");
|
||||
const overlayBatchInput = document.getElementById("overlayBatchInput");
|
||||
const overlayChannelInput = document.getElementById("overlayChannelInput");
|
||||
const overlayMapSelect = document.getElementById("overlayMapSelect");
|
||||
const overlayAlphaInput = document.getElementById("overlayAlphaInput");
|
||||
const overlayAlphaText = document.getElementById("overlayAlphaText");
|
||||
const overlayRenderBtn = document.getElementById("overlayRenderBtn");
|
||||
const overlayCanvas = document.getElementById("overlayCanvas");
|
||||
const heatmapCanvas = document.getElementById("heatmapCanvas");
|
||||
const overlayStats = document.getElementById("overlayStats");
|
||||
const heatmapStats = document.getElementById("heatmapStats");
|
||||
|
||||
let parsedOutputs = [];
|
||||
let sourceImage = null;
|
||||
|
||||
function showError(message) {
|
||||
errorBox.style.display = "block";
|
||||
errorBox.textContent = message;
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
errorBox.style.display = "none";
|
||||
errorBox.textContent = "";
|
||||
}
|
||||
|
||||
function decodeBase64Float32(base64String) {
|
||||
const clean = String(base64String || "").trim();
|
||||
const binary = atob(clean);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
const len = Math.floor(bytes.byteLength / 4);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const out = new Float32Array(len);
|
||||
for (let i = 0; i < len; i++) out[i] = view.getFloat32(i * 4, true);
|
||||
return out;
|
||||
}
|
||||
|
||||
function product(arr) {
|
||||
return arr.reduce((a, b) => a * b, 1);
|
||||
}
|
||||
|
||||
function minArray(values) {
|
||||
let m = Number.POSITIVE_INFINITY;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i] < m) m = values[i];
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
function maxArray(values) {
|
||||
let m = Number.NEGATIVE_INFINITY;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i] > m) m = values[i];
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
function slice2D(data, shape, batchIndex, channelIndex) {
|
||||
if (!Array.isArray(shape) || shape.length < 2) {
|
||||
throw new Error("Unsupported shape");
|
||||
}
|
||||
|
||||
if (shape.length === 4) {
|
||||
const n = shape[0], c = shape[1], h = shape[2], w = shape[3];
|
||||
if (batchIndex < 0 || batchIndex >= n) throw new Error("Batch out of range");
|
||||
if (channelIndex < 0 || channelIndex >= c) throw new Error("Channel out of range");
|
||||
const out = new Float32Array(h * w);
|
||||
let k = 0;
|
||||
const base = ((batchIndex * c) + channelIndex) * h * w;
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
out[k++] = data[base + y * w + x];
|
||||
}
|
||||
}
|
||||
return { w, h, values: out };
|
||||
}
|
||||
|
||||
if (shape.length === 3) {
|
||||
const c = shape[0], h = shape[1], w = shape[2];
|
||||
if (channelIndex < 0 || channelIndex >= c) throw new Error("Channel out of range");
|
||||
const out = new Float32Array(h * w);
|
||||
let k = 0;
|
||||
const base = channelIndex * h * w;
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
out[k++] = data[base + y * w + x];
|
||||
}
|
||||
}
|
||||
return { w, h, values: out };
|
||||
}
|
||||
|
||||
if (shape.length === 2) {
|
||||
const h = shape[0], w = shape[1];
|
||||
return { w, h, values: data.slice(0, h * w) };
|
||||
}
|
||||
|
||||
throw new Error("Unsupported shape length: " + shape.length);
|
||||
}
|
||||
|
||||
function drawGrayscale(canvas, values, w, h) {
|
||||
const min = minArray(values);
|
||||
const max = maxArray(values);
|
||||
const span = max - min || 1;
|
||||
|
||||
const temp = document.createElement("canvas");
|
||||
temp.width = w;
|
||||
temp.height = h;
|
||||
const tctx = temp.getContext("2d");
|
||||
const img = tctx.createImageData(w, h);
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const v = Math.round(((values[i] - min) / span) * 255);
|
||||
const p = i * 4;
|
||||
img.data[p] = v;
|
||||
img.data[p + 1] = v;
|
||||
img.data[p + 2] = v;
|
||||
img.data[p + 3] = 255;
|
||||
}
|
||||
tctx.putImageData(img, 0, 0);
|
||||
|
||||
const targetScale = Math.max(1, Math.floor(300 / Math.max(w, h)));
|
||||
canvas.width = w * targetScale;
|
||||
canvas.height = h * targetScale;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(temp, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
function jetColor(t) {
|
||||
const x = Math.max(0, Math.min(1, t));
|
||||
const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * x - 3)));
|
||||
const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * x - 2)));
|
||||
const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * x - 1)));
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
function drawHeatmapToCanvas(canvas, values, w, h, mapMode) {
|
||||
const min = minArray(values);
|
||||
const max = maxArray(values);
|
||||
const span = max - min || 1;
|
||||
|
||||
const temp = document.createElement("canvas");
|
||||
temp.width = w;
|
||||
temp.height = h;
|
||||
const tctx = temp.getContext("2d");
|
||||
const img = tctx.createImageData(w, h);
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const t = (values[i] - min) / span;
|
||||
const p = i * 4;
|
||||
let color;
|
||||
if (mapMode === "gray") {
|
||||
const g = Math.round(t * 255);
|
||||
color = [g, g, g];
|
||||
} else {
|
||||
color = jetColor(t);
|
||||
}
|
||||
img.data[p] = color[0];
|
||||
img.data[p + 1] = color[1];
|
||||
img.data[p + 2] = color[2];
|
||||
img.data[p + 3] = 255;
|
||||
}
|
||||
tctx.putImageData(img, 0, 0);
|
||||
|
||||
const targetScale = Math.max(1, Math.floor(300 / Math.max(w, h)));
|
||||
canvas.width = w * targetScale;
|
||||
canvas.height = h * targetScale;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(temp, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
return { min, max, temp };
|
||||
}
|
||||
|
||||
function renderOverlay() {
|
||||
if (parsedOutputs.length === 0) {
|
||||
overlayStats.textContent = "No payload parsed yet.";
|
||||
heatmapStats.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const outIndex = Number(overlayOutputSelect.value) || 0;
|
||||
const out = parsedOutputs[outIndex];
|
||||
if (!out) {
|
||||
overlayStats.textContent = "Output not found.";
|
||||
heatmapStats.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const b = Number(overlayBatchInput.value) || 0;
|
||||
const ch = Number(overlayChannelInput.value) || 0;
|
||||
const mapMode = overlayMapSelect.value || "jet";
|
||||
const alpha = Number(overlayAlphaInput.value);
|
||||
const slice = slice2D(out.data, out.shape, b, ch);
|
||||
const h = drawHeatmapToCanvas(heatmapCanvas, Array.from(slice.values), slice.w, slice.h, mapMode);
|
||||
heatmapStats.textContent = `Heatmap ${slice.w}x${slice.h}, min=${h.min.toFixed(6)}, max=${h.max.toFixed(6)}`;
|
||||
|
||||
const ctx = overlayCanvas.getContext("2d");
|
||||
if (sourceImage) {
|
||||
overlayCanvas.width = sourceImage.naturalWidth || sourceImage.width;
|
||||
overlayCanvas.height = sourceImage.naturalHeight || sourceImage.height;
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.drawImage(sourceImage, 0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = isNaN(alpha) ? 0.45 : alpha;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(h.temp, 0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
ctx.restore();
|
||||
overlayStats.textContent = `Overlay on image ${overlayCanvas.width}x${overlayCanvas.height}, alpha=${(isNaN(alpha) ? 0.45 : alpha).toFixed(2)}, output#${outIndex}, batch=${b}, channel=${ch}`;
|
||||
} else {
|
||||
overlayCanvas.width = heatmapCanvas.width;
|
||||
overlayCanvas.height = heatmapCanvas.height;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
ctx.drawImage(heatmapCanvas, 0, 0);
|
||||
overlayStats.textContent = `No source image loaded, showing heatmap only. output#${outIndex}, batch=${b}, channel=${ch}`;
|
||||
}
|
||||
} catch (e) {
|
||||
overlayStats.textContent = "Overlay render failed: " + e.message;
|
||||
heatmapStats.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
function refreshOverlayOutputOptions() {
|
||||
overlayOutputSelect.innerHTML = "";
|
||||
parsedOutputs.forEach((o, idx) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(idx);
|
||||
opt.textContent = `#${idx} node_idx=${o.item.node_idx} shape=${JSON.stringify(o.shape)}`;
|
||||
overlayOutputSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function buildOutputCard(item, idx) {
|
||||
const shape = Array.isArray(item.shape) ? item.shape : [];
|
||||
const data = decodeBase64Float32(item.data_base64);
|
||||
const expected = shape.length ? product(shape) : data.length;
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.className = "title";
|
||||
title.textContent = `Output #${idx} node_idx=${item.node_idx}`;
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
meta.textContent = `dtype=${item.dtype || "unknown"}, shape=${JSON.stringify(shape)}, decoded=${data.length}, expected=${expected}`;
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "row";
|
||||
|
||||
const batchLabel = document.createElement("label");
|
||||
batchLabel.textContent = "batch:";
|
||||
const batchInput = document.createElement("input");
|
||||
batchInput.type = "number";
|
||||
batchInput.value = 0;
|
||||
batchInput.min = 0;
|
||||
|
||||
const channelLabel = document.createElement("label");
|
||||
channelLabel.textContent = "channel:";
|
||||
const channelInput = document.createElement("input");
|
||||
channelInput.type = "number";
|
||||
channelInput.value = 0;
|
||||
channelInput.min = 0;
|
||||
|
||||
const renderBtn = document.createElement("button");
|
||||
renderBtn.textContent = "Render Channel";
|
||||
|
||||
row.append(batchLabel, batchInput, channelLabel, channelInput, renderBtn);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
|
||||
const stats = document.createElement("div");
|
||||
stats.className = "stats";
|
||||
|
||||
card.append(title, meta, row, canvas, stats);
|
||||
|
||||
const doRender = () => {
|
||||
try {
|
||||
const b = Number(batchInput.value) || 0;
|
||||
const ch = Number(channelInput.value) || 0;
|
||||
const slice = slice2D(data, shape, b, ch);
|
||||
const st = drawGrayscale(canvas, Array.from(slice.values), slice.w, slice.h);
|
||||
stats.textContent = `Rendered ${slice.w}x${slice.h}, min=${st.min.toFixed(6)}, max=${st.max.toFixed(6)}`;
|
||||
} catch (e) {
|
||||
stats.textContent = "Render failed: " + e.message;
|
||||
}
|
||||
};
|
||||
|
||||
renderBtn.addEventListener("click", doRender);
|
||||
doRender();
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function parsePayload() {
|
||||
clearError();
|
||||
outputGrid.innerHTML = "";
|
||||
parsedOutputs = [];
|
||||
overlayOutputSelect.innerHTML = "";
|
||||
|
||||
const text = payloadInput.value.trim();
|
||||
if (!text) {
|
||||
showError("Please paste payload JSON first.");
|
||||
return;
|
||||
}
|
||||
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch (e) {
|
||||
showError("JSON parse failed: " + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const outputs = json?.data?.outputs || json?.outputs;
|
||||
if (!Array.isArray(outputs) || outputs.length === 0) {
|
||||
showError("No outputs found. Expected payload.data.outputs array.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
outputs.forEach((o, i) => {
|
||||
parsedOutputs.push({
|
||||
item: o,
|
||||
data: decodeBase64Float32(o.data_base64),
|
||||
shape: Array.isArray(o.shape) ? o.shape : []
|
||||
});
|
||||
outputGrid.appendChild(buildOutputCard(o, i));
|
||||
});
|
||||
refreshOverlayOutputOptions();
|
||||
if (parsedOutputs.length > 0) {
|
||||
overlayOutputSelect.value = "0";
|
||||
renderOverlay();
|
||||
}
|
||||
} catch (e) {
|
||||
showError("Render pipeline failed: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
parseBtn.addEventListener("click", parsePayload);
|
||||
clearBtn.addEventListener("click", () => {
|
||||
payloadInput.value = "";
|
||||
outputGrid.innerHTML = "";
|
||||
parsedOutputs = [];
|
||||
overlayOutputSelect.innerHTML = "";
|
||||
clearError();
|
||||
overlayStats.textContent = "";
|
||||
heatmapStats.textContent = "";
|
||||
});
|
||||
|
||||
imageInput.addEventListener("change", (evt) => {
|
||||
const file = evt.target.files && evt.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
sourceImage = img;
|
||||
renderOverlay();
|
||||
};
|
||||
img.src = String(reader.result || "");
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
clearImageBtn.addEventListener("click", () => {
|
||||
sourceImage = null;
|
||||
imageInput.value = "";
|
||||
renderOverlay();
|
||||
});
|
||||
|
||||
overlayRenderBtn.addEventListener("click", renderOverlay);
|
||||
overlayOutputSelect.addEventListener("change", renderOverlay);
|
||||
overlayBatchInput.addEventListener("change", renderOverlay);
|
||||
overlayChannelInput.addEventListener("change", renderOverlay);
|
||||
overlayMapSelect.addEventListener("change", renderOverlay);
|
||||
overlayAlphaInput.addEventListener("input", () => {
|
||||
overlayAlphaText.textContent = overlayAlphaInput.value;
|
||||
renderOverlay();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
local_service_win/TestRes/Images/bike_cars_street_224x224.bmp
Normal file
BIN
local_service_win/TestRes/Images/bike_cars_street_224x224.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
BIN
local_service_win/TestRes/Images/one_bike_many_cars_800x800.bmp
Normal file
BIN
local_service_win/TestRes/Images/one_bike_many_cars_800x800.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@ -4,26 +4,26 @@
|
||||
- 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_image_inference_post_yolov5/kl520_kn-model-zoo_generic_image_inference_post_yolov5.c`
|
||||
- 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_image_inference_post_fcos/kl520_kn-model-zoo_generic_image_inference_post_fcos.c`
|
||||
- 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_generic_image_inference_post_yolo/kl520_demo_generic_image_inference_post_yolo.c`
|
||||
- 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_generic_image_inference_multithread/kl520_demo_generic_image_inference_multithread.c`
|
||||
- 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_image_inference_post_yolov5/kl720_kn-model-zoo_generic_image_inference_post_yolov5.c`
|
||||
- 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_image_inference_post_fcos/kl720_kn-model-zoo_generic_image_inference_post_fcos.c`
|
||||
- Source: `examples_model_zoo/kl720_kn-model-zoo_generic_inference_post_fcos/kl720_kn-model-zoo_generic_inference_post_fcos.c`
|
||||
|
||||
1
local_service_win/firmware/KL520/VERSION
Normal file
1
local_service_win/firmware/KL520/VERSION
Normal file
@ -0,0 +1 @@
|
||||
2.2.0
|
||||
BIN
local_service_win/firmware/KL520/dfw/minions.bin
Normal file
BIN
local_service_win/firmware/KL520/dfw/minions.bin
Normal file
Binary file not shown.
BIN
local_service_win/firmware/KL520/fw_loader.bin
Normal file
BIN
local_service_win/firmware/KL520/fw_loader.bin
Normal file
Binary file not shown.
BIN
local_service_win/firmware/KL520/fw_ncpu.bin
Normal file
BIN
local_service_win/firmware/KL520/fw_ncpu.bin
Normal file
Binary file not shown.
BIN
local_service_win/firmware/KL520/fw_scpu.bin
Normal file
BIN
local_service_win/firmware/KL520/fw_scpu.bin
Normal file
Binary file not shown.
BIN
local_service_win/firmware/KL520_kdp/fw_ncpu.bin
Normal file
BIN
local_service_win/firmware/KL520_kdp/fw_ncpu.bin
Normal file
Binary file not shown.
BIN
local_service_win/firmware/KL520_kdp/fw_scpu.bin
Normal file
BIN
local_service_win/firmware/KL520_kdp/fw_scpu.bin
Normal file
Binary file not shown.
1
local_service_win/firmware/KL630/VERSION
Normal file
1
local_service_win/firmware/KL630/VERSION
Normal file
@ -0,0 +1 @@
|
||||
SDK-v2.5.7
|
||||
BIN
local_service_win/firmware/KL630/kp_firmware.tar
Normal file
BIN
local_service_win/firmware/KL630/kp_firmware.tar
Normal file
Binary file not shown.
BIN
local_service_win/firmware/KL630/kp_loader.tar
Normal file
BIN
local_service_win/firmware/KL630/kp_loader.tar
Normal file
Binary file not shown.
1
local_service_win/firmware/KL720/VERSION
Normal file
1
local_service_win/firmware/KL720/VERSION
Normal file
@ -0,0 +1 @@
|
||||
2.2.0
|
||||
BIN
local_service_win/firmware/KL720/fw_ncpu.bin
Normal file
BIN
local_service_win/firmware/KL720/fw_ncpu.bin
Normal file
Binary file not shown.
BIN
local_service_win/firmware/KL720/fw_scpu.bin
Normal file
BIN
local_service_win/firmware/KL720/fw_scpu.bin
Normal file
Binary file not shown.
1
local_service_win/firmware/KL730/VERSION
Normal file
1
local_service_win/firmware/KL730/VERSION
Normal file
@ -0,0 +1 @@
|
||||
SDK-v1.3.0
|
||||
BIN
local_service_win/firmware/KL730/kp_firmware.tar
Normal file
BIN
local_service_win/firmware/KL730/kp_firmware.tar
Normal file
Binary file not shown.
BIN
local_service_win/firmware/KL730/kp_loader.tar
Normal file
BIN
local_service_win/firmware/KL730/kp_loader.tar
Normal file
Binary file not shown.
1
local_service_win/third_party/Kneron_DFUT/VERSION
vendored
Normal file
1
local_service_win/third_party/Kneron_DFUT/VERSION
vendored
Normal file
@ -0,0 +1 @@
|
||||
3.0.0
|
||||
BIN
local_service_win/third_party/Kneron_DFUT/bin/D3Dcompiler_47.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/D3Dcompiler_47.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/KneronDFUT.exe
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/KneronDFUT.exe
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/Qt5Core.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/Qt5Core.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/Qt5Gui.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/Qt5Gui.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/Qt5Svg.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/Qt5Svg.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/Qt5Widgets.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/Qt5Widgets.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/iconengines/qsvgicon.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/iconengines/qsvgicon.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qgif.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qgif.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qicns.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qicns.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qico.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qico.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qjpeg.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qjpeg.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qsvg.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qsvg.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtga.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtga.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtiff.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qtiff.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwbmp.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwbmp.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwebp.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/imageformats/qwebp.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/libEGL.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/libEGL.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/libGLESV2.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/libGLESV2.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/libgcc_s_seh-1.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/libgcc_s_seh-1.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/libkplus.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/libkplus.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/libstdc++-6.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/libstdc++-6.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/libusb-1.0.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/libusb-1.0.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/libwdi.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/libwdi.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/libwinpthread-1.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/libwinpthread-1.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/opengl32sw.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/opengl32sw.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/platforms/qwindows.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/platforms/qwindows.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/styles/qwindowsvistastyle.dll
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/styles/qwindowsvistastyle.dll
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ar.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ar.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_bg.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_bg.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ca.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ca.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_cs.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_cs.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_da.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_da.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_de.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_de.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_en.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_en.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_es.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_es.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fi.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fi.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fr.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_fr.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_gd.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_gd.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_he.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_he.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_hu.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_hu.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_it.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_it.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ja.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ja.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ko.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ko.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_lv.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_lv.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_pl.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_pl.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ru.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_ru.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_sk.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_sk.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_uk.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_uk.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_zh_TW.qm
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/bin/translations/qt_zh_TW.qm
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/flash_helper.bin
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/flash_helper.bin
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/fw_loader.bin
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/resource/firmware/KL520/fw_loader.bin
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/resource/firmware/KL630/flash_helper.tar
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/resource/firmware/KL630/flash_helper.tar
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/Kneron_DFUT/resource/firmware/KL730/flash_helper.tar
vendored
Normal file
BIN
local_service_win/third_party/Kneron_DFUT/resource/firmware/KL730/flash_helper.tar
vendored
Normal file
Binary file not shown.
BIN
local_service_win/third_party/kneron_plus_1_2_1/KneronPLUS-1.2.1.zip
vendored
Normal file
BIN
local_service_win/third_party/kneron_plus_1_2_1/KneronPLUS-1.2.1.zip
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user