Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit cc7f84f

Browse files
authored
Merge pull request #5 from WiseLabCMU/nicegui-control-plane
Nicegui control plane
2 parents d75bdae + 0348ea0 commit cc7f84f

14 files changed

Lines changed: 558 additions & 0 deletions

python/control_plane/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# AllSpark Control Plane
2+
3+
This directory contains the NiceGUI-based reactive control plane for the AllSpark Edge Server.
4+
5+
The control plane runs as a decoupled **sidecar** UI alongside the core edge network server. This separation ensures the high-throughput WebSocket operations of the main node remain unaffected by the UI data rendering.
6+
7+
## Setup Requirements
8+
9+
The control plane shares dependencies with the Python Edge Server. Ensure all dependencies are installed:
10+
11+
```bash
12+
cd ../ # Navigate up to the python edge server root
13+
pip install -r requirements.txt
14+
```
15+
16+
> **Note**: The core edge server must run to generate the default `config.json` before the control plane can read the settings correctly.
17+
18+
## Running the Servers (Order of Operations)
19+
20+
Because the control plane binds asynchronously and depends on the main edge configuration and API (`/api/status`), you should boot up the edge server **first**.
21+
22+
**Step 1. Run the Core Edge Server**
23+
In your first terminal:
24+
```bash
25+
cd allspark_agent/edge_server/python
26+
python server.py
27+
```
28+
*You should see output indicating it is running on `http://0.0.0.0:8080` (and `wss://...`). If this is the first run, it will automatically generate a default `config.json` in the `edge_server/` root.*
29+
30+
**Step 2. Run the Control Plane Sidecar**
31+
In a second terminal:
32+
```bash
33+
cd allspark_agent/edge_server/python
34+
python control_plane/main.py
35+
```
36+
*NiceGUI will automatically read your `config.json` and start the remote dashboard on `http://127.0.0.1:8081` (Edge Server Port + 1).*
37+
38+
**Step 3. (Optional) Run the Mock Rerun.io Viewer**
39+
To populate the `/rerun` visualization iframe in the control plane dashboard with dummy data until the true data plane is integrated, launch the rerun mock script in a third terminal:
40+
```bash
41+
cd allspark_agent/edge_server/python
42+
python control_plane/dummy_rerun_server.py
43+
```
44+
*This will spin up a local rerun web viewer on port `9090` which the control plane will iFrame natively.*
45+
46+
## Key Features
47+
48+
1. **Reactive Dashboard:** Monitors your local MQTT anomaly topic streams on `127.0.0.1:1883` in real-time.
49+
2. **Client Health UI:** Actively polls the `/api/status` endpoint to display connected mobile rigs and trigger on-demand data uploads.
50+
3. **Capture Browser:** Traverses the local `.quic`, `.mp4`, and `.json` files inside your edge upload directory and streams them natively to the browser.
51+
4. **Agent Testing Stub:** Quick shortcut for sending anomalies to the main Agentic framework for resolution.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# AllSpark Edge Control Plane - Architecture & Design Decisions
2+
3+
This document captures the core architectural choices and design patterns implemented for the AllSpark Edge Control Plane.
4+
5+
## 1. Framework Selection: NiceGUI
6+
**Decision:** We chose [NiceGUI](https://nicegui.io/) over traditional decoupled SPA frameworks (like React, Vue, or Angular) or heavy template rendering (like Django/Jinja).
7+
**Rationale:**
8+
- **Python-Native:** NiceGUI allows the entire frontend and backend logic to reside in pure Python. This ensures that the AI/backend engineers maintaining the AllSpark ecosystem can easily extend the UI without needing specialized frontend context switching.
9+
- **Reactive Data Binding:** It provides Vue-like reactivity natively in Python, making it trivial to auto-update lists, tables, and graphs without writing websocket/polling boilerplate manually.
10+
11+
## 2. The Sidecar Architecture Pattern
12+
**Decision:** The control plane executes as a completely detached **Sidecar Process** (`python control_plane/main.py`) rather than being tightly integrated into the main `aiohttp` edge server (`server.py`).
13+
**Rationale:**
14+
- **Isolation of Concerns:** The primary Edge Server manages high-frequency WebSocket streams, large QUIC video blob uploads, and Bonjour service discovery. Keeping the UI rendering and polling decoupled ensures that an expensive UI redraw or long-running query doesn't block the asyncio event loop handling critical edge ingestion.
15+
- **Port Offset:** The sidecar automatically reads the Edge Server's configured port (e.g., `8080`) from `config.json` and binds itself to `port + 1` (e.g., `8081`), ensuring no port collisions while remaining predictable.
16+
17+
## 3. Single Source of Truth Configuration
18+
**Decision:** Both the Edge Server and the Sidecar UI read from a unified `config.json` in the root `edge_server/` directory.
19+
**Rationale:**
20+
- **Bootstrapping:** Whichever service boots first (typically `server.py` or the `node` equivalent) checks for `config.json`. If it's missing, it dynamically generates it with internal defaults.
21+
- **Synchronization:** The control plane reliably knows exactly where the `uploadPath` is located, what IP constraints exist, and what the base port is without duplicated environment variables.
22+
23+
## 4. Integration Strategies for Edge Data
24+
25+
We utilized three distinct strategies for populating the reactive UI, optimizing for speed and footprint:
26+
27+
### A. Polling for Client State (REST API)
28+
- **Method:** `aiohttp` loop polling `/api/status` via `ui.timer(5.0)`.
29+
- **Reasoning:** Rather than constructing an inter-process WebSocket bridge between the Edge Server and the Sidecar to sync mobile rig connections, the sidecar leverages the Edge Server's existing lightweight REST API.
30+
31+
### B. Shared File Mounts (Capture Browser)
32+
- **Method:** `os.path` and `glob` traversal to `app.add_media_files()`.
33+
- **Reasoning:** Instead of creating file transfer endpoints, the Sidecar reads the absolute system path defined in `config.json` (`uploads/orgs/default/...`). It dynamically builds UI cards based on what exists on disk in real-time, allowing native HTML5 `video` playback over HTTP immediately.
34+
35+
### C. Direct Broker Attachment (MQTT Anomalies)
36+
- **Method:** Background `paho-mqtt` thread processing wildcard topics (`#`).
37+
- **Reasoning:** System anomalies generated by AllSpark agents bypass the core Edge Server entirely and hit the local MQTT broker (`1883`). The control plane runs a dedicated Paho subscriber thread that captures, structures, and limits anomaly events in memory, pushing updates to the UI natively.
38+
39+
## 5. Rerun.io Data Plane Mocking
40+
**Decision:** Integrated an `iframe` pointing to a local `rerun-sdk` web viewer (`http://localhost:9090`).
41+
**Rationale:** Allows the control plane to seamlessly wrap complex, high-performance rust-rendered robotics 3D visualizations inside the standard Python UI workflow until a unified authentication and embedding pipeline is established.
42+
43+
---
44+
45+
## Next Steps & Future Work
46+
47+
While Phase 1 and 2 established the decoupled control plane, the following steps are planned for the evolution of the AllSpark Edge architecture:
48+
49+
1. **Native Integration (Merging the Sidecar):**
50+
- Although the detached Sidecar pattern (running on `port + 1`) is currently used to guarantee the Edge Server's critical event loop remains unblocked, the ultimate goal is to **natively integrate** the control plane into the main Edge Server application. This will unify the deployment footprint and eliminate the need to run two separate Python processes.
51+
52+
2. **Real Rerun.io Server Integration:**
53+
- Replace the `dummy_rerun_server.py` mock with the actual `rerun-sdk` data integration pipeline. This involves piping the live telemetry and `.quic` video streams directly into the native Rerun data plane for real-time 3D and spatial debugging.
54+
55+
3. **Expanded Configuration & Data Discovery:**
56+
- Extend the `config.json` schema to encompass a broader range of telemetry locations and application logs.
57+
- Update `pages/capture.py` to ingest and parse these diverse logs globally, providing a centralized diagnostic view beyond just the MQTT streams and media uploads.
58+
59+
4. **Agentic Framework Hookup:**
60+
- Replace the UI stubs in `pages/agent.py` with actual programmatic calls to the underlying AllSpark Agentic nodes. This will allow the control plane to seamlessly dispatch identified MQTT anomalies directly to the Vertex/LLM-Farm agents and display their diagnostic reports dynamically within the dashboard.
61+
62+
5. **Unified Security & Authentication:**
63+
- Sync the SSL context and authentication layers from the main Edge app into the control plane to ensure that remote debugging over the network is properly secured with JWT or token-based guards.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
A lightweight dummy script to run a rerun.io web viewer for the AllSpark dashboard demo.
3+
Requirements: pip install rerun-sdk
4+
"""
5+
import rerun as rr
6+
import rerun.blueprint as rrb
7+
import time
8+
import math
9+
10+
def main():
11+
# Initialize the rerun SDK and set it to serve over WebSocket + HTTP mapping for web viewer
12+
rr.init("AllSpark_Demo", spawn=False)
13+
14+
# Create the web viewer
15+
rr.serve_web_viewer(web_port=9090, open_browser=False)
16+
17+
print("Dummy Rerun Server running at http://127.0.0.1:9090")
18+
print("Populating example dummy data...")
19+
20+
# Try sending initial blueprint and some data
21+
rr.send_blueprint(rrb.Blueprint(
22+
rrb.Horizontal(
23+
rrb.Spatial2DView(name="Camera View", origin="camera"),
24+
rrb.TextDocumentView(name="Agent Response", origin="agent_response")
25+
)
26+
))
27+
28+
# Static UI init
29+
rr.log("agent_response", rr.TextDocument("AllSpark Agentic Analysis Results\n\nResults from using the AllSpark Agentic Framework will appear here."))
30+
31+
i = 0
32+
try:
33+
while True:
34+
# Removed time sequence for backward/forward compatibility
35+
# rr.set_time_sequence("frame", i)
36+
# Log a simple moving point payload
37+
rr.log("camera/tracked_point", rr.Points2D([[math.sin(i * 0.1) * 10, math.cos(i * 0.1) * 10]], colors=[255, 0, 0]))
38+
39+
time.sleep(0.1)
40+
i += 1
41+
except KeyboardInterrupt:
42+
print("Shutting down dummy rerun server.")
43+
44+
if __name__ == "__main__":
45+
main()

python/control_plane/main.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from nicegui import app, ui
2+
import os
3+
4+
# Import all pages to register their routes
5+
from pages import dashboard, clients, capture, agent, rerun_view, settings
6+
7+
# Initialize pages
8+
dashboard.create_page()
9+
clients.create_page()
10+
capture.create_page()
11+
agent.create_page()
12+
rerun_view.create_page()
13+
settings.create_page()
14+
15+
if __name__ in {"__main__", "__mp_main__"}:
16+
# Read port from config.json and start NiceGUI on config['port'] + 1 (default 8081).
17+
config = settings.load_config()
18+
edge_port = config.get('port', 8080)
19+
sidecar_port = edge_port + 1
20+
21+
# Mount the dynamic video storage directory for browser playback
22+
upload_path = config.get('uploadPath', 'uploads/orgs/default')
23+
abs_upload_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', upload_path))
24+
os.makedirs(abs_upload_path, exist_ok=True)
25+
app.add_media_files('/videos', abs_upload_path)
26+
27+
# Run the control plane
28+
ui.run(title='AllSpark Control Plane', port=sidecar_port, storage_secret='allspark-secret')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Makes pages a module
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from nicegui import ui
2+
from theme import menu
3+
4+
def create_page():
5+
@ui.page('/agent')
6+
def agent_page():
7+
with menu('Agentic Framework Control'):
8+
9+
with ui.row().classes('w-full gap-6'):
10+
with ui.column().classes('w-1/3'):
11+
ui.label('Investigate Issue').classes('text-xl font-bold mb-4')
12+
ui.select(['Latency > 500ms (Camera Rig A)', 'Connection Dropped (Mobile Client B)'], label='Target Anomaly').classes('w-full mb-4')
13+
ui.textarea(label='Context / Prompt', value='Analyze the provided QUIC videos and MQTT logs to determine why the latency spiked over 500ms on Rig A.').classes('w-full h-32 mb-4')
14+
ui.button('Execute Investigation', icon='science', on_click=lambda: ui.notify('Agent investigation dispatched! Context sent to agentic frameworks.')).classes('w-full bg-blue-600')
15+
16+
with ui.column().classes('flex-1 w-full'):
17+
ui.label('Recent Responses').classes('text-xl font-bold mb-4')
18+
19+
with ui.card().classes('w-full bg-gray-50'):
20+
with ui.row().classes('w-full justify-between mb-2'):
21+
ui.label('Analysis: Issue 742 - Camera Frame Drop').classes('font-bold')
22+
ui.label('2026-03-24 14:02:11').classes('text-xs text-gray-500')
23+
ui.markdown('''
24+
**Agent Summary:**
25+
I have reviewed the logs between 14:00 and 14:05. The frame drop was caused by an underlying network buffer overflow on the edge interface when `agent_client` transmitted a high-frequency telemetry burst.
26+
27+
I have generated a visualization of the synchronized data streams.
28+
''')
29+
ui.button('View in Rerun.io', icon='open_in_new', on_click=lambda: ui.navigate.to('/rerun')).classes('mt-2')
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from nicegui import ui
2+
import os
3+
import glob
4+
from theme import menu
5+
from pages.settings import load_config
6+
7+
def create_page():
8+
@ui.page('/capture')
9+
def capture_page():
10+
config = load_config()
11+
upload_path = config.get('uploadPath', 'uploads/orgs/default')
12+
abs_upload_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', upload_path))
13+
14+
with menu('Data Capture'):
15+
ui.label('Logs & QUIC Videos Review').classes('text-xl font-bold mb-4')
16+
17+
with ui.tabs().classes('w-full') as tabs:
18+
videos_tab = ui.tab('Videos')
19+
logs_tab = ui.tab('Logs')
20+
21+
with ui.tab_panels(tabs, value=videos_tab).classes('w-full bg-transparent'):
22+
with ui.tab_panel(videos_tab):
23+
with ui.row().classes('w-full gap-4 flex-wrap mt-2'):
24+
if os.path.exists(abs_upload_path):
25+
files = glob.glob(os.path.join(abs_upload_path, '*.*'))
26+
vid_files = [f for f in files if f.endswith(('.mp4', '.quic', '.json'))]
27+
if not vid_files:
28+
ui.label('No capture files found in upload path yet.').classes('text-gray-500 italic p-4 mt-2')
29+
30+
for f in sorted(vid_files, reverse=True):
31+
filename = os.path.basename(f)
32+
size_mb = os.path.getsize(f) / (1024 * 1024)
33+
with ui.card().classes('w-64 relative'):
34+
if filename.endswith('.mp4'):
35+
ui.video(f'/videos/{filename}').classes('w-full h-32 bg-black object-contain')
36+
elif filename.endswith('.quic'):
37+
ui.html(f'<div class="w-full text-center p-8 bg-blue-100 text-blue-500 rounded"><i class="fas fa-video fa-2x"></i><br><b>.QUIC</b></div>')
38+
else:
39+
ui.html(f'<div class="w-full text-center p-8 bg-gray-100 text-gray-500 rounded"><i class="fas fa-file fa-2x"></i><br><b>.JSON</b></div>')
40+
41+
ui.label(filename).classes('font-bold mt-2 truncate max-w-full').tooltip(filename)
42+
ui.label(f'Size: {size_mb:.2f} MB').classes('text-sm text-gray-500')
43+
# Create a simple direct download link or trigger notify for quic
44+
ui.button('Play' if filename.endswith('.mp4') else 'Download',
45+
icon='play_arrow' if filename.endswith('.mp4') else 'download',
46+
on_click=lambda fn=filename: ui.download(f'/videos/{fn}')).classes('w-full mt-2')
47+
else:
48+
ui.label('Upload directory not created yet. Awaiting initial mobile rig connection.').classes('text-gray-500 italic')
49+
50+
with ui.tab_panel(logs_tab):
51+
ui.label('Recent Edge Logs (simulated tail)').classes('font-bold mb-2')
52+
log_text = "[2026-03-25 10:15:01] INFO - Connected to rig_alpha\n" \
53+
"[2026-03-25 10:15:05] WARN - Latency spike detected: 154ms\n" \
54+
"[2026-03-25 10:16:22] ERROR - Disconnected from rig_beta\n" \
55+
"[2026-03-25 10:16:45] INFO - Reconnected to rig_beta"
56+
ui.textarea(value=log_text).props('readonly').classes('w-full font-mono text-sm bg-black text-green-400 p-2 rounded h-64')

0 commit comments

Comments
 (0)