Add SettingsTab, StatusTab, DataTab and wire up DeviceController
- SettingsTab: chamber ID, master CSV mapping (families A-Z), wafer behavior toggle, light/dark mode toggle, Edit CSV Metadata dialog - StatusTab: connection status card, wafer info, activity log (blank until Detect Wafer fires from the side rail) - DataTab: empty state, ready for parsed data - HomePage: side rail buttons dispatch device actions and jump to Status tab; selectedSideActionIndex defaults to -1 on startup - DeviceController registered as QML context property in main.py - LocalSettings: added status persistence fields - Theme: added subheadingColor and disabledText - Added serialcomm/ package and backend data/graph modules - gitignore: restore clean version
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""pyqtgraph PlotWidget wrapper for embedding in QML.
|
||||
|
||||
Exposes a QWidget with a pyqtgraph PlotWidget that can be displayed
|
||||
via QML's `import QtWidgets` or `QtWidgets.QWidget` integration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from PySide6.QtCore import QObject, Property, Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Import pyqtgraph after Qt is initialized
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph import PlotWidget
|
||||
|
||||
|
||||
class GraphView(QObject):
|
||||
"""QML-exposed controller for a pyqtgraph line chart.
|
||||
|
||||
Accepts sensor temperature data (list of lists) and renders
|
||||
each sensor as a separate line series.
|
||||
"""
|
||||
|
||||
# ---- signals ----
|
||||
dataReady = Signal(object) # {"success": bool, "sensory_names": list, "series": list}
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._plot_widget: Optional[PlotWidget] = None
|
||||
self._plot_window: Optional[QWidget] = None
|
||||
self._series: list[Any] = []
|
||||
self._sensor_names: list[str] = []
|
||||
|
||||
@Property(object, notify=dataReady)
|
||||
def plotWidget(self) -> Any:
|
||||
"""Return the QWidget hosting the pyqtgraph PlotWidget for QML embedding."""
|
||||
return self._plot_window
|
||||
|
||||
@Slot()
|
||||
def createPlotWidget(self, parent_widget: Optional[QWidget] = None) -> None:
|
||||
"""Create and return a QWidget containing a pyqtgraph PlotWidget.
|
||||
|
||||
Args:
|
||||
parent_widget: Optional parent widget.
|
||||
"""
|
||||
pg.setConfigOption("background", "default")
|
||||
pg.setConfigOption("foreground", "default")
|
||||
|
||||
self._plot_window = QWidget(parent=parent_widget)
|
||||
self._plot_widget = PlotWidget()
|
||||
self._plot_widget.setBackground("default")
|
||||
|
||||
from PySide6.QtWidgets import QVBoxLayout
|
||||
layout = QVBoxLayout(self._plot_window)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(self._plot_widget)
|
||||
|
||||
# Set axis labels
|
||||
self._plot_widget.setLabel("left", "Temperature", units="°C")
|
||||
self._plot_widget.setLabel("bottom", "Measurement Interval")
|
||||
self._plot_widget.setTitle("Sensor Temperature Over Time")
|
||||
|
||||
@Slot(str, str)
|
||||
def updateChart(self, sensor_names_str: str, series_data_str: str) -> None:
|
||||
"""Update the chart with sensor data.
|
||||
|
||||
Args:
|
||||
sensor_names_str: Comma-separated sensor names (e.g. "Sensor1,Sensor2").
|
||||
series_data_str: JSON-like string of nested lists for each sensor's values.
|
||||
"""
|
||||
import json
|
||||
|
||||
if not self._plot_widget:
|
||||
log.warning("PlotWidget not created yet")
|
||||
return
|
||||
|
||||
try:
|
||||
sensor_names = [s.strip() for s in sensor_names_str.split(",") if s.strip()]
|
||||
series_data = json.loads(series_data_str)
|
||||
except (json.JSONDecodeError, AttributeError) as exc:
|
||||
log.error("Failed to parse chart data: %s", exc)
|
||||
return
|
||||
|
||||
# Clear existing series
|
||||
self._plot_widget.clear()
|
||||
self._series = []
|
||||
|
||||
if not series_data:
|
||||
return
|
||||
|
||||
# Determine Y-axis range from all data
|
||||
all_values = []
|
||||
for sensor_values in series_data:
|
||||
for v in sensor_values:
|
||||
try:
|
||||
all_values.append(float(v))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if all_values:
|
||||
y_min = min(all_values)
|
||||
y_max = max(all_values)
|
||||
y_range = y_max - y_min
|
||||
buffer = max(y_range * 0.1, 1.0) # At least 1 degree buffer
|
||||
self._plot_widget.setYRange(y_min - buffer, y_max + buffer)
|
||||
else:
|
||||
self._plot_widget.setYRange(-50, 150)
|
||||
|
||||
# X-axis: measurement intervals (0-based index)
|
||||
num_points = len(series_data[0]) if series_data else 0
|
||||
x_axis = list(range(num_points))
|
||||
|
||||
# Define a set of distinct colors for series
|
||||
colors = [
|
||||
(255, 87, 87), # Red
|
||||
(66, 165, 245), # Blue
|
||||
(102, 187, 106), # Green
|
||||
(255, 167, 38), # Orange
|
||||
(171, 71, 188), # Purple
|
||||
(0, 188, 212), # Cyan
|
||||
(255, 112, 67), # Deep Orange
|
||||
(121, 85, 72), # Brown
|
||||
(92, 107, 192), # Indigo
|
||||
(48, 125, 117), # Teal
|
||||
]
|
||||
|
||||
# Add each sensor as a line series
|
||||
for i, sensor_name in enumerate(sensor_names):
|
||||
if i >= len(series_data):
|
||||
break
|
||||
|
||||
sensor_values = series_data[i]
|
||||
y_values = []
|
||||
for v in sensor_values:
|
||||
try:
|
||||
y_values.append(float(v))
|
||||
except (ValueError, TypeError):
|
||||
y_values.append(0.0)
|
||||
|
||||
color = colors[i % len(colors)]
|
||||
pen = pg.mkPen(color=color, width=1)
|
||||
|
||||
curve = self._plot_widget.plot(x_axis, y_values, name=sensor_name, pen=pen)
|
||||
self._series.append(curve)
|
||||
|
||||
self._sensor_names = sensor_names
|
||||
|
||||
@Slot()
|
||||
def resetChart(self) -> None:
|
||||
"""Clear the chart."""
|
||||
if self._plot_widget:
|
||||
self._plot_widget.clear()
|
||||
self._series = []
|
||||
self._sensor_names = []
|
||||
|
||||
@Slot()
|
||||
def destroyPlotWidget(self) -> None:
|
||||
"""Destroy the plot widget."""
|
||||
if self._plot_window:
|
||||
self._plot_window.deleteLater()
|
||||
self._plot_window = None
|
||||
self._plot_widget = None
|
||||
self._series = []
|
||||
Reference in New Issue
Block a user