Add local service (win)
This commit is contained in:
parent
1902cd0f84
commit
6f3800687e
91
README.md
Normal file
91
README.md
Normal file
@ -0,0 +1,91 @@
|
||||
# web_academy_prototype
|
||||
|
||||
## 專案目標
|
||||
此 repository 的 PoC 主軸是**線上教學平台核心流程**。
|
||||
核心流程定義請參考 `docs/PRD-Integrated.md`。
|
||||
|
||||
`local_service_win` 是整體 PoC 其中一個模組,負責本機硬體控制與推論流程驗證。
|
||||
|
||||
## PoC 範圍與路線圖
|
||||
- 主目標:線上教學平台核心流程 PoC。
|
||||
- Local Service PoC:
|
||||
- Windows:已在本 repo(`local_service_win/`)。
|
||||
- Linux:規劃中(KneronPLUS 已支援)。
|
||||
- macOS:規劃中(待 KneronPLUS 支援)。
|
||||
- 網頁流程 PoC:規劃中(後續加入相關專案或模組)。
|
||||
- `local_agent_win/`:會納入此專案範圍。
|
||||
|
||||
## 目前已存在模組
|
||||
- `local_service_win/`
|
||||
- Python + FastAPI localhost 服務。
|
||||
- 透過 KneronPLUS(`kp`)與 Kneron USB 裝置互動。
|
||||
- 涵蓋掃描、連線、模型載入、推論流程。
|
||||
- 預設位址:`http://127.0.0.1:4398`。
|
||||
|
||||
目前 Windows local service 資料流:
|
||||
`Client (Browser/App) -> LocalAPI (127.0.0.1:4398) -> KneronPLUS kp -> KL520/KL720`
|
||||
|
||||
## 版本相容性(目前觀察)
|
||||
- 你目前環境使用 Python `3.13` 看起來可運作。
|
||||
- KneronPLUS 既有生態資訊常見以 Python `3.9` 為主。
|
||||
- 後續建議補上正式相容矩陣(Python / KP 版本)。
|
||||
|
||||
## 專案結構(目前)
|
||||
```text
|
||||
web_academy_prototype/
|
||||
|- docs/
|
||||
| `- PRD-Integrated.md
|
||||
|- local_service_win/
|
||||
| |- .gitignore
|
||||
| |- KneronPLUS-3.1.2-py3-none-any.whl
|
||||
| |- requirements.txt
|
||||
| |- STRATEGY.md
|
||||
| |- LocalAPI/
|
||||
| | |- __init__.py
|
||||
| | `- main.py
|
||||
| `- TestRes/
|
||||
| `- API 測試素材(模型與圖片;圖片已內嵌為 Base64,可直接放入推論請求)
|
||||
| |- TEST_PAIRS.md
|
||||
| |- Images/
|
||||
| | |- Pic64View.html
|
||||
| | |- bike_cars_street_224x224.html
|
||||
| | `- one_bike_many_cars_800x800.html
|
||||
| `- Models/
|
||||
| |- models_520.nef
|
||||
| |- kl520_20004_fcos-drk53s_w512h512.nef
|
||||
| |- kl520_20005_yolov5-noupsample_w640h640.nef
|
||||
| |- kl720_20004_fcos-drk53s_w512h512.nef
|
||||
| `- kl720_20005_yolov5-noupsample_w640h640.nef
|
||||
`- README.md
|
||||
```
|
||||
|
||||
## Pic64View 工具說明
|
||||
- 檔案:`local_service_win/TestRes/Images/Pic64View.html`
|
||||
- 用途:本機快速預覽 Base64 圖片字串,方便測試 `/inference/run` 的 `image_base64` 內容是否正確。
|
||||
- 輸入格式:
|
||||
- 可直接貼 `data:image/...;base64,...`。
|
||||
- 也可只貼純 Base64,工具會自動補上 `data:image/png;base64,` 前綴再渲染。
|
||||
- 操作:
|
||||
- `Render`:顯示預覽圖。
|
||||
- `Clear`:清空輸入與預覽結果。
|
||||
|
||||
## 快速開始(local_service_win)
|
||||
1. 安裝相依套件:
|
||||
```powershell
|
||||
cd local_service_win
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. 安裝 KneronPLUS wheel:
|
||||
```powershell
|
||||
python -m pip install .\KneronPLUS-3.1.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
3. 啟動本機服務:
|
||||
```powershell
|
||||
python .\LocalAPI\main.py
|
||||
```
|
||||
|
||||
## 參考文件
|
||||
- 核心流程與產品規劃:`docs/PRD-Integrated.md`
|
||||
- Windows local service 策略:`local_service_win/STRATEGY.md`
|
||||
140
local_service_win/.gitignore
vendored
Normal file
140
local_service_win/.gitignore
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
BIN
local_service_win/KneronPLUS-3.1.2-py3-none-any.whl
Normal file
BIN
local_service_win/KneronPLUS-3.1.2-py3-none-any.whl
Normal file
Binary file not shown.
1
local_service_win/LocalAPI/__init__.py
Normal file
1
local_service_win/LocalAPI/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# LocalAPI package
|
||||
383
local_service_win/LocalAPI/main.py
Normal file
383
local_service_win/LocalAPI/main.py
Normal file
@ -0,0 +1,383 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import kp
|
||||
|
||||
|
||||
SERVICE_VERSION = "0.1.0"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceState:
|
||||
device_group: Optional[kp.DeviceGroup] = None
|
||||
port_id: Optional[int] = None
|
||||
model_desc: Optional[kp.ModelNefDescriptor] = None
|
||||
|
||||
|
||||
STATE = DeviceState()
|
||||
STATE_LOCK = threading.Lock()
|
||||
|
||||
|
||||
app = FastAPI(title="Kneron LocalAPI", version=SERVICE_VERSION)
|
||||
|
||||
|
||||
def _ok(data: Any) -> Dict[str, Any]:
|
||||
return {"ok": True, "data": data, "error": None}
|
||||
|
||||
|
||||
def _err(code: str, message: str) -> Dict[str, Any]:
|
||||
return {"ok": False, "data": None, "error": {"code": code, "message": message}}
|
||||
|
||||
|
||||
def _require_device() -> kp.DeviceGroup:
|
||||
if STATE.device_group is None:
|
||||
raise HTTPException(status_code=400, detail=_err("NO_DEVICE", "No connected device"))
|
||||
return STATE.device_group
|
||||
|
||||
|
||||
def _image_format_from_str(value: str) -> kp.ImageFormat:
|
||||
value = value.upper()
|
||||
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,
|
||||
}
|
||||
if value not in mapping:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=_err("INVALID_IMAGE_FORMAT", f"Unsupported image_format: {value}"),
|
||||
)
|
||||
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,
|
||||
}
|
||||
if value not in mapping:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=_err("INVALID_CHANNEL_ORDERING", f"Unsupported channels_ordering: {value}"),
|
||||
)
|
||||
return mapping[value]
|
||||
|
||||
|
||||
def _product_name_from_id(product_id: int) -> str:
|
||||
try:
|
||||
return kp.ProductId(product_id).name.replace("KP_DEVICE_", "")
|
||||
except ValueError:
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
def http_exception_handler(_: Request, exc: HTTPException) -> JSONResponse:
|
||||
if isinstance(exc.detail, dict) and "ok" in exc.detail:
|
||||
return JSONResponse(status_code=exc.status_code, content=exc.detail)
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=_err("HTTP_ERROR", str(exc.detail)),
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
def unhandled_exception_handler(_: Request, exc: Exception) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=_err("INTERNAL_ERROR", str(exc)),
|
||||
)
|
||||
|
||||
|
||||
class ConnectRequest(BaseModel):
|
||||
port_id: Optional[int] = Field(default=None)
|
||||
scan_index: Optional[int] = Field(default=None)
|
||||
timeout_ms: Optional[int] = Field(default=5000)
|
||||
|
||||
|
||||
class FirmwareLoadRequest(BaseModel):
|
||||
scpu_path: str
|
||||
ncpu_path: str
|
||||
|
||||
|
||||
class ModelLoadRequest(BaseModel):
|
||||
nef_path: str
|
||||
|
||||
|
||||
class InferenceRunRequest(BaseModel):
|
||||
model_id: int
|
||||
image_format: str
|
||||
width: int
|
||||
height: int
|
||||
image_base64: str
|
||||
channels_ordering: str = "DEFAULT"
|
||||
output_dtype: str = "float32"
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> Dict[str, Any]:
|
||||
return _ok({"status": "up"})
|
||||
|
||||
|
||||
@app.get("/version")
|
||||
def version() -> Dict[str, Any]:
|
||||
return _ok(
|
||||
{
|
||||
"service_version": SERVICE_VERSION,
|
||||
"kneronplus_version": kp.core.get_version(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/devices")
|
||||
def devices() -> Dict[str, Any]:
|
||||
device_list = kp.core.scan_devices()
|
||||
devices_out = []
|
||||
for idx, device in enumerate(device_list.device_descriptor_list):
|
||||
devices_out.append(
|
||||
{
|
||||
"scan_index": idx,
|
||||
"usb_port_id": device.usb_port_id,
|
||||
"vendor_id": device.vendor_id,
|
||||
"product_id": f"0x{device.product_id:X}",
|
||||
"product_name": _product_name_from_id(device.product_id),
|
||||
"link_speed": device.link_speed.name,
|
||||
"usb_port_path": device.usb_port_path,
|
||||
"kn_number": device.kn_number,
|
||||
"is_connectable": device.is_connectable,
|
||||
"firmware": device.firmware,
|
||||
}
|
||||
)
|
||||
return _ok({"devices": devices_out})
|
||||
|
||||
|
||||
@app.post("/devices/connect")
|
||||
def connect(req: ConnectRequest) -> Dict[str, Any]:
|
||||
with STATE_LOCK:
|
||||
if STATE.device_group is not None:
|
||||
try:
|
||||
kp.core.disconnect_devices(STATE.device_group)
|
||||
except kp.ApiKPException:
|
||||
pass
|
||||
STATE.device_group = None
|
||||
STATE.port_id = None
|
||||
STATE.model_desc = None
|
||||
|
||||
port_id = 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
|
||||
|
||||
try:
|
||||
device_group = kp.core.connect_devices([int(port_id)])
|
||||
if req.timeout_ms is not None:
|
||||
kp.core.set_timeout(device_group, int(req.timeout_ms))
|
||||
except kp.ApiKPException as exc:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
|
||||
STATE.device_group = device_group
|
||||
STATE.port_id = int(port_id)
|
||||
return _ok({"connected": True, "port_id": STATE.port_id})
|
||||
|
||||
|
||||
@app.post("/devices/disconnect")
|
||||
def disconnect() -> Dict[str, Any]:
|
||||
with STATE_LOCK:
|
||||
if STATE.device_group is not None:
|
||||
try:
|
||||
kp.core.disconnect_devices(STATE.device_group)
|
||||
except kp.ApiKPException as exc:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
STATE.device_group = None
|
||||
STATE.port_id = None
|
||||
STATE.model_desc = None
|
||||
return _ok({"connected": False})
|
||||
|
||||
|
||||
@app.post("/firmware/load")
|
||||
def firmware_load(req: FirmwareLoadRequest) -> Dict[str, Any]:
|
||||
device_group = _require_device()
|
||||
try:
|
||||
kp.core.load_firmware_from_file(device_group, req.scpu_path, req.ncpu_path)
|
||||
except kp.ApiKPException as exc:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
return _ok({"loaded": True})
|
||||
|
||||
|
||||
@app.post("/models/load")
|
||||
def models_load(req: ModelLoadRequest) -> Dict[str, Any]:
|
||||
device_group = _require_device()
|
||||
try:
|
||||
model_desc = kp.core.load_model_from_file(device_group, req.nef_path)
|
||||
except kp.ApiKPException as exc:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
|
||||
with STATE_LOCK:
|
||||
STATE.model_desc = model_desc
|
||||
|
||||
models = []
|
||||
for model in model_desc.models:
|
||||
models.append(
|
||||
{
|
||||
"id": model.id,
|
||||
"input_nodes": len(model.input_nodes),
|
||||
"output_nodes": len(model.output_nodes),
|
||||
"max_raw_out_size": model.max_raw_out_size,
|
||||
}
|
||||
)
|
||||
|
||||
return _ok({"models": models})
|
||||
|
||||
|
||||
def _reset_device_and_clear_state(device_group: kp.DeviceGroup) -> None:
|
||||
kp.core.reset_device(device_group, kp.ResetMode.KP_RESET_REBOOT)
|
||||
kp.core.disconnect_devices(device_group)
|
||||
|
||||
|
||||
@app.post("/models/clear")
|
||||
def models_clear() -> Dict[str, Any]:
|
||||
device_group = _require_device()
|
||||
try:
|
||||
_reset_device_and_clear_state(device_group)
|
||||
except kp.ApiKPException as exc:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
|
||||
with STATE_LOCK:
|
||||
STATE.device_group = None
|
||||
STATE.port_id = None
|
||||
STATE.model_desc = None
|
||||
|
||||
return _ok({"cleared": True})
|
||||
|
||||
|
||||
@app.post("/models/reset")
|
||||
def models_reset() -> Dict[str, Any]:
|
||||
return models_clear()
|
||||
|
||||
|
||||
@app.post("/inference/run")
|
||||
def inference_run(req: InferenceRunRequest) -> Dict[str, Any]:
|
||||
device_group = _require_device()
|
||||
image_format = _image_format_from_str(req.image_format)
|
||||
channels_ordering = _channels_ordering_from_str(req.channels_ordering)
|
||||
if req.output_dtype.lower() != "float32":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=_err("INVALID_OUTPUT_DTYPE", "Only float32 output is supported in PoC"),
|
||||
)
|
||||
|
||||
try:
|
||||
if STATE.port_id is not None:
|
||||
kp.core.get_model_info(device_group, STATE.port_id)
|
||||
except kp.ApiKPException as exc:
|
||||
if exc.api_return_code == kp.ApiReturnCode.KP_ERROR_MODEL_NOT_LOADED_35:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(
|
||||
"KP_ERROR_MODEL_NOT_LOADED_35",
|
||||
str(kp.ApiReturnCode.KP_ERROR_MODEL_NOT_LOADED_35),
|
||||
),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
|
||||
try:
|
||||
image_bytes = base64.b64decode(req.image_base64)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=_err("INVALID_BASE64", "image_base64 is not valid base64 data"),
|
||||
)
|
||||
|
||||
input_image = kp.GenericInputNodeImage(
|
||||
image=image_bytes,
|
||||
width=req.width,
|
||||
height=req.height,
|
||||
image_format=image_format,
|
||||
)
|
||||
|
||||
input_desc = kp.GenericImageInferenceDescriptor(
|
||||
model_id=req.model_id,
|
||||
input_node_image_list=[input_image],
|
||||
)
|
||||
|
||||
try:
|
||||
kp.inference.generic_image_inference_send(device_group, input_desc)
|
||||
result = kp.inference.generic_image_inference_receive(device_group)
|
||||
except kp.ApiKPException as exc:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
|
||||
outputs = []
|
||||
for node_idx in range(result.header.num_output_node):
|
||||
try:
|
||||
node_output = kp.inference.generic_inference_retrieve_float_node(
|
||||
node_idx, result, channels_ordering
|
||||
)
|
||||
except kp.ApiKPException as exc:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=_err(str(exc.api_return_code), str(exc)),
|
||||
)
|
||||
|
||||
data_bytes = node_output.ndarray.astype("float32").tobytes()
|
||||
outputs.append(
|
||||
{
|
||||
"node_idx": node_idx,
|
||||
"name": node_output.name,
|
||||
"dtype": "float32",
|
||||
"shape": node_output.shape,
|
||||
"data_base64": base64.b64encode(data_bytes).decode("ascii"),
|
||||
"channels_ordering": channels_ordering.name,
|
||||
}
|
||||
)
|
||||
|
||||
return _ok({"outputs": outputs})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=4398)
|
||||
298
local_service_win/STRATEGY.md
Normal file
298
local_service_win/STRATEGY.md
Normal file
@ -0,0 +1,298 @@
|
||||
# Kneron Dongle PoC (Windows) - Strategy
|
||||
|
||||
## Scope (PoC)
|
||||
- OS: Windows only.
|
||||
- Devices: KL520, KL720.
|
||||
- Control path: Browser -> localhost HTTP service -> KneronPLUS (kp wrapper + DLL).
|
||||
- Non-goals: macOS/Linux support, production hardening, installer automation for all platforms.
|
||||
|
||||
## Required Installation (Windows)
|
||||
Before running the local service, install Python dependencies and the KneronPLUS wheel.
|
||||
|
||||
### 1. Install dependencies from requirements
|
||||
```powershell
|
||||
cd local_service_win
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Install KneronPLUS wheel
|
||||
```powershell
|
||||
cd local_service_win
|
||||
python -m pip install .\KneronPLUS-3.1.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 3. (Optional) Force reinstall KneronPLUS wheel
|
||||
Use this when switching versions or seeing package mismatch issues.
|
||||
```powershell
|
||||
cd local_service_win
|
||||
python -m pip install --force-reinstall .\KneronPLUS-3.1.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
## Cross-Project Workflow
|
||||
This repo is the main PoC implementation. If additional references are required, we can switch to
|
||||
other repos during the same conversation and return here as needed. This is workable.
|
||||
|
||||
## High-Level Architecture
|
||||
- Browser UI
|
||||
- Talks to localhost HTTP service for control APIs.
|
||||
- Uses WebSocket for streaming inference.
|
||||
- No direct USB access from browser.
|
||||
- Local Service (Windows)
|
||||
- Owns Kneron device lifecycle and IO.
|
||||
- Uses Python `kp` high-level API (backed by `libkplus.dll`).
|
||||
- Exposes HTTP endpoints for scan/connect/model/firmware/inference.
|
||||
- KneronPLUS Runtime
|
||||
- `kp` Python wrapper + DLLs + required USB driver.
|
||||
- Version pinned inside installer to avoid mismatches.
|
||||
|
||||
## API Spec (PoC)
|
||||
### Conventions
|
||||
- Base URL: `http://127.0.0.1:4398`
|
||||
- WebSocket URL: `ws://127.0.0.1:4398/ws`
|
||||
- Response envelope:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"data": null,
|
||||
"error": { "code": "KP_ERROR_CONNECT_FAILED", "message": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /health`
|
||||
Response
|
||||
```json
|
||||
{ "ok": true, "data": { "status": "up" }, "error": null }
|
||||
```
|
||||
|
||||
### `GET /version`
|
||||
Response
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"service_version": "0.1.0",
|
||||
"kneronplus_version": "3.0.0"
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /devices`
|
||||
Response
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"devices": [
|
||||
{
|
||||
"scan_index": 0,
|
||||
"usb_port_id": 32,
|
||||
"product_id": 0x520,
|
||||
"link_speed": "High-Speed",
|
||||
"usb_port_path": "1-3",
|
||||
"kn_number": 12345,
|
||||
"is_connectable": true,
|
||||
"firmware": "KDP2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /devices/connect`
|
||||
Request
|
||||
```json
|
||||
{ "port_id": 32 }
|
||||
```
|
||||
Response
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"connected": true,
|
||||
"port_id": 32
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /devices/disconnect`
|
||||
Response
|
||||
```json
|
||||
{ "ok": true, "data": { "connected": false }, "error": null }
|
||||
```
|
||||
|
||||
### `POST /firmware/load`
|
||||
Request
|
||||
```json
|
||||
{
|
||||
"scpu_path": "C:\\path\\fw_scpu.bin",
|
||||
"ncpu_path": "C:\\path\\fw_ncpu.bin"
|
||||
}
|
||||
```
|
||||
Response
|
||||
```json
|
||||
{ "ok": true, "data": { "loaded": true }, "error": null }
|
||||
```
|
||||
|
||||
### `POST /models/load`
|
||||
Request
|
||||
```json
|
||||
{ "nef_path": "C:\\path\\model.nef" }
|
||||
```
|
||||
Response
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"model_id": 1,
|
||||
"input_tensor_count": 1,
|
||||
"output_tensor_count": 1
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /models/clear`
|
||||
Notes
|
||||
- PoC uses device reset to clear RAM model.
|
||||
Response
|
||||
```json
|
||||
{ "ok": true, "data": { "cleared": true }, "error": null }
|
||||
```
|
||||
|
||||
### `POST /models/reset`
|
||||
Notes
|
||||
- Alias of `/models/clear`, uses device reset to clear RAM model.
|
||||
Response
|
||||
```json
|
||||
{ "ok": true, "data": { "reset": true }, "error": null }
|
||||
```
|
||||
|
||||
### `POST /inference/run`
|
||||
Request (image inference, single image)
|
||||
```json
|
||||
{
|
||||
"model_id": 1,
|
||||
"image_format": "RGB888",
|
||||
"width": 224,
|
||||
"height": 224,
|
||||
"image_base64": "..."
|
||||
}
|
||||
```
|
||||
Response
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"outputs": [
|
||||
{ "node_idx": 0, "dtype": "float", "shape": [1, 1000], "data_base64": "..." }
|
||||
]
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### `WS /ws` (streaming inference)
|
||||
Notes
|
||||
- For camera/video stream, use WebSocket for low-latency send/receive.
|
||||
- HTTP endpoints remain for control operations during PoC.
|
||||
Message (client -> server)
|
||||
```json
|
||||
{
|
||||
"type": "inference_frame",
|
||||
"model_id": 1,
|
||||
"image_format": "RGB888",
|
||||
"width": 224,
|
||||
"height": 224,
|
||||
"image_base64": "..."
|
||||
}
|
||||
```
|
||||
Message (server -> client)
|
||||
```json
|
||||
{
|
||||
"type": "inference_result",
|
||||
"outputs": [
|
||||
{ "node_idx": 0, "dtype": "float", "shape": [1, 1000], "data_base64": "..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /firmware/update`
|
||||
- Reserved for flash update (later; may need C wrapper).
|
||||
|
||||
## Packaging (PoC)
|
||||
- Single Windows installer:
|
||||
- Includes driver, `kp` wrapper, DLLs, and service.
|
||||
- Ensures fixed versions (no external Kneron tools required).
|
||||
- Reference from `C:\Users\user\Documents\KNEOX\README.md`:
|
||||
- Install KneronPLUS wheel from `external/kneron_plus_{version}/package/{platform}/`
|
||||
- `pip install KneronPLUS-{version}-py3-none-any.whl` (use `--force-reinstall` if needed)
|
||||
- PyInstaller must bundle `kp\lib` with the app.
|
||||
- Example:
|
||||
```shell
|
||||
pyinstaller --onefile --windowed main.py --additional-hooks-dir=hooks --add-data "uxui;uxui" --add-data "src;src" --add-data "C:\path\to\venv\Lib\site-packages\kp\lib;kp\lib"
|
||||
```
|
||||
|
||||
## Risks / Constraints
|
||||
- Flash model update / flash firmware update may not be exposed in Python.
|
||||
- Use C library or request Kneron to expose in wrapper if required.
|
||||
- Browser security model prevents direct USB access; local service is required.
|
||||
|
||||
## API Test Progress (Windows PoC)
|
||||
### Completed
|
||||
- `GET /health`
|
||||
- `GET /version`
|
||||
- `GET /devices`
|
||||
- `POST /devices/connect`
|
||||
- `POST /devices/disconnect`
|
||||
|
||||
### Pending
|
||||
- `POST /firmware/load`
|
||||
- `POST /models/load`
|
||||
- `POST /models/clear`
|
||||
- `POST /models/reset`
|
||||
- `POST /inference/run`
|
||||
- `WS /ws`
|
||||
- `POST /firmware/update`
|
||||
|
||||
### Paired Test Requirement
|
||||
- `POST /models/load` and `POST /inference/run` must be tested as a pair in the same flow.
|
||||
- Test pairs are defined in `local_service_win/TestRes/TEST_PAIRS.md`.
|
||||
|
||||
### Model/Inference Test Pairs
|
||||
#### KL520
|
||||
1. YOLOv5 (model zoo)
|
||||
- Model: `kl520_20005_yolov5-noupsample_w640h640.nef`
|
||||
- Image: `one_bike_many_cars_800x800` (Base64)
|
||||
2. FCOS (model zoo)
|
||||
- Model: `kl520_20004_fcos-drk53s_w512h512.nef`
|
||||
- Image: `one_bike_many_cars_800x800` (Base64)
|
||||
3. Tiny YOLO v3 (generic demo)
|
||||
- Model: `models_520.nef`
|
||||
- Image: `bike_cars_street_224x224` (Base64)
|
||||
4. Tiny YOLO v3 (multithread demo)
|
||||
- Model: `models_520.nef`
|
||||
- Image: `bike_cars_street_224x224` (Base64)
|
||||
|
||||
#### KL720
|
||||
1. YOLOv5 (model zoo)
|
||||
- Model: `kl720_20005_yolov5-noupsample_w640h640.nef`
|
||||
- Image: `one_bike_many_cars_800x800` (Base64)
|
||||
2. FCOS (model zoo)
|
||||
- Model: `kl720_20004_fcos-drk53s_w512h512.nef`
|
||||
- Image: `one_bike_many_cars_800x800` (Base64)
|
||||
|
||||
## Next Steps (After Strategy)
|
||||
- Confirm endpoint payloads (JSON schema).
|
||||
- Decide service framework (FastAPI/Flask).
|
||||
- Define error model and device state machine.
|
||||
- Plan installer workflow (driver + service).
|
||||
83
local_service_win/TestRes/Images/Pic64View.html
Normal file
83
local_service_win/TestRes/Images/Pic64View.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Pic64View</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 16px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
.controls {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
}
|
||||
.hint {
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Pic64View</h1>
|
||||
<p class="hint">
|
||||
Paste a Base64 image string (with or without data URL prefix) and click "Render".
|
||||
</p>
|
||||
<textarea id="base64Input" placeholder="Paste Base64 here..."></textarea>
|
||||
<div class="controls">
|
||||
<button id="renderBtn">Render</button>
|
||||
<button id="clearBtn">Clear</button>
|
||||
</div>
|
||||
<img id="preview" alt="Preview will appear here" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const input = document.getElementById("base64Input");
|
||||
const preview = document.getElementById("preview");
|
||||
const renderBtn = document.getElementById("renderBtn");
|
||||
const clearBtn = document.getElementById("clearBtn");
|
||||
|
||||
function normalizeBase64(value) {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.startsWith("data:image")) {
|
||||
return trimmed;
|
||||
}
|
||||
return "data:image/png;base64," + trimmed;
|
||||
}
|
||||
|
||||
renderBtn.addEventListener("click", () => {
|
||||
const value = input.value;
|
||||
if (!value.trim()) {
|
||||
return;
|
||||
}
|
||||
preview.src = normalizeBase64(value);
|
||||
});
|
||||
|
||||
clearBtn.addEventListener("click", () => {
|
||||
input.value = "";
|
||||
preview.removeAttribute("src");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
local_service_win/TestRes/Models/models_520.nef
Normal file
BIN
local_service_win/TestRes/Models/models_520.nef
Normal file
Binary file not shown.
29
local_service_win/TestRes/TEST_PAIRS.md
Normal file
29
local_service_win/TestRes/TEST_PAIRS.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Model/Image Test Pairs (from kneron_plus examples)
|
||||
|
||||
## KL520
|
||||
- YOLOv5 (model zoo)
|
||||
- Model: `res/models/KL520/yolov5-noupsample_w640h640_kn-model-zoo/kl520_20005_yolov5-noupsample_w640h640.nef`
|
||||
- Image: `res/images/one_bike_many_cars_800x800.bmp`
|
||||
- Source: `examples_model_zoo/kl520_kn-model-zoo_generic_image_inference_post_yolov5/kl520_kn-model-zoo_generic_image_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`
|
||||
- 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`
|
||||
- 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`
|
||||
|
||||
## 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`
|
||||
- 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`
|
||||
15
local_service_win/requirements.txt
Normal file
15
local_service_win/requirements.txt
Normal file
@ -0,0 +1,15 @@
|
||||
# Core SDK (installed via local wheel; see STRATEGY.md)
|
||||
# KneronPLUS==3.0.0
|
||||
|
||||
# HTTP service
|
||||
fastapi
|
||||
uvicorn
|
||||
|
||||
# Reference packages from C:\Users\user\Documents\KNEOX\README.md
|
||||
PyQt5
|
||||
opencv-python
|
||||
pyinstaller
|
||||
pyarmor
|
||||
|
||||
# Common dependency for kp data handling
|
||||
numpy
|
||||
Loading…
x
Reference in New Issue
Block a user