mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +02:00
refactor: configs for BECMonitor are validated by pydantic outside the main widget
This commit is contained in:
@ -192,7 +192,7 @@ class ModularApp(QMainWindow):
|
|||||||
|
|
||||||
# hook plots, configs and buttons together
|
# hook plots, configs and buttons together
|
||||||
for plot, config, button in zip(plots, configs, buttons):
|
for plot, config, button in zip(plots, configs, buttons):
|
||||||
plot.update_config(config)
|
plot.on_config_update(config)
|
||||||
button.clicked.connect(plot.show_config_dialog)
|
button.clicked.connect(plot.show_config_dialog)
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
# from .monitor_config import validate_monitor_config, ValidationError
|
||||||
|
from .monitor_config_validator import MonitorConfigValidator
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
from pydantic import BaseModel, Field, ValidationError
|
|
||||||
from typing import List, Dict, Union, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class Signal(BaseModel):
|
|
||||||
"""
|
|
||||||
Represents a signal in a plot configuration.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
name (str): The name of the signal.
|
|
||||||
entry (Optional[str]): The entry point of the signal, optional.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
entry: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class PlotAxis(BaseModel):
|
|
||||||
"""
|
|
||||||
Represents an axis (X or Y) in a plot configuration.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
label (Optional[str]): The label for the axis.
|
|
||||||
signals (List[Signal]): A list of signals to be plotted on this axis.
|
|
||||||
"""
|
|
||||||
|
|
||||||
label: Optional[str]
|
|
||||||
signals: List[Signal]
|
|
||||||
|
|
||||||
|
|
||||||
class PlotConfig(BaseModel):
|
|
||||||
"""
|
|
||||||
Configuration for a single plot.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
plot_name (Optional[str]): Name of the plot.
|
|
||||||
x (PlotAxis): Configuration for the X axis.
|
|
||||||
y (PlotAxis): Configuration for the Y axis.
|
|
||||||
"""
|
|
||||||
|
|
||||||
plot_name: Optional[str]
|
|
||||||
x: PlotAxis
|
|
||||||
y: PlotAxis
|
|
||||||
|
|
||||||
|
|
||||||
class PlotSettings(BaseModel):
|
|
||||||
"""
|
|
||||||
Global settings for plotting.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
background_color (str): Color of the plot background.
|
|
||||||
num_columns (int): Number of columns in the plot layout.
|
|
||||||
colormap (str): Colormap to be used.
|
|
||||||
scan_types (bool): Indicates if the configuration is for different scan types.
|
|
||||||
"""
|
|
||||||
|
|
||||||
background_color: str
|
|
||||||
num_columns: int
|
|
||||||
colormap: str
|
|
||||||
scan_types: bool
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceMonitorConfig(BaseModel):
|
|
||||||
"""
|
|
||||||
Configuration model for the device monitor mode.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
plot_settings (PlotSettings): Global settings for plotting.
|
|
||||||
plot_data (List[PlotConfig]): List of plot configurations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
plot_settings: PlotSettings
|
|
||||||
plot_data: List[PlotConfig]
|
|
||||||
|
|
||||||
|
|
||||||
class ScanModeConfig(BaseModel):
|
|
||||||
"""
|
|
||||||
Configuration model for scan mode.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
plot_settings (PlotSettings): Global settings for plotting.
|
|
||||||
plot_data (Dict[str, List[PlotConfig]]): Dictionary of plot configurations,
|
|
||||||
keyed by scan type.
|
|
||||||
"""
|
|
||||||
|
|
||||||
plot_settings: PlotSettings
|
|
||||||
plot_data: Dict[str, List[PlotConfig]]
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config(config_data: dict) -> Union[DeviceMonitorConfig, ScanModeConfig]:
|
|
||||||
"""
|
|
||||||
Validates the configuration data based on the provided schema.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_data (dict): Configuration data to be validated.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Union[DeviceMonitorConfig, ScanModeConfig]: Validated configuration object.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValidationError: If the configuration data does not conform to the schema.
|
|
||||||
"""
|
|
||||||
if config_data["plot_settings"]["scan_types"]:
|
|
||||||
return ScanModeConfig(**config_data)
|
|
||||||
else:
|
|
||||||
return DeviceMonitorConfig(**config_data)
|
|
190
bec_widgets/validation/monitor_config_validator.py
Normal file
190
bec_widgets/validation/monitor_config_validator.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
from typing import List, Dict, Union, Optional
|
||||||
|
|
||||||
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
Field,
|
||||||
|
field_validator,
|
||||||
|
)
|
||||||
|
from pydantic_core import PydanticCustomError
|
||||||
|
|
||||||
|
|
||||||
|
class Signal(BaseModel):
|
||||||
|
"""
|
||||||
|
Represents a signal in a plot configuration.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name (str): The name of the signal.
|
||||||
|
entry (Optional[str]): The entry point of the signal, optional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
entry: Optional[str] = Field(None, validate_default=True)
|
||||||
|
|
||||||
|
@field_validator("name")
|
||||||
|
@classmethod
|
||||||
|
def validate_name(cls, v):
|
||||||
|
device_manager = MonitorConfigValidator.device_manager
|
||||||
|
# Check if device name provided
|
||||||
|
if v is None:
|
||||||
|
raise PydanticCustomError(
|
||||||
|
"no_device_name",
|
||||||
|
"Device name must be provided",
|
||||||
|
dict(wrong_value=v),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if device exists in BEC
|
||||||
|
try:
|
||||||
|
device = getattr(device_manager, v)
|
||||||
|
except:
|
||||||
|
raise PydanticCustomError(
|
||||||
|
"no_device_bec",
|
||||||
|
'Device "{wrong_value}" not found in current BEC session',
|
||||||
|
dict(wrong_value=v),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if device have signals #TODO not sure if devices can be defined also without signals
|
||||||
|
if not hasattr(device, "signals"):
|
||||||
|
raise PydanticCustomError(
|
||||||
|
"no_device_signals",
|
||||||
|
'Device "{wrong_value}" do not have "signals" defined. Check device configuration.',
|
||||||
|
dict(wrong_value=v),
|
||||||
|
)
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("entry")
|
||||||
|
@classmethod
|
||||||
|
def set_and_validate_entry(cls, v, values):
|
||||||
|
device_manager = MonitorConfigValidator.device_manager
|
||||||
|
|
||||||
|
device_name = values.data.get("name")
|
||||||
|
device = getattr(device_manager, device_name, None)
|
||||||
|
|
||||||
|
# Set entry based on hints if not provided
|
||||||
|
if v is None and hasattr(device, "_hints"):
|
||||||
|
v = next(iter(device._hints), device_name)
|
||||||
|
elif v is None:
|
||||||
|
v = device_name
|
||||||
|
|
||||||
|
# Validate that the entry exists in device signals
|
||||||
|
if v not in device.signals:
|
||||||
|
raise PydanticCustomError(
|
||||||
|
"no_entry_for_device",
|
||||||
|
"Entry '{wrong_value}' not found in device '{device_name}' signals",
|
||||||
|
dict(wrong_value=v, device_name=device_name),
|
||||||
|
)
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class PlotAxis(BaseModel):
|
||||||
|
"""
|
||||||
|
Represents an axis (X or Y) in a plot configuration.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
label (Optional[str]): The label for the axis.
|
||||||
|
signals (List[Signal]): A list of signals to be plotted on this axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
label: Optional[str]
|
||||||
|
signals: List[Signal] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class PlotConfig(BaseModel):
|
||||||
|
"""
|
||||||
|
Configuration for a single plot.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
plot_name (Optional[str]): Name of the plot.
|
||||||
|
x (PlotAxis): Configuration for the X axis.
|
||||||
|
y (PlotAxis): Configuration for the Y axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
plot_name: Optional[str]
|
||||||
|
x: PlotAxis = Field(...)
|
||||||
|
y: PlotAxis = Field(...)
|
||||||
|
|
||||||
|
@field_validator("x")
|
||||||
|
def validate_x_signals(cls, v):
|
||||||
|
if len(v.signals) != 1:
|
||||||
|
raise PydanticCustomError(
|
||||||
|
"no_entry_for_device",
|
||||||
|
"There must be exactly one signal for x axis. Number of x signals: '{wrong_value}'",
|
||||||
|
dict(wrong_value=v),
|
||||||
|
)
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class PlotSettings(BaseModel):
|
||||||
|
"""
|
||||||
|
Global settings for plotting.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
background_color (str): Color of the plot background.
|
||||||
|
num_columns (int): Number of columns in the plot layout.
|
||||||
|
colormap (str): Colormap to be used.
|
||||||
|
scan_types (bool): Indicates if the configuration is for different scan types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
background_color: str
|
||||||
|
num_columns: int
|
||||||
|
colormap: str
|
||||||
|
scan_types: bool
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceMonitorConfig(BaseModel):
|
||||||
|
"""
|
||||||
|
Configuration model for the device monitor mode.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
plot_settings (PlotSettings): Global settings for plotting.
|
||||||
|
plot_data (List[PlotConfig]): List of plot configurations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
plot_settings: PlotSettings
|
||||||
|
plot_data: List[PlotConfig]
|
||||||
|
|
||||||
|
|
||||||
|
class ScanModeConfig(BaseModel):
|
||||||
|
"""
|
||||||
|
Configuration model for scan mode.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
plot_settings (PlotSettings): Global settings for plotting.
|
||||||
|
plot_data (Dict[str, List[PlotConfig]]): Dictionary of plot configurations,
|
||||||
|
keyed by scan type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
plot_settings: PlotSettings
|
||||||
|
plot_data: Dict[str, List[PlotConfig]]
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorConfigValidator:
|
||||||
|
device_manager = None
|
||||||
|
|
||||||
|
def __init__(self, device_manager):
|
||||||
|
# self.device_manager = device_manager
|
||||||
|
MonitorConfigValidator.device_manager = device_manager
|
||||||
|
|
||||||
|
def validate_monitor_config(
|
||||||
|
self, config_data: dict
|
||||||
|
) -> Union[DeviceMonitorConfig, ScanModeConfig]:
|
||||||
|
"""
|
||||||
|
Validates the configuration data based on the provided schema.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_data (dict): Configuration data to be validated.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Union[DeviceMonitorConfig, ScanModeConfig]: Validated configuration object.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If the configuration data does not conform to the schema.
|
||||||
|
"""
|
||||||
|
if config_data["plot_settings"]["scan_types"]:
|
||||||
|
validated_config = ScanModeConfig(**config_data)
|
||||||
|
else:
|
||||||
|
validated_config = DeviceMonitorConfig(**config_data)
|
||||||
|
|
||||||
|
return validated_config
|
@ -2,18 +2,134 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from bec_lib import MessageEndpoints
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from bec_lib.core import MessageEndpoints
|
||||||
from PyQt5 import QtCore
|
from PyQt5 import QtCore
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtWidgets import QApplication, QTableWidgetItem, QWidget
|
from PyQt5.QtWidgets import QApplication, QTableWidgetItem, QWidget, QMessageBox
|
||||||
from pyqtgraph import mkPen, mkBrush
|
from pyqtgraph import mkPen, mkBrush
|
||||||
from PyQt5 import uic
|
from PyQt5 import uic
|
||||||
|
|
||||||
from bec_widgets.bec_dispatcher import bec_dispatcher
|
from bec_widgets.bec_dispatcher import bec_dispatcher
|
||||||
from bec_widgets.qt_utils import Crosshair, Colors
|
from bec_widgets.qt_utils import Crosshair, Colors
|
||||||
|
|
||||||
# just for demonstration purposes is script run directly
|
# from bec_widgets.validation import validate_monitor_config, ValidationError
|
||||||
|
from bec_widgets.validation import MonitorConfigValidator
|
||||||
|
|
||||||
|
# just for demonstration purposes if script run directly
|
||||||
|
config_scan_mode = {
|
||||||
|
"plot_settings": {
|
||||||
|
"background_color": "white",
|
||||||
|
"num_columns": 3,
|
||||||
|
"colormap": "plasma",
|
||||||
|
"scan_types": True,
|
||||||
|
},
|
||||||
|
"plot_data": {
|
||||||
|
"grid_scan": [
|
||||||
|
{
|
||||||
|
"plot_name": "Grid plot 1",
|
||||||
|
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
|
||||||
|
"y": {
|
||||||
|
"label": "BPM",
|
||||||
|
"signals": [
|
||||||
|
{"name": "gauss_bpm", "entry": "gauss_bpm"},
|
||||||
|
{"name": "gauss_adc1", "entry": "gauss_adc1"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plot_name": "Grid plot 2",
|
||||||
|
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
|
||||||
|
"y": {
|
||||||
|
"label": "BPM",
|
||||||
|
"signals": [
|
||||||
|
{"name": "gauss_bpm", "entry": "gauss_bpm"},
|
||||||
|
{"name": "gauss_adc1", "entry": "gauss_adc1"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plot_name": "Grid plot 3",
|
||||||
|
"x": {"label": "Motor Y", "signals": [{"name": "samx", "entry": "samx"}]},
|
||||||
|
"y": {
|
||||||
|
"label": "BPM",
|
||||||
|
"signals": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plot_name": "Grid plot 4",
|
||||||
|
"x": {"label": "Motor Y", "signals": [{"name": "samx", "entry": "samx"}]},
|
||||||
|
"y": {
|
||||||
|
"label": "BPM",
|
||||||
|
"signals": [{"name": "gauss_adc3", "entry": "gauss_adc3"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"line_scan": [
|
||||||
|
{
|
||||||
|
"plot_name": "BPM plot",
|
||||||
|
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
|
||||||
|
"y": {
|
||||||
|
"label": "BPM",
|
||||||
|
"signals": [
|
||||||
|
{"name": "gauss_bpm", "entry": "gauss_bpm"},
|
||||||
|
{"name": "gauss_adc1"},
|
||||||
|
{"name": "gauss_adc2", "entry": "gauss_adc2"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plot_name": "Multi",
|
||||||
|
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
|
||||||
|
"y": {
|
||||||
|
"label": "Multi",
|
||||||
|
"signals": [
|
||||||
|
{"name": "gauss_bpm", "entry": "gauss_bpm"},
|
||||||
|
{"name": "samx", "entry": "samx"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
config_simple = {
|
config_simple = {
|
||||||
|
"plot_settings": {
|
||||||
|
"background_color": "black",
|
||||||
|
"num_columns": 2,
|
||||||
|
"colormap": "plasma",
|
||||||
|
"scan_types": False,
|
||||||
|
},
|
||||||
|
"plot_data": [
|
||||||
|
{
|
||||||
|
"plot_name": "BPM4i plots vs samx",
|
||||||
|
"x": {
|
||||||
|
"label": "Motor Y",
|
||||||
|
# "signals": [{"name": "samx", "entry": "samx"}],
|
||||||
|
"signals": [{"name": "samy"}],
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"label": "bpm4i",
|
||||||
|
"signals": [{"name": "bpm4i", "entry": "bpm4i"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plot_name": "Gauss plots vs samx",
|
||||||
|
"x": {
|
||||||
|
"label": "Motor X",
|
||||||
|
"signals": [{"name": "samx", "entry": "samx"}],
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"label": "Gauss",
|
||||||
|
# "signals": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
||||||
|
"signals": [{"name": "gauss_bpm"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
config_wrong = {
|
||||||
"plot_settings": {
|
"plot_settings": {
|
||||||
"background_color": "black",
|
"background_color": "black",
|
||||||
"num_columns": 2,
|
"num_columns": 2,
|
||||||
@ -40,7 +156,7 @@ config_simple = {
|
|||||||
},
|
},
|
||||||
"y": {
|
"y": {
|
||||||
"label": "Gauss",
|
"label": "Gauss",
|
||||||
"signals": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
"signals": [{"name": "gauss_bpm", "entry": "BS"}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -63,6 +179,9 @@ class BECMonitor(pg.GraphicsLayoutWidget):
|
|||||||
# Client and device manager from BEC
|
# Client and device manager from BEC
|
||||||
self.client = bec_dispatcher.client if client is None else client
|
self.client = bec_dispatcher.client if client is None else client
|
||||||
self.dev = self.client.device_manager.devices
|
self.dev = self.client.device_manager.devices
|
||||||
|
self.queue = self.client.queue
|
||||||
|
|
||||||
|
self.validator = MonitorConfigValidator(self.dev)
|
||||||
|
|
||||||
if gui_id is None:
|
if gui_id is None:
|
||||||
self.gui_id = self.__class__.__name__ + str(time.time()) # TODO still in discussion
|
self.gui_id = self.__class__.__name__ + str(time.time()) # TODO still in discussion
|
||||||
@ -248,7 +367,9 @@ class BECMonitor(pg.GraphicsLayoutWidget):
|
|||||||
)
|
)
|
||||||
x_signal_config = x_config["signals"][0]
|
x_signal_config = x_config["signals"][0]
|
||||||
x_name = x_signal_config.get("name", "")
|
x_name = x_signal_config.get("name", "")
|
||||||
x_entry = x_signal_config.get("entry", x_name)
|
x_entry = x_signal_config.get(
|
||||||
|
"entry", x_name
|
||||||
|
) # TODO this is buggy if the entry is not specified in the config for x
|
||||||
|
|
||||||
key = (x_name, x_entry, y_name, y_entry)
|
key = (x_name, x_entry, y_name, y_entry)
|
||||||
data_x = self.data.get(key, {}).get("x", [])
|
data_x = self.data.get(key, {}).get("x", [])
|
||||||
@ -265,7 +386,7 @@ class BECMonitor(pg.GraphicsLayoutWidget):
|
|||||||
from .config_dialog import ConfigDialog
|
from .config_dialog import ConfigDialog
|
||||||
|
|
||||||
dialog = ConfigDialog(default_config=self.config)
|
dialog = ConfigDialog(default_config=self.config)
|
||||||
dialog.config_updated.connect(self.update_config)
|
dialog.config_updated.connect(self.on_config_update)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
def update_client(self, client) -> None:
|
def update_client(self, client) -> None:
|
||||||
@ -279,12 +400,21 @@ class BECMonitor(pg.GraphicsLayoutWidget):
|
|||||||
@pyqtSlot(dict)
|
@pyqtSlot(dict)
|
||||||
def on_config_update(self, config: dict) -> None:
|
def on_config_update(self, config: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Update the configuration settings for the PlotApp.
|
Validate and update the configuration settings for the PlotApp.
|
||||||
Args:
|
Args:
|
||||||
config(dict): Configuration settings
|
config(dict): Configuration settings
|
||||||
"""
|
"""
|
||||||
self.config = config
|
|
||||||
self._init_config()
|
# self.config = config
|
||||||
|
# self._init_config()
|
||||||
|
try:
|
||||||
|
validated_config = self.validator.validate_monitor_config(config)
|
||||||
|
self.config = validated_config.model_dump()
|
||||||
|
self._init_config()
|
||||||
|
except ValidationError as e:
|
||||||
|
error_message = f"Monitor configuration validation error: {e}"
|
||||||
|
print(error_message)
|
||||||
|
# QMessageBox.critical(self, "Configuration Error", error_message) #TODO do better error popups
|
||||||
|
|
||||||
@pyqtSlot(dict, dict)
|
@pyqtSlot(dict, dict)
|
||||||
def on_scan_segment(self, msg, metadata):
|
def on_scan_segment(self, msg, metadata):
|
||||||
@ -402,6 +532,6 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
client.start()
|
client.start()
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
monitor = BECMonitor(config=config_simple)
|
monitor = BECMonitor(config=config_wrong)
|
||||||
monitor.show()
|
monitor.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
Reference in New Issue
Block a user