diff --git a/launch_env.sh b/launch_env.sh index 7cf6ccc2d..9c9162564 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -24,13 +24,6 @@ fi export STAGING_ROOT="/data/safe_staging" -# Comma4-UI-Streamer (MJPEG UI stream) - set STREAM=0 to disable -export STREAM=1 -export STREAM_PORT=8082 -export STREAM_QUALITY=40 -export STREAM_FPS=5 -export STREAM_MAX_WIDTH=540 - # --- system time (this hardware has no battery-backed RTC) --- # On a cold boot there is no sane clock: AGNOS restores a stale timestamp (or the epoch), # which is still far earlier than the TLS certificate validity and makes the model diff --git a/openpilot/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py index 6d4ee00d7..4c89c1ad0 100644 --- a/openpilot/system/ui/lib/application.py +++ b/openpilot/system/ui/lib/application.py @@ -250,7 +250,6 @@ class GuiApplication(GuiApplicationExt): self._ffmpeg_stop_event: threading.Event | None = None self._textures: dict[str, rl.Texture] = {} self._target_fps: int = _DEFAULT_FPS - self._ui_stream = None self._last_fps_log_time: float = time.monotonic() self._frame = 0 self._window_close_requested = False @@ -354,18 +353,6 @@ class GuiApplication(GuiApplicationExt): rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps) self._target_fps = fps - if os.getenv("STREAM") == "1": - try: - from openpilot.system.ui.lib import ui_stream - self._ui_stream = ui_stream - if self._render_texture is None: - self._render_texture = rl.load_render_texture(self._scaled_width, self._scaled_height) - rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) - ui_stream.start(int(os.getenv("STREAM_PORT", "8082"))) - cloudlog.warning("MJPEG UI stream on :" + str(os.getenv("STREAM_PORT", "8082"))) - except Exception as e: - cloudlog.error("UI stream init failed: " + str(e)) - self._ui_stream = None self._load_fonts() self._patch_text_functions() self._patch_scissor_mode() @@ -714,9 +701,6 @@ class GuiApplication(GuiApplicationExt): self._ffmpeg_queue.put(data) # Async write via background thread rl.unload_image(image) - - if self._ui_stream is not None and self._render_texture is not None: - self._ui_stream.capture_frame(self, int(os.getenv("STREAM_QUALITY", "50")), int(os.getenv("STREAM_FPS", "10"))) self._monitor_fps() self._frame += 1 diff --git a/openpilot/system/ui/lib/ui_stream.py b/openpilot/system/ui/lib/ui_stream.py deleted file mode 100644 index 79e56af21..000000000 --- a/openpilot/system/ui/lib/ui_stream.py +++ /dev/null @@ -1,551 +0,0 @@ -"""MJPEG stream server for sunnypilot UI with telemetry overlay. -Imported lazily when STREAM=1.""" -import threading -import io -import os -import json -import queue -import time -import base64 -import hashlib -import struct -from http.server import HTTPServer, BaseHTTPRequestHandler -from socketserver import ThreadingMixIn - -class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): - daemon_threads = True - -from PIL import Image -import pyray as rl - - -class StreamState: - def __init__(self): - self.frame = b"" - self.lock = threading.Lock() - self.event = threading.Event() - self.clients = 0 - self.snapshot_pending = False - - def update(self, jpeg): - with self.lock: - self.frame = jpeg - self.snapshot_pending = False - self.event.set() - - def get(self): - with self.lock: - return self.frame - - def client_enter(self): - with self.lock: - self.clients += 1 - - def client_exit(self): - with self.lock: - self.clients = max(0, self.clients - 1) - - def request_snapshot(self): - with self.lock: - self.snapshot_pending = True - - def has_demand(self): - with self.lock: - return self.clients > 0 or self.snapshot_pending - - def wait(self, t=2.0): - self.event.wait(t) - self.event.clear() - - -_CTRL_HTML = '\n\nopenpilot remote\n\n\n
CTRL: OFF
\n
ENABLE TOUCH
\n' - -_OVERLAY_HTML = """ - -openpilot live - - - -
- -
⛶
-
OFF
- -
-
SET
-
--
-
- -
-
--
-
--
-
- - - - - -
-
ACCEL
-
-
-
-
-
-
0.0
-
- -
--%
grade
-
-
GAS
0
-
BRK
0
-
-
-
MODEL
--
-
DROPS
--
-
CPU
--
-
MEM
--
-
CPU TEMP
--
-
-
-
-""" - - -class StreamHandler(BaseHTTPRequestHandler): - protocol_version = "HTTP/1.1" - - def do_GET(self): - if self.path == "/ws": - _handle_ws(self) - elif self.path == "/ctrl": - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.send_header("Content-Length", str(len(_CTRL_HTML))) - self.end_headers() - self.wfile.write(_CTRL_HTML.encode()) - elif self.path == "/stream": - self.send_response(200) - self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=--frame") - self.send_header("Cache-Control", "no-cache") - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - self.server._state.client_enter() - try: - while True: - self.server._state.wait(2.0) - f = self.server._state.get() - if f: - self.wfile.write(b"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " + str(len(f)).encode() + b"\r\n\r\n") - self.wfile.write(f) - self.wfile.write(b"\r\n") - except (BrokenPipeError, ConnectionResetError): - pass - finally: - self.server._state.client_exit() - elif self.path == "/snapshot": - self.server._state.request_snapshot() - self.server._state.wait(1.0) - f = self.server._state.get() - if f: - self.send_response(200) - self.send_header("Content-Type", "image/jpeg") - self.send_header("Content-Length", str(len(f))) - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - self.wfile.write(f) - else: - self.send_response(503) - self.end_headers() - elif self.path == "/telemetry": - try: - with open("/tmp/telemetry.json", "r") as f: - data = f.read().encode() - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(data))) - self.send_header("Cache-Control", "no-cache") - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - self.wfile.write(data) - except: - self.send_response(503) - self.send_header("Content-Length", "0") - self.send_header("Connection", "close") - self.end_headers() - elif self.path == "/": - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.send_header("Content-Length", str(len(_OVERLAY_HTML))) - self.end_headers() - self.wfile.write(_OVERLAY_HTML.encode()) - else: - self.send_response(404) - self.end_headers() - - def log_message(self, *a): - pass - - -_state = None -_counter = 0 - -_state = None -_counter = 0 -_last_tel = 0.0 -_enc_queue = None -_app = None -_slot_pressed = [False, False] -_inject_logged = False - - - -def _ws_accept(key): - guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - return base64.b64encode(hashlib.sha1((key + guid).encode()).digest()).decode() - -def _ws_read_frame(rfile): - hdr = rfile.read(2) - if len(hdr) < 2: - return None, None - b0, b1 = hdr[0], hdr[1] - opcode = b0 & 0x0F - ln = b1 & 0x7F - if ln == 126: - e = rfile.read(2) - if len(e) < 2: return None, None - ln = struct.unpack(">H", e)[0] - elif ln == 127: - e = rfile.read(8) - if len(e) < 8: return None, None - ln = struct.unpack(">Q", e)[0] - mask = rfile.read(4) if (b1 & 0x80) else b"" - payload = rfile.read(ln) if ln else b"" - if mask and len(mask) == 4: - payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) - return opcode, payload - -def _ws_send_frame(wfile, opcode, payload=b""): - hdr = bytes([0x80 | opcode]) - ln = len(payload) - if ln < 126: - hdr += bytes([ln]) - elif ln < 65536: - hdr += bytes([126]) + struct.pack(">H", ln) - else: - hdr += bytes([127]) + struct.pack(">Q", ln) - wfile.write(hdr + payload) - wfile.flush() - -def _inject_event(kind, x, y, slot): - global _app, _slot_pressed - app = _app - if app is None: - return - try: - from openpilot.system.ui.lib.application import MouseEvent, MousePos - mouse = app._mouse - w = float(app.width) - h = float(app.height) - px = max(0.0, min(w, float(x) * w)) - py = max(0.0, min(h, float(y) * h)) - now = time.monotonic() - slot = 0 if int(slot) % 2 == 0 else 1 - pressed = _slot_pressed[slot] - if kind == "down": - if pressed: return - ev = MouseEvent(MousePos(px, py), slot, True, False, True, now) - _slot_pressed[slot] = True - elif kind == "move": - if not pressed: return - ev = MouseEvent(MousePos(px, py), slot, False, False, True, now) - elif kind == "up": - if not pressed: return - ev = MouseEvent(MousePos(px, py), slot, False, True, False, now) - _slot_pressed[slot] = False - else: - return - with mouse._lock: - mouse._events.append(ev) - global _inject_logged - if not _inject_logged: - _inject_logged = True - try: - from openpilot.common.swaglog import cloudlog - cloudlog.warning("remote UI input active (first event)") - except Exception: - pass - except Exception as e: - try: - from openpilot.common.swaglog import cloudlog - cloudlog.error("remote input error: " + str(e)) - except Exception: - pass - -def _handle_ws(handler): - try: - key = handler.headers.get("Sec-WebSocket-Key", "") - if not key: - return - handler.send_response(101, "Switching Protocols") - handler.send_header("Upgrade", "websocket") - handler.send_header("Connection", "Upgrade") - handler.send_header("Sec-WebSocket-Accept", _ws_accept(key)) - handler.end_headers() - handler.wfile.flush() - while True: - opcode, payload = _ws_read_frame(handler.rfile) - if opcode is None: - break - if opcode == 0x8: - break - if opcode == 0x9: - _ws_send_frame(handler.wfile, 0xA, payload) - continue - if opcode == 0x1: - try: - msg = json.loads(payload.decode("utf-8", "replace")) - t = msg.get("t") - if t in ("down", "move", "up"): - _inject_event(t, float(msg.get("x", 0.0)), float(msg.get("y", 0.0)), int(msg.get("i", 0))) - except Exception: - pass - except Exception: - pass - -def _encode_worker(): - while True: - item = _enc_queue.get() - if item is None: - break - raw, w, h, quality = item - try: - img = Image.frombytes("RGBA", (w, h), raw).transpose(Image.FLIP_TOP_BOTTOM).convert("RGB") - max_width = int(os.getenv("STREAM_MAX_WIDTH", "540")) - if img.width > max_width: - img.thumbnail((max_width, max_width * img.height // img.width), Image.Resampling.BILINEAR) - buf = io.BytesIO() - img.save(buf, "JPEG", quality=quality) - if _state is not None: - _state.update(buf.getvalue()) - except Exception: - pass - - -def start(port=8082): - """Start the MJPEG HTTP server + JPEG encode worker in background threads.""" - global _state, _enc_queue - _state = StreamState() - _enc_queue = queue.Queue(maxsize=3) - threading.Thread(target=_encode_worker, daemon=True).start() - srv = ThreadingHTTPServer(("0.0.0.0", port), StreamHandler) - srv._state = _state - threading.Thread(target=srv.serve_forever, daemon=True).start() - return _state - - -def _write_telemetry(): - global _last_tel - now = time.time() - if now - _last_tel < 0.25: - return - _last_tel = now - try: - from openpilot.selfdrive.ui.ui_state import ui_state as us - sm = us.sm - except Exception: - return - data = {} - try: - cs = sm["carState"] - data["vEgo"] = round(float(cs.vEgo), 2) - data["aEgo"] = round(float(cs.aEgo), 2) - data["gas"] = 100 if cs.gasPressed else 0 - data["brake"] = 100 if cs.brakePressed else 0 - except Exception: - pass - try: - ss = sm["selfdriveState"] - data["cruiseEnabled"] = bool(ss.enabled) - data["driveState"] = "active" if ss.enabled else ("standby" if getattr(us, "started", False) else "off") - vc = float(ss.vCruiseCluster if ss.vCruiseCluster > 0 else ss.vCruise) - if vc > 0: - data["setSpeed"] = int(round(vc * 2.23694)) # mph - except Exception: - pass - try: - mv = sm["modelV2"] - if len(mv.lead) > 0: - data["leadDist"] = round(float(mv.lead[0].dRel), 1) # meters - data["modelExec"] = int(round(float(mv.modelExecutionTime) * 1000.0)) # ms - except Exception: - pass - try: - dev = sm["deviceState"] - temps = [float(t) for t in dev.cpuTempC] - if temps: - data["cpuTemp"] = int(round(max(temps))) - cpus = [int(c) for c in dev.cpuUsagePercent] - if cpus: - data["cpuUsage"] = max(cpus) - data["memUsed"] = int(dev.memoryUsagePercent) - except Exception: - pass - try: - with open("/tmp/telemetry.json", "w") as f: - json.dump(data, f) - except Exception: - pass - - -def capture_frame(app, quality=50, target_fps=10): - global _app - if _app is None: - _app = app - """Call from the render loop: snapshot texture, queue raw for async JPEG encode.""" - global _counter - if _state is None or app._render_texture is None or not _state.has_demand(): - return - _counter += 1 - skip = max(1, int(getattr(app, "_target_fps", 60)) // max(1, int(target_fps))) - if _counter % skip != 0: - return - _write_telemetry() - try: - si = rl.load_image_from_texture(app._render_texture.texture) - w, h = si.width, si.height - raw = bytes(rl.ffi.buffer(si.data, w * h * 4)) - rl.unload_image(si) - if _enc_queue is not None: - try: - _enc_queue.put_nowait((raw, w, h, int(quality))) - except Exception: - pass - except Exception: - pass