diff --git a/plugin_gui.py b/plugin_gui.py new file mode 100644 index 0000000..ac8a7b9 --- /dev/null +++ b/plugin_gui.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# +# 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 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 _('Lab_GUI') + + def get_description(self) -> str: + return _('Lab 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/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 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..d9c2258 --- /dev/null +++ b/pylab_ml/widgets/gui_widget.py @@ -0,0 +1,348 @@ +# -*- 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 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.mqtt = None + self.logger = None + 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 + + + + + + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml index e3ac2d5..f78d866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,23 +42,26 @@ classifiers = [ ] dependencies = [ "hightime", - "h5py", - "ipywidgets", - "nidcpower==1.4.2", - "nidmm==1.4.2", - "niscope==1.4.1", - "nitclk==1.4.1", - "matplotlib", + "xlrd", + "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", + "psutil", + "pythonnet", + "pyserial", + "pyvisa", + "pyvisa-py", + "regex", + "svgelements", "semi-ate-common", - "pyyaml", + "pyyaml" ] [project.optional-dependencies] @@ -80,6 +83,9 @@ test = [ [project.entry-points."ate.org"] pylabml = "pylab_ml.plugins.pylabml.instruments:Plugin" - + +[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 + enter_semiate = "pylab_ml.scripts.enter_semiate:main"