# -*- coding: utf-8 -*-
"""
This script defines a GUI class for a softscope instrument, which is part of the PyLab-ML project.
The GUI allows users to interact with the softscope instrument, send MQTT commands, and receive updates.
The class inherits from a base instrument GUI class and implements specific functionality for the softscope instrument.
Created on Wed April 21 13:43:20 2021
@author: C. Jung
TODO:
Bug: no bugs known, but in work.....
"""
import sys
import os
from PyQt5 import QtWidgets, QtGui
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib import pyplot
from pylab_ml.gui.instruments.base_instrument import Gui as Guibase
from pylab_ml.gui.instruments.base_instrument import load_ui
import time
__author__ = "Zlin526F"
__credits__ = ["Zlin526F"]
__email__ = "Zlin526F@github"
__version__ = "0.0.8"
[docs]
class Scope(FigureCanvas):
""" Scope class for the softscope instrument, responsible for plotting data in real-time.
It uses Matplotlib to create a dynamic plot that updates with new data points.
The class handles the initialization of the plot, updating the data, and adjusting the axes as needed.
"""
sampleSteps = 100
[docs]
def __init__(self, gui, frame=None, width=5, height=5, dpi=100, min_y=None, max_y=None, bgcolor=None):
"""
Initialize the Scope class with the given parameters and set up the Matplotlib figure and axes for plotting.
Parameters
----------
gui: object
The GUI instance to which this scope belongs.
frame: object, optional
The frame in which the scope will be displayed.
width: int, optional
The width of the figure in inches.
height: int, optional
The height of the figure in inches.
dpi: int, optional
The dots per inch (DPI) of the figure.
min_y: float, optional
The minimum value for the y-axis.
max_y: float, optional
The maximum value for the y-axis.
bgcolor: str, optional
The background color of the figure.
"""
self.gui = gui
self.frame = frame
self._min_y = min_y if min_y is not None else 0
self._max_y = max_y if max_y is not None else 5
self.x_value = [0]
self.y = [0]
self.samples = 0
self._max_x = 100
self.data = []
self.first = 0
self.samplesperframe = 1
fig = pyplot.figure(figsize=(width, height), dpi=dpi, facecolor=bgcolor)
FigureCanvas.__init__(self, fig)
self.setParent(frame.scope)
self.ax = pyplot.axes(xlim=(0, self.max_x), ylim=(self.min_y, self.max_y))
self.ax.tick_params(colors="gray", which="both")
self.ax.xaxis.label.set_color("gray")
self.ax.yaxis.label.set_color("gray")
self.ax.grid()
self.ax.set_xlabel("samples") # time (s)
(self.line,) = self.ax.plot([], [], lw=2)
self.onoff = 0
self.oldtime = time.time()
[docs]
def newdata(self, y):
"""
Update the scope with new data.
Parameters
----------
y: float
The new data point to be added to the scope.
"""
timedelta = time.time() - self.oldtime
self.samplerate = timedelta
self.oldtime = time.time()
y_average = 0
y_average = y + y_average
# x=time()
self.samples += 1
while self.x_value[-1] - self.x_value[0] > self.max_x:
del self.x_value[0]
del self.y[0]
self.x_value.append(self.samples)
self.y.append(y)
self.data.append((self.samples, y))
# calculate y-Axis label:
self.min_y = y
self.max_y = y
self.line.set_data(self.x_value, self.y)
# calculate x-Axis label:
if self.first == 0 and self.x_value[-1] - self.x_value[0] == self.max_x: # fill scope until max samples
self.first = 1
elif self.first == 1: # max samples reached -> now scroll
self.ax.set_xlim(self.x_value[0], self.x_value[-1])
self.ax.figure.canvas.draw()
if len(self.x_value) < self.max_x:
self.setrange()
y_average = y_average / self.samplesperframe
self.value = y_average
[docs]
def setrange(self):
"""Adjust the y-axis range based on the current minimum and maximum y values."""
tmin = self.min_y
tmax = self.max_y
diff = tmax - tmin
if type(diff) is int and diff < 10:
diff = 10
self.ax.set_ylim(tmin - 0.1 * diff, tmax + 0.1 * diff)
# ax.set_ylim(auto=True)
self.ax.figure.canvas.draw()
return
[docs]
def init(self):
"""Initialization function."""
self.line.set_data([], [])
return (self.line,)
@property
def value(self):
"""Get the current value of the scope."""
return self._value
@value.setter
def value(self, value):
"""Set the current value of the scope."""
self._value = value
self.gui.setLabel(self.frame.Lvalue, self._value)
@property
def samplerate(self):
"""Get the current sample rate of the scope."""
return self._samplerate
@samplerate.setter
def samplerate(self, value):
"""Set the current sample rate of the scope."""
if value != 0:
self._samplerate = 1 / value
self.gui.setLabel(self.frame.Lsamplerate, f"{self._samplerate:0.1f}", "/s")
@property
def max_x(self):
"""Maximum samples."""
return self._max_x
@max_x.setter
def max_x(self, value):
value = (value // self.sampleSteps) * self.sampleSteps
if value < self._max_x:
self._max_x = value
self.clear()
self.first = 0
self._max_x = value
self.ax.set_xlim(self.x_value[0], self.x_value[0] + value)
self.ax.figure.canvas.draw()
self.ax.relim()
@property
def min_y(self):
"""Get the current minimum y value of the scope."""
if self._min_y is None:
return 0
return self._min_y
@min_y.setter
def min_y(self, value):
"""Set the current minimum y value of the scope."""
if self._min_y is None or self.min_y > value:
self._min_y = value
self.gui.setLabel(self.frame.Lmin, self._min_y)
@property
def max_y(self):
"""Get the current maximum y value of the scope."""
if self._max_y is None:
return 0
return self._max_y
@max_y.setter
def max_y(self, value):
"""Set the current maximum y value of the scope."""
if self._max_y is None or self._max_y < value:
self._max_y = value
self.gui.setLabel(self.frame.Lmax, self._max_y)
@property
def samples(self):
"""Get the current number of samples in the scope."""
return self._samples
@samples.setter
def samples(self, value):
"""Set the current number of samples in the scope."""
self._samples = value
self.gui.setLabel(self.frame.Lsamples, self._samples)
[docs]
def clear(self):
"""Clear the scope and reset all relevant parameters."""
self.samples = 0
self.line.set_data([], [])
self.x_value = [0]
self.y = [self.y[-1]]
self.data = []
self._min_y = None
self._max_y = None
self.ax.set_xlim([0, self.max_x])
def __del__(self):
pass
[docs]
class Gui(Guibase):
"""
Softscope GUI.
Inherited from base_instrument
status
"""
_states = {
"connect": ["connect", "color: rgb(0, 255, 0)"], # overwrite the definitions in base_instrument
"disconnect": ["disconnect", "color: rgb(255, 0, 0)"],
"chFound": ["channel ok", "color: rgb(0, 255, 0)"],
"chNotDef": ["no channel defined", "color: rgb(255, 0, 0)"],
"chNotFound": ["channel name not found", "color: rgb(255, 0, 0)"],
"on": ["running", "color: rgb(0, 255, 0)"],
"off": ["waiting for start...", "color: palette(HighlightedText)"],
"waitChannel": ["waiting for channel name", "color: rgb(255, 165, 0)"],
}
[docs]
def __init__(self, parent=None, name="softscope", parentwindow=None, channel=None):
"""
Initialize the GUI for the softscope instrument.
Parameters
----------
parent: object, optional
The parent widget for this GUI. Default is None.
name: str, optional
The name of the instrument. Default is "softscope".
parentwindow: object, optional
The parent window for this GUI. Default is None.
channel: int, optional
The channel number. Default is None.
"""
super().__init__(grandparent=parent, name=name, parentwindow=parentwindow)
self.myframe = load_ui(self.gui.myframe, __file__)
bgcolor = self.gui.palette().color(QtGui.QPalette.Background).name() # getRgb()
self.scope = Scope(self, self.myframe, width=5, height=4, bgcolor=bgcolor) # 8,4
self.scope.move(5, 5)
self.myadjustUI()
self.gui.closeEvent = self.close
self._scopeChannel = None
self.parent = parent
# self.mqtt_initlist = ['all attributes do you need, widgets start with MQTT will add automaticaly']
[docs]
def myadjustUI(self):
"""Adjust the GUI elements for the softscope instrument."""
# set icons:
self.add_menuicon("onoff") # add existing icon and connection from the base-instrument
self.add_menuicon("clear", connect=self.scope.clear)
# set connection fom Gui-object to functions:
self.myframe.Dsamples.valueChanged.connect(self.rangeSamples)
self.myframe.Echname.editingFinished.connect(self.editchname)
self.mqttConnectWidgets() # connect all widget which start with MQTT_
geometry = self.gui.geometry()
wh = self.myframe.geometry()
self.gui.setWindowTitle(f' {os.path.basename(__file__).split(".")[0]} (Version: {__version__})')
self.myframe.setGeometry(geometry.x(), geometry.y(), wh.width() + 100, wh.height() + 250)
# ======================================================
# attributes which connect to an extern call (mqtt-command)
[docs]
def mqttreceive(self, instName, msg):
"""
Handle incoming MQTT messages for the softscope instrument.
Parameters
----------
instName: str
The name of the instrument that sent the message.
msg: dict
The MQTT message received.
"""
self.logger.debug(f" {self.instName}.mqttreceive: {instName}: {msg} ")
if super().mqttreceive(instName, msg):
return
if "cmd" in tuple(msg.keys()) and "payload" in tuple(msg.keys()):
if msg["cmd"] == self._scopeChannel:
value = msg["payload"]
if self.myframe.CBfilter.currentText().find("Integer") == 0:
n = int(self.myframe.CBfilter.currentText().split(" ")[1])
if value > 2 ** (n - 1):
value = -(~value + 2**n + 1)
self.scope.newdata(value)
else:
self.logger.debug(f" softscope {msg['cmd']} != {self._scopeChannel} -> not for me, do nothing")
@property
def scopeChannel(self):
"""Get the current scope channel."""
return self._scopeChannel
@scopeChannel.setter
def scopeChannel(self, value):
"""Set the current scope channel and update the status accordingly."""
value = self.filtersubtopic(value)
self.logger.debug(f"get scopeChannel = {value}, subtopic = {self.topinstname}.{self.subtopic}, ")
if value == 0:
return
msg = "chNotFound" if value == "ERROR" else "chFound"
channel = value[value.find(".") + 1:]
channel = channel[:-2] if channel[-1] == ")" else channel
self.logger.debug(f"set channel = {channel} -->")
self._scopeChannel = channel
self.status = msg
self._onoff_ = 0
# end extern calling attributes
# ======================================================================================
#
# connected GUI-function to buttons or menues
#
[docs]
def rangeSamples(self):
"""Handle the change in the number of samples and update the scope accordingly."""
self.scope.max_x = self.myframe.Dsamples.value()
self.myframe.QLsamples.setText(f"{self.scope.max_x} Samples")
[docs]
def editchname(self):
"""Handle the change in the channel name and update the scope accordingly."""
channel = self.myframe.Echname.text()
self._scopeChannel = None
self._onoff_ = None
channel = self.filtersubtopic(channel)
if channel != "":
self.status = "disconnect"
self.subtopic = channel
self.publish("setchannel()", channel)
else:
self.status = "chNotDef"
[docs]
def close(self, event=None):
"""Handle the close event for the GUI by publishing an 'off' command, clearing the subtopic, and then calling the base class's close method."""
self.publish("off()")
self.subtopic = []
super().close()
if __name__ == "__main__":
# from pylab_ml.common.mqtt_client import mqtt_init
# broker = 127.0.0.1'
# message_client = 'ate/DT1604092/matrix'
# mqttclient = mqtt_init() # prepare mqtt
# mqttclient.init(None, message_client=message_client.split('/')[0]) # mqtt client connect to broker
# mqttclient.init()
app = QtWidgets.QApplication(sys.argv)
window = Gui(app)
app.exec_()
# window.scope.anim.event_source.stop()