tiandytmd

This commit is contained in:
Ankle committed 2026-09-15 09:53:00 +08:00
1 parent f7a782dcef
commit ddea6d0578
6 files changed
+389 -34

No files matched your search

+95 -8
View File
@@ -8,6 +8,7 @@ from datetime import datetime
import json
import math
from pathlib import Path
import re
import subprocess
import sys
import time
@@ -26,6 +27,7 @@ API_RECOVERY_TIMEOUT = 90.0
API_RECOVERY_POLL_INTERVAL = 2.0
TRANSIENT_CURL_EXIT_CODES = {7, 28, 52, 55, 56}
_HELPER_READY = False
REQUIRED_PTZ_CAPABILITIES = {"get_position", "move_absolute"}
def fail(message: str) -> "NoReturn":
@@ -69,6 +71,45 @@ def transient_api_failure(result: subprocess.CompletedProcess[str]) -> bool:
))
def api_error_detail(result: subprocess.CompletedProcess[str]) -> str:
"""Turn scan-service JSON errors into field-operator instructions."""
message = ""
code = ""
try:
payload = json.loads(result.stdout)
error = payload.get("error", {})
if isinstance(error, dict):
code = str(error.get("code") or "")
message = str(error.get("message") or "")
except (json.JSONDecodeError, AttributeError, TypeError):
message = result.stdout.strip()
reached = re.search(
r"move_not_reached\s+pan_error=(-?\d+)\s+tilt_error=(-?\d+)\s+"
r"zoom_error=(-?\d+)\s+target=(-?\d+),(-?\d+),(-?\d+)\s+"
r"actual=(-?\d+),(-?\d+),(-?\d+)",
message,
)
if reached:
pe, te, ze, tp, tt, tz, ap, at, az = (
int(value) for value in reached.groups()
)
return (
"摄像头在限定时间内没有完全到位。\n"
f" 目标:pan={tp} ({tp / 100:.2f}°), "
f"tilt={tt} ({tt / 100:.2f}°), zoom={tz} ({tz / 100:.2f}×)\n"
f" 实际:pan={ap} ({ap / 100:.2f}°), "
f"tilt={at} ({at / 100:.2f}°), zoom={az} ({az / 100:.2f}×)\n"
f" 未到位差值:pan={pe / 100:.2f}°, "
f"tilt={te / 100:.2f}°, zoom={ze / 100:.2f}×\n"
"现场处理:请勿再操作Web云台,把第四步标定位置放在"
"扫描区域中部后重试;程序会自动限制后续测试步长。"
)
if message:
return f"摄像头控制失败({code or 'unknown'}):{message}"
return result.stderr.strip() or f"请求退出码={result.returncode}"
def scan_http_ready() -> bool:
try:
state = subprocess.run(
@@ -114,9 +155,7 @@ def api(method: str, path: str, body: dict | None = None, timeout: int = 60) ->
except json.JSONDecodeError:
fail(f"scan HTTP返回的不是JSON:{result.stdout}")
last_detail = result.stderr.strip() or result.stdout.strip() or (
f"退出码={result.returncode}"
)
last_detail = api_error_detail(result)
if not transient_api_failure(result) or attempt == API_MAX_ATTEMPTS:
break
warn(
@@ -131,17 +170,65 @@ def api(method: str, path: str, body: dict | None = None, timeout: int = 60) ->
# the in-container overlap helper before the next image comparison.
_HELPER_READY = False
fail(f"命令失败:{' '.join(command)}\n{last_detail}")
fail(f"scan HTTP请求失败:{method} {path}\n{last_detail}")
def validate_control_status(payload: dict) -> dict:
"""Validate the vendor-neutral PTZ contract exposed by smart-cam."""
if not isinstance(payload, dict):
fail("scan HTTP控制状态不是JSON对象")
vendor = str(payload.get("vendor") or "unknown").strip().lower()
if payload.get("driver_ready") is not True:
detail = str(payload.get("driver_error") or "未返回具体原因")
fail(f"摄像头驱动未就绪(vendor={vendor}):{detail}")
capabilities = {
str(item).strip().lower()
for item in payload.get("capabilities", [])
if str(item).strip()
}
missing = sorted(REQUIRED_PTZ_CAPABILITIES - capabilities)
if missing:
fail(
f"当前smart-cam的摄像头适配层缺少标定必需能力:"
f"{', '.join(missing)}(vendor={vendor})"
)
camera = payload.get("camera")
return {
"vendor": vendor,
"capabilities": capabilities,
"camera": camera if isinstance(camera, dict) else {},
}
def get_control_status() -> tuple[dict, dict]:
payload = api("GET", "/api/v1/control/status", timeout=15)
return payload, validate_control_status(payload)
def parse_ptz_payload(payload: dict) -> dict[str, int]:
try:
ptz = payload["ptz"]
result = {key: int(ptz[key]) for key in ("pan", "tilt", "zoom")}
except (KeyError, TypeError, ValueError):
fail(f"scan HTTP未返回合法PTZ坐标:{payload}")
if not 0 <= result["pan"] <= 36000:
fail(f"scan HTTP返回的pan超出统一坐标范围:{result['pan']}")
if not -18000 <= result["tilt"] <= 18000 or result["zoom"] <= 0:
fail(f"scan HTTP返回的tilt/zoom无效:{result}")
return result
def get_ptz() -> dict[str, int]:
payload = api("GET", "/api/v1/ptz", timeout=15)
return {key: int(payload["ptz"][key]) for key in ("pan", "tilt", "zoom")}
payload = api("GET", "/api/v1/ptz?source=scan_calibrator", timeout=50)
return parse_ptz_payload(payload)
def move(ptz: dict[str, int]) -> dict[str, int]:
payload = api("POST", "/api/v1/ptz/absolute", ptz, timeout=50)
return {key: int(payload["ptz"][key]) for key in ("pan", "tilt", "zoom")}
payload = api(
"POST", "/api/v1/ptz/absolute",
{**ptz, "source": "scan_calibrator"}, timeout=50,
)
return parse_ptz_payload(payload)
def snapshot() -> str: