Files
nmfs-agents/services/edge_validator/main.py
T

208 lines
7.2 KiB
Python
Raw Normal View History

"""EdgeValidatorService: 部署在 RK3588 上,为开发机 agents 提供 HTTP 接口。
启动:uvicorn main:app --host 0.0.0.0 --port 8899
"""
from __future__ import annotations
import base64
import glob
import re
import subprocess
import time
from datetime import datetime
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
app = FastAPI(title="EdgeValidatorService", version="1.0.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"],
allow_methods=["*"], allow_headers=["*"])
# 设备配置(本机 + 接入的 MCU)
SERIAL_DEVICES: dict[str, dict] = {}
# 推理日志路径
LOG_PATHS = {
"default": "/tmp/infer.log",
"yolo": "/tmp/yolo_infer.log",
}
class RunTestRequest(BaseModel):
cmd: str
cwd: str = "/home/pi/Desktop"
timeout: int = 120
class FlashRequest(BaseModel):
firmware_b64: str
offset: str = "0x0"
port: str = "/dev/ttyUSB0"
# ── 端点 ──────────────────────────────────────────────────────────
@app.get("/health")
def health():
return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}
@app.get("/devices")
def list_devices():
"""列出识别到的串口设备"""
serial_ports = glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*")
return {"rk3588": "linux/ssh", "serial_ports": serial_ports,
"configured_mcus": list(SERIAL_DEVICES.keys())}
@app.get("/metrics/{device_name}")
def get_metrics(device_name: str):
if device_name == "rk3588":
return _linux_metrics()
if device_name in SERIAL_DEVICES:
return _mcu_metrics(SERIAL_DEVICES[device_name])
raise HTTPException(404, f"未知设备: {device_name}")
@app.post("/run-test/{device_name}")
def run_test(device_name: str, req: RunTestRequest):
if device_name == "rk3588":
return _run_local(req)
if device_name in SERIAL_DEVICES:
return _run_serial(SERIAL_DEVICES[device_name], req)
raise HTTPException(404, f"未知设备: {device_name}")
@app.post("/screenshot")
def screenshot():
"""截取 RK3588 屏幕(需要 scrot 或 ffmpeg"""
import tempfile
import os
tmp = tempfile.mktemp(suffix=".png")
r = subprocess.run(["scrot", tmp], capture_output=True, timeout=10)
if r.returncode != 0:
r = subprocess.run(
f"cat /dev/fb0 | ffmpeg -vcodec rawvideo -f rawvideo "
f"-pix_fmt rgb32 -s 1920x1080 -i - -f image2 -vcodec png {tmp} -y",
shell=True, capture_output=True, timeout=15,
)
if r.returncode != 0:
raise HTTPException(500, "截图失败,请确认 scrot 或 ffmpeg 已安装")
with open(tmp, "rb") as f:
img_b64 = base64.b64encode(f.read()).decode()
os.unlink(tmp)
return {"image_b64": img_b64, "format": "png"}
@app.get("/logs/{device_name}/{n}")
def get_logs(device_name: str, n: int = 50):
log_path = LOG_PATHS.get(device_name, LOG_PATHS["default"])
try:
r = subprocess.run(["tail", f"-{n}", log_path],
capture_output=True, text=True, timeout=5)
return {"lines": r.stdout.splitlines(), "file": log_path}
except Exception as e:
return {"lines": [], "error": str(e), "file": log_path}
# ── 内部函数 ──────────────────────────────────────────────────────
def _run_local(req: RunTestRequest) -> dict:
t0 = time.monotonic()
r = subprocess.run(
req.cmd, shell=True, capture_output=True, text=True,
cwd=req.cwd, timeout=req.timeout,
)
duration = time.monotonic() - t0
stdout = r.stdout
pass_ = r.returncode == 0 and not re.search(r"\b(FAILED|ERROR)\b", stdout + r.stderr)
return {"rc": r.returncode, "stdout": stdout, "stderr": r.stderr,
"duration_s": round(duration, 2), "pass_": pass_}
def _run_serial(dev: dict, req: RunTestRequest) -> dict:
try:
import serial # type: ignore
except ImportError:
return {"rc": 1, "stdout": "", "stderr": "pyserial 未安装",
"duration_s": 0, "pass_": False}
t0 = time.monotonic()
lines: list[str] = []
try:
ser = serial.Serial(dev["port"], dev.get("baud", 115200), timeout=req.timeout)
ser.write((req.cmd + "\r\n").encode())
deadline = time.monotonic() + req.timeout
while time.monotonic() < deadline:
line = ser.readline().decode(errors="replace").strip()
if line:
lines.append(line)
if line.startswith(("PASS", "FAIL", "ERROR")):
break
ser.close()
except Exception as e:
return {"rc": 1, "stdout": "\n".join(lines), "stderr": str(e),
"duration_s": time.monotonic() - t0, "pass_": False}
stdout = "\n".join(lines)
rc = 0 if any(l.startswith("PASS") for l in lines) else 1
return {"rc": rc, "stdout": stdout, "stderr": "",
"duration_s": round(time.monotonic() - t0, 2), "pass_": rc == 0}
def _linux_metrics() -> dict:
m: dict = {"device": "rk3588", "timestamp": datetime.utcnow().isoformat()}
try:
with open("/proc/stat") as f:
parts = f.readline().split()
user, nice, sys_, idle = int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4])
total = user + nice + sys_ + idle
m["cpu_pct"] = round(100 * (user + sys_) / total, 1) if total else 0
except Exception:
m["cpu_pct"] = None
try:
r = subprocess.run(["free", "-m"], capture_output=True, text=True, timeout=3)
for line in r.stdout.splitlines():
if line.startswith("Mem:"):
parts = line.split()
m["mem_used_mb"] = float(parts[2])
break
except Exception:
m["mem_used_mb"] = None
try:
r = subprocess.run(
"tail -10 /tmp/infer.log 2>/dev/null | grep -oP 'fps=\\K[0-9.]+' | tail -1",
shell=True, capture_output=True, text=True, timeout=3,
)
raw = r.stdout.strip()
m["fps"] = float(raw) if raw else None
except Exception:
m["fps"] = None
return m
def _mcu_metrics(dev: dict) -> dict:
m: dict = {"device": dev.get("name", "mcu"),
"timestamp": datetime.utcnow().isoformat()}
try:
import serial # type: ignore
ser = serial.Serial(dev["port"], dev.get("baud", 115200), timeout=3)
ser.write(b"GET_METRICS\r\n")
for _ in range(20):
line = ser.readline().decode(errors="replace").strip()
if line.startswith("METRICS:"):
for kv in line[8:].split():
k, _, v = kv.partition("=")
if k == "fps":
m["fps"] = float(v)
elif k == "latency_ms":
m["inference_latency_ms"] = float(v)
elif k == "ram_kb":
m["ram_used_kb"] = int(v)
break
ser.close()
except Exception as e:
m["error"] = str(e)
return m