summaryrefslogtreecommitdiff
path: root/gr-qtgui/python/qtgui
diff options
context:
space:
mode:
authorghostop14 <ghostop14@gmail.com>2020-02-02 13:52:39 -0500
committerdevnulling <devnulling@users.noreply.github.com>2020-02-19 15:46:54 -0800
commit5cd7b4cd472e9dca41f19e2cdfed4393374c9fe0 (patch)
treecb6a607483d03510b90d43c5956e88b691744fff /gr-qtgui/python/qtgui
parentbebed18070bb3366c53f8d1b775c5ed5959859ea (diff)
gr-qtgui: Incorporate new GUI controls
These updates expand the user interface capabilities of GNU Radio. This PR includes all of the controls more fully documented here: https://github.com/ghostop14/gr-guiextra
Diffstat (limited to 'gr-qtgui/python/qtgui')
-rw-r--r--gr-qtgui/python/qtgui/CMakeLists.txt19
-rw-r--r--gr-qtgui/python/qtgui/__init__.py21
-rw-r--r--gr-qtgui/python/qtgui/auto_correlator_sink.py132
-rw-r--r--gr-qtgui/python/qtgui/azelplot.py128
-rw-r--r--gr-qtgui/python/qtgui/compass.py290
-rw-r--r--gr-qtgui/python/qtgui/dialcontrol.py142
-rw-r--r--gr-qtgui/python/qtgui/dialgauge.py208
-rw-r--r--gr-qtgui/python/qtgui/digitalnumbercontrol.py346
-rw-r--r--gr-qtgui/python/qtgui/distanceradar.py119
-rw-r--r--gr-qtgui/python/qtgui/graphicitem.py194
-rw-r--r--gr-qtgui/python/qtgui/graphicoverlay.py103
-rw-r--r--gr-qtgui/python/qtgui/ledindicator.py197
-rw-r--r--gr-qtgui/python/qtgui/levelgauge.py214
-rw-r--r--gr-qtgui/python/qtgui/msgcheckbox.py119
-rw-r--r--gr-qtgui/python/qtgui/msgpushbutton.py56
-rw-r--r--gr-qtgui/python/qtgui/qa_qtgui.py2
-rw-r--r--gr-qtgui/python/qtgui/togglebutton.py122
-rw-r--r--gr-qtgui/python/qtgui/toggleswitch.py210
18 files changed, 2618 insertions, 4 deletions
diff --git a/gr-qtgui/python/qtgui/CMakeLists.txt b/gr-qtgui/python/qtgui/CMakeLists.txt
index 2041038c84..bdac9ede9c 100644
--- a/gr-qtgui/python/qtgui/CMakeLists.txt
+++ b/gr-qtgui/python/qtgui/CMakeLists.txt
@@ -15,8 +15,23 @@ configure_file(util.py.cmakein "${CMAKE_CURRENT_BINARY_DIR}/util.py" @ONLY)
GR_PYTHON_INSTALL(
FILES __init__.py
- "${CMAKE_CURRENT_BINARY_DIR}/range.py"
- "${CMAKE_CURRENT_BINARY_DIR}/util.py"
+ compass.py
+ togglebutton.py
+ msgpushbutton.py
+ msgcheckbox.py
+ distanceradar.py
+ azelplot.py
+ digitalnumbercontrol.py
+ dialcontrol.py
+ ledindicator.py
+ graphicitem.py
+ levelgauge.py
+ dialgauge.py
+ toggleswitch.py
+ graphicoverlay.py
+ auto_correlator_sink.py
+ "${CMAKE_CURRENT_BINARY_DIR}/range.py"
+ "${CMAKE_CURRENT_BINARY_DIR}/util.py"
DESTINATION ${GR_PYTHON_DIR}/gnuradio/qtgui
)
diff --git a/gr-qtgui/python/qtgui/__init__.py b/gr-qtgui/python/qtgui/__init__.py
index b56e2f78bd..d727748130 100644
--- a/gr-qtgui/python/qtgui/__init__.py
+++ b/gr-qtgui/python/qtgui/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright 2011 Free Software Foundation, Inc.
+# Copyright 2011, 2020 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
@@ -25,3 +25,22 @@ except ImportError:
from .range import Range, RangeWidget
from . import util
+
+from .compass import GrCompass
+from .togglebutton import ToggleButton
+from .msgpushbutton import MsgPushButton
+from .distanceradar import DistanceRadar
+from .azelplot import AzElPlot
+from .msgcheckbox import MsgCheckBox
+from .digitalnumbercontrol import MsgDigitalNumberControl
+from .dialcontrol import GrDialControl
+from .ledindicator import GrLEDIndicator
+from .graphicitem import GrGraphicItem
+from .levelgauge import GrLevelGauge
+from .dialgauge import GrDialGauge
+from .toggleswitch import GrToggleSwitch
+from .graphicoverlay import GrGraphicOverlay
+from .auto_correlator_sink import AutoCorrelatorSink
+from .auto_correlator_sink import AutoCorrelator
+from .auto_correlator_sink import Normalize
+
diff --git a/gr-qtgui/python/qtgui/auto_correlator_sink.py b/gr-qtgui/python/qtgui/auto_correlator_sink.py
new file mode 100644
index 0000000000..877e60d0ad
--- /dev/null
+++ b/gr-qtgui/python/qtgui/auto_correlator_sink.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+import math
+import sip
+
+from gnuradio import gr
+from gnuradio import qtgui
+from gnuradio import blocks, fft, filter
+
+from PyQt5 import QtGui
+from PyQt5.QtWidgets import QWidget
+
+class Normalize(gr.hier_block2):
+ def __init__(self, vecsize=1024):
+ gr.hier_block2.__init__(
+ self, "Normalize",
+ gr.io_signature(1, 1, gr.sizeof_float*vecsize),
+ gr.io_signature(1, 1, gr.sizeof_float*vecsize),
+ )
+
+ ##################################################
+ # Parameters
+ ##################################################
+ self.vecsize = vecsize
+
+ ##################################################
+ # Blocks
+ ##################################################
+ self.blocks_stream_to_vector_0 = blocks.stream_to_vector(gr.sizeof_float, vecsize)
+ self.blocks_repeat_0 = blocks.repeat(gr.sizeof_float, vecsize)
+ self.blocks_max_xx_0 = blocks.max_ff(vecsize)
+ self.blocks_divide_xx_0 = blocks.divide_ff(vecsize)
+
+ ##################################################
+ # Connections
+ ##################################################
+ self.connect((self.blocks_divide_xx_0, 0), (self, 0))
+ self.connect((self.blocks_stream_to_vector_0, 0), (self.blocks_divide_xx_0, 1))
+ self.connect((self, 0), (self.blocks_max_xx_0, 0))
+ self.connect((self.blocks_repeat_0, 0), (self.blocks_stream_to_vector_0, 0))
+ self.connect((self.blocks_max_xx_0, 0), (self.blocks_repeat_0, 0))
+ self.connect((self, 0), (self.blocks_divide_xx_0, 0))
+
+
+ def get_vecsize(self):
+ return self.vecsize
+
+ def set_vecsize(self, vecsize):
+ self.vecsize = vecsize
+
+class AutoCorrelator(gr.hier_block2):
+ """
+ This block uses the Wiener Khinchin theorem that the FFT of a signal's
+ power spectrum is its auto-correlation function.
+ FAC Size controls the FFT size and therefore the length of time
+ (samp_rate/fac_size) the auto-correlation runs over.
+ """
+ def __init__(self, sample_rate, fac_size, fac_decimation, use_db):
+ gr.hier_block2.__init__(self,"AutoCorrelator",
+ gr.io_signature(1, 1, gr.sizeof_gr_complex), # Input sig
+ gr.io_signature(1, 1, gr.sizeof_float*fac_size)) # Output sig
+
+ self.fac_size = fac_size
+ self.fac_decimation = fac_decimation
+ self.sample_rate = sample_rate
+
+ streamToVec = blocks.stream_to_vector(gr.sizeof_gr_complex, self.fac_size)
+ # Make sure N is at least 1
+ decimation = int(self.sample_rate/self.fac_size/self.fac_decimation)
+ self.one_in_n = blocks.keep_one_in_n(gr.sizeof_gr_complex * self.fac_size, max(1, decimation))
+
+ # FFT Note: No windowing.
+ fac = fft.fft_vcc(self.fac_size, True, ())
+
+ complex2Mag = blocks.complex_to_mag(self.fac_size)
+ self.avg = filter.single_pole_iir_filter_ff_make(1.0, self.fac_size)
+
+ fac_fac = fft.fft_vfc(self.fac_size, True, ())
+ fac_c2mag = blocks.complex_to_mag_make(fac_size)
+
+ # There's a note in Baz's block about needing to add 3 dB to each bin but the DC bin, however it was never implemented
+ n = 20
+ k = -20 * math.log10(self.fac_size)
+ log = blocks.nlog10_ff_make(n, self.fac_size, k)
+
+ if use_db:
+ self.connect(self, streamToVec, self.one_in_n, fac, complex2Mag, fac_fac, fac_c2mag, self.avg, log, self)
+ else:
+ self.connect(self, streamToVec, self.one_in_n, fac, complex2Mag, fac_fac, fac_c2mag, self.avg, self)
+
+class AutoCorrelatorSink(gr.hier_block2):
+ """
+ docstring for block AutoCorrelatorSink
+ """
+ def __init__(self, sample_rate, fac_size, fac_decimation, title, autoScale, grid, yMin, yMax, use_db):
+ gr.hier_block2.__init__(self,
+ "AutoCorrelatorSink",
+ gr.io_signature(1, 1, gr.sizeof_gr_complex), # Input signature
+ gr.io_signature(0, 0, 0)) # Output signature
+
+ self.fac_size = fac_size
+ self.fac_decimation = fac_decimation
+ self.sample_rate = sample_rate
+
+ autoCorr = AutoCorrelator(sample_rate, fac_size, fac_decimation, use_db)
+ vecToStream = blocks.vector_to_stream(gr.sizeof_float, self.fac_size)
+
+ self.timeSink = qtgui.time_sink_f(self.fac_size/2, sample_rate, title, 1)
+ self.timeSink.enable_grid(grid)
+ self.timeSink.set_y_axis(yMin, yMax)
+ self.timeSink.enable_autoscale(autoScale)
+ self.timeSink.disable_legend()
+ self.timeSink.set_update_time(0.1)
+
+ if use_db:
+ self.connect(self, autoCorr, vecToStream, self.timeSink)
+ else:
+ norm = Normalize(self.fac_size)
+ self.connect(self, autoCorr, norm, vecToStream, self.timeSink)
+
+
+ def getWidget(self):
+ return sip.wrapinstance(self.timeSink.pyqwidget(), QWidget)
diff --git a/gr-qtgui/python/qtgui/azelplot.py b/gr-qtgui/python/qtgui/azelplot.py
new file mode 100644
index 0000000000..d23d39234d
--- /dev/null
+++ b/gr-qtgui/python/qtgui/azelplot.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from PyQt5 import QtWidgets
+import numpy as np
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.figure import Figure
+import math
+
+from gnuradio import gr
+import pmt
+
+class AzElPlot(gr.sync_block, FigureCanvas):
+ """
+ This block creates a polar plot with azimuth represented as the angle
+ clockwise around the circle, and elevation represented as the radius.
+ 90 degrees elevation is center (directly overhead),
+ while the horizon (0 degrees) is the outside circe. Note that if an
+ elevation < 0 is provided, the marker will turn to an open circle
+ on the perimeter at the specified azimuth angle.
+ """
+ def __init__(self, lbl, backgroundColor, dotColor, Parent=None,
+ width=4, height=4, dpi=90):
+ gr.sync_block.__init__(self, name = "MsgPushButton", in_sig = None,
+ out_sig = None)
+
+ self.lbl = lbl
+
+ self.message_port_register_in(pmt.intern("azel"))
+ self.set_msg_handler(pmt.intern("azel"), self.msgHandler)
+
+ self.dotColor = dotColor
+ self.backgroundColor = backgroundColor
+ self.scaleColor = 'black'
+ if (self.backgroundColor == 'black'):
+ self.scaleColor = 'white'
+
+ self.fig = Figure(figsize=(width, height), dpi=dpi)
+ self.fig.patch.set_facecolor(self.backgroundColor)
+
+ self.axes = self.fig.add_subplot(111, polar=True, facecolor=self.backgroundColor)
+
+ # Create an "invisible" line at 90 to set the max for the plot
+ self.axes.plot(np.linspace(0, 2*np.pi, 90), np.ones(90)*90, color=self.scaleColor,
+ linestyle='')
+
+ # Plot line: Initialize out to 90 and blank
+ radius = 90
+ self.blackline = self.axes.plot(np.linspace(0, 2*np.pi, 90), np.ones(90)*radius,
+ color=self.scaleColor, linestyle='-')
+ self.reddot = None
+
+ # Rotate zero up
+ self.axes.set_theta_zero_location("N")
+
+ # Set limits:
+ self.axes.set_rlim(0, 90)
+
+ self.axes.set_yticklabels([], color=self.scaleColor)
+ self.axes.set_xticklabels(['0', '315', '270', '225', '180', '135', '90', '45'],
+ color=self.scaleColor)
+
+ FigureCanvas.__init__(self, self.fig)
+ self.setParent(Parent)
+
+ self.title = self.fig.suptitle(self.lbl, fontsize=8, fontweight='bold', color='black')
+
+ FigureCanvas.setSizePolicy(self,
+ QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding)
+
+ self.setMinimumSize(240, 230)
+ FigureCanvas.updateGeometry(self)
+
+ def msgHandler(self, msg):
+ new_val = None
+
+ try:
+ new_val = pmt.to_python(pmt.car(msg))
+ if new_val is not None:
+ if type(new_val) == dict:
+ if 'az' in new_val and 'el' in new_val:
+ self.updateData(float(new_val['az']), float(new_val['el']))
+ else:
+ gr.log.error("az and el keys were not found in the dictionary.")
+ else:
+ gr.log.error("Value received was not a dictionary. Expecting a dictionary "
+ "in the car message component with az and el keys.")
+ else:
+ gr.log.error("The CAR section of the inbound message was None. "
+ "This part should contain the dictionary with 'az' and 'el' float keys.")
+ except Exception as e:
+ gr.log.error("[AzElPlot] Error with message conversion: %s" % str(e))
+ if new_val is not None:
+ gr.log.error(str(new_val))
+
+ def updateData(self, azimuth, elevation):
+ if self.reddot is not None:
+ self.reddot.pop(0).remove()
+
+ # Plot is angle, radius where angle is in radians
+
+ if (elevation > 0):
+ # Need to reverse elevation. 90 degrees is center (directly overhead),
+ # and 90 degrees is horizon.
+ if (elevation > 90.0):
+ elevation = 90.0
+
+ convertedElevation = 90.0 - elevation
+ # Note: +azimuth for the plot is measured counter-clockwise, so need to reverse it.
+ self.reddot = self.axes.plot(-azimuth * math.pi/180.0, convertedElevation, self.dotColor,
+ markersize=8)
+ else:
+ # It's below the horizon. Show an open circle at the perimeter
+ elevation = 0.0
+ self.reddot = self.axes.plot(-azimuth * math.pi/180.0, 89.0, self.dotColor,
+ markerfacecolor="None", markersize=16, fillstyle=None)
+
+ self.draw()
+
diff --git a/gr-qtgui/python/qtgui/compass.py b/gr-qtgui/python/qtgui/compass.py
new file mode 100644
index 0000000000..2c8a766f14
--- /dev/null
+++ b/gr-qtgui/python/qtgui/compass.py
@@ -0,0 +1,290 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+import time
+import numpy
+from gnuradio import gr
+import pmt
+
+# First Qt and 2nd Qt are different. You'll get errors if they're both not available,
+# hence the import-as to avoid name collisions
+
+from PyQt5 import Qt
+from PyQt5.QtCore import Qt as Qtc
+from PyQt5.QtCore import pyqtSignal, QPoint, pyqtProperty
+from PyQt5.QtWidgets import QFrame, QWidget, QVBoxLayout, QHBoxLayout, QLabel
+from PyQt5.QtGui import QPainter, QPalette, QFont, QFontMetricsF, QPen, QPolygon, QColor, QBrush
+
+NeedleFull = 1
+NeedleIndicator = 0
+NeedleMirrored = 2
+
+class LabeledCompass(QFrame):
+ def __init__(self, lbl, min_size, update_time, setDebug=False,
+ needleType=NeedleFull, position=1, backgroundColor='default'):
+ # Positions: 1 = above, 2=below, 3=left, 4=right
+ QFrame.__init__(self)
+ self.numberControl = Compass(min_size, update_time, setDebug,
+ needleType, position, backgroundColor)
+
+ if position < 3:
+ layout = QVBoxLayout()
+ else:
+ layout = QHBoxLayout()
+
+ self.lbl = lbl
+ self.lblcontrol = QLabel(lbl, self)
+ self.lblcontrol.setAlignment(Qtc.AlignCenter)
+
+ # add top or left
+ if lbl:
+ if position == 1 or position == 3:
+ layout.addWidget(self.lblcontrol)
+ else:
+ self.hasLabel = False
+
+ layout.addWidget(self.numberControl)
+
+ # Add bottom or right
+ if lbl:
+ if position == 2 or position == 4:
+ layout.addWidget(self.lblcontrol)
+
+ layout.setAlignment(Qtc.AlignCenter | Qtc.AlignVCenter)
+ self.setLayout(layout)
+
+ if lbl:
+ self.setMinimumSize(min_size+30, min_size+35)
+ else:
+ self.setMinimumSize(min_size, min_size)
+
+ self.show()
+ def change_angle(self, angle):
+ self.numberControl.change_angle(angle)
+
+ def setColors(self, backgroundColor='default', needleTip='red', needleBody='black',
+ scaleColor='black'):
+ self.numberControl.setColors(backgroundColor, needleTip, needleBody, scaleColor)
+
+class Compass(QWidget):
+ angleChanged = pyqtSignal(float)
+
+ def __init__(self, min_size, update_time, setDebug=False, needleType=NeedleFull,
+ position=1, backgroundColor='default'):
+ QWidget.__init__(self, None)
+
+ # Set parameters
+ self.debug = setDebug
+ self.needleType = needleType
+ self.update_period = update_time
+ self.last = time.time()
+ self.next_angle = 0
+
+ self._angle = 0.0
+ self._margins = 2
+ self._pointText = {0: "0", 45: "45", 90: "90", 135: "135", 180: "180",
+ 225: "225", 270: "270", 315: "315"}
+
+ self.setMinimumSize(min_size, min_size)
+ self.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding)
+
+ self.backgroundColor = backgroundColor
+ self.needleTipColor = 'red'
+ self.needleBodyColor = 'black'
+ self.scaleColor = 'black'
+
+ def setColors(self, backgroundColor='default', needleTipColor='red', needleBodyColor='black',
+ scaleColor='black'):
+ self.backgroundColor = backgroundColor
+ self.needleTipColor = needleTipColor
+ self.needleBodyColor = needleBodyColor
+ self.scaleColor = scaleColor
+
+ super().update()
+
+ def paintEvent(self, event):
+ painter = QPainter()
+ painter.begin(self)
+ painter.setRenderHint(QPainter.Antialiasing)
+
+ if self.backgroundColor == 'default':
+ painter.fillRect(event.rect(), self.palette().brush(QPalette.Window))
+ else:
+ size = self.size()
+ center_x = size.width()/2
+ diameter = size.height()
+ brush = QBrush(QColor(self.backgroundColor), Qtc.SolidPattern)
+ painter.setBrush(brush)
+ painter.setPen(QPen(QColor(self.scaleColor), 2))
+ painter.setRenderHint(QPainter.Antialiasing)
+ painter.drawEllipse(center_x-diameter/2+1, 1, diameter-4, diameter-4)
+
+ self.drawMarkings(painter)
+ self.drawNeedle(painter)
+
+ painter.end()
+
+ def drawMarkings(self, painter):
+ painter.save()
+ painter.translate(self.width()/2, self.height()/2)
+ scale = min((self.width() - self._margins)/120.0,
+ (self.height() - self._margins)/120.0)
+ painter.scale(scale, scale)
+
+ font = QFont(self.font())
+ font.setPixelSize(8)
+ metrics = QFontMetricsF(font)
+
+ painter.setFont(font)
+ painter.setPen(QPen(QColor(self.scaleColor)))
+ tickInterval = 5
+ i = 0
+ while i < 360:
+
+ if i % 45 == 0:
+ painter.drawLine(0, -40, 0, -50)
+ painter.drawText(-metrics.width(self._pointText[i])/2.0, -52, self._pointText[i])
+ else:
+ painter.drawLine(0, -45, 0, -50)
+
+ painter.rotate(tickInterval)
+ i += tickInterval
+
+ painter.restore()
+
+ def drawNeedle(self, painter):
+ painter.save()
+ # Set up painter
+ painter.translate(self.width()/2, self.height()/2)
+ scale = min((self.width() - self._margins)/120.0,
+ (self.height() - self._margins)/120.0)
+ painter.scale(scale, scale)
+ painter.setPen(QPen(Qtc.NoPen))
+
+ # Rotate surface for painting
+ intAngle = int(round(self._angle))
+ painter.rotate(intAngle)
+
+ # Draw the full black needle first if needed
+ if self.needleType == NeedleFull:
+ needleTailBrush = self.palette().brush(QPalette.Shadow)
+ needleTailColor = QColor(self.needleBodyColor)
+ needleTailBrush.setColor(needleTailColor)
+ painter.setBrush(needleTailBrush)
+
+ painter.drawPolygon(QPolygon([QPoint(-6, 0), QPoint(0, -45), QPoint(6, 0),
+ QPoint(0, 45), QPoint(-6, 0)]))
+
+ # Now draw the red tip (on top of the black needle)
+ needleTipBrush = self.palette().brush(QPalette.Highlight)
+ needleTipColor = QColor(self.needleTipColor)
+ needleTipBrush.setColor(needleTipColor)
+ painter.setBrush(needleTipBrush)
+
+ # First QPoint is the center bottom apex of the needle
+ painter.drawPolygon(QPolygon([QPoint(-3, -24), QPoint(0, -45), QPoint(3, -23),
+ QPoint(0, -30), QPoint(-3, -23)]))
+
+ if self.needleType == NeedleMirrored:
+ # Rotate
+ # Need to account for the initial rotation to see how much more to rotate it.
+ if (intAngle == 90 or intAngle == -90 or intAngle == 270):
+ mirrorRotation = 180
+ else:
+ mirrorRotation = 180 - intAngle - intAngle
+ painter.rotate(mirrorRotation)
+
+ # Paint shadowed indicator
+ needleTipBrush = self.palette().brush(QPalette.Highlight)
+ needleTipColor = Qtc.gray
+ needleTipBrush.setColor(needleTipColor)
+ painter.setBrush(needleTipBrush)
+
+ painter.drawPolygon(
+ QPolygon([QPoint(-3, -25), QPoint(0, -45), QPoint(3, -25),
+ QPoint(0, -30), QPoint(-3, -25)])
+ )
+
+ painter.restore()
+
+ def angle(self):
+ return self._angle
+
+ def change_angle(self, angle):
+ if angle != self._angle:
+ if self.debug:
+ gr.log.info(("Compass angle: " + str(angle)))
+
+ if angle < 0.0:
+ angle = 360.0 + angle # Angle will already be negative
+
+ self._angle = angle
+ self.angleChanged.emit(angle)
+ self.update()
+
+ angle = pyqtProperty(float, angle, change_angle)
+
+
+class GrCompass(gr.sync_block, LabeledCompass):
+ """
+ This block takes angle in degrees as input and displays it on a compass.
+ Three different needle formats are available, Full, indicator only,
+ and mirrored (mirrored is useful for direction-finding where an
+ ambiguity exists in front/back detection angle).
+ """
+ def __init__(self, title, min_size, update_time, setDebug=False, needleType=NeedleFull,
+ usemsg=False, position=1, backgroundColor='default'):
+ if usemsg:
+ gr.sync_block.__init__(self, name="QTCompass", in_sig=[], out_sig=[])
+ else:
+ gr.sync_block.__init__(self, name="QTCompass", in_sig=[numpy.float32], out_sig=[])
+
+ LabeledCompass.__init__(self, title, min_size, update_time, setDebug, needleType,
+ position, backgroundColor)
+
+ self.last = time.time()
+ self.update_period = update_time
+ self.useMsg = usemsg
+
+ self.next_angle = 0.0
+
+ self.message_port_register_in(pmt.intern("angle"))
+ self.set_msg_handler(pmt.intern("angle"), self.msgHandler)
+
+ def msgHandler(self, msg):
+ try:
+ new_val = pmt.to_python(pmt.cdr(msg))
+
+ if type(new_val) == float or type(new_val) == int:
+ super().change_angle(float(new_val))
+ else:
+ gr.log.error("Value received was not an int or a float: %s" % str(type(new_val)))
+
+ except Exception as e:
+ gr.log.error("Error with message conversion: %s" % str(e))
+
+ def setColors(self, backgroundColor='default', needleTip='red', needleBody='black',
+ scaleColor='black'):
+ super().setColors(backgroundColor, needleTip, needleBody, scaleColor)
+
+ def work(self, input_items, output_items):
+ if self.useMsg:
+ return len(input_items[0])
+
+ # Average inputs
+ self.next_angle = numpy.mean(input_items[0])
+
+ if (time.time() - self.last) > self.update_period:
+ self.last = time.time()
+ super().change_angle(self.next_angle)
+
+ # Consume all inputs
+ return len(input_items[0])
diff --git a/gr-qtgui/python/qtgui/dialcontrol.py b/gr-qtgui/python/qtgui/dialcontrol.py
new file mode 100644
index 0000000000..e5c7618b9f
--- /dev/null
+++ b/gr-qtgui/python/qtgui/dialcontrol.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from PyQt5.QtWidgets import QFrame, QVBoxLayout, QLabel
+from PyQt5 import Qt
+from PyQt5.QtCore import Qt as Qtc
+from PyQt5.QtCore import QSize
+from gnuradio import gr
+import pmt
+
+class LabeledDialControl(QFrame):
+ def __init__(self, lbl='', parent=None, minimum=0, maximum=100, defaultvalue=0,
+ backgroundColor='default', changedCallback=None,
+ minsize=100, isFloat=False, scaleFactor=1, showvalue=False,
+ outputmsgname='value'):
+ QFrame.__init__(self, parent)
+ self.numberControl = DialControl(minimum, maximum, defaultvalue, backgroundColor,
+ self.valChanged, changedCallback, minsize)
+
+ layout = QVBoxLayout()
+
+ self.outputmsgname = outputmsgname
+ self.showvalue = showvalue
+ self.isFloat = isFloat
+ self.scaleFactor = scaleFactor
+ self.lbl = lbl
+ self.lblcontrol = QLabel(lbl, self)
+ self.lblcontrol.setAlignment(Qtc.AlignCenter)
+
+ if self.showvalue:
+ textstr = self.buildTextStr(defaultvalue*self.scaleFactor)
+ self.lblcontrol.setText(textstr)
+
+ if len or self.showvalue:
+ self.hasLabel = True
+ layout.addWidget(self.lblcontrol)
+ else:
+ self.hasLabel = False
+
+ layout.addWidget(self.numberControl)
+
+ layout.setAlignment(Qtc.AlignCenter)
+ self.setLayout(layout)
+ self.show()
+
+ def buildTextStr(self, new_value):
+ textstr = ""
+ if self.lbl:
+ textstr = self.lbl + " - "
+
+ if self.isFloat:
+ textstr += "%.2f" % (new_value)
+ else:
+ textstr += str(new_value)
+
+ return textstr
+
+ def valChanged(self, new_value):
+ if not self.showvalue:
+ return
+
+ if int(self.scaleFactor) != 1:
+ new_value = new_value * self.scaleFactor
+
+ textstr = self.buildTextStr(new_value)
+ self.lblcontrol.setText(textstr)
+
+class DialControl(Qt.QDial):
+ def __init__(self, minimum=0, maximum=100, defaultvalue=0, backgroundColor='default',
+ lablelCallback=None, changedCallback=None, minsize=100):
+ Qt.QDial.__init__(self)
+
+ if backgroundColor != "default":
+ self.setStyleSheet("background-color: " + backgroundColor + ";")
+
+ self.minsize = minsize
+ self.changedCallback = changedCallback
+ self.lablelCallback = lablelCallback
+ super().setMinimum(minimum)
+ super().setMaximum(maximum)
+ super().setValue(defaultvalue)
+ super().valueChanged.connect(self.sliderMoved)
+
+ def minimumSizeHint(self):
+ return QSize(self.minsize, self.minsize)
+
+ def sliderMoved(self):
+ if self.changedCallback is not None:
+ self.changedCallback(self.value())
+
+ if self.lablelCallback is not None:
+ self.lablelCallback(self.value())
+
+class GrDialControl(gr.sync_block, LabeledDialControl):
+ """
+ This block creates a dial control. The control does control a
+ variable which can be used for other items. Leave the label
+ blank to use the variable id as the label. The block also
+ creates an optional message with the control value that
+ can be used in message-based applications.
+
+ Note: Dials only produce integer values, so the scale factor
+ can be used with the min/max to adjust the output value to
+ the desired range. Think of the min/max as the increments,
+ and the scale factor as the adjustment to get the values you want.
+ """
+ def __init__(self, lbl, parent, minimum, maximum, defaultvalue, backgroundColor='default',
+ varCallback=None, isFloat=False,
+ scaleFactor=1, minsize=100, showvalue=False, outputmsgname='value'):
+ gr.sync_block.__init__(self, name="GrDialControl", in_sig=None, out_sig=None)
+ LabeledDialControl.__init__(self, lbl, parent, minimum, maximum, defaultvalue,
+ backgroundColor, self.valueChanged, minsize, isFloat,
+ scaleFactor, showvalue)
+
+ self.outputmsgname = outputmsgname
+ self.varCallback = varCallback
+ self.scaleFactor = scaleFactor
+ self.isFloat = isFloat
+ self.message_port_register_out(pmt.intern("value"))
+
+ def valueChanged(self, new_value):
+ if int(self.scaleFactor) != 1:
+ new_value = new_value * self.scaleFactor
+
+ if self.varCallback is not None:
+ self.varCallback(new_value)
+
+ if self.isFloat:
+ self.message_port_pub(pmt.intern("value"), pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_float(new_value)))
+ else:
+ self.message_port_pub(pmt.intern("value"), pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_long(new_value)))
+
diff --git a/gr-qtgui/python/qtgui/dialgauge.py b/gr-qtgui/python/qtgui/dialgauge.py
new file mode 100644
index 0000000000..af96f2e096
--- /dev/null
+++ b/gr-qtgui/python/qtgui/dialgauge.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+import sys
+from PyQt5.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QLabel
+from PyQt5.QtGui import QPainter, QColor, QPen, QFont, QFontMetricsF
+from PyQt5 import QtCore
+from PyQt5.QtCore import Qt as Qtc
+
+from gnuradio import gr
+import pmt
+
+class LabeledDialGauge(QFrame):
+ # Positions: 1 = above, 2=below, 3=left, 4=right
+ def __init__(self, lbl='', barColor='blue', backgroundColor='white', fontColor='black',
+ minValue=0, maxValue=100, maxSize=80, position=1,
+ isFloat=False, showValue=False, fixedOrMin=True, parent=None):
+ QFrame.__init__(self, parent)
+ self.numberControl = DialGauge(barColor, backgroundColor, fontColor, minValue,
+ maxValue, maxSize, isFloat, showValue, fixedOrMin, parent)
+
+ if position < 3:
+ layout = QVBoxLayout()
+ else:
+ layout = QHBoxLayout()
+
+ self.lbl = lbl
+ self.showvalue = showValue
+ self.isFloat = isFloat
+
+ self.lblcontrol = QLabel(lbl, self)
+ self.lblcontrol.setAlignment(Qtc.AlignCenter)
+
+ # For whatever reason, the progressbar doesn't show the number in the bar if it's
+ # vertical, only if it's horizontal
+ if len:
+ self.lblcontrol.setText(lbl)
+
+ if fontColor != 'default':
+ self.lblcontrol.setStyleSheet("QLabel { color : " + fontColor + "; }")
+
+ # add top or left
+ if len:
+ if position == 1 or position == 3:
+ layout.addWidget(self.lblcontrol)
+ else:
+ self.hasLabel = False
+
+ layout.addWidget(self.numberControl)
+
+ # Add bottom or right
+ if len:
+ if position == 2 or position == 4:
+ layout.addWidget(self.lblcontrol)
+
+ layout.setAlignment(Qtc.AlignCenter | Qtc.AlignVCenter)
+ self.setLayout(layout)
+
+ self.show()
+
+ def setValue(self, new_value):
+ self.numberControl.setValue(new_value)
+
+class DialGauge(QFrame):
+ def __init__(self, barColor='blue', backgroundColor='white', fontColor='black',
+ minValue=0, maxValue=100, maxSize=80,
+ isFloat=False, showValue=False, fixedOrMin=True, parent=None):
+ QFrame.__init__(self, parent)
+
+ self.maxSize = maxSize
+ super().setMinimumSize(maxSize, maxSize)
+ if fixedOrMin:
+ super().setMaximumSize(maxSize, maxSize)
+
+ self.backgroundColor = backgroundColor
+ self.barColor = barColor
+ self.fontColor = fontColor
+ self.isFloat = isFloat
+ self.showValue = showValue
+
+ self.value = minValue
+
+ self.minValue = minValue
+ self.maxValue = maxValue
+
+ self.textfont = QFont(self.font())
+ self.textfont.setPixelSize(16)
+ self.metrics = QFontMetricsF(self.textfont)
+
+ self.startAngle = 0.0
+ self.endAngle = 360.0
+ self.degScaler = 16.0 # The span angle must be specified in 1/16 of a degree units
+ self.penWidth = max(int(0.1 * maxSize), 6)
+ self.halfPenWidth = int(self.penWidth / 2)
+
+ def getValue(self):
+ if self.isFloat:
+ return float(self.value)
+ else:
+ return int(self.value)
+
+ def setValue(self, new_value):
+ if new_value > self.maxValue:
+ new_value = self.maxValue
+ elif new_value < self.minValue:
+ new_value = self.minValue
+
+ self.value = float(new_value)
+
+ super().update()
+
+ def paintEvent(self, event):
+ super().paintEvent(event)
+
+ size = self.size()
+
+ percentRange = float(self.value - self.minValue) / float(self.maxValue - self.minValue)
+ endAngle = self.startAngle + round(percentRange * float(self.endAngle - self.startAngle), 0)
+
+ # Now convert angles to 1/16 scale
+ startAngle = int(round(self.startAngle * self.degScaler, 0))
+ endAngle = int(round(endAngle * self.degScaler, 0))
+
+ rect = QtCore.QRect(self.halfPenWidth, self.halfPenWidth, size.width()-self.penWidth,
+ size.height()-self.penWidth)
+
+ # Set up the painting canvass
+ painter = QPainter()
+ painter.begin(self)
+ painter.setRenderHint(QPainter.Antialiasing)
+
+ if self.showValue:
+ painter.setFont(self.textfont)
+ painter.setPen(QPen(QColor(self.fontColor)))
+
+ if self.isFloat:
+ printText = "%.2f" % self.value
+ else:
+ printText = str(int(self.value))
+
+ painter.drawText(size.width()/2-self.metrics.width(printText)/2, size.height()/2,
+ printText)
+
+ painter.save()
+ painter.translate(self.width(), 0)
+ painter.rotate(90.0)
+
+ # First draw complete circle
+ painter.setPen(QPen(QColor(self.backgroundColor), self.penWidth))
+ painter.drawArc(rect, startAngle, self.endAngle*self.degScaler)
+ # First draw complete circle
+ painter.setPen(QPen(QColor(self.barColor), self.penWidth))
+ painter.drawArc(rect, startAngle, -endAngle)
+ painter.setPen(QPen(QColor('darkgray'), 2))
+ painter.drawEllipse(1, 1, rect.width()+self.penWidth-2, rect.width()+self.penWidth-2)
+ painter.drawEllipse(1+self.penWidth, 1+self.penWidth, rect.width()-self.penWidth-2,
+ rect.width()-self.penWidth-2)
+ painter.restore()
+
+ painter.end()
+
+class GrDialGauge(gr.sync_block, LabeledDialGauge):
+ """
+ This block creates a dial-style gauge. The value can be set
+ either with a variable or an input message.
+ """
+ def __init__(self, lbl='', barColor='blue', backgroundColor='white', fontColor='black',
+ minValue=0, maxValue=100, maxSize=80,
+ position=1, isFloat=False, showValue=False, fixedOrMin=True, parent=None):
+ gr.sync_block.__init__(self, name="DialGauge", in_sig=None, out_sig=None)
+ LabeledDialGauge.__init__(self, lbl, barColor, backgroundColor, fontColor, minValue,
+ maxValue, maxSize, position, isFloat, showValue, fixedOrMin,
+ parent)
+ self.lbl = lbl
+
+ if minValue > maxValue:
+ gr.log.error("Min value is greater than max value.")
+ sys.exit(1)
+
+ self.message_port_register_in(pmt.intern("value"))
+ self.set_msg_handler(pmt.intern("value"), self.msgHandler)
+
+
+ def msgHandler(self, msg):
+ try:
+ new_val = pmt.to_python(pmt.cdr(msg))
+
+ if type(new_val) == float or type(new_val) == int:
+ super().setValue(new_val)
+ else:
+ gr.log.error("Value received was not an int or a float. "
+ "Received %s" % str(type(new_val)))
+
+ except Exception as e:
+ gr.log.error("Error with message conversion: %s" % str(e))
+
+
+ def setValue(self, new_value):
+ super().setValue(new_value)
+
diff --git a/gr-qtgui/python/qtgui/digitalnumbercontrol.py b/gr-qtgui/python/qtgui/digitalnumbercontrol.py
new file mode 100644
index 0000000000..ea8bdd7eb5
--- /dev/null
+++ b/gr-qtgui/python/qtgui/digitalnumbercontrol.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from PyQt5.QtWidgets import QFrame, QVBoxLayout, QLabel
+from PyQt5.QtGui import QPainter, QPixmap, QFont, QFontMetrics, QBrush, QColor
+from PyQt5.QtCore import Qt, QSize
+from PyQt5 import QtCore
+from PyQt5.QtCore import Qt as Qtc
+from PyQt5.QtCore import pyqtSignal
+
+from gnuradio import gr
+import pmt
+
+# -------------- Support Classes ---------------------------------
+#
+#
+
+class LabeledDigitalNumberControl(QFrame):
+ def __init__(self, lbl='', min_freq_hz=0, max_freq_hz=6000000000, parent=None,
+ thousands_separator=',', background_color='black', fontColor='white',
+ click_callback=None):
+ QFrame.__init__(self, parent)
+ self.numberControl = DigitalNumberControl(min_freq_hz, max_freq_hz, self,
+ thousands_separator, background_color, fontColor, click_callback)
+
+ layout = QVBoxLayout()
+
+ self.lbl = QLabel(lbl, self)
+ if len:
+ self.hasLabel = True
+ layout.addWidget(self.lbl)
+ else:
+ self.hasLabel = False
+
+ layout.addWidget(self.numberControl)
+ layout.setAlignment(Qtc.AlignCenter | Qtc.AlignVCenter)
+ self.setLayout(layout)
+ self.show()
+
+ def minimumSizeHint(self):
+ if self.hasLabel:
+ return QSize(self.numberControl.minimumWidth()+10, 100)
+ else:
+ return QSize(self.numberControl.minimumWidth()+10, 50)
+
+ def setReadOnly(self, b_read_only):
+ self.numberControl.setReadOnly(b_read_only)
+
+ def setFrequency(self, new_freq):
+ self.numberControl.setFrequency(new_freq)
+
+ def getFrequency(self):
+ return self.numberControl.getFrequency()
+
+class DigitalNumberControl(QFrame):
+ # Notifies to avoid thread conflicts on paints
+ updateInt = pyqtSignal(int)
+ updateFloat = pyqtSignal(float)
+
+ def __init__(self, min_freq_hz=0, max_freq_hz=6000000000, parent=None, thousands_separator=',',
+ background_color='black', fontColor='white', click_callback=None):
+ QFrame.__init__(self, parent)
+
+ self.updateInt.connect(self.onUpdateInt)
+ self.updateFloat.connect(self.onUpdateFloat)
+
+ self.min_freq = int(min_freq_hz)
+ self.max_freq = int(max_freq_hz)
+ self.numDigitsInFreq = len(str(max_freq_hz))
+
+ self.thousands_separator = thousands_separator
+ self.click_callback = click_callback
+
+ self.read_only = False
+
+ self.setColors(QColor(background_color), QColor(fontColor))
+ self.numberFont = QFont("Arial", 12, QFont.Normal)
+
+ self.cur_freq = min_freq_hz
+
+ self.debug_click = False
+
+ # Determine what our width minimum is
+ teststr = ""
+ for i in range(0, self.numDigitsInFreq):
+ teststr += "0"
+
+ fm = QFontMetrics(self.numberFont)
+ if len(self.thousands_separator) > 0:
+ # The -1 makes sure we don't count an extra for 123,456,789. Answer should be 2 not 3.
+ numgroups = int(float(self.numDigitsInFreq-1) / 3.0)
+ if numgroups > 0:
+ for i in range(0, numgroups):
+ teststr += self.thousands_separator
+
+ textstr = teststr
+ else:
+ textstr = teststr
+
+ width = fm.width(textstr)
+
+ self.minwidth = width
+
+ if self.minwidth < 410:
+ self.minwidth = 410
+
+ self.setMaximumHeight(70)
+ self.setMinimumWidth(self.minwidth)
+ # Show the control
+ self.show()
+
+ def minimumSizeHint(self):
+ return QSize(self.minwidth, 50)
+
+ def setReadOnly(self, b_read_only):
+ self.read_only = b_read_only
+
+ def mousePressEvent(self, event):
+ super(DigitalNumberControl, self).mousePressEvent(event)
+ self.offset = event.pos()
+
+ if self.read_only:
+ return
+
+ fm = QFontMetrics(self.numberFont)
+
+ if len(self.thousands_separator) > 0:
+ if self.thousands_separator != ".":
+ textstr = format(self.getFrequency(), self.thousands_separator)
+ else:
+ textstr = format(self.getFrequency(), ",")
+ textstr = textstr.replace(",", ".")
+ else:
+ textstr = str(self.getFrequency())
+
+ width = fm.width(textstr)
+
+ # So we know:
+ # - the width of the text
+ # - The mouse click position relative to 0 (pos relative to string start
+ # will be size().width() - 2 - pos.x
+
+ clickpos = self.size().width() - 2 - self.offset.x()
+
+ found_number = False
+ clicked_thousands = False
+ for i in range(1, len(textstr)+1):
+ width = fm.width(textstr[-i:])
+ charstr = textstr[-i:]
+ widthchar = fm.width(charstr[0])
+ if clickpos >= (width-widthchar) and clickpos <= width:
+ clicked_char = i-1
+
+ clicked_num_index = clicked_char
+
+ found_number = True
+
+ if len(self.thousands_separator) > 0:
+ if charstr[0] != self.thousands_separator:
+ numSeps = charstr.count(self.thousands_separator)
+ clicked_num_index -= numSeps
+ if self.debug_click:
+ gr.log.info("clicked number: " + str(clicked_num_index))
+ else:
+ clicked_thousands = True
+ if self.debug_click:
+ gr.log.info("clicked thousands separator")
+ else:
+ if self.debug_click:
+ gr.log.info("clicked number: " + str(clicked_char))
+
+ # Remember y=0 is at the top so this is reversed
+ clicked_up = False
+ if self.offset.y() > self.size().height()/2:
+ if self.debug_click:
+ gr.log.info('clicked down')
+ else:
+ if self.debug_click:
+ gr.log.info('clicked up')
+ clicked_up = True
+
+ if not clicked_thousands:
+ cur_freq = self.getFrequency()
+ increment = pow(10, clicked_num_index)
+ if clicked_up:
+ cur_freq += increment
+ else:
+ cur_freq -= increment
+
+ self.setFrequency(cur_freq)
+ if self.click_callback is not None:
+ self.click_callback(self.getFrequency())
+
+ break
+
+ if (not found_number) and (not clicked_thousands):
+ # See if we clicked in the high area, if so, increment there.
+ clicked_up = False
+ if self.offset.y() > self.size().height()/2:
+ if self.debug_click:
+ gr.log.info('clicked down in the high area')
+ else:
+ if self.debug_click:
+ gr.log.info('clicked up in the high area')
+ clicked_up = True
+
+ textstr = str(self.getFrequency())
+ numNumbers = len(textstr)
+ increment = pow(10, numNumbers)
+ cur_freq = self.getFrequency()
+ if clicked_up:
+ cur_freq += increment
+ else:
+ cur_freq -= increment
+
+ self.setFrequency(cur_freq)
+ if self.click_callback is not None:
+ self.click_callback(self.getFrequency())
+
+ def setColors(self, background, fontColor):
+ self.background_color = background
+ self.fontColor = fontColor
+
+ def reverseString(self, astring):
+ astring = astring[::-1]
+ return astring
+
+ def onUpdateInt(self, new_freq):
+ if (new_freq >= self.min_freq) and (new_freq <= self.max_freq):
+ self.cur_freq = int(new_freq)
+
+ self.update()
+
+ def onUpdateFloat(self, new_freq):
+ if (new_freq >= self.min_freq) and (new_freq <= self.max_freq):
+ self.cur_freq = int(new_freq)
+
+ self.update()
+
+ def setFrequency(self, new_freq):
+ if type(new_freq) == int:
+ self.updateInt.emit(new_freq)
+ else:
+ self.updateFloat.emit(new_freq)
+
+ def getFrequency(self):
+ return self.cur_freq
+
+ def resizeEvent(self, event):
+ self.pxMap = QPixmap(self.size())
+ self.pxMap.fill(self.background_color)
+
+ self.update()
+
+ def paintEvent(self, event):
+ super().paintEvent(event)
+
+ painter = QPainter(self)
+
+ size = self.size()
+ brush = QBrush()
+ brush.setColor(self.background_color)
+ brush.setStyle(Qt.SolidPattern)
+ rect = QtCore.QRect(2, 2, size.width()-4, size.height()-4)
+ painter.fillRect(rect, brush)
+
+ self.numberFont.setPixelSize(0.9 * size.height())
+ painter.setFont(self.numberFont)
+ painter.setPen(self.fontColor)
+ rect = event.rect()
+
+ if len(self.thousands_separator) > 0:
+ if self.thousands_separator != ".":
+ textstr = format(self.getFrequency(), self.thousands_separator)
+ else:
+ textstr = format(self.getFrequency(), ",")
+ textstr = textstr.replace(",", ".")
+ else:
+ textstr = str(self.getFrequency())
+ rect = QtCore.QRect(0, 0, size.width()-4, size.height())
+ painter.drawText(rect, Qt.AlignRight + Qt.AlignVCenter, textstr)
+
+# ################################################################################
+
+# GNU Radio Class
+class MsgDigitalNumberControl(gr.sync_block, LabeledDigitalNumberControl):
+ def __init__(self, lbl='', min_freq_hz=0, max_freq_hz=6000000000, parent=None,
+ thousands_separator=',', background_color='black', fontColor='white',
+ var_callback=None, outputmsgname='freq'):
+ gr.sync_block.__init__(self, name="MsgDigitalNumberControl",
+ in_sig=None, out_sig=None)
+ LabeledDigitalNumberControl.__init__(self, lbl, min_freq_hz, max_freq_hz, parent,
+ thousands_separator, background_color, fontColor, self.click_callback)
+
+ self.var_callback = var_callback
+ self.outputmsgname = outputmsgname
+
+ self.message_port_register_in(pmt.intern("valuein"))
+ self.set_msg_handler(pmt.intern("valuein"), self.msgHandler)
+ self.message_port_register_out(pmt.intern("valueout"))
+
+ def msgHandler(self, msg):
+ try:
+ new_val = pmt.to_python(pmt.cdr(msg))
+
+ if type(new_val) == float or type(new_val) == int:
+ self.call_var_callback(new_val)
+
+ self.setValue(new_val)
+ else:
+ gr.log.error("Value received was not an int or a float. %s" % str(type(new_val)))
+
+ except Exception as e:
+ gr.log.error("Error with message conversion: %s" % str(e))
+
+ def call_var_callback(self, new_value):
+ if (self.var_callback is not None):
+ if type(self.var_callback) is float:
+ self.var_callback = float(new_value)
+ else:
+ self.var_callback(float(new_value))
+
+ def click_callback(self, new_value):
+ self.call_var_callback(new_value)
+
+ self.message_port_pub(pmt.intern("valueout"), pmt.cons(pmt.intern(self.outputmsgname), pmt.from_float(float(new_value))))
+
+ def setValue(self, new_val):
+ self.setFrequency(new_val)
+
+ self.message_port_pub(pmt.intern("valueout"), pmt.cons(pmt.intern(self.outputmsgname), pmt.from_float(float(self.getFrequency()))))
+
+ def getValue(self):
+ self.getFrequency()
+
+ def setReadOnly(self, b_read_only):
+ super().setReadOnly(b_read_only)
+
diff --git a/gr-qtgui/python/qtgui/distanceradar.py b/gr-qtgui/python/qtgui/distanceradar.py
new file mode 100644
index 0000000000..170cf81f40
--- /dev/null
+++ b/gr-qtgui/python/qtgui/distanceradar.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+import sys
+from PyQt5 import QtWidgets
+import numpy as np
+import matplotlib.pyplot as plt
+try:
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+ from matplotlib.figure import Figure
+except:
+ gr.log.error("Unable to import matplotlib. Please install matplotlib first "
+ "(e.g., via pip/pip3/dpkg/MacPorts).")
+ sys.exit(1)
+
+from gnuradio import gr
+import pmt
+
+class DistanceRadar(gr.sync_block, FigureCanvas):
+ """
+ This block creates a radar-like screen used to represent distance or size.
+ This can be used in many ways such as circles closer to the center are
+ closer, or just the opposite where closer to the center is smaller.
+
+ Note: Incoming values should range between 0 (center bullseye) and
+ 100 (all the way out)
+ """
+ def __init__(self, lbl, ticklabels, backgroundColor, fontColor, ringColor, Parent=None,
+ width=4, height=4, dpi=100):
+ gr.sync_block.__init__(self, name="distanceradar", in_sig=None, out_sig=None)
+
+ self.lbl = lbl
+
+ self.message_port_register_in(pmt.intern("radius"))
+ self.set_msg_handler(pmt.intern("radius"), self.msgHandler)
+
+ self.fontColor = fontColor
+ self.backgroundColor = backgroundColor
+ self.ringColor = ringColor
+
+ self.fig = Figure(figsize=(width, height), dpi=dpi)
+ self.fig.patch.set_facecolor(self.backgroundColor)
+ self.axes = self.fig.add_subplot(111, polar=True, facecolor=self.backgroundColor)
+
+ # Create an "invisible" line at 100 to set the max for the plot
+ self.axes.plot(np.linspace(0, 2*np.pi, 100), np.ones(100)*100, color=self.fontColor,
+ linestyle='')
+
+ # Plot line: Initialize out to 100 and blank
+ radius = 100
+ self.blackline = self.axes.plot(np.linspace(0, 2*np.pi, 100), np.ones(100)*radius,
+ color=self.fontColor, linestyle='-')
+ self.redline = None
+
+ self.filledcircle = None
+ # Create bullseye
+ circle = plt.Circle((0.0, 0.0), 20, transform=self.axes.transData._b, color=self.fontColor,
+ alpha=0.4)
+ self.bullseye = self.axes.add_artist(circle)
+
+ # Rotate zero up
+ self.axes.set_theta_zero_location("N")
+
+ self.axes.set_yticklabels(ticklabels, color=self.fontColor)
+ self.axes.set_xticklabels([], color=self.fontColor)
+
+ FigureCanvas.__init__(self, self.fig)
+ self.setParent(Parent)
+
+
+ self.title = self.fig.suptitle(self.lbl, fontsize=8, fontweight='bold',
+ color=self.fontColor)
+
+ FigureCanvas.setSizePolicy(self,
+ QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding)
+ FigureCanvas.updateGeometry(self)
+
+ def msgHandler(self, msg):
+ try:
+ new_val = pmt.to_python(pmt.cdr(msg))
+
+ if type(new_val) == float or type(new_val) == int:
+ self.updateData(new_val)
+ else:
+ gr.log.error("Value received was not an int or a "
+ "float: %s" % str(type(new_val)))
+
+ except Exception as e:
+ gr.log.error("Error with message conversion: %s" % str(e))
+
+ def updateData(self, radius):
+ if self.redline is not None:
+ self.redline.pop(0).remove()
+ self.redline = self.axes.plot(np.linspace(0, 2*np.pi, 100), np.ones(100)*radius,
+ color='r', linestyle='-')
+
+ if self.filledcircle:
+ self.filledcircle.remove()
+
+ self.bullseye.remove()
+ circle = plt.Circle((0.0, 0.0), radius, transform=self.axes.transData._b,
+ color=self.ringColor, alpha=0.4)
+ self.filledcircle = self.axes.add_artist(circle)
+ # Create bullseye
+ circle = plt.Circle((0.0, 0.0), 20, transform=self.axes.transData._b,
+ color=self.fontColor, alpha=0.4)
+ self.bullseye = self.axes.add_artist(circle)
+
+ self.draw()
+
diff --git a/gr-qtgui/python/qtgui/graphicitem.py b/gr-qtgui/python/qtgui/graphicitem.py
new file mode 100644
index 0000000000..b1386ab586
--- /dev/null
+++ b/gr-qtgui/python/qtgui/graphicitem.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from PyQt5.QtWidgets import QLabel
+from PyQt5.QtGui import QPixmap, QPainter
+from PyQt5.QtCore import Qt as Qtc
+from PyQt5.QtCore import QSize
+
+import os
+import sys
+
+from gnuradio import gr
+import pmt
+
+class GrGraphicItem(gr.sync_block, QLabel):
+ """
+ This block displays the selected graphic item. You can pass a
+ filename as a string in a message to change the image on the fly.
+ overlays can also be added by passing in a message with a
+ dictionary of a list of dictionaries in the car portion of the
+ message. Each dicationary should have the following keys:
+ 'filename','x','y', and an optional 'scalefactor'.
+ Setting the x/y attributes to -1,-1 will remove an overlay.
+ Otherwise items are indexed by filename and can be animated
+ throughout the backgound image.
+ """
+ def __init__(self, image_file, scaleImage=True, fixedSize=False, setWidth=0, setHeight=0):
+ gr.sync_block.__init__(self, name="GrGraphicsItem", in_sig=None, out_sig=None)
+ QLabel.__init__(self)
+
+ if not os.path.isfile(image_file):
+ gr.log.error("ERROR: Unable to find file " + image_file)
+ sys.exit(1)
+
+ try:
+ self.pixmap = QPixmap(image_file)
+ self.originalPixmap = QPixmap(image_file)
+ except OSError as e:
+ gr.log.error("ERROR: " + e.strerror)
+ sys.exit(1)
+
+ self.image_file = image_file
+ self.scaleImage = scaleImage
+ self.fixedSize = fixedSize
+ self.setWidth = setWidth
+ self.setHeight = setHeight
+ super().setPixmap(self.pixmap)
+ super().setMinimumSize(1, 1)
+ self.overlays = {}
+
+ self.message_port_register_in(pmt.intern("filename"))
+ self.set_msg_handler(pmt.intern("filename"), self.msgHandler)
+
+ self.message_port_register_in(pmt.intern("overlay"))
+ self.set_msg_handler(pmt.intern("overlay"), self.overlayHandler)
+
+ def overlayHandler(self, msg):
+ try:
+ overlayitem = pmt.to_python(pmt.car(msg))
+ if overlayitem is None:
+ gr.log.error('Overlay message contains None in the car portion '
+ 'of the message. Please pass in a dictionary or list of dictionaries in this '
+ 'portion of the message. Each dictionary should have the following keys: '
+ 'filename,x,y. Use x=y=-1 to remove an overlay item.')
+ return
+
+ if type(overlayitem) is dict:
+ itemlist = []
+ itemlist.append(overlayitem)
+ elif type(overlayitem) is list:
+ itemlist = overlayitem
+ else:
+ gr.log.error("Overlay message type is not correct. Please pass in "
+ "a dictionary or list of dictionaries in this portion of the message. Each "
+ "dictionary should have the following keys: filename,x,y. Use x=y=-1 to "
+ "remove an overlay item.")
+ return
+
+ # Check each dict item to make sure it's valid.
+ for curitem in itemlist:
+ if type(curitem) == dict:
+ if 'filename' not in curitem:
+ gr.log.error("Dictionary item did not contain the 'filename' key.")
+ gr.log.error("Received " + str(curitem))
+ continue
+
+ if 'x' not in curitem:
+ gr.log.error("The dictionary for filename " +
+ curitem['filename'] + " did not contain an 'x' key.")
+ gr.log.error("Received " + str(curitem))
+ continue
+
+ if 'y' not in curitem:
+ gr.log.error("The dictionary for filename " +
+ curitem['filename'] + " did not contain an 'y' key.")
+ gr.log.error("Received " + str(curitem))
+ continue
+
+ if not os.path.isfile(curitem['filename']):
+ gr.log.error("Unable to find overlay file " +
+ curitem['filename'])
+ gr.log.error("Received " + str(curitem))
+ continue
+
+ # Now either add/update our list or remove the item.
+ if curitem['x'] == -1 and curitem['y'] == -1:
+ try:
+ del self.overlays[curitem['filename']] # remove item
+ except:
+ pass
+ else:
+ self.overlays[curitem['filename']] = curitem
+
+ self.updateGraphic()
+ except Exception as e:
+ gr.log.error("Error with overlay message conversion: %s" % str(e))
+
+ def updateGraphic(self):
+ if (len(self.overlays.keys()) == 0):
+ try:
+ super().setPixmap(self.pixmap)
+ except Exception as e:
+ gr.log.error("Error updating graphic: %s" % str(e))
+ return
+ else:
+ # Need to deal with overlays
+ tmpPxmap = self.pixmap.copy(self.pixmap.rect())
+ painter = QPainter(tmpPxmap)
+ for curkey in self.overlays.keys():
+ curOverlay = self.overlays[curkey]
+ try:
+ newOverlay = QPixmap(curkey)
+ if 'scalefactor' in curOverlay:
+ scale = curOverlay['scalefactor']
+ w = newOverlay.width()
+ h = newOverlay.height()
+ newOverlay = newOverlay.scaled(int(w*scale), int(h*scale),
+ Qtc.KeepAspectRatio)
+ painter.drawPixmap(curOverlay['x'], curOverlay['y'], newOverlay)
+ except Exception as e:
+ gr.log.error("Error adding overlay: %s" % str(e))
+ return
+
+ painter.end()
+
+ super().setPixmap(tmpPxmap)
+
+ def msgHandler(self, msg):
+ try:
+ new_val = pmt.to_python(pmt.cdr(msg))
+ image_file = new_val
+ if type(new_val) == str:
+ if not os.path.isfile(image_file):
+ gr.log.error("ERROR: Unable to find file " + image_file)
+ return
+
+ try:
+ self.pixmap = QPixmap(image_file)
+ self.image_file = image_file
+ except OSError as e:
+ gr.log.error("ERROR: " + e.strerror)
+ return
+
+ self.updateGraphic()
+ else:
+ gr.log.error("Value received was not an int or "
+ "a bool: %s" % str(type(new_val)))
+
+ except Exception as e:
+ gr.log.error("Error with message conversion: %s" % str(e))
+
+ def minimumSizeHint(self):
+ return QSize(self.pixmap.width(),self.pixmap.height())
+
+ def resizeEvent(self, event):
+ if self.scaleImage:
+ w = super().width()
+ h = super().height()
+
+ self.pixmap = self.originalPixmap.scaled(w, h, Qtc.KeepAspectRatio)
+ elif self.fixedSize and self.setWidth > 0 and self.setHeight > 0:
+ self.pixmap = self.originalPixmap.scaled(self.setWidth, self.setHeight,
+ Qtc.KeepAspectRatio)
+
+ self.updateGraphic()
+
diff --git a/gr-qtgui/python/qtgui/graphicoverlay.py b/gr-qtgui/python/qtgui/graphicoverlay.py
new file mode 100644
index 0000000000..defef31d17
--- /dev/null
+++ b/gr-qtgui/python/qtgui/graphicoverlay.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+import sys
+import threading
+import time
+
+from gnuradio import gr
+import pmt
+
+# This thread just gets us out of the sync_block's init function so the messaging
+# system and scheduler are active.
+class offloadThread(threading.Thread):
+ def __init__(self, callback, overlayList, listDelay, repeat):
+ threading.Thread.__init__(self)
+ self.callback = callback
+ self.overlayList = overlayList
+ self.listDelay = listDelay
+ self.threadRunning = False
+ self.stopThread = False
+ self.repeat = repeat
+
+ def run(self):
+ self.stopThread = False
+ self.threadRunning = True
+
+ # Wait for main __init__ to finish
+ time.sleep(0.5)
+
+ if (type(self.overlayList) == list and self.listDelay > 0.0):
+ while self.repeat and not self.stopThread:
+ for curItem in self.overlayList:
+ self.callback(curItem)
+
+ if self.stopThread:
+ break
+
+ time.sleep(self.listDelay)
+
+ if self.stopThread:
+ break
+ else:
+ self.callback(self.overlayList)
+
+ self.threadRunning = False
+
+class GrGraphicOverlay(gr.sync_block):
+ """
+ This block is an example of how to feed an overlay to a graphic item.
+ The graphic item overlay is expecting a dictionary with the following
+ keys: 'filename','x','y', and optionally a 'scalefactor'. A list of
+ dictionaries can also be supplied to support multiple items.
+
+ Any file can be added to the graphic item as an overlay and the
+ particular item indexed by its filename can be updated by passing
+ in new x/y coordinates. To remove an overlay, use coordinates -1,-1
+ for the x,y coordinates.
+
+ This sample block sends either a dictionary or list of dictionaries
+ to the graphicitem block. To test updating a single overlay item,
+ you can use a list with the same file but different coordinates and
+ use the update delay > 0.0 to animate it.
+ """
+ def __init__(self, overlayList, listDelay, repeat):
+ gr.sync_block.__init__(self, name="GrGraphicsOverlay", in_sig=None,
+ out_sig=None)
+
+ self.overlayList = overlayList
+ self.listDelay = listDelay
+ if type(self.overlayList) is not dict and type(self.overlayList) is not list:
+ gr.log.error("The specified input is not valid. "
+ "Please specify either a dictionary item with the following keys: "
+ "'filename','x','y'[,'scalefactor'] or a list of dictionary items.")
+ sys.exit(1)
+
+ self.message_port_register_out(pmt.intern("overlay"))
+
+ self.thread = offloadThread(self.overlayCallback, self.overlayList, listDelay,
+ repeat)
+ self.thread.start()
+
+ def overlayCallback(self, msgData):
+ # Need to let init finish before this can be called so need to thread it out.
+ meta = pmt.to_pmt(msgData)
+ pdu = pmt.cons(meta, pmt.PMT_NIL)
+ self.message_port_pub(pmt.intern('overlay'), pdu)
+
+ def stop(self):
+ self.thread.stopThread = True
+
+ while self.thread.threadRunning:
+ time.sleep(0.1)
+
+ return True
+
diff --git a/gr-qtgui/python/qtgui/ledindicator.py b/gr-qtgui/python/qtgui/ledindicator.py
new file mode 100644
index 0000000000..09b2bd8e19
--- /dev/null
+++ b/gr-qtgui/python/qtgui/ledindicator.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from PyQt5.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QLabel
+from PyQt5.QtGui import QPainter, QBrush, QColor, QPen, QFontMetricsF
+from PyQt5.QtCore import Qt as Qtc
+from PyQt5.QtCore import QPoint
+from PyQt5.QtGui import QRadialGradient
+
+from gnuradio import gr
+import pmt
+
+class LabeledLEDIndicator(QFrame):
+ # Positions: 1 = above, 2=below, 3=left, 4=right
+ def __init__(self, lbl='', onColor='green', offColor='red', initialState=False, maxSize=80,
+ position=1, alignment=1, valignment=1, parent=None):
+ QFrame.__init__(self, parent)
+ self.numberControl = LEDIndicator(onColor, offColor, initialState, maxSize, parent)
+
+ if position < 3:
+ layout = QVBoxLayout()
+ else:
+ layout = QHBoxLayout()
+
+ if not lbl:
+ lbl = " "
+
+ self.lbl = lbl
+ self.lblcontrol = QLabel(lbl, self)
+ self.lblcontrol.setAlignment(Qtc.AlignCenter)
+
+ # add top or left
+ if len:
+ if position == 1 or position == 3:
+ layout.addWidget(self.lblcontrol)
+ else:
+ self.hasLabel = False
+
+ layout.addWidget(self.numberControl)
+
+ # Add bottom or right
+ if len:
+ if position == 2 or position == 4:
+ layout.addWidget(self.lblcontrol)
+
+ if alignment == 1:
+ halign = Qtc.AlignCenter
+ elif alignment == 2:
+ halign = Qtc.AlignLeft
+ else:
+ halign = Qtc.AlignRight
+
+ if valignment == 1:
+ valign = Qtc.AlignVCenter
+ elif valignment == 2:
+ valign = Qtc.AlignTop
+ else:
+ valign = Qtc.AlignBottom
+
+ layout.setAlignment(halign | valign)
+ self.setLayout(layout)
+
+ if len:
+ textfont = self.lblcontrol.font()
+ metrics = QFontMetricsF(textfont)
+
+ maxWidth = max((maxSize+30), (maxSize + metrics.width(lbl)+4))
+ maxHeight = max((maxSize+35), (maxSize + metrics.height()+2))
+ self.setMinimumSize(maxWidth, maxHeight)
+ else:
+ self.setMinimumSize(maxSize+2, maxSize+2)
+
+ self.show()
+
+ def setState(self, on_off):
+ self.numberControl.setState(on_off)
+
+class LEDIndicator(QFrame):
+ def __init__(self, onColor='green', offColor='red', initialState=False, maxSize=80,
+ parent=None):
+ QFrame.__init__(self, parent)
+
+ self.maxSize = maxSize
+ self.curState = initialState
+ self.on_color = QColor(onColor)
+ self.off_color = QColor(offColor)
+
+ self.setMinimumSize(maxSize, maxSize)
+ self.setMaximumSize(maxSize, maxSize)
+
+ def setState(self, on_off):
+ self.curState = on_off
+ super().update()
+
+ def paintEvent(self, event):
+ super().paintEvent(event)
+
+ painter = QPainter(self)
+
+ size = self.size()
+ brush = QBrush()
+
+ smallest_dim = size.width()
+ if smallest_dim > size.height():
+ smallest_dim = size.height()
+
+ smallest_dim = smallest_dim/2
+ smallest_dim -= 2
+
+ center_x = size.width()/2
+ center_y = size.height()/2
+ centerpoint = QPoint(center_x, center_y)
+
+ radius = smallest_dim
+
+ painter.setPen(QPen(QColor('lightgray'), 0))
+ brush.setStyle(Qtc.SolidPattern)
+
+ radial = QRadialGradient(center_x, center_y/2, radius)
+ radial.setColorAt(0, Qtc.white)
+ radial.setColorAt(0.8, Qtc.darkGray)
+ painter.setBrush(QBrush(radial))
+ painter.drawEllipse(centerpoint, radius, radius)
+
+ # Draw the colored center
+ radial = QRadialGradient(center_x, center_y/2, radius)
+ radial.setColorAt(0, Qtc.white)
+
+ if self.curState:
+ radial.setColorAt(.7, self.on_color)
+ brush.setColor(self.on_color)
+ painter.setPen(QPen(self.on_color, 0))
+ else:
+ radial.setColorAt(.7, self.off_color)
+ brush.setColor(self.off_color)
+ painter.setPen(QPen(self.off_color, 0))
+
+ brush.setStyle(Qtc.SolidPattern)
+ painter.setBrush(QBrush(radial))
+ if smallest_dim <= 30:
+ radius = radius - 3
+ elif smallest_dim <= 60:
+ radius = radius - 4
+ elif smallest_dim <= 100:
+ radius = radius - 5
+ elif smallest_dim <= 200:
+ radius = radius - 6
+ elif smallest_dim <= 300:
+ radius = radius - 7
+ else:
+ radius = radius - 9
+ painter.drawEllipse(centerpoint, radius, radius)
+
+class GrLEDIndicator(gr.sync_block, LabeledLEDIndicator):
+ """
+ This block makes a basic LED indicator
+ """
+ def __init__(self, lbl='', onColor='green', offColor='red', initialState=False,
+ maxSize=80, position=1, alignment=1, valignment=1, parent=None):
+ gr.sync_block.__init__(self, name="LEDIndicator", in_sig=None, out_sig=None)
+ LabeledLEDIndicator.__init__(self, lbl, onColor, offColor, initialState,
+ maxSize, position, alignment, valignment, parent)
+ self.lbl = lbl
+
+ self.message_port_register_in(pmt.intern("state"))
+ self.set_msg_handler(pmt.intern("state"), self.msgHandler)
+
+
+ def msgHandler(self, msg):
+ try:
+ new_val = pmt.to_python(pmt.cdr(msg))
+
+ if type(new_val) == bool or type(new_val) == int:
+ if type(new_val) == bool:
+ super().setState(new_val)
+ else:
+ if new_val == 1:
+ super().setState(True)
+ else:
+ super().setState(False)
+ else:
+ gr.log.error("Value received was not an int or a bool: %s" % str(type(new_val)))
+
+ except Exception as e:
+ gr.log.error("Error with message conversion: %s" % str(e))
+
+ def setState(self, on_off):
+ super().setState(on_off)
+
diff --git a/gr-qtgui/python/qtgui/levelgauge.py b/gr-qtgui/python/qtgui/levelgauge.py
new file mode 100644
index 0000000000..cee33eb58d
--- /dev/null
+++ b/gr-qtgui/python/qtgui/levelgauge.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from threading import Lock
+import sys
+
+from PyQt5.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QLabel, QProgressBar
+from PyQt5.QtGui import QColor
+from PyQt5.QtCore import Qt as Qtc
+from PyQt5.QtCore import pyqtSignal
+from PyQt5.QtGui import QPalette
+
+from gnuradio import gr
+import pmt
+
+class LabeledLevelGauge(QFrame):
+ # Positions: 1 = above, 2=below, 3=left, 4=right
+ def __init__(self, lbl='', barColor='blue', backgroundColor='white', fontColor='black',
+ minValue=0, maxValue=100, maxSize=80, position=1, isVertical=True,
+ isFloat=False, scaleFactor=1, showValue=False, parent=None):
+ QFrame.__init__(self, parent)
+ self.numberControl = LevelGauge(barColor, backgroundColor, minValue, maxValue,
+ maxSize, isVertical, isFloat, scaleFactor, showValue,
+ parent)
+
+ if position < 3:
+ layout = QVBoxLayout()
+ else:
+ layout = QHBoxLayout()
+
+ self.lbl = lbl
+ self.showvalue = showValue
+ self.isFloat = isFloat
+ self.isVertical = isVertical
+ self.scaleFactor = scaleFactor
+
+ self.lblcontrol = QLabel(lbl, self)
+ self.lblcontrol.setAlignment(Qtc.AlignCenter)
+
+ # For whatever reason, the progressbar doesn't show the number in the bar if it's
+ # vertical, only if it's horizontal
+ if self.showvalue and (isFloat or self.isVertical):
+ textstr = self.buildTextStr(minValue/self.scaleFactor)
+ self.lblcontrol.setText(textstr)
+
+ if fontColor != 'default':
+ self.lblcontrol.setStyleSheet("QLabel { color : " + fontColor + "; }")
+
+ # add top or left
+ if len:
+ if position == 1 or position == 3:
+ layout.addWidget(self.lblcontrol)
+ else:
+ self.hasLabel = False
+
+ layout.addWidget(self.numberControl)
+
+ # Add bottom or right
+ if len:
+ if position == 2 or position == 4:
+ layout.addWidget(self.lblcontrol)
+
+ layout.setAlignment(Qtc.AlignCenter | Qtc.AlignVCenter)
+ self.setLayout(layout)
+
+ self.show()
+
+ def buildTextStr(self, new_value):
+ textstr = ""
+ if len(self.lbl) > 0:
+ textstr = self.lbl + " - "
+
+ if self.isFloat:
+ textstr += "%.2f" % (new_value)
+ else:
+ textstr += str(new_value)
+
+ return textstr
+
+ def valChanged(self, new_value):
+ if not self.showvalue:
+ return
+
+ if self.isFloat or self.isVertical:
+ if self.lbl:
+ textstr = self.buildTextStr(new_value)
+ self.lblcontrol.setText(textstr)
+
+ def setValue(self, new_value):
+ self.valChanged(new_value)
+
+ if int(self.scaleFactor) != 1:
+ new_value = int(new_value * self.scaleFactor)
+
+ self.numberControl.setValue(new_value)
+
+class LevelGauge(QProgressBar):
+ # Notifies to avoid thread conflicts on paints
+ updateInt = pyqtSignal(int)
+ updateFloat = pyqtSignal(float)
+
+ def __init__(self, barColor='blue', backgroundColor='white', minValue=0, maxValue=100,
+ maxSize=80, isVertical=True, isFloat=False, scaleFactor=1, showValue=False,
+ parent=None):
+ super().__init__(parent)
+
+ self.updateInt.connect(self.onUpdateInt)
+ self.updateFloat.connect(self.onUpdateFloat)
+
+ self.lock = Lock()
+
+ self.maxSize = maxSize
+
+ p = super().palette()
+
+ if backgroundColor != 'default':
+ p.setColor(QPalette.Base, QColor(backgroundColor))
+
+ if barColor != 'default':
+ p.setColor(QPalette.Highlight, QColor(barColor))
+
+ if backgroundColor != 'default' or barColor != 'default':
+ super().setPalette(p)
+
+ if (not isFloat) and showValue:
+ super().setFormat("%v") # This shows the number in the bar itself.
+ super().setTextVisible(True)
+ else:
+ super().setTextVisible(False)
+
+ super().setMinimum(minValue)
+ super().setMaximum(maxValue)
+
+ if isVertical:
+ super().setOrientation(Qtc.Vertical)
+ else:
+ super().setOrientation(Qtc.Horizontal)
+
+ def onUpdateInt(self, new_value):
+ if new_value > super().maximum():
+ new_value = super().maximum()
+ elif new_value < super().minimum():
+ new_value = super().minimum()
+
+ self.lock.acquire()
+ super().setValue(new_value)
+ self.lock.release()
+
+ def onUpdateFloat(self, new_value):
+ if new_value > super().maximum():
+ new_value = super().maximum()
+ elif new_value < super().minimum():
+ new_value = super().minimum()
+
+ self.lock.acquire()
+ super().setValue(new_value)
+ self.lock.release()
+
+ def setValue(self, new_value):
+ if type(new_value) == int:
+ self.updateInt.emit(new_value)
+ else:
+ self.updateFloat.emit(new_value)
+
+class GrLevelGauge(gr.sync_block, LabeledLevelGauge):
+ """
+ This block creates a level gauge. The value can be set either
+ with a variable or an input message.
+
+ NOTE: This control has some quirks due to the fact that
+ QProgressBar only accepts integers. If you want to work with
+ floats, you have to use the scaleFactor to map incoming values
+ to the specified min/max range. For instance if the min/max
+ are 0-100 but your incoming values are 0.0-1.0, you will need
+ to set a scalefactor of 100.
+ """
+ def __init__(self, lbl='', barColor='blue', backgroundColor='white', fontColor='black',
+ minValue=0, maxValue=100, maxSize=80, isVertical=True, position=1,
+ isFloat=False, scaleFactor=1, showValue=False, parent=None):
+ gr.sync_block.__init__(self, name="LevelGauge", in_sig=None, out_sig=None)
+ LabeledLevelGauge.__init__(self, lbl, barColor, backgroundColor, fontColor, minValue,
+ maxValue, maxSize, position, isVertical, isFloat,
+ scaleFactor, showValue, parent)
+ self.lbl = lbl
+
+ if minValue > maxValue:
+ gr.log.error("Min value is greater than max value.")
+ sys.exit(1)
+
+ self.message_port_register_in(pmt.intern("value"))
+ self.set_msg_handler(pmt.intern("value"), self.msgHandler)
+
+ def msgHandler(self, msg):
+ try:
+ new_val = pmt.to_python(pmt.cdr(msg))
+
+ if type(new_val) == float or type(new_val) == int:
+ super().setValue(new_val)
+ else:
+ gr.log.error("Value received was not an int or a float: %s" % str(type(new_val)))
+
+ except Exception as e:
+ gr.log.error("Error with message conversion: %s" % str(e))
+
+ def setValue(self, new_value):
+ super().setValue(new_value)
diff --git a/gr-qtgui/python/qtgui/msgcheckbox.py b/gr-qtgui/python/qtgui/msgcheckbox.py
new file mode 100644
index 0000000000..8d2a52ae07
--- /dev/null
+++ b/gr-qtgui/python/qtgui/msgcheckbox.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from PyQt5 import Qt
+from PyQt5.QtWidgets import QFrame, QVBoxLayout
+from PyQt5.QtCore import Qt as Qtc
+
+from gnuradio import gr
+import pmt
+
+class CheckBoxEx(Qt.QCheckBox):
+ def __init__(self, lbl, callback=None):
+ Qt.QCheckBox.__init__(self)
+ self.setText(lbl)
+ self.callback = callback
+
+ self.stateChanged.connect(self.onToggleClicked)
+
+ def onToggleClicked(self):
+ if self.callback is not None:
+ self.callback(super().isChecked())
+
+class MsgCheckBox(gr.sync_block, QFrame):
+ """
+ This block creates a variable checkbox. Leave the label blank to
+ use the variable id as the label. A checkbox selects between
+ two values of similar type, but will stay depressed until
+ clicked again. The variable will take on one value or the other
+ depending on whether the button is pressed or released.
+
+ This control will also produce a state message matching the
+ set values.
+ """
+ def __init__(self, callback, lbl, pressedReleasedDict, initPressed, alignment,
+ valignment, outputmsgname='value'):
+ gr.sync_block.__init__(self, name="MsgCheckBox", in_sig=None, out_sig=None)
+ QFrame.__init__(self)
+
+ self.outputmsgname = outputmsgname
+ self.chkBox = CheckBoxEx(lbl, self.onToggleClicked)
+
+ layout = QVBoxLayout()
+
+ layout.addWidget(self.chkBox)
+
+ if alignment == 1:
+ halign = Qtc.AlignCenter
+ elif alignment == 2:
+ halign = Qtc.AlignLeft
+ else:
+ halign = Qtc.AlignRight
+
+ if valignment == 1:
+ valign = Qtc.AlignVCenter
+ elif valignment == 2:
+ valign = Qtc.AlignTop
+ else:
+ valign = Qtc.AlignBottom
+
+ layout.setAlignment(halign | valign)
+ self.setLayout(layout)
+
+ self.callback = callback
+ self.pressReleasedDict = pressedReleasedDict
+
+ self.message_port_register_out(pmt.intern("state"))
+
+ if initPressed:
+ self.chkctl.setChecked(True)
+
+ self.show()
+
+ def onToggleClicked(self, checked):
+ if self.chkBox.isChecked():
+ self.callback(self.pressReleasedDict['Pressed'])
+
+ if type(self.pressReleasedDict['Pressed']) == bool:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_bool(self.pressReleasedDict['Pressed'])))
+ elif type(self.pressReleasedDict['Pressed']) == int:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_long(self.pressReleasedDict['Pressed'])))
+ elif type(self.pressReleasedDict['Pressed']) == float:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_float(self.pressReleasedDict['Pressed'])))
+ else:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.intern(self.pressReleasedDict['Pressed'])))
+ else:
+ self.callback(self.pressReleasedDict['Released'])
+
+ if type(self.pressReleasedDict['Released']) == bool:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_bool(self.pressReleasedDict['Released'])))
+ elif type(self.pressReleasedDict['Released']) == int:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_long(self.pressReleasedDict['Released'])))
+ elif type(self.pressReleasedDict['Released']) == float:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_float(self.pressReleasedDict['Released'])))
+ else:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.intern(self.pressReleasedDict['Released'])))
diff --git a/gr-qtgui/python/qtgui/msgpushbutton.py b/gr-qtgui/python/qtgui/msgpushbutton.py
new file mode 100644
index 0000000000..bee142cac7
--- /dev/null
+++ b/gr-qtgui/python/qtgui/msgpushbutton.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from PyQt5 import Qt
+from gnuradio import gr
+import pmt
+
+class MsgPushButton(gr.sync_block, Qt.QPushButton):
+ """
+ This block creates a variable push button that creates a message
+ when clicked. Leave the label blank to use the variable id as
+ the label. You can define both the output message pmt name as
+ well as the value and value type.
+ """
+ def __init__(self, lbl, msgName, msgValue, relBackColor, relFontColor):
+ gr.sync_block.__init__(self, name="MsgPushButton", in_sig=None, out_sig=None)
+ Qt.QPushButton.__init__(self, lbl)
+
+ self.lbl = lbl
+ self.msgName = msgName
+ self.msgValue = msgValue
+
+ styleStr = ""
+ if relBackColor != 'default':
+ styleStr = "background-color: " + relBackColor + "; "
+
+ if relFontColor:
+ styleStr += "color: " + relFontColor + "; "
+
+ self.setStyleSheet(styleStr)
+
+ self.clicked[bool].connect(self.onBtnClicked)
+
+ self.message_port_register_out(pmt.intern("pressed"))
+
+ def onBtnClicked(self, pressed):
+ if type(self.msgValue) == int:
+ self.message_port_pub(pmt.intern("pressed"),
+ pmt.cons(pmt.intern(self.msgName), pmt.from_long(self.msgValue)))
+ elif type(self.msgValue) == float:
+ self.message_port_pub(pmt.intern("pressed"),
+ pmt.cons(pmt.intern(self.msgName), pmt.from_float(self.msgValue)))
+ elif type(self.msgValue) == str:
+ self.message_port_pub(pmt.intern("pressed"),
+ pmt.cons(pmt.intern(self.msgName), pmt.intern(self.msgValue)))
+ elif type(self.msgValue) == bool:
+ self.message_port_pub(pmt.intern("pressed"),
+ pmt.cons(pmt.intern(self.msgName), pmt.from_bool(self.msgValue)))
diff --git a/gr-qtgui/python/qtgui/qa_qtgui.py b/gr-qtgui/python/qtgui/qa_qtgui.py
index 5d1c98bef4..457b6f8c9f 100644
--- a/gr-qtgui/python/qtgui/qa_qtgui.py
+++ b/gr-qtgui/python/qtgui/qa_qtgui.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
#
-# Copyright 2011 Free Software Foundation, Inc.
+# Copyright 2011, 2020 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
diff --git a/gr-qtgui/python/qtgui/togglebutton.py b/gr-qtgui/python/qtgui/togglebutton.py
new file mode 100644
index 0000000000..0c0cf187f6
--- /dev/null
+++ b/gr-qtgui/python/qtgui/togglebutton.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+
+from PyQt5 import Qt
+from gnuradio import gr
+import pmt
+
+class ToggleButton(gr.sync_block, Qt.QPushButton):
+ """
+ This block creates a variable toggle button. Leave the label
+ blank to use the variable id as the label. A toggle button
+ selects between two values of similar type, but will stay
+ depressed until clicked again. The variable will take on one
+ value or the other depending on whether the button is pressed
+ or released. This button will also produce a state message
+ matching the set values.
+ """
+ def __init__(self, callback, lbl, pressedReleasedDict, initPressed, outputmsgname='value'):
+ gr.sync_block.__init__(self, name="ToggleButton", in_sig=None, out_sig=None)
+ Qt.QPushButton.__init__(self, lbl)
+ self.setCheckable(True)
+ self.lbl = lbl
+ self.callback = callback
+ self.pressReleasedDict = pressedReleasedDict
+
+ self.outputmsgname = outputmsgname
+ self.relBackColor = 'default'
+ self.relFontColor = 'default'
+ self.pressBackColor = 'default'
+ self.pressFontColor = 'default'
+
+ self.message_port_register_out(pmt.intern("state"))
+
+ if initPressed:
+ self.setChecked(True)
+ self.state = 1
+ else:
+ self.state = 0
+
+ self.clicked[bool].connect(self.onToggleClicked)
+
+ def setColors(self, relBackColor, relFontColor, pressBackColor, pressFontColor):
+ self.relBackColor = relBackColor
+ self.relFontColor = relFontColor
+ self.pressBackColor = pressBackColor
+ self.pressFontColor = pressFontColor
+
+ self.setColor()
+
+ def setColor(self):
+ if self.state:
+ styleStr = ""
+ if self.pressBackColor != 'default':
+ styleStr = "background-color: " + self.pressBackColor + "; "
+
+ if self.pressFontColor:
+ styleStr += "color: " + self.pressFontColor + "; "
+
+ self.setStyleSheet(styleStr)
+ else:
+ styleStr = ""
+ if self.relBackColor != 'default':
+ styleStr = "background-color: " + self.relBackColor + "; "
+
+ if self.relFontColor:
+ styleStr += "color: " + self.relFontColor + "; "
+
+ self.setStyleSheet(styleStr)
+
+ def onToggleClicked(self, pressed):
+ if pressed:
+ self.state = 1
+ self.callback(self.pressReleasedDict['Pressed'])
+ else:
+ self.state = 0
+ self.callback(self.pressReleasedDict['Released'])
+
+ self.setColor()
+
+ if pressed:
+ if type(self.pressReleasedDict['Pressed']) == bool:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_bool(self.pressReleasedDict['Pressed'])))
+ elif type(self.pressReleasedDict['Pressed']) == int:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_long(self.pressReleasedDict['Pressed'])))
+ elif type(self.pressReleasedDict['Pressed']) == float:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_float(self.pressReleasedDict['Pressed'])))
+ else:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.intern(self.pressReleasedDict['Pressed'])))
+ else:
+ if type(self.pressReleasedDict['Released']) == bool:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_bool(self.pressReleasedDict['Released'])))
+ elif type(self.pressReleasedDict['Released']) == int:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_long(self.pressReleasedDict['Released'])))
+ elif type(self.pressReleasedDict['Released']) == float:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_float(self.pressReleasedDict['Released'])))
+ else:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.intern(self.pressReleasedDict['Released'])))
diff --git a/gr-qtgui/python/qtgui/toggleswitch.py b/gr-qtgui/python/qtgui/toggleswitch.py
new file mode 100644
index 0000000000..7ef5a113fa
--- /dev/null
+++ b/gr-qtgui/python/qtgui/toggleswitch.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+#
+
+from gnuradio import gr
+import pmt
+
+from PyQt5.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QLabel
+from PyQt5.QtGui import QPainter, QBrush, QColor, QPen, QFontMetricsF
+from PyQt5.QtCore import Qt as Qtc
+from PyQt5.QtCore import QRect
+
+class LabeledToggleSwitch(QFrame):
+ # Positions: 1 = above, 2=below, 3=left, 4=right
+ def __init__(self, lbl='', onColor='green', offColor='red', initialState=False,
+ maxSize=50, position=1, parent=None, callback=None, alignment=1, valignment=1):
+ QFrame.__init__(self, parent)
+ self.numberControl = ToggleSwitch(onColor, offColor, initialState, maxSize,
+ parent, callback)
+
+ if position < 3:
+ layout = QVBoxLayout()
+ else:
+ layout = QHBoxLayout()
+
+ self.lbl = lbl
+ self.lblcontrol = QLabel(lbl, self)
+
+ if position == 3: # left of switch
+ self.lblcontrol.setAlignment(Qtc.AlignRight)
+ elif position == 4: # right of switch
+ self.lblcontrol.setAlignment(Qtc.AlignLeft)
+ else:
+ # Above or below
+ self.lblcontrol.setAlignment(Qtc.AlignCenter)
+
+ # add top or left
+ if len:
+ if position == 1 or position == 3:
+ layout.addWidget(self.lblcontrol)
+
+ layout.addWidget(self.numberControl)
+
+ # Add bottom or right
+ if len:
+ if position == 2 or position == 4:
+ layout.addWidget(self.lblcontrol)
+
+ if alignment == 1:
+ halign = Qtc.AlignCenter
+ elif alignment == 2:
+ halign = Qtc.AlignLeft
+ else:
+ halign = Qtc.AlignRight
+
+ if valignment == 1:
+ valign = Qtc.AlignVCenter
+ elif valignment == 2:
+ valign = Qtc.AlignTop
+ else:
+ valign = Qtc.AlignBottom
+
+ layout.setAlignment(halign | valign)
+
+ self.setLayout(layout)
+
+ textfont = self.lblcontrol.font()
+ metrics = QFontMetricsF(textfont)
+
+ maxWidth = max((maxSize+4), (maxSize*2 + metrics.width(lbl)))
+ maxHeight = max((maxSize/2+4), (maxSize/2 + metrics.height()+2))
+
+ self.setMinimumSize(int(maxWidth), int(maxHeight))
+
+ self.show()
+
+ def setState(self, on_off):
+ self.numberControl.setState(on_off)
+
+class ToggleSwitch(QFrame):
+ def __init__(self, onColor='green', offColor='red', initialState=False, maxSize=50,
+ parent=None, callback=None):
+ QFrame.__init__(self, parent)
+
+ self.maxSize = maxSize
+ self.curState = initialState
+ self.onColor = QColor(onColor)
+ self.offColor = QColor(offColor)
+ self.callback = callback
+ self.setMinimumSize(maxSize, maxSize/2)
+ self.setMaximumSize(maxSize, maxSize/2)
+
+ def setState(self, on_off):
+ self.curState = on_off
+ if self.callback is not None:
+ self.callback(on_off)
+
+ super().update()
+
+ def paintEvent(self, event):
+ super().paintEvent(event)
+
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.Antialiasing)
+
+ size = self.size()
+ brush = QBrush()
+
+ center_x = size.width()/2
+
+ if self.curState:
+ brush.setColor(self.onColor)
+ painter.setPen(QPen(self.onColor, 0))
+ else:
+ brush.setColor(self.offColor)
+ painter.setPen(QPen(self.offColor, 0))
+
+ brush.setStyle(Qtc.SolidPattern)
+ painter.setBrush(brush)
+
+ # Draw the switch background
+ centerRect = QRect(size.width()/4, 0, size.width()/2-4, size.height())
+ painter.drawRect(centerRect)
+ painter.drawEllipse(0, 0, size.height(), size.height())
+ painter.drawEllipse(size.width()/2, 0, size.height(), size.height())
+
+ # Draw the switch itself
+ brush.setColor(QColor('white'))
+ painter.setBrush(brush)
+ painter.setPen(QPen(QColor('white'), 0))
+ if self.curState:
+ painter.drawEllipse(2, 2, size.height() - 4, size.height() - 4)
+ else:
+ painter.drawEllipse(center_x+2, 2, size.height() - 4, size.height() - 4)
+
+ def mousePressEvent(self, event):
+ if event.x() <= self.size().width()/2:
+ self.setState(True)
+ else:
+ self.setState(False)
+
+ super().update()
+
+class GrToggleSwitch(gr.sync_block, LabeledToggleSwitch):
+ """
+ This block creates a modern toggle switch. The variable will take
+ on one value or the other as set in the dialog. This button will
+ also produce a state message matching the set values.
+ """
+ def __init__(self, callback, lbl, pressedReleasedDict, initialState=False,
+ onColor='green', offColor='silver', position=3, maxSize=50,
+ alignment=1, valignment=1, parent=None, outputmsgname='value'):
+ gr.sync_block.__init__(self, name="ToggleSwitch", in_sig=None, out_sig=None)
+ LabeledToggleSwitch.__init__(self, lbl, onColor, offColor, initialState,
+ maxSize, position, parent, self.notifyUpdate,
+ alignment, valignment)
+
+ self.outputmsgname = outputmsgname
+ self.pressReleasedDict = pressedReleasedDict
+ self.callback = callback
+ self.message_port_register_out(pmt.intern("state"))
+
+ def notifyUpdate(self, new_val):
+ if self.callback is not None:
+ if new_val:
+ self.callback(self.pressReleasedDict['Pressed'])
+ else:
+ self.callback(self.pressReleasedDict['Released'])
+
+ if new_val:
+ if type(self.pressReleasedDict['Pressed']) == bool:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_bool(self.pressReleasedDict['Pressed'])))
+ elif type(self.pressReleasedDict['Pressed']) == int:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_long(self.pressReleasedDict['Pressed'])))
+ elif type(self.pressReleasedDict['Pressed']) == float:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_float(self.pressReleasedDict['Pressed'])))
+ else:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.intern(self.pressReleasedDict['Pressed'])))
+ else:
+ if type(self.pressReleasedDict['Released']) == bool:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_bool(self.pressReleasedDict['Released'])))
+ elif type(self.pressReleasedDict['Released']) == int:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_long(self.pressReleasedDict['Released'])))
+ elif type(self.pressReleasedDict['Released']) == float:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.from_float(self.pressReleasedDict['Released'])))
+ else:
+ self.message_port_pub(pmt.intern("state"),
+ pmt.cons(pmt.intern(self.outputmsgname),
+ pmt.intern(self.pressReleasedDict['Released'])))