From ea766d3d72d1f4ca167c5c345402f45230f709f9 Mon Sep 17 00:00:00 2001 From: heli2src Date: Tue, 29 Sep 2026 17:32:19 +0200 Subject: [PATCH 1/8] mv widget LAB_GUI to pylab_ml --- plugin_gui.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 ++- 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 plugin_gui.py diff --git a/plugin_gui.py b/plugin_gui.py new file mode 100644 index 0000000..9207c8a --- /dev/null +++ b/plugin_gui.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Semi-ATE +# Licensed under the terms of the GPLv2 License +# (see LICENSE.txt for more details) + + +# Standard library imports + +# Third-party imports + +# Spyder imports +from spyder.api.plugins import Plugins, SpyderDockablePlugin +from spyder.api.translations import get_translation +from spyder.api.plugin_registration.decorators import on_plugin_available, on_plugin_teardown + +from qtpy.QtCore import Signal + +# Local imports +# from ate_spyder_lab_control.widgets.gui_widget import LabGui +from pylab_ml.widgets.gui_widget import LabGui +from ate_spyder.plugin import ATE + +# Localization +_ = get_translation("spyder") + + +class LabGuiPlugin(SpyderDockablePlugin): + """PyLab-ML Gui dockable plugin.""" + + NAME = 'lab_gui' + WIDGET_CLASS = LabGui + CONF_SECTION = NAME + REQUIRES = [ATE.NAME] + OPTIONAL = [Plugins.MainMenu, Plugins.Projects] + CONF_FILE = False + TABIFY = [Plugins.Help] + + sig_edit_goto_requested = Signal(str, int, str) + sig_run_cell = Signal() + sig_debug_cell = Signal() + sig_stop_debugging = Signal() + + # --- SpyderDockablePlugin API + # ------------------------------------------------------------------------ + + @staticmethod + def get_name() -> str: + return _('MLab_GUI') + + def get_description(self) -> str: + return _('MLab Gui integration') + + def get_icon(self): + return self.create_icon('mdi.chip') + + def on_initialize(self): + pass + # widget = self.get_widget() + + def update_font(self): + pass + + # -------------------- Plugin initialization ------------------------------ + @on_plugin_available(plugin=ATE.NAME) + def on_ate_available(self): + widget: LabGui = self.get_widget() + ate: ATE = self.get_plugin(ATE.NAME) + ate.sig_ate_project_loaded.connect(self._setup_test_runner_widget) + ate.sig_ate_progname.connect(self.runflow_changed) + ate.sig_test_tree_update.connect(self.update_actions) + ate.sig_stop_debugging.connect(widget.debug_stop) + + def _setup_test_runner_widget(self): + widget: LabGui = self.get_widget() + ate: ATE = self.get_plugin(ATE.NAME) + project_info = ate.get_project_navigation() + widget.setup_widget(project_info) + + @on_plugin_available(plugin=Plugins.Projects) + def on_projects_available(self): + projects = self.get_plugin(Plugins.Projects) + projects.sig_project_closed.connect(self.project_closed) + + def project_closed(self): + pass + + def runflow_changed(self, progname: str): + widget: LabGui = self.get_widget() + widget.update_labgui(progname) + + def update_actions(self): + widget: LabGui = self.get_widget() + widget.update_actions() + + # ----------------------- Plugin teardown --------------------------------- + + @on_plugin_teardown(plugin=ATE.NAME) + def on_ate_teardown(self): + widget: LabGui = self.get_widget() + ate: ATE = self.get_plugin(ATE.NAME) + + widget.set_project_information(None) + ate.sig_ate_project_changed.disconnect(widget.notify_project_status) diff --git a/pyproject.toml b/pyproject.toml index e3ac2d5..4e3a787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "nidcpower==1.4.2", "nidmm==1.4.2", "niscope==1.4.1", - "nitclk==1.4.1", + "nitclk==1.4.1", "matplotlib", "paho-mqtt", "pandas", @@ -80,6 +80,7 @@ test = [ [project.entry-points."ate.org"] pylabml = "pylab_ml.plugins.pylabml.instruments:Plugin" - + pylabml_gui = "plugin_gui:LabGuiPlugin" + [project.scripts] enter_semiate = "pylab_ml.scripts.enter_semiate:main" \ No newline at end of file From 6af61cafc67f2c2abe45517b1d1c07c36ea8919d Mon Sep 17 00:00:00 2001 From: heli2src Date: Tue, 29 Sep 2026 17:47:35 +0200 Subject: [PATCH 2/8] mv widget LAB_GUI to pylab_ml --- plugin_gui.py | 4 +- pylab_ml/widgets/__init__.py | 0 pylab_ml/widgets/buttons.py | 281 ++++++++++++++++++++++++++ pylab_ml/widgets/gui_widget.py | 347 +++++++++++++++++++++++++++++++++ pylab_ml/widgets/gui_widget.ui | 142 ++++++++++++++ 5 files changed, 772 insertions(+), 2 deletions(-) create mode 100644 pylab_ml/widgets/__init__.py create mode 100644 pylab_ml/widgets/buttons.py create mode 100644 pylab_ml/widgets/gui_widget.py create mode 100644 pylab_ml/widgets/gui_widget.ui diff --git a/plugin_gui.py b/plugin_gui.py index 9207c8a..1fff3d3 100644 --- a/plugin_gui.py +++ b/plugin_gui.py @@ -46,10 +46,10 @@ class LabGuiPlugin(SpyderDockablePlugin): @staticmethod def get_name() -> str: - return _('MLab_GUI') + return _('Lab_GUI') def get_description(self) -> str: - return _('MLab Gui integration') + return _('Lab Gui integration') def get_icon(self): return self.create_icon('mdi.chip') diff --git a/pylab_ml/widgets/__init__.py b/pylab_ml/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pylab_ml/widgets/buttons.py b/pylab_ml/widgets/buttons.py new file mode 100644 index 0000000..ea78ce1 --- /dev/null +++ b/pylab_ml/widgets/buttons.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 23 09:17:01 2020 + +@author: ZLin526F + +""" + +import subprocess +import importlib +from time import sleep +import qtawesome as qta +from qtpy.QtCore import Signal +from PyQt5 import QtWidgets, QtCore +from pydantic import BaseModel +from pylab_ml.common.common import kill_proc_tree +from ate_semiateplugins.pluginmanager import get_plugin_manager + +__author__ = "Zlin526F" +__credits__ = ["Zlin526F"] +__email__ = "Zlin526F@github" +__version__ = "0.0.1" + + +BUTTON_COLUMNS = 3 # define how many buttons are in one line + + +class ButtonConfig(BaseModel): + lib: str + gui: str + + +class Button(QtWidgets.QToolButton): + """ + Buttons with Menu and Icon for Lab Gui + """ + + sytle_button = {'disabled': "color: rgb(128, 128, 128);", + 'crash': "color: rgb(255, 64, 64);", + 'instance_ok': "color: rgb(240, 240, 240);", + 'available': "color: rgb(64, 164, 64);", + 'connect': "color: rgb(64, 164, 64);", + 'disconnect': "color: rgb(255, 64, 64);", + } + + def __init__(self, parent, instanceName, name, index): + """Initialise the class Lab Button.""" + self.instanceName = instanceName + self.name = name + self.parent = parent + self.mqtt = parent.mqtt + self.logger = parent.logger + super().__init__(parent.gui.frame) + self.mystyle = self.sytle_button['disabled'] + + self.setGeometry(QtCore.QRect(10, 10, 88, 60)) + self.setIconSize(QtCore.QSize(40, 40)) + self.guiLib = '' + self.guiLibImport = None + self.instance = None + self.guiInstance = None + self.move(index) + if name is not None: + displayname = f'{name}\n' if len(name) < 13 else f'{name[:12]}\n' + else: + displayname = instanceName + self.setText(displayname) + + if instanceName == 'tester': + instance = get_plugin_manager().hook.get_tester_type(tester_name=name) + self.setToolTip(f'{self.name}') + if len(instance) >= 1: + instance = instance[0] + self.mystyle = self.sytle_button['instance_ok'] + else: + instance = None + self.mystyle = self.sytle_button['crash'] + elif name is None: + instance = None + self.mystyle = self.sytle_button['instance_ok'] + else: + instance = self.create_instance(False) + + self.menu = None if (instanceName == 'tester' or name is None) else QtWidgets.QMenu(self) + if self.menu is not None: + self.menu.setStyle(self.parent.style()) + self.menues = {} + self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) # InstantPopup, MenuButtonPopup, DelayedPopup + items = ["change lib", "terminate"] if self.instance is not None else ["change lib"] + for item in items: + menu_item = QtWidgets.QAction(self.menu) + menu_item.setText(item) + menu_item.triggered.connect(lambda item, function=item: self.menu_clicked(instanceName, function)) + self.addAction(menu_item) + self.menues[item] = menu_item + self.clicked.connect(self.button_clicked) + self.setStyleSheet(self.mystyle) + self.instance = instance + self.subtopic = [] + self.show() + + def create_instance(self, setstyle=True): + instance = None + error = "" + parameters = self.parent.get(self.parent.project_info.file_operator, self.parent.project_info.active_hardware).definition + lib = parameters['Actuator'][self.parent.project_info.active_base][self.name]['lib']\ + if self.name in parameters['Actuator'][self.parent.project_info.active_base].keys() else "" + self.guiLib = parameters['Actuator'][self.parent.project_info.active_base][self.name]['gui']\ + if self.name in parameters['Actuator'][self.parent.project_info.active_base].keys() else "" + actuators_name = self.name.replace(' ', '_') + actuators_lib = importlib.import_module(f'ate_test_app.actuators.{actuators_name}.{actuators_name}') + self.instanceName = actuators_lib.periphery_type if hasattr(actuators_lib, 'periphery_type') else actuators_name + del(actuators_lib) + if lib != "": + try: # 1. try to import as module + instance = importlib.import_module(lib) + self.mystyle = self.sytle_button['instance_ok'] + except Exception as ex: # ModuleNotFoundError + error = ex + self.mystyle = self.sytle_button['crash'] + if instance is None: # 2. call as subprocess + instance = subprocess.Popen(lib, shell=True) + sleep(0.2) + if instance.poll() is None: + self.mystyle = self.sytle_button['instance_ok'] + else: + del(instance) + instance = None + self.lib = lib + if instance is not None: + if hasattr(instance, 'icon'): + self.setIcon(self.lib.icon) + self.instance = instance + elif instance is None: # no gui for this module available + self.setToolTip(f'{self.name} not available,\nlib = {self.lib if self.lib!= "" else "empty"}\n{error}') + if setstyle: + self.setStyleSheet(self.mystyle) + return instance + + def set_gui(self, guiLib): + self.guiLib = guiLib + self.instance = '' + self.name = self.instanceName + #self.button_clicked() + #self.guiInstance.hide() + + def msg2gui(self, topic, msg): + if self.guiInstance is None: + # print(f" {self.instanceName}.msg2gui: guiInstance is None ") + return + # print(f" {app.instance.topinstname} {app.name}: subtopic: {app.instance.subtopic}") + name = None + if len(self.subtopic) > 0: + for subtopic in self.subtopic: + if subtopic in msg.keys(): + name = subtopic + elif hasattr(self.guiInstance, "subtopic"): # you can also add subtopic to a each GUI, e.q. scope also listen to 'regs' + for subtopic in self.guiInstance.subtopic: + if subtopic in msg.keys(): + name = subtopic + if self.name in msg.keys() and self.guiInstance is not None: + name = self.name + if name is not None: + msg = msg[name] + cmd = msg["cmd"] + value = msg["payload"] + print(f" {self.instanceName}.msg2gui: {self.name}, {self.instance}, {topic}, {msg}") + if cmd == 'mqtt_status': + self.setStyleSheet(self.sytle_button[msg["payload"]]) + self.setToolTip(f"{self.instanceName} {msg['payload']}") + try: + if msg["type"] in ["set"] and hasattr(self.guiInstance, cmd): # set attribute + self.guiInstance.__setattr__(cmd, value) + elif msg["type"] in ["get"] and hasattr(self.guiInstance, cmd): # get attribute/function call + if value == []: # it is a function call? + self.guiInstance.__getattribute__(cmd)() + else: + self.guiInstance.__setattr__(cmd, value) # get from extern is a set for displaying.... + elif msg["type"] in ["set", "get"] and hasattr(self.guiInstance, "mqttreceive"): # get more information as only the payload + self.guiInstance.mqttreceive(name, msg) + msg = {} + elif not hasattr(self.guiInstance, cmd): + print(f"Warning! {name} has no attribute: '{cmd} = {msg}'") + else: + print(f"Error: {name} I don't now what to do with this message: '{cmd} = {msg}'") + except Exception as ex: + msg = f"{name} something goes wrong: '{topic} = {msg}' {ex}" + if value is not None: + print(msg) + self.logger.error(msg) + + def gui_disconnect(self): + self.setStyleSheet(self.sytle_button["disconnect"]) + self.setToolTip(f"{self.instanceName} disconnect") + if self.guiInstance is not None: + print(f'{self.instanceName}.guiInstance disconnect') + self.guiInstance.gui_disconnect() + + def display_msg(self, topic, msg): + if 'status' in msg.keys(): + self.setStyleSheet(self.sytle_button[msg['status']]) + self.setToolTip(f"{self.instanceName} {msg['status']}") + if msg['status'] == 'crash': + self.menues['terminate'].setText('restart') + elif msg['status'] == 'available': + self.menues['terminate'].setText('terminate') + elif 'ioctl_name' in msg.keys(): + text = self.text().split('\n')[0] + tooltip = self.toolTip().split('\n')[0] + value = msg['ioctl_name'] + if 'result' in msg.keys(): + value = f"{value}={msg['result']}" + self.setText(f'{text}\n\n{value[:13]}') + self.setToolTip(f'{tooltip}\n{value}') + + def button_clicked(self): + if self.instance is not None: + if self.guiLib != "" and self.guiLibImport is None: + try: + self.guiLibImport = importlib.import_module(self.guiLib) + self.guiInstance = self.guiLibImport.Gui(self, self.name, self.parent.project_info.parent) # start Gui + self.guiInstance.subtopic = self.subtopic + self.guiInstance.topinstname = self.name + except Exception: + print(f'button {self.name} could not load gui={self.guiLib}') + self.state = 'nogui' + elif self.guiInstance is not None and not self.guiInstance.isVisible(): + self.guiInstance.show() + elif self.guiInstance is not None and self.guiInstance.isVisible(): + self.guiInstance.hide() + elif self.guiInstance is None and self.guiLibImport is not None: + print(f'button {self.name} reload {self.guiLibImport}') + importlib.reload(self.guiLibImport) + self.guiInstance = self.guiLibImport.Gui(self, self.name, self.parent.project_info.parent) + + def menu_clicked(self, name, function): + if function == 'change lib': + from ate_spyder.widgets.actions_on.hardwaresetup.PluginConfigurationDialog import PluginConfigurationDialog + + parameters = self.parent.get(self.parent.project_info.file_operator, self.parent.project_info.active_hardware).definition + lib_type = guilib = '' + if self.name in parameters['Actuator'][self.parent.project_info.active_base].keys(): + lib_type = parameters['Actuator'][self.parent.project_info.active_base][self.name]['lib'] + guilib = parameters['Actuator'][self.parent.project_info.active_base][self.name]['gui'] + default_parameter = {"lib": lib_type, "gui": guilib} + current_config = ButtonConfig(**default_parameter) + self.setStyle(self.parent.style()) + dialog = PluginConfigurationDialog(self, name, current_config.dict().keys(), self.parent.project_info.active_hardware, + self.parent.project_info, current_config.dict(), False) + dialog.exec_() + newparameters = dialog.get_cfg() + del(dialog) + newparameters['lib'] = newparameters['lib'].strip() + newparameters['gui'] = newparameters['gui'].strip() + if newparameters != default_parameter: + parameters['Actuator'][self.parent.project_info.active_base][self.name] = newparameters + self.parent.update_definition(self.parent.project_info.file_operator, self.parent.project_info.active_hardware, parameters) + kill_proc_tree(instance=self.instance) + self.instance = self.create_instance() + elif function == 'terminate': + if self.instance is not None: + kill_proc_tree(instance=self.instance) + self.instance = None + else: + print(f'restart button {self.name}.....') + self.instance = self.create_instance() + + def move(self, index): + x, y = index % BUTTON_COLUMNS, index // BUTTON_COLUMNS + super().move(x*100, y*70) + self.show() + + def close(self): + print(f'{self.name}.close()') + if self.instanceName != 'tester': + print(f' kill {self.name} with {self.instance}') + kill_proc_tree(instance=self.instance) + self.instance = None + if self.guiInstance is not None: + self.guiInstance.close() + super().close() diff --git a/pylab_ml/widgets/gui_widget.py b/pylab_ml/widgets/gui_widget.py new file mode 100644 index 0000000..07fb474 --- /dev/null +++ b/pylab_ml/widgets/gui_widget.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 23 09:17:01 2020 + +@author: ZLin526F + +INFO: + icons von https://github.com/spyder-ide/qtawesome + +Todo: + change path for setting (should not be in definitions) + remove unnessary code + +""" +import os +import time +import importlib +import qtawesome as qta +import qdarkstyle +from PyQt5 import uic +from qtpy.QtCore import Signal +from PyQt5 import QtWidgets, QtCore + +from qtpy.QtWidgets import QHBoxLayout +from spyder.api.translations import get_translation + +if __name__ == "__main__": + import sys + from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) +from spyder.api.widgets.main_widget import PluginMainWidget +from ate_projectdatabase.Utils import DB_KEYS +from ate_projectdatabase.FileOperator import DBObject, FileOperator +from pylab_ml.widgets import buttons + +# Localization +_ = get_translation("spyder") + + +__author__ = "Zlin526F" +__credits__ = ["Zlin526F"] +__email__ = "Zlin526F@github" +__version__ = "0.0.1" + + +class LabGuiDialog(QtWidgets.QDialog): + def __init__(self): + super().__init__() + uic.loadUi(__file__.replace(".py", ".ui"), self) + + +class LabGuiButton(QtWidgets.QWidget): # .QDialog + def __init__(self, parent): + super().__init__() + uic.loadUi(os.path.dirname(__file__)+"\\guibutton_widget.ui", parent) + + +class LabGui(PluginMainWidget): + """ + Gui for instruments. + + + - mqtt command will be handle in mqtt_receive() + + for extend the icon-buttons: + - send mqtt with {'semictrl': {'type': 'cmd', 'cmd': 'button', 'payload': {'scope': 'gui.instruments.softscope.softscope'}}} + - app must have the class GUI(parent=None, name=None) + - if you use more than one instName for the same GUI: + use subtopic[] for the other instName in the Gui + """ + + default_tester_icon = qta.icon("mdi6.remote", color="green", scale_factor=1.0) + definition = {'Actuator': {'PR': {}, 'FT': {}}} + + _states = { + "unknown": ["unknown", None], + "error": ["error", "color: rgb(0, 0, 0);background-color: #ff0000"], + "nogui": ["GUi not available", "color: rgb(255, 165, 0)"], + } + + command = { # see https://github.com/Semi-ATE/Semi-ATE/blob/master/docs/project/interfacedefinitions/testapp_interface.md + "SetParameter": { + "type": "cmd", + "command": "setparameter", + "parameters": [], + "sites": ["0"], + }, + } + # peripherie. e.q. Magnetfield: {"type": "io-control-request", "periphery_type": "MagField", "ioctl_name": "set_field", "parameters": {"millitesla": 10}} + # see: Tester\TES\apps\testApp\sequencers\SequencerHarness.py + + change_status_display = Signal(str, str) + + def __init__(self, name, plugin, parent=None): + """Initialise the class Lab Gui.""" + QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) + super().__init__(name, plugin, parent) + self.plugin = plugin + self.gui = LabGuiDialog() + self.adjustUI() + self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) + self.closeEvent = self.close + self.reset() + self.project_info = None + self.topinstname = "" + self._state = "unknown" + self.state = "unknown" + self.buttons = {} + self.instruments_buttons = {} + self.lab_control = None + self.flagAppclosed = False + + @staticmethod + def get(session: FileOperator, name: str) -> DBObject: + return session.query('lab_control')\ + .filter(lambda Hardware: Hardware.name == name)\ + .one() + + @staticmethod + def add(session: FileOperator, name: str, definition: dict): + hw = { + DB_KEYS.HARDWARE.NAME: name, + DB_KEYS.HARDWARE.DEFINITION.KEY(): definition, + } + session.query_with_subtype('lab_control', name).add(hw) + session.commit() + + @staticmethod + def remove(session: FileOperator, name: str): + session.query('lab_control')\ + .filter(lambda Hardware: Hardware.name == name)\ + .delete() + session.commit() + + def update_definition(self, session: FileOperator, name: str, definition: dict): + hw = self.get(session, name) + hw.definition = definition + session.commit() + + def setup_widget(self, project_info): + self.project_info = project_info + self.update_actions() + + def setup(self): + layout = QHBoxLayout() + layout.addWidget(self.gui) + self.setLayout(layout) + + def update_actions(self): + from ate_projectdatabase.Hardware import Hardware + + actual_buttons = {} + for button in self.buttons.values(): + actual_buttons[button.name] = button.instanceName + if self.project_info is None: + return + if self.lab_control is None and hasattr(self.project_info, 'lab_control'): + self.lab_control = self.project_info.lab_control + self.lab_control.receive_msg_for_instrument.connect(self.receive_msg_for_instrument) + self.mqtt = self.lab_control.mqtt if hasattr(self.lab_control, 'mqtt') else None + self.logger = self.lab_control.logger + hw = self.project_info.active_hardware + base = self.project_info.active_base + if hw is None or hw == '': + return + hardware_def = Hardware.get(self.project_info.file_operator, hw).definition + tester_name = hardware_def['tester'] + actuators = hardware_def['Actuator'][base] + + if tester_name not in actual_buttons: + if 'tester' in self.buttons.keys(): + self.buttons['tester'].close() + self.buttons['tester'] = buttons.Button(self, 'tester', tester_name, 0) + else: + del(actual_buttons[tester_name]) + + try: + self.get(self.project_info.file_operator, hw).definition + except AssertionError: + self.add(self.project_info.file_operator, hw, self.definition) + for actuator_name in actuators: # add new actuator-buttons + if actuator_name not in actual_buttons: + button = buttons.Button(self, None, actuator_name, len(self.buttons)) + self.buttons[button.instanceName] = button + else: + del(actual_buttons[actuator_name]) + for actuator_name in actual_buttons: # remove unnecessary actuator-button + self.buttons[actual_buttons[actuator_name]].close() + del(self.buttons[actual_buttons[actuator_name]]) + index = 0 + for name in self.buttons: # rearrange the actuator-buttons + self.buttons[name].move(index) + index += 1 + + def remove_buttons(self): + for name in self.buttons.copy(): + button = self.buttons[name] + button.close() + self.buttons.pop(name) + + def get_title(self): + return _("Lab Gui") + + def update_labgui(self, test_program_name): + self.update_actions() + # self.state = 'testing' + + def adjustUI(self): + self.gui.frame.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) + # set Menubar + menuBar = QtWidgets.QMenuBar(self) + self.menuBar = menuBar + menuBar.titles = [] + + # set connect: + self.gui.newMenu = [] + + def reset(self): + print("Reset Lab Gui") + self._state = "reset" + self.state = "reset" + self.last_mqtt_time = time.time() + + # mqtt messages: + def mqtt_send(self, topic, cmd): + """Send messages via mqtt.""" + if cmd in self.command: + msg = self.command[cmd] + topic = self.sendtopic + topic + self.mqtt.publish(topic, msg) + self.change_status_display.emit("send " + str(msg["command"] + ", wait for answer"), "") + print("Lab Gui.mqtt_send {}:{}".format(topic, msg)) + self.wait4answer = True + else: + self.error(f"Lab Gui.mqtt_send {topic}:{cmd} not found in list") + + def receive_msg_for_instrument(self, topic, msg): + """ + call if a message comes from an instrument, or from semictrl (is also a instrument). + + """ + if type(msg) is dict and "type" in msg and msg["type"] == "cmd" and msg["cmd"] == "button": # get command to create a new button + for name in msg["payload"]: + if name not in self.instruments_buttons and name not in self.buttons: + self.instruments_buttons[name] = buttons.Button(self, name, None, len(self.instruments_buttons)+buttons.BUTTON_COLUMNS*3) + self.instruments_buttons[name].set_gui(msg["payload"][name]) + elif topic == '' and msg["payload"] == "terminated": + for name in self.instruments_buttons: + self.instruments_buttons[name].gui_disconnect() + elif type(msg) is dict and len(msg.keys()) == 1: # received a message for the buttons directly or for the guis + instanceName = list(msg.keys())[0] + # print(f" message for the extended GUI received: {instanceName}: '{topic}= {msg}'") + for name in self.instruments_buttons: # transmit the message to the guis from all instruments_buttons + self.instruments_buttons[name].msg2gui(topic, msg) + if instanceName not in self.buttons: # perhaps it is a status message for a button + instanceName = topic.split("/")[2] + if instanceName in self.buttons.keys(): # display received message in the actuator buttons + self.buttons[instanceName].display_msg(topic, msg) + elif topic.split("/")[2] in self.buttons: # get a message for an actuator button + self.buttons[topic.split("/")[2]].display_msg(topic, msg) + + @property + def state(self): + return self._state + + @state.setter + def state(self, value): + msg = "" + if type(value) == tuple: + msg = value[1] + value = value[0] + print(f"Lab Gui.state = {value}, oldstate ={self._state}") + self._state = value if value in self._states else "unknown" + self.change_status_display.emit(value, msg) + + @QtCore.pyqtSlot(str, str) + def _change_status_display(self, msg, extendmsg=""): + print(f" state = {msg}") + style = self._states[msg][1] if msg in self._states else "color: rgb(255, 255, 255);background-color: transparent;" + msg = self._states[msg][0] if msg in self._states else msg + self.gui.Lstatus.setText(f"{extendmsg} {msg}") + self.gui.Lstatus.setStyleSheet(style) + +# todo: necessarry ???????????????? + def appclosed(self, name): + """Call from an instrumet if the instrument closed itself.""" + if self.flagAppclosed: # TODO change to a Signal + print(f" Control.appclosed({name})") + if name in self.gui.newMenu: + app = self.gui.newMenu[name] + settings = [app.libname, app.isChecked()] + if app.instance is not None and hasattr(app.instance, "geometry"): + settings.append( + [ + app.instance.geometry().x(), + app.instance.geometry().y(), + app.instance.width(), + app.instance.height(), + ] + ) + settings.append(app.subtopic) + app.instance = None + elif name in self.buttons: + self.buttons[name].guiInstance = None + + def close(self, event=None): + print("Lab Gui.close()") + self.remove_buttons() + self.flagAppclosed = False + super().close() + + def __del__(self): + print("Lab Gui.__del__") + self.close() + super().__del__ + + def debug_stop(self): + ''' do update the state of lab gui when the test application is stoped ''' + pass + + +if __name__ == "__main__": + from ate_spyder.widgets.navigation import ProjectNavigation + from PyQt5.QtWidgets import QApplication + from qtpy.QtWidgets import QMainWindow + import sys + + QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) + app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) + app.references = set() + main = QMainWindow() + homedir = os.path.expanduser("~") + project_directory = homedir + r'\ATE\packages\envs\semi-ate-awinia1\HATB' # path to your semi-ate project + project_directory = homedir + r'\Work Folders\Projecte\Repository\hatc\0203\units\lab\source\python\HATC' + project_info = ProjectNavigation(project_directory, homedir, main) + project_info.active_hardware = 'HW0' + project_info.active_base = 'FT' + project_info.active_target = 'Device1' + + main.NAME = 'lab_gui' + main.WIDGET_CLASS = LabGui + main.CONF_SECTION = main.NAME + + labgui = LabGui('Lab Gui', main, main) + labgui.setup_widget(project_info) + main.show() + sys.exit(app.exec_()) diff --git a/pylab_ml/widgets/gui_widget.ui b/pylab_ml/widgets/gui_widget.ui new file mode 100644 index 0000000..55ae42e --- /dev/null +++ b/pylab_ml/widgets/gui_widget.ui @@ -0,0 +1,142 @@ + + + Dialog + + + + 0 + 0 + 721 + 396 + + + + Lab-Gui + + + + + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 70 + 16777215 + + + + + 9 + 75 + true + + + + Status: + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 30 + 20 + + + + + + + + my actual MQTT-status + + + unknown + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + true + + + + 0 + 0 + + + + + + + + + + + + + + + + + From 78d75dc17a8157ee682c63c95a57f91f55402174 Mon Sep 17 00:00:00 2001 From: heli2src Date: Thu, 1 Oct 2026 10:09:46 +0200 Subject: [PATCH 3/8] fix wrong entry point for the LabGui --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4e3a787..cc83b5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,9 @@ test = [ [project.entry-points."ate.org"] pylabml = "pylab_ml.plugins.pylabml.instruments:Plugin" - pylabml_gui = "plugin_gui:LabGuiPlugin" + +[project.entry-points."spyder.plugins"] + lab_gui = "plugin_gui:LabGuiPlugin" [project.scripts] enter_semiate = "pylab_ml.scripts.enter_semiate:main" \ No newline at end of file From 85ce9d799eb66fea8241be633c65975e7b4aaf43 Mon Sep 17 00:00:00 2001 From: heli2src Date: Mon, 5 Oct 2026 11:42:08 +0200 Subject: [PATCH 4/8] rm unnecesssary libs --- pylab_ml/widgets/gui_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylab_ml/widgets/gui_widget.py b/pylab_ml/widgets/gui_widget.py index 07fb474..d9c2258 100644 --- a/pylab_ml/widgets/gui_widget.py +++ b/pylab_ml/widgets/gui_widget.py @@ -14,7 +14,6 @@ """ import os import time -import importlib import qtawesome as qta import qdarkstyle from PyQt5 import uic @@ -107,6 +106,8 @@ def __init__(self, name, plugin, parent=None): self.state = "unknown" self.buttons = {} self.instruments_buttons = {} + self.mqtt = None + self.logger = None self.lab_control = None self.flagAppclosed = False From a24440423b7cb60ae034c92cc32510c6c5efd6aa Mon Sep 17 00:00:00 2001 From: heli2src Date: Mon, 5 Oct 2026 11:43:11 +0200 Subject: [PATCH 5/8] add softcope to the intrument plugin --- pylab_ml/plugins/pylabml/instruments.py | 144 ++++++++++++------------ pylab_ml/scope/softscope.py | 70 +++++++++--- 2 files changed, 132 insertions(+), 82 deletions(-) diff --git a/pylab_ml/plugins/pylabml/instruments.py b/pylab_ml/plugins/pylabml/instruments.py index 97fbc01..eb78cfd 100644 --- a/pylab_ml/plugins/pylabml/instruments.py +++ b/pylab_ml/plugins/pylabml/instruments.py @@ -22,20 +22,17 @@ from pylab_ml.common import environment from pylab_ml.common import projectsetup from pylab_ml.misc import registermaster +from pylab_ml.scope.softscope import Softscope -__author__ = "Zlin526F" -__credits__ = ["Zlin526F"] -__email__ = "Zlin526F@github" -__version__ = '0.6.0' - +__version__ = '0.7.0' NAME = "Pylab_Ml" class Plugin: - """ - This class defines the plugin for the Pylab-Ml environment. - It implements the hooks that will be called by the environment to get the plugin's functionality. + """ + This class defines the plugin for the Pylab-Ml environment. + It implements the hooks that will be called by the environment to get the plugin's functionality. """ @hookimpl @@ -48,9 +45,9 @@ def get_plugin_identification(): @hookimpl def get_importer_names(): - """ - Return a list of importer names that this plugin provides. - Each importer is represented as a dictionary with display name, version, and unique name. + """ + Return a list of importer names that this plugin provides. + Each importer is represented as a dictionary with display name, version, and unique name. """ return [ {"display_name": "Dummy Importer", @@ -59,9 +56,9 @@ def get_importer_names(): @hookimpl def get_exporter_names(): - """ - Return a list of exporter names that this plugin provides. - Each exporter is represented as a dictionary with display name, version, and unique name. + """ + Return a list of exporter names that this plugin provides. + Each exporter is represented as a dictionary with display name, version, and unique name. """ return [ {"display_name": "Dummy Exporter", @@ -70,9 +67,9 @@ def get_exporter_names(): @hookimpl def get_equipment_names(): - """ - Return a list of equipment names that this plugin provides. - Each equipment is represented as a dictionary with display name, version, and unique name. + """ + Return a list of equipment names that this plugin provides. + Each equipment is represented as a dictionary with display name, version, and unique name. """ return [ {"display_name": "Dummy Equipment", @@ -81,9 +78,9 @@ def get_equipment_names(): @hookimpl def get_devicepin_importer_names(): - """ - Return a list of device pin importer names that this plugin provides. - Each importer is represented as a dictionary with display name, version, and unique name. + """ + Return a list of device pin importer names that this plugin provides. + Each importer is represented as a dictionary with display name, version, and unique name. """ return [ {"display_name": "Dummy Pinimport", @@ -92,15 +89,21 @@ def get_devicepin_importer_names(): @hookimpl def get_instrument_names(): - """ - Return a list of instrument names that this plugin provides. - Each instrument is represented as a dictionary with display name, version, manufacturer, and unique name. + """ + Return a list of instrument names that this plugin provides. + Each instrument is represented as a dictionary with display name, version, manufacturer, and unique name. """ return [ {"display_name": "PyLab-ML Instruments V" + __version__, "version": __version__, "manufacturer": "PyLab-ML Labor", - "name": f"{NAME}.Instruments"}] + "name": f"{NAME}.Instruments"}, + {"display_name": "PyLab-ML Softscope V" + __version__, + "version": __version__, + "manufacturer": "PyLab-ML Labor", + "name": f"{NAME}.Softscope"} + ] + # @hookimpl # def get_tester_names(): @@ -112,9 +115,9 @@ def get_instrument_names(): @hookimpl def get_general_purpose_function_names(): - """ - Return a list of general purpose function names that this plugin provides. - Each function is represented as a dictionary with display name, version, manufacturer, and unique name. + """ + Return a list of general purpose function names that this plugin provides. + Each function is represented as a dictionary with display name, version, manufacturer, and unique name. """ return [ {"display_name": "Project Setup", @@ -129,14 +132,14 @@ def get_general_purpose_function_names(): @hookimpl def get_importer(importer_name): - """ - Return an instance of the importer class corresponding to the given importer name. - + """ + Return an instance of the importer class corresponding to the given importer name. + Parameters ---------- importer_name: str The unique name of the importer to retrieve, as defined in get_importer_names(). - + Returns ------- An instance of the importer class corresponding to the given importer name, or None if no matching importer is found. @@ -147,14 +150,14 @@ def get_importer(importer_name): @hookimpl def get_exporter(exporter_name): - """ - Return an instance of the exporter class corresponding to the given exporter name. - + """ + Return an instance of the exporter class corresponding to the given exporter name. + Parameters ---------- exporter_name: str The unique name of the exporter to retrieve, as defined in get_exporter_names(). - + Returns ------- An instance of the exporter class corresponding to the given exporter name, or None if no matching exporter is found. @@ -165,14 +168,14 @@ def get_exporter(exporter_name): @hookimpl def get_equipment(equipment_name): - """ - Return an instance of the equipment class corresponding to the given equipment name. - + """ + Return an instance of the equipment class corresponding to the given equipment name. + Parameters ---------- equipment_name: str The unique name of the equipment to retrieve, as defined in get_equipment_names(). - + Returns ------- An instance of the equipment class corresponding to the given equipment name, or None if no matching equipment is found. @@ -183,14 +186,14 @@ def get_equipment(equipment_name): @hookimpl def get_devicepin_importer(importer_name): - """ - Return an instance of the device pin importer class corresponding to the given importer name. - + """ + Return an instance of the device pin importer class corresponding to the given importer name. + Parameters ---------- importer_name: str The unique name of the device pin importer to retrieve, as defined in get_devicepin_importer_names(). - + Returns ------- An instance of the device pin importer class corresponding to the given importer name, or None if no matching importer is found. @@ -201,58 +204,58 @@ def get_devicepin_importer(importer_name): @hookimpl def get_instrument(instrument_name: str, logger): - """ - Return an instance of the instrument class corresponding to the given instrument name. - + """ + Return an instance of the instrument class corresponding to the given instrument name. + Parameters ---------- instrument_name: str The unique name of the instrument to retrieve, as defined in get_instrument_names(). logger: logging.Logger The logger instance to use for the instrument. - + Returns ------- An instance of the instrument class corresponding to the given instrument name, or None if no matching instrument is found. """ if instrument_name == f"{NAME}.Instruments": return Instruments(logger) + elif instrument_name == f"{NAME}.Softscope": + return Softscope(logger) @hookimpl def get_instrument_proxy(instrument_name): - """ - Return an instance of the instrument proxy class corresponding to the given instrument name. - + """ + Return an instance of the instrument proxy class corresponding to the given instrument name. + Parameters ---------- instrument_name: str The unique name of the instrument proxy to retrieve, as defined in get_instrument_proxy_names(). - + Returns ------- An instance of the instrument proxy class corresponding to the given instrument name, or None if no matching instrument proxy is found. """ - if f"{NAME}." in instrument_name: + if instrument_name == f"{NAME}.Instruments": print(f'{NAME}.get_instrument_proxy') return Instruments() - -# @hookimpl -# def get_tester(tester_name: str): -# if tester_name == f"{NAME}.NIPXIe": -# return NIPXIe.NIPXIe() + elif instrument_name == f"{NAME}.Softscope": + print(f'{NAME}.get_instrument_proxy') + return Softscope() @hookimpl def get_general_purpose_function(func_name: str, logger): - """ - Return an instance of the general purpose function class corresponding to the given function name. - + """ + Return an instance of the general purpose function class corresponding to the given function name. + Parameters ---------- func_name: str The unique name of the general purpose function to retrieve, as defined in get_general_purpose_function_names(). logger: logging.Logger The logger instance to use for the general purpose function. - + Returns ------- An instance of the general purpose function class corresponding to the given function name, or None if no matching function is found. @@ -266,12 +269,13 @@ def get_general_purpose_function(func_name: str, logger): def get_configuration_options(object_name): """ Return a list of configuration options for the given object name. - + Parameters ---------- object_name: str The unique name of the object for which to retrieve configuration options, as defined in the plugin. - + + Returns ------- A list of configuration option names for the given object name, or an empty list if no matching object is found. @@ -282,6 +286,8 @@ def get_configuration_options(object_name): return ["Network prefix", "working directory", "add path", 'instance name', 'filename'] elif object_name == f"{NAME}.Registermaster": return ['instance name', 'filename', 'read mod write', 'reset value'] + if object_name == f"{NAME}.Softscope": + return Softscope() class Instruments: @@ -303,7 +309,7 @@ class Instruments: 2. If not my_config.py, search for: 2.1 my_init.py in your path = individual init settings 2.2. when not found: search in project/version/harness/tb_project_init.py = project specific initialisation, e.q. kind of communication: halapb, sti, msp - + Use this files together as $$$HW_config.py - add $WORKAREA/units/lab/source/python/harness to path @@ -375,18 +381,18 @@ def set_configuration_values(self, data): pass def apply_configuration(self, data): - """ + """ Apply the configuration for the instruments by loading the appropriate configuration files based on the provided data and environment variables. - + Parameters ---------- data: dict - A dictionary containing configuration data for the instruments, which may include keys such as "working directory", "add path", and "Network prefix". + A dictionary containing configuration data for the instruments, which may include keys such as "working directory", "add path", and "Network prefix". The values of these keys will be used to determine the paths to search for instrument configuration files and to set environment variables accordingly. - + Returns ------- - None. + None. """ # {"working directory": "$WORKAREA/units/lab/source/python; $WORKAREA/units/lab.Win64/source/python/", the last exist path win # "add path": "harness/", diff --git a/pylab_ml/scope/softscope.py b/pylab_ml/scope/softscope.py index 6ffd5f9..94ec5ff 100644 --- a/pylab_ml/scope/softscope.py +++ b/pylab_ml/scope/softscope.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """ -This script defines the Softscope class, which is responsible for communicating with a softscope GUI via MQTT. -The class allows for starting and stopping data sampling, setting the channel to be monitored, and adjusting the sample time. -It uses a Timer thread to manage the sampling loop and handles MQTT commands to control its behavior. +This script defines the Softscope class, which is responsible for communicating with a softscope GUI via MQTT. +The class allows for starting and stopping data sampling, setting the channel to be monitored, and adjusting the sample time. +It uses a Timer thread to manage the sampling loop and handles MQTT commands to control its behavior. The class also includes error handling for cases where the specified channel is not defined or returns an error. Created on Thu Jan 7 17:18:03 2021 @@ -10,6 +10,7 @@ @author: jung """ from ate_common.logger import LogLevel +from pylab_ml.common.mqtt_client import mylogger from pylab_ml.common.mqtt_client import mqtt_deviceattributes from pylab_ml.common import common from time import time @@ -38,14 +39,12 @@ class Softscope(mqtt_deviceattributes): max sample Time ~50Hz """ - def __init__(self, mqttc, logger, instName="softscope"): + def __init__(self, logger=mylogger, instName="softscope"): """ Initialise the softscope class and subscribe for mqtt if mqttc is not None - + Parameters ---------- - mqttc : MQTT client object - The MQTT client to use for communication. If None, MQTT functionality will be disabled. logger : Logger object The logger to use for logging messages. instName : str, optional @@ -54,10 +53,8 @@ def __init__(self, mqttc, logger, instName="softscope"): self.instName = instName self.logger = logger super().__init__() - self.gui = "pylab_ml.gui.instruments.softscope.softscope" # semi-ctrl use this lib for the scope gui + self.gui = "pylab_ml.gui.instruments.softscope.softscope" # semi-ctrl use this lib for the scope gui self.mqtt_all = ["setchannel()", "on()", "off()", "sampleTime", "test"] # approved commands for mqtt - if mqttc is not None: - self.mqtt_add(mqttc, self) # subscribe for mqtt and send information about the gui self.scopeChannel = None self._samplethread = None self.sampleTime = 0.1 @@ -67,14 +64,24 @@ def __init__(self, mqttc, logger, instName="softscope"): self.busy = False def init(self, topinstname): - """Initialize the topinstname for the strcall to get the value for the scopeChannel""" + """Initialize the topinstname for the strcall to get the value for the scopeChannel. + + connect to the mqttc if available + """ self.topinstname = topinstname + try: + from labml_adjutancy.ctrl.labctrl import mqttc + except Exception: + mqttc = None + + if mqttc is not None: + self.mqtt_add(mqttc, self) def setchannel(self, value): """Set the scopeChannel via mqtt""" + print(f'setchannel, {value}, type={type(value)}, topinstname={self.topinstname}') result = common.strcall(self.topinstname, value) self.scopeChannel = value if result != "ERROR" else "ERROR" - # print(f'Softscope.setchannel: {value} -> {result}') self.publish_set("scopeChannel", self.scopeChannel) def on(self): @@ -131,7 +138,7 @@ def _newsample(self): def test(self): """ Get the current test value for the softscope. - + Returns ------- int @@ -146,6 +153,43 @@ def test(self): def __repr__(self): return f"{self.__class__}" + def setshortnames(self, env): + """Get all available instruments. + """ + self.init(env) + + +# Functions for the plugin: + def do_import(self): + print("Softscope: do_import") + + def do_export(self): + print("Softscope: do_import") + + def get_abort_reason(self): + print("Softscope: get_abort_reason") + + def set_mqtt_client(self, mqtt): + print(f"Softscope: set_mqtt_client {mqtt}") + + def set_configuration_values(self, data): + print(f"Softscope: set_configuration_values {data}") + + def apply_configuration(self, data): + """ + Call from the Plugin if the modul placed in the hardwaresetups. + + Parameters + ---------- + data : TYPE + DESCRIPTION. + + Returns + ------- + None + """ + pass + if __name__ == "__main__": from pylab_ml.common.mqtt_client import mylogger From 732ca3245806455b2d8f4df19a048fc6375add75 Mon Sep 17 00:00:00 2001 From: heli2src Date: Mon, 5 Oct 2026 13:59:48 +0200 Subject: [PATCH 6/8] lib path corrected --- plugin_gui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin_gui.py b/plugin_gui.py index 1fff3d3..ac8a7b9 100644 --- a/plugin_gui.py +++ b/plugin_gui.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # -# Copyright © Semi-ATE # Licensed under the terms of the GPLv2 License # (see LICENSE.txt for more details) @@ -17,7 +16,6 @@ from qtpy.QtCore import Signal # Local imports -# from ate_spyder_lab_control.widgets.gui_widget import LabGui from pylab_ml.widgets.gui_widget import LabGui from ate_spyder.plugin import ATE From 74a7b22c7e8369edf397ef4e9514d82bcf0a81aa Mon Sep 17 00:00:00 2001 From: heli2src Date: Mon, 5 Oct 2026 14:02:32 +0200 Subject: [PATCH 7/8] add necessary lib --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cc83b5a..870b685 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -[build-system] +pwd[build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -42,8 +42,10 @@ classifiers = [ ] dependencies = [ "hightime", + "xlrd", "h5py", "ipywidgets", + "lxml", "nidcpower==1.4.2", "nidmm==1.4.2", "niscope==1.4.1", @@ -57,6 +59,7 @@ dependencies = [ "pyvisa", "pyvisa-py", "regex", + "svgelements", "semi-ate-common", "pyyaml", ] @@ -85,4 +88,4 @@ test = [ lab_gui = "plugin_gui:LabGuiPlugin" [project.scripts] - enter_semiate = "pylab_ml.scripts.enter_semiate:main" \ No newline at end of file + enter_semiate = "pylab_ml.scripts.enter_semiate:main" From 007b3b9d1ba0b6ad3cd2526f3b4ba82d8b57f7e2 Mon Sep 17 00:00:00 2001 From: heli2src Date: Tue, 6 Oct 2026 11:50:28 +0200 Subject: [PATCH 8/8] fix syntax error --- pyproject.toml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 870b685..f78d866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -pwd[build-system] +[build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -43,25 +43,25 @@ classifiers = [ dependencies = [ "hightime", "xlrd", - "h5py", - "ipywidgets", - "lxml", - "nidcpower==1.4.2", - "nidmm==1.4.2", - "niscope==1.4.1", - "nitclk==1.4.1", - "matplotlib", + "h5py", + "ipywidgets", + "lxml", + "nidcpower==1.4.2", + "nidmm==1.4.2", + "niscope==1.4.1", + "nitclk==1.4.1", + "matplotlib", "paho-mqtt", "pandas", - "psutil", - "pythonnet", - "pyserial", - "pyvisa", - "pyvisa-py", - "regex", - "svgelements", + "psutil", + "pythonnet", + "pyserial", + "pyvisa", + "pyvisa-py", + "regex", + "svgelements", "semi-ate-common", - "pyyaml", + "pyyaml" ] [project.optional-dependencies] @@ -85,7 +85,7 @@ test = [ pylabml = "pylab_ml.plugins.pylabml.instruments:Plugin" [project.entry-points."spyder.plugins"] - lab_gui = "plugin_gui:LabGuiPlugin" + lab_gui = "plugin_gui:LabGuiPlugin" [project.scripts] enter_semiate = "pylab_ml.scripts.enter_semiate:main"