2026-09-13 20:34:10 +08:00
|
|
|
|
"""
|
|
|
|
|
|
AI Box Sophon Adapter — IN2 数字输入控制任务启停
|
|
|
|
|
|
|
|
|
|
|
|
监听盒子 IN2 DI 输入(复用 diwatch 的 DIChannel + SysfsBackend,sysfs 轮询),
|
|
|
|
|
|
电平变化时通过盒子本地 HTTP 接口控制所有算法任务:
|
|
|
|
|
|
|
|
|
|
|
|
- IN2 有电(电平 1)→ 启动全部任务(ControlCommand = start_command,默认 1)
|
|
|
|
|
|
- IN2 断电(电平 0)→ 停止全部任务(ControlCommand = stop_command,默认 0)
|
|
|
|
|
|
|
|
|
|
|
|
盒子接口(POST,JSON):
|
|
|
|
|
|
- /api/alg_task_fetch 查询全部任务:{} → {"Content": [{"AlgTaskSession": ...}], "Result": {"Code": 0}}
|
|
|
|
|
|
- /api/alg_task_control 控制单个任务:{"AlgTaskSession": ..., "ControlCommand": 0/1}
|
|
|
|
|
|
→ {"Result": {"Code": 0, "Desc": "已停止"}}
|
|
|
|
|
|
|
|
|
|
|
|
说明:
|
|
|
|
|
|
- 只响应电平跳变,不按启动时的初始电平动作(避免插件重启时误触发全量启停)。
|
|
|
|
|
|
- DIChannel 阻塞轮询跑在独立线程,回调经 run_coroutine_threadsafe 调度回事件循环,
|
|
|
|
|
|
HTTP 控制在事件循环内串行执行(apply_lock 防止快速抖动导致并发穿插)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import threading
|
|
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
2026-09-17 15:34:34 +08:00
|
|
|
|
from .diwatch.backends import EventBackend, make_in2_backend
|
2026-09-13 20:34:10 +08:00
|
|
|
|
from .diwatch.core import DIChannel
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class In2TaskSwitch:
|
2026-09-17 15:34:34 +08:00
|
|
|
|
"""IN2 数字输入 → 全部算法任务启停开关。
|
|
|
|
|
|
|
|
|
|
|
|
后端自适应(make_in2_backend):SE9 走 sysfs gpio_status 轮询;
|
|
|
|
|
|
SE5 无 gpio-in-2 时回退 gpio-keys event 设备(默认键码 KEY_DOWN=108,
|
|
|
|
|
|
可用 device/key 参数覆盖,键码用 diwatch detect 查询)。
|
|
|
|
|
|
"""
|
2026-09-13 20:34:10 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
api_base: str = "http://127.0.0.1",
|
|
|
|
|
|
start_command: int = 1,
|
|
|
|
|
|
stop_command: int = 0,
|
|
|
|
|
|
poll_interval: float = 0.05,
|
|
|
|
|
|
debounce: int = 3,
|
|
|
|
|
|
active_low: bool = False,
|
2026-09-17 15:34:34 +08:00
|
|
|
|
device: str | None = None,
|
|
|
|
|
|
key: int | None = None,
|
2026-09-13 20:34:10 +08:00
|
|
|
|
timeout: float = 10.0,
|
|
|
|
|
|
):
|
|
|
|
|
|
self.api_base = api_base.rstrip("/")
|
|
|
|
|
|
self.start_command = start_command
|
|
|
|
|
|
self.stop_command = stop_command
|
|
|
|
|
|
self.poll_interval = poll_interval
|
|
|
|
|
|
self.debounce = debounce
|
|
|
|
|
|
self.active_low = active_low
|
2026-09-17 15:34:34 +08:00
|
|
|
|
self.device = device
|
|
|
|
|
|
self.key = key
|
2026-09-13 20:34:10 +08:00
|
|
|
|
self.timeout = timeout
|
|
|
|
|
|
|
|
|
|
|
|
self._channel: DIChannel | None = None
|
|
|
|
|
|
self._thread: threading.Thread | None = None
|
|
|
|
|
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
|
|
|
|
self._apply_lock = asyncio.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 生命周期 ----
|
|
|
|
|
|
def start(self, loop: asyncio.AbstractEventLoop) -> bool:
|
2026-09-17 15:34:34 +08:00
|
|
|
|
"""启动监听线程;找不到 IN2 可用后端时返回 False(非盒子环境属正常)。"""
|
|
|
|
|
|
backend = make_in2_backend(
|
|
|
|
|
|
device=self.device, key=self.key,
|
|
|
|
|
|
interval=self.poll_interval, debounce=self.debounce,
|
|
|
|
|
|
)
|
|
|
|
|
|
if backend is None:
|
2026-09-13 20:34:10 +08:00
|
|
|
|
logger.warning(
|
2026-09-17 15:34:34 +08:00
|
|
|
|
"[TaskSwitch] 找不到 IN2 可用后端(sysfs 状态文件与 gpio-keys "
|
|
|
|
|
|
"event 设备均不存在),任务启停开关未启动(非盒子环境属正常现象)"
|
2026-09-13 20:34:10 +08:00
|
|
|
|
)
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
self._channel = DIChannel(
|
|
|
|
|
|
"IN2", backend,
|
|
|
|
|
|
active_low=self.active_low,
|
|
|
|
|
|
on_change=self._on_level_change,
|
|
|
|
|
|
log=lambda m: logger.debug(f"[TaskSwitch] {m}"),
|
|
|
|
|
|
)
|
|
|
|
|
|
self._loop = loop
|
|
|
|
|
|
self._thread = threading.Thread(
|
|
|
|
|
|
target=self._run_channel, name="in2-task-switch", daemon=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
self._thread.start()
|
2026-09-17 15:34:34 +08:00
|
|
|
|
backend_desc = (
|
|
|
|
|
|
f"event:{getattr(backend, 'device', '?')}, 键码={backend.key}"
|
|
|
|
|
|
if isinstance(backend, EventBackend)
|
|
|
|
|
|
else f"sysfs:{backend.path}"
|
|
|
|
|
|
)
|
2026-09-13 20:34:10 +08:00
|
|
|
|
logger.info(
|
2026-09-17 15:34:34 +08:00
|
|
|
|
f"[TaskSwitch] IN2 任务启停开关已启动: 后端={backend_desc}, "
|
2026-09-13 20:34:10 +08:00
|
|
|
|
f"api_base={self.api_base}, "
|
|
|
|
|
|
f"启动命令={self.start_command}, 停止命令={self.stop_command}"
|
|
|
|
|
|
)
|
2026-09-17 15:34:34 +08:00
|
|
|
|
if isinstance(backend, EventBackend):
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"[TaskSwitch] event 后端无初始电平,以首个事件为准;"
|
|
|
|
|
|
"若操作 IN2 无反应,用 diwatch detect 查实际键码并配置 task_switch.key"
|
|
|
|
|
|
)
|
2026-09-13 20:34:10 +08:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def stop(self) -> None:
|
|
|
|
|
|
if self._channel is not None:
|
|
|
|
|
|
self._channel.request_stop()
|
|
|
|
|
|
if self._thread is not None:
|
|
|
|
|
|
self._thread.join(timeout=2.0)
|
|
|
|
|
|
self._thread = None
|
|
|
|
|
|
logger.info("[TaskSwitch] IN2 任务启停开关已停止")
|
|
|
|
|
|
|
|
|
|
|
|
def _run_channel(self) -> None:
|
|
|
|
|
|
try:
|
2026-09-17 15:34:34 +08:00
|
|
|
|
self._channel.run() # type: ignore
|
2026-09-13 20:34:10 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"[TaskSwitch] IN2 监听线程异常退出: {type(e).__name__}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# ---- DI 回调(轮询线程内执行)----
|
|
|
|
|
|
def _on_level_change(self, old: int, new: int, ts: float) -> None:
|
|
|
|
|
|
logger.info(f"[TaskSwitch] IN2 电平变化: {old} -> {new}")
|
|
|
|
|
|
if self._loop is not None:
|
|
|
|
|
|
asyncio.run_coroutine_threadsafe(self.apply(new), self._loop)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 任务控制(事件循环内执行)----
|
|
|
|
|
|
async def apply(self, level: int) -> None:
|
|
|
|
|
|
"""按目标电平控制全部任务:1=启动,0=停止。"""
|
|
|
|
|
|
async with self._apply_lock:
|
|
|
|
|
|
command = self.start_command if level == 1 else self.stop_command
|
|
|
|
|
|
action = "启动" if level == 1 else "停止"
|
|
|
|
|
|
|
|
|
|
|
|
sessions = await self._fetch_task_sessions()
|
|
|
|
|
|
if not sessions:
|
|
|
|
|
|
logger.warning("[TaskSwitch] 未获取到任何任务,跳过控制")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"[TaskSwitch] IN2 {'有电' if level else '断电'},"
|
|
|
|
|
|
f"{action}全部 {len(sessions)} 个任务: {sessions}"
|
|
|
|
|
|
)
|
|
|
|
|
|
ok = 0
|
|
|
|
|
|
failed = 0
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
|
|
|
|
for session in sessions:
|
|
|
|
|
|
try:
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
|
f"{self.api_base}/api/alg_task_control",
|
|
|
|
|
|
json={"AlgTaskSession": session, "ControlCommand": command},
|
|
|
|
|
|
)
|
|
|
|
|
|
result = resp.json().get("Result", {})
|
|
|
|
|
|
if resp.is_success and result.get("Code") == 0:
|
|
|
|
|
|
ok += 1
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"[TaskSwitch] 任务 {session} {action}成功: "
|
|
|
|
|
|
f"{result.get('Desc', '')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
failed += 1
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"[TaskSwitch] 任务 {session} {action}失败: "
|
|
|
|
|
|
f"Status={resp.status_code}, Body={resp.text[:200]}"
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
failed += 1
|
|
|
|
|
|
logger.error(
|
|
|
|
|
|
f"[TaskSwitch] 任务 {session} {action}异常: "
|
|
|
|
|
|
f"{type(e).__name__}: {e}"
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"[TaskSwitch] {action}完成: 成功 {ok}, 失败 {failed}")
|
|
|
|
|
|
|
|
|
|
|
|
async def _fetch_task_sessions(self) -> list[str]:
|
|
|
|
|
|
"""查询全部任务的 AlgTaskSession 列表;失败返回空列表。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
2026-09-17 15:34:34 +08:00
|
|
|
|
# resp = await client.post(f"{self.api_base}/api/alg_task_fetch", json={})
|
|
|
|
|
|
resp = await client.get(f"{self.api_base}/api/alg_task_fetch")
|
2026-09-13 20:34:10 +08:00
|
|
|
|
if not resp.is_success:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"[TaskSwitch] 查询任务失败: Status={resp.status_code}, "
|
|
|
|
|
|
f"Body={resp.text[:200]}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return []
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
result = data.get("Result", {})
|
|
|
|
|
|
if result.get("Code") != 0:
|
|
|
|
|
|
logger.warning(f"[TaskSwitch] 查询任务返回错误: {result}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
content = data.get("Content") or []
|
|
|
|
|
|
return [t.get("AlgTaskSession") for t in content if t.get("AlgTaskSession")]
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"[TaskSwitch] 查询任务异常: {type(e).__name__}: {e}")
|
|
|
|
|
|
return []
|