Skip to main content

SSE & Real-time

EagleEye uses Server-Sent Events (SSE) to push real-time updates to the WebUI. The SSE stream is served at GET /sse/stream as a persistent text/event-stream HTTP response.

Queue architecture

A single queue.Queue buffers events for the connected client:

self._sse_queue: queue.Queue | None = None
self._sse_queue_lock = threading.Lock()

When a client connects to /sse/stream, _sse_stream() creates a fresh queue.Queue(maxsize=100) and installs it as the queue (last connection wins; only one active SSE connection is supported). On connect it also replays cached pipeline operation errors and any cached system-update progress.

The generator blocks on q.get(timeout=1.0); on timeout it yields a : keepalive comment so proxies do not close the connection. Messages are formatted by _format_sse:

event: <event_name>\ndata: <json_payload>\n\n

_publish_event(name, data) JSON-serializes with allow_nan=False (serialization failures are logged at most once every 5 s and dropped) and uses put_nowait. When the queue is full it drops the oldest event and retries, logging the drop.

Named events

EventPublished byRatePayload
heartbeat_sse_heartbeat_loopEvery 5 s{"ts": float}
log_update_log_monitor_loopOn new log lines (polls every 0.1 s){"messages": [...]} — all formatted log lines
system_status_system_status_loopEvery 1.5 sNested dicts: cpu, memory, storage, pipelines, network_table
profiling_update_sse_heartbeat_loop via _publish_profiling_updatesAt most every 0.3 s per pipeline, only for a new frame_seqProfiling snapshot (below)
pipeline_operation_errors_sse_heartbeat_loop via _publish_cached_pipeline_errors, and on config writesWhen the error cache is dirtyNormalized per-pipeline operation error payload
system_update_progressSystem update routinesDuring an updateProgress payload (replayed to new clients)
mx3_compilation_progressmodel_library_mixinDuring MX3 compilationMx3CompilationStatus.to_dict(log_limit=5)
update_robot_transformupdate_robot_transform()On call{"transform_matrix": [...]}
update_camera_poseupdate_camera_pose()On call{"camera_bus_id": str, "camera_name": str, ...}
update_detected_objectsupdate_detected_objects()On call{"detections": [...]}

Background threads

Three daemon threads are started in EagleEyeInterface.__init__():

Heartbeat thread

_sse_heartbeat_loop wakes every min(profiling_interval, 0.1) seconds. On each pass it publishes cached pipeline errors and pending profiling snapshots, and emits heartbeat when 5 s have elapsed. Profiling and error events therefore originate on this thread, not on pipeline threads.

Log monitor thread

_log_monitor_loop polls logger.message_history every 0.1 s and publishes log_update when the message count grows.

System status thread

_system_status_loop builds the payload with psutil every 1.5 s. If psutil fails, cpu/memory/storage are replaced with {"status": "unavailable", "error": ...} and the failure is logged once.

system_status payload shape

{
"cpu": {"percent": 12.5, "cores": 8, "temperature_c": 46.0, "status": "ok"},
"memory": {"percent": 41.2, "used_mb": 6600.0, "total_mb": 16000.0, "status": "ok"},
"storage": {"percent": 55.0, "used_gb": 110.0, "total_gb": 200.0, "status": "ok"},
"pipelines": [],
"network_table": {}
}

CPU temperature is read from /sys/class/thermal/thermal_zone*/temp when available, otherwise from psutil sensors, and may be null.

Pipeline error events

Pipeline construction and config writes populate an error cache. _publish_cached_pipeline_errors uses _pipeline_error_dirty_pipelines and _pipeline_error_last_seq_sent so each unique error is sent once per sequence number, and re-sends the whole cache when a new SSE client connects.

Pipeline profiling events

FlowManager._record_profile_snapshot() captures per-operation and per-timestep wall-clock runtimes after each frame and stores the latest snapshot under a lock. _publish_profiling_updates reads pipeline.get_latest_profile_snapshot() for every pipeline, skips snapshots whose frame_seq is not greater than the last sent, and emits profiling_update.

Profiling snapshot shape

{
"pipeline_name": "Test",
"frame_seq": 42,
"frame_time_ms": 18.3,
"timestamp_ms": 1706000000000,
"operations": {
"op-mljk5q36-arwb": {
"name": "device_input.py",
"timestep": 0,
"thread": 1,
"execution_time_ms": 2.1
}
},
"timesteps": [
{
"timestep": 0,
"total_time_ms": 3.5,
"max_operation_uuid": "op-mljk5q36-arwb",
"max_operation_name": "device_input.py",
"max_operation_time_ms": 2.1,
"operation_count": 1
}
]
}

CORS

CORS_ALLOWED_ORIGINS in src/webui/web_server_utils/constants.py is "*", so any origin may connect. This allows the Vite dev server on another port to reach the backend during development; in production the UI and API share http://<host>:5001.