462 lines
19 KiB
Python
462 lines
19 KiB
Python
"""
|
|
Local Dashboard Tab - Direct InfluxDB visualization without Grafana
|
|
"""
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout,
|
|
QGroupBox, QComboBox, QPushButton, QFrame, QScrollArea
|
|
)
|
|
from PySide6.QtCore import QTimer, QThread, Signal, QObject
|
|
from PySide6.QtGui import QFont, QPalette, QColor
|
|
from datetime import datetime, timedelta
|
|
import time
|
|
|
|
# Try to import optional dependencies
|
|
try:
|
|
from influxdb_client import InfluxDBClient, Point, WritePrecision
|
|
INFLUXDB_AVAILABLE = True
|
|
except ImportError:
|
|
print("⚠️ InfluxDB client not available. Dashboard will show placeholder data.")
|
|
INFLUXDB_AVAILABLE = False
|
|
|
|
try:
|
|
import pyqtgraph as pg
|
|
import numpy as np
|
|
PYQTGRAPH_AVAILABLE = True
|
|
except ImportError:
|
|
print("⚠️ PyQtGraph not available. Dashboard will use basic widgets.")
|
|
PYQTGRAPH_AVAILABLE = False
|
|
|
|
# InfluxDB Configuration
|
|
INFLUX_URL = "http://localhost:8086"
|
|
INFLUX_TOKEN = "kNFfXEpPQoWrk5Tteowda21Dzv6xD3jY7QHSHHQHb5oYW6VH6mkAgX9ZMjQJkaHHa8FwzmyVFqDG7qqzxN09uQ=="
|
|
INFLUX_ORG = "smart-intersection-org"
|
|
INFLUX_BUCKET = "traffic_monitoring"
|
|
|
|
class InfluxDBQueryThread(QThread):
|
|
"""Background thread for querying InfluxDB"""
|
|
|
|
data_ready = Signal(str, list, list) # query_type, timestamps, values
|
|
error_occurred = Signal(str)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.client = None
|
|
self.query_api = None
|
|
self.running = True
|
|
self.queries = {}
|
|
|
|
def setup_connection(self):
|
|
"""Setup InfluxDB connection"""
|
|
if not INFLUXDB_AVAILABLE:
|
|
self.error_occurred.emit("InfluxDB client not available")
|
|
return False
|
|
try:
|
|
self.client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
|
|
self.query_api = self.client.query_api()
|
|
print("✅ InfluxDB Query Thread connected")
|
|
return True
|
|
except Exception as e:
|
|
print(f"❌ InfluxDB Query Thread connection failed: {e}")
|
|
self.error_occurred.emit(f"Connection failed: {e}")
|
|
return False
|
|
|
|
def add_query(self, query_type, flux_query):
|
|
"""Add a query to be executed"""
|
|
self.queries[query_type] = flux_query
|
|
|
|
def run(self):
|
|
"""Main thread loop"""
|
|
if not self.setup_connection():
|
|
return
|
|
|
|
while self.running:
|
|
for query_type, flux_query in self.queries.items():
|
|
try:
|
|
result = self.query_api.query(flux_query, org=INFLUX_ORG)
|
|
|
|
timestamps = []
|
|
values = []
|
|
|
|
for table in result:
|
|
for record in table.records:
|
|
timestamps.append(record.get_time())
|
|
values.append(record.get_value())
|
|
|
|
self.data_ready.emit(query_type, timestamps, values)
|
|
|
|
except Exception as e:
|
|
print(f"❌ Query error for {query_type}: {e}")
|
|
self.error_occurred.emit(f"Query error: {e}")
|
|
|
|
self.msleep(2000) # Update every 2 seconds
|
|
|
|
def stop(self):
|
|
"""Stop the thread"""
|
|
self.running = False
|
|
if self.client:
|
|
self.client.close()
|
|
|
|
class DashboardTab(QWidget):
|
|
"""Local Dashboard Tab with real-time charts"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setObjectName("DashboardTab")
|
|
self.setup_ui()
|
|
self.setup_charts()
|
|
self.setup_influxdb_queries()
|
|
|
|
# Data storage for charts
|
|
self.chart_data = {
|
|
'performance_fps': {'x': [], 'y': []},
|
|
'performance_latency': {'x': [], 'y': []},
|
|
'vehicle_count': {'x': [], 'y': []},
|
|
'traffic_light': {'x': [], 'y': []},
|
|
'violations': {'x': [], 'y': []}
|
|
}
|
|
|
|
# Start data fetching
|
|
self.start_data_fetching()
|
|
|
|
def setup_ui(self):
|
|
"""Setup the user interface"""
|
|
main_layout = QVBoxLayout(self)
|
|
main_layout.setContentsMargins(10, 10, 10, 10)
|
|
main_layout.setSpacing(10)
|
|
|
|
# Header
|
|
header_layout = QHBoxLayout()
|
|
|
|
title_label = QLabel("📊 Real-Time Dashboard")
|
|
title_font = QFont()
|
|
title_font.setPointSize(16)
|
|
title_font.setBold(True)
|
|
title_label.setFont(title_font)
|
|
|
|
# Time range selector
|
|
time_range_label = QLabel("Time Range:")
|
|
self.time_range_combo = QComboBox()
|
|
self.time_range_combo.addItems(["Last 5 minutes", "Last 15 minutes", "Last 30 minutes", "Last 1 hour"])
|
|
self.time_range_combo.setCurrentText("Last 15 minutes")
|
|
self.time_range_combo.currentTextChanged.connect(self.on_time_range_changed)
|
|
|
|
refresh_btn = QPushButton("🔄 Refresh")
|
|
refresh_btn.clicked.connect(self.refresh_all_charts)
|
|
|
|
header_layout.addWidget(title_label)
|
|
header_layout.addStretch()
|
|
header_layout.addWidget(time_range_label)
|
|
header_layout.addWidget(self.time_range_combo)
|
|
header_layout.addWidget(refresh_btn)
|
|
|
|
main_layout.addLayout(header_layout)
|
|
|
|
# Create scroll area for charts
|
|
scroll_area = QScrollArea()
|
|
scroll_widget = QWidget()
|
|
scroll_layout = QVBoxLayout(scroll_widget)
|
|
|
|
# Chart grid
|
|
chart_grid = QGridLayout()
|
|
|
|
# Performance Charts Group
|
|
performance_group = QGroupBox("🚀 Performance Metrics")
|
|
performance_layout = QHBoxLayout(performance_group)
|
|
|
|
self.fps_chart_widget = QWidget()
|
|
self.fps_chart_layout = QVBoxLayout(self.fps_chart_widget)
|
|
fps_label = QLabel("FPS")
|
|
fps_label.setStyleSheet("font-weight: bold; color: #00ff00;")
|
|
self.fps_chart_layout.addWidget(fps_label)
|
|
|
|
self.latency_chart_widget = QWidget()
|
|
self.latency_chart_layout = QVBoxLayout(self.latency_chart_widget)
|
|
latency_label = QLabel("Processing Time (ms)")
|
|
latency_label.setStyleSheet("font-weight: bold; color: #ff8c00;")
|
|
self.latency_chart_layout.addWidget(latency_label)
|
|
|
|
performance_layout.addWidget(self.fps_chart_widget)
|
|
performance_layout.addWidget(self.latency_chart_widget)
|
|
|
|
# Detection Charts Group
|
|
detection_group = QGroupBox("🚗 Detection Metrics")
|
|
detection_layout = QHBoxLayout(detection_group)
|
|
|
|
self.vehicle_chart_widget = QWidget()
|
|
self.vehicle_chart_layout = QVBoxLayout(self.vehicle_chart_widget)
|
|
vehicle_label = QLabel("Vehicle Count")
|
|
vehicle_label.setStyleSheet("font-weight: bold; color: #4da6ff;")
|
|
self.vehicle_chart_layout.addWidget(vehicle_label)
|
|
|
|
detection_layout.addWidget(self.vehicle_chart_widget)
|
|
|
|
# Traffic Light Group
|
|
traffic_group = QGroupBox("🚦 Traffic Light Status")
|
|
traffic_layout = QVBoxLayout(traffic_group)
|
|
|
|
self.traffic_chart_widget = QWidget()
|
|
self.traffic_chart_layout = QVBoxLayout(self.traffic_chart_widget)
|
|
traffic_label = QLabel("Traffic Light Color")
|
|
traffic_label.setStyleSheet("font-weight: bold; color: #ff6b6b;")
|
|
self.traffic_chart_layout.addWidget(traffic_label)
|
|
|
|
traffic_layout.addWidget(self.traffic_chart_widget)
|
|
|
|
# Violations Group
|
|
violations_group = QGroupBox("🚨 Violations")
|
|
violations_layout = QVBoxLayout(violations_group)
|
|
|
|
self.violations_chart_widget = QWidget()
|
|
self.violations_chart_layout = QVBoxLayout(self.violations_chart_widget)
|
|
violations_label = QLabel("Red Light Violations")
|
|
violations_label.setStyleSheet("font-weight: bold; color: #ff4757;")
|
|
self.violations_chart_layout.addWidget(violations_label)
|
|
|
|
violations_layout.addWidget(self.violations_chart_widget)
|
|
|
|
# Add groups to scroll layout
|
|
scroll_layout.addWidget(performance_group)
|
|
scroll_layout.addWidget(detection_group)
|
|
scroll_layout.addWidget(traffic_group)
|
|
scroll_layout.addWidget(violations_group)
|
|
scroll_layout.addStretch()
|
|
|
|
scroll_area.setWidget(scroll_widget)
|
|
scroll_area.setWidgetResizable(True)
|
|
main_layout.addWidget(scroll_area)
|
|
|
|
# Status bar
|
|
self.status_label = QLabel("🔄 Connecting to InfluxDB...")
|
|
self.status_label.setStyleSheet("color: #888; font-size: 12px;")
|
|
main_layout.addWidget(self.status_label)
|
|
|
|
def setup_charts(self):
|
|
"""Setup PyQtGraph charts or placeholder widgets"""
|
|
if not PYQTGRAPH_AVAILABLE:
|
|
self.setup_placeholder_charts()
|
|
return
|
|
|
|
# Configure PyQtGraph
|
|
pg.setConfigOptions(antialias=True, useOpenGL=True)
|
|
pg.setConfigOption('background', '#2b2b2b')
|
|
pg.setConfigOption('foreground', 'w')
|
|
|
|
# FPS Chart
|
|
self.fps_chart = pg.PlotWidget()
|
|
self.fps_chart.setLabel('left', 'FPS')
|
|
self.fps_chart.setLabel('bottom', 'Time')
|
|
self.fps_chart.setYRange(0, 30)
|
|
self.fps_chart.showGrid(x=True, y=True, alpha=0.3)
|
|
self.fps_plot = self.fps_chart.plot(pen=pg.mkPen('#00ff00', width=2))
|
|
self.fps_chart_layout.addWidget(self.fps_chart)
|
|
|
|
# Latency Chart
|
|
self.latency_chart = pg.PlotWidget()
|
|
self.latency_chart.setLabel('left', 'Processing Time (ms)')
|
|
self.latency_chart.setLabel('bottom', 'Time')
|
|
self.latency_chart.setYRange(0, 200)
|
|
self.latency_chart.showGrid(x=True, y=True, alpha=0.3)
|
|
self.latency_plot = self.latency_chart.plot(pen=pg.mkPen('#ff8c00', width=2))
|
|
self.latency_chart_layout.addWidget(self.latency_chart)
|
|
|
|
# Vehicle Count Chart
|
|
self.vehicle_chart = pg.PlotWidget()
|
|
self.vehicle_chart.setLabel('left', 'Vehicle Count')
|
|
self.vehicle_chart.setLabel('bottom', 'Time')
|
|
self.vehicle_chart.setYRange(0, 20)
|
|
self.vehicle_chart.showGrid(x=True, y=True, alpha=0.3)
|
|
self.vehicle_plot = self.vehicle_chart.plot(pen=pg.mkPen('#4da6ff', width=2), symbol='o')
|
|
self.vehicle_chart_layout.addWidget(self.vehicle_chart)
|
|
|
|
# Traffic Light Chart
|
|
self.traffic_chart = pg.PlotWidget()
|
|
self.traffic_chart.setLabel('left', 'Status')
|
|
self.traffic_chart.setLabel('bottom', 'Time')
|
|
self.traffic_chart.setYRange(0, 4)
|
|
self.traffic_chart.showGrid(x=True, y=True, alpha=0.3)
|
|
# Custom Y-axis labels for traffic light
|
|
self.traffic_chart.getAxis('left').setTicks([[(0, 'Unknown'), (1, 'Red'), (2, 'Yellow'), (3, 'Green')]])
|
|
self.traffic_plot = self.traffic_chart.plot(pen=pg.mkPen('#ff6b6b', width=3), symbol='s', symbolSize=8)
|
|
self.traffic_chart_layout.addWidget(self.traffic_chart)
|
|
|
|
# Violations Chart (Bar chart style)
|
|
self.violations_chart = pg.PlotWidget()
|
|
self.violations_chart.setLabel('left', 'Violations Count')
|
|
self.violations_chart.setLabel('bottom', 'Time')
|
|
self.violations_chart.setYRange(0, 10)
|
|
self.violations_chart.showGrid(x=True, y=True, alpha=0.3)
|
|
self.violations_plot = self.violations_chart.plot(pen=pg.mkPen('#ff4757', width=2), symbol='t', symbolSize=10)
|
|
self.violations_chart_layout.addWidget(self.violations_chart)
|
|
|
|
def setup_placeholder_charts(self):
|
|
"""Setup placeholder widgets when PyQtGraph is not available"""
|
|
# FPS placeholder
|
|
fps_placeholder = QLabel("📊 FPS Chart\n(Install PyQtGraph for real-time charts)")
|
|
fps_placeholder.setStyleSheet("border: 2px dashed #00ff00; padding: 20px; text-align: center; color: #00ff00; background: #1a1a1a;")
|
|
self.fps_chart_layout.addWidget(fps_placeholder)
|
|
|
|
# Latency placeholder
|
|
latency_placeholder = QLabel("📊 Latency Chart\n(Install PyQtGraph for real-time charts)")
|
|
latency_placeholder.setStyleSheet("border: 2px dashed #ff8c00; padding: 20px; text-align: center; color: #ff8c00; background: #1a1a1a;")
|
|
self.latency_chart_layout.addWidget(latency_placeholder)
|
|
|
|
# Vehicle placeholder
|
|
vehicle_placeholder = QLabel("📊 Vehicle Count Chart\n(Install PyQtGraph for real-time charts)")
|
|
vehicle_placeholder.setStyleSheet("border: 2px dashed #4da6ff; padding: 20px; text-align: center; color: #4da6ff; background: #1a1a1a;")
|
|
self.vehicle_chart_layout.addWidget(vehicle_placeholder)
|
|
|
|
# Traffic Light placeholder
|
|
traffic_placeholder = QLabel("📊 Traffic Light Chart\n(Install PyQtGraph for real-time charts)")
|
|
traffic_placeholder.setStyleSheet("border: 2px dashed #ff6b6b; padding: 20px; text-align: center; color: #ff6b6b; background: #1a1a1a;")
|
|
self.traffic_chart_layout.addWidget(traffic_placeholder)
|
|
|
|
# Violations placeholder
|
|
violations_placeholder = QLabel("📊 Violations Chart\n(Install PyQtGraph for real-time charts)")
|
|
violations_placeholder.setStyleSheet("border: 2px dashed #ff4757; padding: 20px; text-align: center; color: #ff4757; background: #1a1a1a;")
|
|
self.violations_chart_layout.addWidget(violations_placeholder)
|
|
|
|
def setup_influxdb_queries(self):
|
|
"""Setup InfluxDB queries"""
|
|
if not INFLUXDB_AVAILABLE:
|
|
return
|
|
|
|
self.query_thread = InfluxDBQueryThread()
|
|
self.query_thread.data_ready.connect(self.on_data_received)
|
|
self.query_thread.error_occurred.connect(self.on_error_occurred)
|
|
|
|
# Define Flux queries
|
|
time_range = self.get_time_range()
|
|
|
|
# Performance Queries
|
|
fps_query = f'''
|
|
from(bucket: "{INFLUX_BUCKET}")
|
|
|> range(start: {time_range})
|
|
|> filter(fn: (r) => r._measurement == "performance")
|
|
|> filter(fn: (r) => r._field == "fps")
|
|
|> aggregateWindow(every: 10s, fn: mean, createEmpty: false)
|
|
|> yield(name: "fps")
|
|
'''
|
|
|
|
latency_query = f'''
|
|
from(bucket: "{INFLUX_BUCKET}")
|
|
|> range(start: {time_range})
|
|
|> filter(fn: (r) => r._measurement == "performance")
|
|
|> filter(fn: (r) => r._field == "processing_time_ms")
|
|
|> aggregateWindow(every: 10s, fn: mean, createEmpty: false)
|
|
|> yield(name: "latency")
|
|
'''
|
|
|
|
# Vehicle Count Query
|
|
vehicle_query = f'''
|
|
from(bucket: "{INFLUX_BUCKET}")
|
|
|> range(start: {time_range})
|
|
|> filter(fn: (r) => r._measurement == "detection_events")
|
|
|> filter(fn: (r) => r._field == "vehicle_count")
|
|
|> aggregateWindow(every: 30s, fn: mean, createEmpty: false)
|
|
|> yield(name: "vehicles")
|
|
'''
|
|
|
|
# Traffic Light Query
|
|
traffic_query = f'''
|
|
from(bucket: "{INFLUX_BUCKET}")
|
|
|> range(start: {time_range})
|
|
|> filter(fn: (r) => r._measurement == "traffic_light_status")
|
|
|> filter(fn: (r) => r._field == "color_numeric")
|
|
|> last()
|
|
|> yield(name: "traffic_light")
|
|
'''
|
|
|
|
# Violations Query
|
|
violations_query = f'''
|
|
from(bucket: "{INFLUX_BUCKET}")
|
|
|> range(start: {time_range})
|
|
|> filter(fn: (r) => r._measurement == "violation_events")
|
|
|> filter(fn: (r) => r.violation_type == "red_light_violation")
|
|
|> aggregateWindow(every: 1m, fn: count, createEmpty: false)
|
|
|> yield(name: "violations")
|
|
'''
|
|
|
|
# Add queries to thread
|
|
self.query_thread.add_query("fps", fps_query)
|
|
self.query_thread.add_query("latency", latency_query)
|
|
self.query_thread.add_query("vehicles", vehicle_query)
|
|
self.query_thread.add_query("traffic_light", traffic_query)
|
|
self.query_thread.add_query("violations", violations_query)
|
|
|
|
def start_data_fetching(self):
|
|
"""Start fetching data from InfluxDB"""
|
|
if not INFLUXDB_AVAILABLE:
|
|
self.status_label.setText("⚠️ InfluxDB client not available - Install influxdb-client package")
|
|
return
|
|
|
|
self.query_thread.start()
|
|
self.status_label.setText("✅ Connected to InfluxDB - Real-time data streaming")
|
|
|
|
def get_time_range(self):
|
|
"""Get time range based on combo box selection"""
|
|
time_ranges = {
|
|
"Last 5 minutes": "-5m",
|
|
"Last 15 minutes": "-15m",
|
|
"Last 30 minutes": "-30m",
|
|
"Last 1 hour": "-1h"
|
|
}
|
|
return time_ranges.get(self.time_range_combo.currentText(), "-15m")
|
|
|
|
def on_time_range_changed(self):
|
|
"""Handle time range change"""
|
|
self.setup_influxdb_queries()
|
|
self.refresh_all_charts()
|
|
|
|
def on_data_received(self, query_type, timestamps, values):
|
|
"""Handle data received from InfluxDB"""
|
|
if not timestamps or not values or not PYQTGRAPH_AVAILABLE:
|
|
return
|
|
|
|
# Convert timestamps to seconds since epoch for plotting
|
|
x_data = [int(ts.timestamp()) for ts in timestamps]
|
|
y_data = values
|
|
|
|
# Update chart data
|
|
if query_type == "fps":
|
|
self.chart_data['performance_fps'] = {'x': x_data, 'y': y_data}
|
|
self.fps_plot.setData(x_data, y_data)
|
|
|
|
elif query_type == "latency":
|
|
self.chart_data['performance_latency'] = {'x': x_data, 'y': y_data}
|
|
self.latency_plot.setData(x_data, y_data)
|
|
|
|
elif query_type == "vehicles":
|
|
self.chart_data['vehicle_count'] = {'x': x_data, 'y': y_data}
|
|
self.vehicle_plot.setData(x_data, y_data)
|
|
|
|
elif query_type == "traffic_light":
|
|
self.chart_data['traffic_light'] = {'x': x_data, 'y': y_data}
|
|
self.traffic_plot.setData(x_data, y_data)
|
|
|
|
elif query_type == "violations":
|
|
self.chart_data['violations'] = {'x': x_data, 'y': y_data}
|
|
self.violations_plot.setData(x_data, y_data)
|
|
|
|
# Update status
|
|
self.status_label.setText(f"📈 Last updated: {datetime.now().strftime('%H:%M:%S')} - Query: {query_type}")
|
|
|
|
def on_error_occurred(self, error_msg):
|
|
"""Handle errors from InfluxDB"""
|
|
self.status_label.setText(f"❌ Error: {error_msg}")
|
|
|
|
def refresh_all_charts(self):
|
|
"""Refresh all charts"""
|
|
if INFLUXDB_AVAILABLE:
|
|
self.setup_influxdb_queries()
|
|
self.status_label.setText("🔄 Refreshing charts...")
|
|
else:
|
|
self.status_label.setText("⚠️ InfluxDB client not available")
|
|
|
|
def closeEvent(self, event):
|
|
"""Clean up when tab is closed"""
|
|
if hasattr(self, 'query_thread') and INFLUXDB_AVAILABLE:
|
|
self.query_thread.stop()
|
|
self.query_thread.wait(3000) # Wait up to 3 seconds
|
|
event.accept()
|