#!/usr/bin/env python # # Copyright 2012,2013 Free Software Foundation, Inc. # # This file is part of GNU Radio # # GNU Radio is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # GNU Radio is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with GNU Radio; see the file COPYING. If not, write to # the Free Software Foundation, Inc., 51 Franklin Street, # Boston, MA 02110-1301, USA. # from gnuradio import gr, ctrlport from PyQt4 import QtCore,Qt import PyQt4.QtGui as QtGui import os, sys, time import Ice from gnuradio.ctrlport.IceRadioClient import * from gnuradio.ctrlport.GrDataPlotter import * from gnuradio.ctrlport import GNURadio class RateDialog(QtGui.QDialog): def __init__(self, delay, parent=None): super(RateDialog, self).__init__(parent) self.gridLayout = QtGui.QGridLayout(self) self.setWindowTitle("Update Delay (ms)"); self.delay = QtGui.QLineEdit(self); self.delay.setText(str(delay)); self.buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) self.gridLayout.addWidget(self.delay); self.gridLayout.addWidget(self.buttonBox); self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) def accept(self): self.done(1); def reject(self): self.done(0); class MAINWindow(QtGui.QMainWindow): def minimumSizeHint(self): return Qtgui.QSize(800,600) def __init__(self, radio, port, interface): super(MAINWindow, self).__init__() self.updateRate = 1000; self.conns = [] self.plots = [] self.knobprops = [] self.interface = interface self.mdiArea = QtGui.QMdiArea() self.mdiArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.mdiArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.setCentralWidget(self.mdiArea) self.mdiArea.subWindowActivated.connect(self.updateMenus) self.windowMapper = QtCore.QSignalMapper(self) self.windowMapper.mapped[QtGui.QWidget].connect(self.setActiveSubWindow) self.createActions() self.createMenus() self.createToolBars() self.createStatusBar() self.updateMenus() self.setWindowTitle("GNU Radio Control Port Monitor") self.setUnifiedTitleAndToolBarOnMac(True) self.newCon(radio, port) icon = QtGui.QIcon(ctrlport.__path__[0] + "/icon.png" ) self.setWindowIcon(icon) # Locally turn off ControlPort export from GR. This prevents # our GR-based plotters from launching their own ControlPort # instance (and possibly causing a port collision if one has # been specified). os.environ['GR_CONF_CONTROLPORT_ON'] = 'False' def setUpdateRate(self,nur): self.updateRate = int(nur); for c in self.conns: c.updateRate = self.updateRate; c.timer.setInterval(self.updateRate); def newCon(self, radio=None, port=None): child = MForm(radio, port, len(self.conns), self.updateRate, self) if(child.radio is not None): child.setWindowTitle(str(child.radio)) self.mdiArea.addSubWindow(child) child.showMaximized() self.conns.append(child) self.plots.append([]) def propertiesMenu(self, key, radio, uid): r = str(radio).split(" ") title = "{0}:{1}".format(r[3], r[5]) props = radio.properties([key]) pmin = props[key].min.value pmax = props[key].max.value if pmin == []: pmin = None else: pmin = 1.1*pmin if pmax == []: pmax = None else: pmax = 1.1*pmax # Use display option mask of item to set up available plot # types and default options. disp = self.knobprops[uid][key].display cplx = disp & gr.DISPOPTCPLX | disp & gr.DISPXY strip = disp & gr.DISPOPTSTRIP stem = disp & gr.DISPOPTSTEM log = disp & gr.DISPOPTLOG scatter = disp & gr.DISPOPTSCATTER def newUpdaterProxy(): self.newUpdater(key, radio) def newPlotterFProxy(): self.newPlotF(key, uid, title, pmin, pmax, log, strip, stem) def newPlotterCProxy(): self.newPlotC(key, uid, title, pmin, pmax, log, strip, stem) def newPlotterConstProxy(): self.newPlotConst(key, uid, title, pmin, pmax, scatter) def newPlotterPsdFProxy(): self.newPlotPsdF(key, uid, title) def newPlotterPsdCProxy(): self.newPlotPsdC(key, uid, title) def newPlotterRasterFProxy(): self.newPlotRasterF(key, uid, title, pmin, pmax) def newPlotterRasterBProxy(): self.newPlotRasterB(key, uid, title, pmin, pmax) menu = QtGui.QMenu(self) menu.setTitle("Item Actions") menu.setTearOffEnabled(False) # object properties menu.addAction("Properties", newUpdaterProxy) # displays available if(cplx == 0): menu.addAction("Plot Time", newPlotterFProxy) menu.addAction("Plot PSD", newPlotterPsdFProxy) menu.addAction("Plot Raster (real)", newPlotterRasterFProxy) #menu.addAction("Plot Raster (bits)", newPlotterRasterBProxy) else: menu.addAction("Plot Time", newPlotterCProxy) menu.addAction("Plot PSD", newPlotterPsdCProxy) menu.addAction("Plot Constellation", newPlotterConstProxy) menu.popup(QtGui.QCursor.pos()) def newUpdater(self, key, radio): updater = UpdaterWindow(key, radio, None) updater.setWindowTitle("Updater: " + key) updater.setModal(False) updater.exec_() def newSub(self, e): tag = str(e.text(0)) tree = e.treeWidget().parent() uid = tree.uid knobprop = self.knobprops[uid][tag] r = str(tree.radio).split(" ") title = "{0}:{1}".format(r[3], r[5]) pmin = knobprop.min.value pmax = knobprop.max.value if pmin == []: pmin = None else: pmin = 1.1*pmin if pmax == []: pmax = None else: pmax = 1.1*pmax disp = knobprop.display if(disp & gr.DISPTIME): strip = disp & gr.DISPOPTSTRIP stem = disp & gr.DISPOPTSTEM log = disp & gr.DISPOPTLOG if(disp & gr.DISPOPTCPLX == 0): self.newPlotF(tag, uid, title, pmin, pmax, log, strip, stem) else: self.newPlotC(tag, uid, title, pmin, pmax, log, strip, stem) elif(disp & gr.DISPXY): scatter = disp & gr.DISPOPTSCATTER self.newPlotConst(tag, uid, title, pmin, pmax, scatter) elif(disp & gr.DISPPSD): if(disp & gr.DISPOPTCPLX == 0): self.newPlotPsdF(tag, uid, title) else: self.newPlotPsdC(tag, uid, title) def startDrag(self, e): drag = QtGui.QDrag(self) mime_data = QtCore.QMimeData() tag = str(e.text(0)) tree = e.treeWidget().parent() knobprop = self.knobprops[tree.uid][tag] disp = knobprop.display iscomplex = (disp & gr.DISPOPTCPLX) or (disp & gr.DISPXY) if(disp != gr.DISPNULL): data = "PlotData:::{0}:::{1}".format(tag, iscomplex) else: data = "OtherData:::{0}:::{1}".format(tag, iscomplex) mime_data.setText(data) drag.setMimeData(mime_data) drop = drag.start() def createPlot(self, plot, uid, title): plot.start() self.plots[uid].append(plot) self.mdiArea.addSubWindow(plot) plot.setWindowTitle("{0}: {1}".format(title, plot.name())) self.connect(plot.qwidget(), QtCore.SIGNAL('destroyed(QObject*)'), self.destroyPlot) # when the plot is updated via drag-and-drop, we need to be # notified of the new qwidget that's created so we can # properly destroy it. plot.plotupdated.connect(self.plotUpdated) plot.show() def plotUpdated(self, q): # the plot has been updated with a new qwidget; make sure this # gets dies to the destroyPlot function. for i, plots in enumerate(self.plots): for p in plots: if(p == q): #plots.remove(p) #plots.append(q) self.connect(q.qwidget(), QtCore.SIGNAL('destroyed(QObject*)'), self.destroyPlot) break def destroyPlot(self, obj): for plots in self.plots: for p in plots: if p.qwidget() == obj: plots.remove(p) break def newPlotConst(self, tag, uid, title="", pmin=None, pmax=None, scatter=False): plot = GrDataPlotterConst(tag, 32e6, pmin, pmax) plot.scatter(scatter) self.createPlot(plot, uid, title) def newPlotF(self, tag, uid, title="", pmin=None, pmax=None, logy=False, stripchart=False, stem=False): plot = GrDataPlotterF(tag, 32e6, pmin, pmax, stripchart) plot.semilogy(logy) plot.stem(stem) self.createPlot(plot, uid, title) def newPlotC(self, tag, uid, title="", pmin=None, pmax=None, logy=False, stripchart=False, stem=False): plot = GrDataPlotterC(tag, 32e6, pmin, pmax, stripchart) plot.semilogy(logy) plot.stem(stem) self.createPlot(plot, uid, title) def newPlotPsdF(self, tag, uid, title="", pmin=None, pmax=None): plot = GrDataPlotterPsdF(tag, 32e6, pmin, pmax) self.createPlot(plot, uid, title) def newPlotPsdC(self, tag, uid, title="", pmin=None, pmax=None): plot = GrDataPlotterPsdC(tag, 32e6, pmin, pmax) self.createPlot(plot, uid, title) def newPlotRasterF(self, tag, uid, title="", pmin=None, pmax=None): plot = GrTimeRasterF(tag, 32e6, pmin, pmax) self.createPlot(plot, uid, title) def newPlotRasterB(self, tag, uid, title="", pmin=None, pmax=None): plot = GrTimeRasterB(tag, 32e6, pmin, pmax) self.createPlot(plot, uid, title) def update(self, knobs, uid): #sys.stderr.write("KNOB KEYS: {0}\n".format(knobs.keys())) for plot in self.plots[uid]: data = [] for n in plot.knobnames: data.append(knobs[n].value) plot.update(data) plot.stop() plot.wait() plot.start() def setActiveSubWindow(self, window): if window: self.mdiArea.setActiveSubWindow(window) def createActions(self): self.newConAct = QtGui.QAction("&New Connection", self, shortcut=QtGui.QKeySequence.New, statusTip="Create a new file", triggered=self.newCon) #self.newAct = QtGui.QAction(QtGui.QIcon(':/images/new.png'), "&New Plot", self.newPlotAct = QtGui.QAction("&New Plot", self, statusTip="Create a new file", triggered=self.newPlotF) self.exitAct = QtGui.QAction("E&xit", self, shortcut="Ctrl+Q", statusTip="Exit the application", triggered=QtGui.qApp.closeAllWindows) self.closeAct = QtGui.QAction("Cl&ose", self, shortcut="Ctrl+F4", statusTip="Close the active window", triggered=self.mdiArea.closeActiveSubWindow) self.closeAllAct = QtGui.QAction("Close &All", self, statusTip="Close all the windows", triggered=self.mdiArea.closeAllSubWindows) self.urAct = QtGui.QAction("Update Rate", self, shortcut="F5", statusTip="Change Update Rate", triggered=self.updateRateShow) qks = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_T); self.tileAct = QtGui.QAction("&Tile", self, statusTip="Tile the windows", triggered=self.mdiArea.tileSubWindows, shortcut=qks) qks = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_C); self.cascadeAct = QtGui.QAction("&Cascade", self, statusTip="Cascade the windows", shortcut=qks, triggered=self.mdiArea.cascadeSubWindows) self.nextAct = QtGui.QAction("Ne&xt", self, shortcut=QtGui.QKeySequence.NextChild, statusTip="Move the focus to the next window", triggered=self.mdiArea.activateNextSubWindow) self.previousAct = QtGui.QAction("Pre&vious", self, shortcut=QtGui.QKeySequence.PreviousChild, statusTip="Move the focus to the previous window", triggered=self.mdiArea.activatePreviousSubWindow) self.separatorAct = QtGui.QAction(self) self.separatorAct.setSeparator(True) self.aboutAct = QtGui.QAction("&About", self, statusTip="Show the application's About box", triggered=self.about) self.aboutQtAct = QtGui.QAction("About &Qt", self, statusTip="Show the Qt library's About box", triggered=QtGui.qApp.aboutQt) def createMenus(self): self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.newConAct) self.fileMenu.addAction(self.newPlotAct) self.fileMenu.addAction(self.urAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.exitAct) self.windowMenu = self.menuBar().addMenu("&Window") self.updateWindowMenu() self.windowMenu.aboutToShow.connect(self.updateWindowMenu) self.menuBar().addSeparator() self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.aboutAct) self.helpMenu.addAction(self.aboutQtAct) def updateRateShow(self): askrate = RateDialog(self.updateRate, self); if askrate.exec_(): ur = float(str(askrate.delay.text())); self.setUpdateRate(ur); return; else: return; def createToolBars(self): self.fileToolBar = self.addToolBar("File") self.fileToolBar.addAction(self.newConAct) self.fileToolBar.addAction(self.newPlotAct) self.fileToolBar.addAction(self.urAct) self.fileToolBar = self.addToolBar("Window") self.fileToolBar.addAction(self.tileAct) self.fileToolBar.addAction(self.cascadeAct) def createStatusBar(self): self.statusBar().showMessage("Ready") def activeMdiChild(self): activeSubWindow = self.mdiArea.activeSubWindow() if activeSubWindow: return activeSubWindow.widget() return None def updateMenus(self): hasMdiChild = (self.activeMdiChild() is not None) self.closeAct.setEnabled(hasMdiChild) self.closeAllAct.setEnabled(hasMdiChild) self.tileAct.setEnabled(hasMdiChild) self.cascadeAct.setEnabled(hasMdiChild) self.nextAct.setEnabled(hasMdiChild) self.previousAct.setEnabled(hasMdiChild) self.separatorAct.setVisible(hasMdiChild) def updateWindowMenu(self): self.windowMenu.clear() self.windowMenu.addAction(self.closeAct) self.windowMenu.addAction(self.closeAllAct) self.windowMenu.addSeparator() self.windowMenu.addAction(self.tileAct) self.windowMenu.addAction(self.cascadeAct) self.windowMenu.addSeparator() self.windowMenu.addAction(self.nextAct) self.windowMenu.addAction(self.previousAct) self.windowMenu.addAction(self.separatorAct) def about(self): about_info = \ '''Copyright 2012 Free Software Foundation, Inc.\n This program is part of GNU Radio.\n GNU Radio is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version.\n GNU Radio is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n You should have received a copy of the GNU General Public License along with GNU Radio; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Boston, MA 02110-1301, USA.''' QtGui.QMessageBox.about(None, "gr-ctrlport-monitor", about_info) class ConInfoDialog(QtGui.QDialog): def __init__(self, parent=None): super(ConInfoDialog, self).__init__(parent) self.gridLayout = QtGui.QGridLayout(self) self.host = QtGui.QLineEdit(self); self.port = QtGui.QLineEdit(self); self.host.setText("localhost"); self.port.setText("43243"); self.buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) self.gridLayout.addWidget(self.host); self.gridLayout.addWidget(self.port); self.gridLayout.addWidget(self.buttonBox); self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) def accept(self): self.done(1); def reject(self): self.done(0); class UpdaterWindow(QtGui.QDialog): def __init__(self, key, radio, parent): QtGui.QDialog.__init__(self, parent) self.key = key; self.radio = radio self.resize(300,200) self.layout = QtGui.QVBoxLayout() self.props = radio.properties([key])[key] info = str(self.props) self.infoLabel = QtGui.QLabel(info) self.layout.addWidget(self.infoLabel) # Test here to make sure that a 'set' function try: a = radio.set(radio.get([key])) has_set = True except Ice.UnknownException: has_set = False if(has_set is False): self.cancelButton = QtGui.QPushButton("Ok") self.cancelButton.connect(self.cancelButton, QtCore.SIGNAL('clicked()'), self.reject) self.buttonlayout = QtGui.QHBoxLayout() self.buttonlayout.addWidget(self.cancelButton) self.layout.addLayout(self.buttonlayout) else: # we have a set function self.textInput = QtGui.QLineEdit() self.layout.addWidget(self.textInput) self.applyButton = QtGui.QPushButton("Apply") self.setButton = QtGui.QPushButton("OK") self.cancelButton = QtGui.QPushButton("Cancel") rv = radio.get([key]) self.textInput.setText(str(rv[key].value)) self.sv = rv[key] self.applyButton.connect(self.applyButton, QtCore.SIGNAL('clicked()'), self._apply) self.setButton.connect(self.setButton, QtCore.SIGNAL('clicked()'), self._set) self.cancelButton.connect(self.cancelButton, QtCore.SIGNAL('clicked()'), self.reject) self.is_num = ((type(self.sv.value)==float) or (type(self.sv.value)==int)) if(self.is_num): self.sliderlayout = QtGui.QHBoxLayout() self.slider = QtGui.QSlider(QtCore.Qt.Horizontal) self.sliderlayout.addWidget(QtGui.QLabel(str(self.props.min.value))) self.sliderlayout.addWidget(self.slider) self.sliderlayout.addWidget(QtGui.QLabel(str(self.props.max.value))) self.steps = 10000 self.valspan = self.props.max.value - self.props.min.value self.slider.setRange(0, 10000) self._set_slider_value(self.sv.value) self.connect(self.slider, QtCore.SIGNAL("sliderReleased()"), self._slide) self.layout.addLayout(self.sliderlayout) self.buttonlayout = QtGui.QHBoxLayout() self.buttonlayout.addWidget(self.applyButton) self.buttonlayout.addWidget(self.setButton) self.buttonlayout.addWidget(self.cancelButton) self.layout.addLayout(self.buttonlayout) # set layout and go... self.setLayout(self.layout) def _set_slider_value(self, val): self.slider.setValue(self.steps*(val-self.props.min.value)/self.valspan) def _slide(self): val = (self.slider.value()*self.valspan + self.props.min.value)/float(self.steps) self.textInput.setText(str(val)) def _apply(self): if(type(self.sv.value) == str): val = str(self.textInput.text()) elif(type(self.sv.value) == int): val = int(round(float(self.textInput.text()))) elif(type(self.sv.value) == float): val = float(self.textInput.text()) else: sys.stderr.write("set type not supported! ({0})\n".format(type(self.sv.value))) sys.exit(-1) self.sv.value = val km = {} km[self.key] = self.sv self.radio.set(km) self._set_slider_value(self.sv.value) def _set(self): self._apply() self.done(0) class MForm(QtGui.QWidget): def update(self): try: st = time.time(); knobs = self.radio.get([]); ft = time.time(); latency = ft-st; self.parent.statusBar().showMessage("Current GNU Radio Control Port Query Latency: %f ms"%(latency*1000)) except Exception, e: sys.stderr.write("ctrlport-monitor: radio.get threw exception ({0}).\n".format(e)) if(type(self.parent) is MAINWindow): # Find window of connection remove = [] for p in self.parent.mdiArea.subWindowList(): if self.parent.conns[self.uid] == p.widget(): remove.append(p) # Find any subplot windows of connection for p in self.parent.mdiArea.subWindowList(): for plot in self.parent.plots[self.uid]: if plot.qwidget() == p.widget(): remove.append(p) # Clean up local references to these self.parent.conns.remove(self.parent.conns[self.uid]) self.parent.plots.remove(self.parent.plots[self.uid]) # Remove subwindows for connection and plots for r in remove: self.parent.mdiArea.removeSubWindow(r) # Clean up self self.close() else: sys.exit(1) return tableitems = knobs.keys() #UPDATE TABLE: self.table.updateItems(knobs, self.knobprops) #UPDATE PLOTS self.parent.update(knobs, self.uid) def __init__(self, radio=None, port=None, uid=0, updateRate=2000, parent=None): super(MForm, self).__init__() if(radio == None or port == None): askinfo = ConInfoDialog(self); if askinfo.exec_(): host = str(askinfo.host.text()); port = str(askinfo.port.text()); radio = parent.interface.getRadio(host, port) else: self.radio = None return self.uid = uid self.parent = parent self.horizontalLayout = QtGui.QVBoxLayout(self) self.gridLayout = QtGui.QGridLayout() self.radio = radio self.knobprops = self.radio.properties([]) self.parent.knobprops.append(self.knobprops) self.resize(775,500) self.timer = QtCore.QTimer() self.constupdatediv = 0 self.tableupdatediv = 0 plotsize=250 # make table self.table = GrDataPlotterValueTable(uid, self, 0, 0, 400, 200) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) self.table.treeWidget.setSizePolicy(sizePolicy) self.table.treeWidget.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed) self.table.treeWidget.setSortingEnabled(True) self.table.treeWidget.setDragEnabled(True) # add things to layouts self.horizontalLayout.addWidget(self.table.treeWidget) # set up timer self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.update) self.updateRate = updateRate; self.timer.start(self.updateRate) # set up context menu .. self.table.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.table.treeWidget.customContextMenuRequested.connect(self.openMenu) # Set up double-click to launch default plotter self.connect(self.table.treeWidget, QtCore.SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.parent.newSub); # Allow drag/drop event from table item to plotter self.connect(self.table.treeWidget, QtCore.SIGNAL('itemPressed(QTreeWidgetItem*, int)'), self.parent.startDrag) def openMenu(self, pos): index = self.table.treeWidget.selectedIndexes() item = self.table.treeWidget.itemFromIndex(index[0]) itemname = str(item.text(0)) self.parent.propertiesMenu(itemname, self.radio, self.uid) class MyClient(IceRadioClient): def __init__(self): IceRadioClient.__init__(self, MAINWindow) sys.exit(MyClient().main(sys.argv))