Files
IQ.Pilot/tools/iqmacvisiond/server.py
2026-08-22 23:42:42 -05:00

291 lines
9.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import signal
import socket
import subprocess
import sys
import threading
import time
from pathlib import Path
os.environ.setdefault("DEV", "METAL")
os.environ.setdefault("JIT_BATCH_SIZE", "0")
REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))
import cv2
import numpy as np
from openpilot.iqpilot.iqvd_private_src.offload.protocol import (
DISCOVERY_MAGIC, DISCOVERY_REPLY, DISCOVERY_PORT_DEFAULT, MSG_HELLO, MSG_HELLO_ACK, MSG_INFER,
MSG_PING, MSG_PONG, MSG_RESULT, ProtocolError, recv_msg, send_msg,
)
from openpilot.iqpilot.iqvd_private_src.offload.perception import Detector
log = logging.getLogger("iqmacvisiond")
DEFAULT_PORT = 51998
STATUS_PORT = 51995
MODEL_NAME = "yolov8n"
SESSION_IDLE_TIMEOUT_S = 8.0
STATUS: dict = {
"connected": False, "peer": "", "model": MODEL_NAME,
"exec_p50_ms": 0.0, "exec_p99_ms": 0.0, "infer_count": 0,
"last_seen": 0.0, "awake": False,
}
class KeepAwake:
def __init__(self):
self._proc: subprocess.Popen | None = None
def start(self) -> None:
if sys.platform != "darwin" or self._proc is not None:
return
try:
self._proc = subprocess.Popen(["caffeinate", "-dimsu"])
STATUS["awake"] = True
log.info("keep-awake active (caffeinate pid=%d)", self._proc.pid)
except OSError:
log.warning("caffeinate unavailable; display may sleep")
def stop(self) -> None:
if self._proc is not None:
self._proc.terminate()
self._proc = None
STATUS["awake"] = False
class DiscoveryResponder:
def __init__(self, tcp_port: int, disc_port: int = DISCOVERY_PORT_DEFAULT):
self.tcp_port = tcp_port
self.disc_port = disc_port
def start(self) -> None:
threading.Thread(target=self._serve, daemon=True).start()
def _serve(self) -> None:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind(("0.0.0.0", self.disc_port))
except OSError:
log.exception("discovery bind failed on udp/%d", self.disc_port)
return
reply = DISCOVERY_REPLY + f":{self.tcp_port}".encode()
log.info("discovery responder on udp/%d -> tcp/%d", self.disc_port, self.tcp_port)
while True:
try:
data, addr = sock.recvfrom(256)
except OSError:
continue
if data.startswith(DISCOVERY_MAGIC):
try:
sock.sendto(reply, addr)
except OSError:
pass
class StatusServer:
def __init__(self, port: int = STATUS_PORT):
self.port = port
def start(self) -> None:
threading.Thread(target=self._serve, daemon=True).start()
def _serve(self) -> None:
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def _send(self, code, ctype, body):
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path.startswith("/status.json"):
st = dict(STATUS)
st["fresh"] = (time.time() - st["last_seen"]) < 6 if st["last_seen"] else False
self._send(200, "application/json", json.dumps(st).encode())
else:
self._send(200, "text/html; charset=utf-8", _STATUS_HTML.encode())
try:
HTTPServer(("127.0.0.1", self.port), H).serve_forever()
except OSError:
log.exception("status server failed on %d", self.port)
_STATUS_HTML = """<!doctype html><html><head><meta charset=utf-8>
<title>IQ Vision</title><meta name=viewport content="width=device-width,initial-scale=1">
<style>
:root{color-scheme:dark}
body{margin:0;font:15px -apple-system,system-ui,sans-serif;background:#0b0d10;color:#e6e9ef}
.wrap{max-width:520px;margin:0 auto;padding:32px 20px}
h1{font-size:20px;margin:0 0 20px;display:flex;align-items:center;gap:10px}
.dot{width:12px;height:12px;border-radius:50%;background:#555}
.dot.green{background:#28d2c8;box-shadow:0 0 10px #28d2c8}
.dot.red{background:#e74c3c}
.row{display:flex;justify-content:space-between;padding:12px 0;border-bottom:1px solid #1c2027}
.k{color:#8a91a0}.v{font-variant-numeric:tabular-nums}
</style></head><body><div class=wrap>
<h1><span class=dot id=dot></span><span id=title>IQ Vision</span></h1>
<div class=row><span class=k>Device</span><span class="v" id=peer>—</span></div>
<div class=row><span class=k>Model</span><span class="v" id=model>—</span></div>
<div class=row><span class=k>Inference (p50 / p99)</span><span class="v" id=exec>—</span></div>
<div class=row><span class=k>Frames served</span><span class="v" id=count>—</span></div>
<div class=row><span class=k>Keep awake</span><span class="v" id=awake>—</span></div>
</div><script>
async function tick(){
try{
const s=await (await fetch('/status.json')).json();
const live=s.connected&&s.fresh;
document.getElementById('dot').className='dot '+(live?'green':'red');
document.getElementById('title').textContent=live?'IQ Vision — connected':'IQ Vision — waiting for device';
document.getElementById('peer').textContent=s.peer||'not connected';
document.getElementById('model').textContent=s.model||'';
document.getElementById('exec').textContent=s.exec_p50_ms?`${s.exec_p50_ms.toFixed(1)} / ${s.exec_p99_ms.toFixed(1)} ms`:'';
document.getElementById('count').textContent=s.infer_count?s.infer_count.toLocaleString():'';
document.getElementById('awake').textContent=s.awake?'on':'off';
}catch(e){document.getElementById('dot').className='dot red';}
}
tick();setInterval(tick,1000);
</script></body></html>"""
class Session:
def __init__(self, conn: socket.socket, detector: Detector):
self.conn = conn
self.detector = detector
try:
self.peer = conn.getpeername()[0]
except OSError:
self.peer = ""
self.infer_count = 0
self.exec_ms: list[float] = []
def handshake(self) -> bool:
msg_type, header, _ = recv_msg(self.conn)
if msg_type != MSG_HELLO:
raise ProtocolError(f"expected HELLO, got {msg_type}")
send_msg(self.conn, MSG_HELLO_ACK, {"ok": True, "model": MODEL_NAME, "hostname": socket.gethostname()})
STATUS.update(connected=True, peer=self.peer, last_seen=time.time())
log.info("device connected: %s dongle=%s", self.peer, header.get("dongle_id", ""))
return True
def serve(self) -> None:
while True:
msg_type, header, blob = recv_msg(self.conn)
if msg_type == MSG_INFER:
self._infer(header, blob)
elif msg_type == MSG_PING:
send_msg(self.conn, MSG_PONG, {})
else:
raise ProtocolError(f"unexpected message type {msg_type}")
def _infer(self, header: dict, jpeg: bytes) -> None:
st = time.perf_counter()
tracks = []
try:
rgb = cv2.imdecode(np.frombuffer(jpeg, np.uint8), cv2.IMREAD_COLOR)
if rgb is not None:
tracks = self.detector.detect(cv2.cvtColor(rgb, cv2.COLOR_BGR2RGB))
except Exception:
log.exception("inference failed for frame %s", header.get("frame_id"))
dt = (time.perf_counter() - st) * 1e3
send_msg(self.conn, MSG_RESULT, {"frame_id": header.get("frame_id", 0), "tracks": tracks,
"exec_ms": dt})
self.infer_count += 1
self.exec_ms.append(dt)
if len(self.exec_ms) > 400:
del self.exec_ms[:200]
if self.infer_count % 10 == 0 or self.infer_count == 1:
recent = self.exec_ms[-200:]
STATUS.update(connected=True, infer_count=self.infer_count, last_seen=time.time(),
exec_p50_ms=float(np.percentile(recent, 50)),
exec_p99_ms=float(np.percentile(recent, 99)))
def _weights_ok() -> bool:
from openpilot.iqpilot.iqvd_private_src.offload.perception import _weights_dir
return (_weights_dir() / "yolov8n.safetensors").exists()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--no-keep-awake", action="store_true")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
if not _weights_ok():
log.error("yolov8n.safetensors not found; the app ships the model with it")
sys.exit(1)
keep_awake = KeepAwake()
if not args.no_keep_awake:
keep_awake.start()
def _shutdown(*_):
keep_awake.stop()
sys.exit(0)
try:
signal.signal(signal.SIGTERM, _shutdown)
signal.signal(signal.SIGINT, _shutdown)
except ValueError:
pass
t0 = time.perf_counter()
detector = Detector(None)
for _ in range(3):
detector.detect(np.zeros((416, 640, 3), dtype=np.uint8))
log.info("model warm in %.1fs", time.perf_counter() - t0)
DiscoveryResponder(args.port).start()
StatusServer().start()
log.info("status dashboard on http://127.0.0.1:%d", STATUS_PORT)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((args.host, args.port))
server.listen(1)
log.info("READY listening on %s:%d", args.host, args.port)
try:
while True:
conn, addr = server.accept()
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
conn.settimeout(SESSION_IDLE_TIMEOUT_S)
try:
session = Session(conn, detector)
if session.handshake():
session.serve()
except (ConnectionError, ProtocolError, OSError) as e:
log.info("session ended: %s", e)
finally:
conn.close()
STATUS.update(connected=False, peer="")
finally:
keep_awake.stop()
if __name__ == "__main__":
main()