diff options
author | Tom Rondeau <trondeau@vt.edu> | 2012-12-03 19:42:59 -0500 |
---|---|---|
committer | Tom Rondeau <trondeau@vt.edu> | 2012-12-03 19:42:59 -0500 |
commit | 1fea3358fbb5796ca50475e55dd7f0d443f1cd73 (patch) | |
tree | e15b9b27a607c96df5f42f58e2ab578e6e27a34b /gnuradio-core/src/python | |
parent | 76906d9a6bab589abaa96e0dc115c45927f4570f (diff) |
Merging ctrlport2 into next branch.
Diffstat (limited to 'gnuradio-core/src/python')
-rw-r--r-- | gnuradio-core/src/python/gnuradio/ctrlport/CMakeLists.txt | 129 | ||||
-rw-r--r-- | gnuradio-core/src/python/gnuradio/ctrlport/DataPlotter.py | 382 | ||||
-rw-r--r-- | gnuradio-core/src/python/gnuradio/ctrlport/GrDataPlotter.py | 423 | ||||
-rw-r--r-- | gnuradio-core/src/python/gnuradio/ctrlport/IceRadioClient.py | 102 | ||||
-rw-r--r-- | gnuradio-core/src/python/gnuradio/ctrlport/__init__.py | 30 | ||||
-rwxr-xr-x | gnuradio-core/src/python/gnuradio/ctrlport/ctrlport-monitor | 477 | ||||
-rwxr-xr-x | gnuradio-core/src/python/gnuradio/ctrlport/gr-ctrlport-monitor | 581 | ||||
-rw-r--r-- | gnuradio-core/src/python/gnuradio/ctrlport/icon.png | bin | 0 -> 1532 bytes | |||
-rw-r--r-- | gnuradio-core/src/python/gnuradio/ctrlport/monitor.py | 60 | ||||
-rwxr-xr-x | gnuradio-core/src/python/gnuradio/ctrlport/qa_cpp_py_binding.py | 169 | ||||
-rwxr-xr-x | gnuradio-core/src/python/gnuradio/ctrlport/qa_cpp_py_binding_set.py | 148 |
11 files changed, 2501 insertions, 0 deletions
diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/CMakeLists.txt b/gnuradio-core/src/python/gnuradio/ctrlport/CMakeLists.txt new file mode 100644 index 0000000000..1268030ebb --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/CMakeLists.txt @@ -0,0 +1,129 @@ +# Copyright 2012 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. + +######################################################################## +include(GrPython) + +ADD_CUSTOM_COMMAND( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/../../gnuradio_ice.py + ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/__init__.py + ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/Booter/__init__.py + COMMAND ${ICE_SLICE2PY} -I${CMAKE_CURRENT_SOURCE_DIR}/../../../lib/runtime + --output-dir=${CMAKE_CURRENT_BINARY_DIR}/../../ + ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib/runtime/gnuradio.ice + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib/runtime/gnuradio.ice + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Slicing gnuradio.slice" +) + +ADD_CUSTOM_COMMAND( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/frontend_ice.py + ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/Frontend/__init__.py + COMMAND ${ICE_SLICE2PY} -I${CMAKE_CURRENT_SOURCE_DIR}/../../../lib/runtime + --output-dir=${CMAKE_CURRENT_BINARY_DIR}/../../ + ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib/runtime/frontend.ice + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib/runtime/frontend.ice + ${CMAKE_CURRENT_BINARY_DIR}/../../gnuradio_ice.py + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Slicing frontend.slice" +) + +GR_PYTHON_INSTALL( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py + ${CMAKE_CURRENT_SOURCE_DIR}/IceRadioClient.py + DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/ + COMPONENT "core_python" +) + +# We don't want to install these in the root Python directory, but we +# aren't given a choice based on the way slice2py generates the +# information. +GR_PYTHON_INSTALL( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/../../gnuradio_ice.py + ${CMAKE_CURRENT_BINARY_DIR}/../../frontend_ice.py + DESTINATION ${GR_PYTHON_DIR} + COMPONENT "core_python" +) + +GR_PYTHON_INSTALL( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/__init__.py + DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/GNURadio + COMPONENT "core_python" +) + +GR_PYTHON_INSTALL( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/Booter/__init__.py + DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/GNURadio/Booter + COMPONENT "core_python" +) + +GR_PYTHON_INSTALL( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/Frontend/__init__.py + DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/GNURadio/Frontend + COMPONENT "core_python" +) + +install( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/icon.png + DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport + COMPONENT "core_python" +) + +GR_PYTHON_INSTALL( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/DataPlotter.py + ${CMAKE_CURRENT_SOURCE_DIR}/GrDataPlotter.py + ${CMAKE_CURRENT_SOURCE_DIR}/monitor.py + DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/ + COMPONENT "core_python" +) + +GR_PYTHON_INSTALL( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/ctrlport-monitor + ${CMAKE_CURRENT_SOURCE_DIR}/gr-ctrlport-monitor + DESTINATION ${GR_RUNTIME_DIR} + PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + COMPONENT "core_python" +) + +######################################################################## +# Handle the unit tests +######################################################################## +if(ENABLE_GR_CTRLPORT) + if(ENABLE_TESTING) + include(GrTest) + file(GLOB py_qa_test_files "qa_*.py") + foreach(py_qa_test_file ${py_qa_test_files}) + get_filename_component(py_qa_test_name ${py_qa_test_file} NAME_WE) + set(GR_TEST_PYTHON_DIRS + ${CMAKE_BINARY_DIR}/gnuradio-core/src/python + ${CMAKE_BINARY_DIR}/gnuradio-core/src/lib/swig + ) + set(GR_TEST_TARGET_DEPS gruel gnuradio-core) + GR_ADD_TEST(${py_qa_test_name} ${PYTHON_EXECUTABLE} ${py_qa_test_file}) + endforeach(py_qa_test_file) + endif(ENABLE_TESTING) +endif(ENABLE_GR_CTRLPORT) diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/DataPlotter.py b/gnuradio-core/src/python/gnuradio/ctrlport/DataPlotter.py new file mode 100644 index 0000000000..c689bfa055 --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/DataPlotter.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python +# +# Copyright 2012 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. +# + +import sys + +try: + from PyQt4.QtCore import Qt; + from PyQt4 import QtGui, QtCore +except ImportError: + print "Error: Program requires PyQt4." + sys.exit(1) + +try: + import PyQt4.Qwt5 as Qwt + from PyQt4.Qwt5.anynumpy import * +except ImportError: + print "Error: Program requires PyQwt5." + sys.exit(1) + + +class Zoomer(Qwt.QwtPlotZoomer): + def __init__(self, a,b,c,d,e): + Qwt.QwtPlotZoomer.__init__(self,a,b,c,d,e); + self.zoomers = []; + + def zoom(self, r): + Qwt.QwtPlotZoomer.zoom(self,r); + if(r == 0): + #self.plot().setAxisAutoScale(True); + self.plot().setAxisAutoScale(Qwt.QwtPlot.xBottom) + self.plot().setAxisAutoScale(Qwt.QwtPlot.yLeft) + self.plot().replot(); + + +class DataPlotterBase(Qwt.QwtPlot): + DefaultColors = ( Qt.green, Qt.red, Qt.blue, + Qt.cyan, Qt.magenta, Qt.black, Qt.darkRed, + Qt.darkGray, Qt.darkGreen, Qt.darkBlue, Qt.yellow) + + dropSignal = QtCore.pyqtSignal(QtCore.QEvent) + + def contextMenuEvent(self,e): + menu = QtGui.QMenu(self); + menu.addAction(self.gridAct); + menu.addAction(self.axesAct); + menu.addAction(self.curvAct); + menu.exec_(e.globalPos()); + + def dragEnterEvent(self,e): + e.accept(); + + def dropEvent(self, e): + Qwt.QwtPlot.dropEvent(self,e) + self.dropSignal.emit( e ); + + + + def __init__(self, parent, title, xlabel, ylabel, size, x, y): + Qwt.QwtPlot.__init__(self, parent) + self.callback = None; + + self.gridAct = QtGui.QAction("Toggle &Grid", self, triggered=self.toggleGrid); + self.axesAct = QtGui.QAction("Toggle &Axes", self, triggered=self.toggleAxes); + self.curvAct = QtGui.QAction("Toggle &Lines", self, triggered=self.toggleCurve); + + # Set up the zoomer + self.zoomer = Zoomer(Qwt.QwtPlot.xBottom, + Qwt.QwtPlot.yLeft, + Qwt.QwtPicker.DragSelection, + Qwt.QwtPicker.AlwaysOff, + self.canvas()) + + # Crosshairs + Data labels + self.picker = Qwt.QwtPlotPicker( + Qwt.QwtPlot.xBottom, + Qwt.QwtPlot.yLeft, + Qwt.QwtPicker.PointSelection | Qwt.QwtPicker.DragSelection, + Qwt.QwtPlotPicker.CrossRubberBand, + Qwt.QwtPicker.AlwaysOn, + self.canvas()) + self.picker.setRubberBandPen(QtGui.QPen(QtCore.Qt.white, 1, QtCore.Qt.DashLine)) + self.picker.setTrackerPen(QtGui.QPen(QtCore.Qt.white)) + + self.axisEnable = False; + # Turn off bloated data labels + self.enableAxis(Qwt.QwtPlot.yLeft, False); + self.enableAxis(Qwt.QwtPlot.xBottom, False); + + # Allow panning with middle mouse + panner = Qwt.QwtPlotPanner(self.canvas()) + panner.setAxisEnabled(Qwt.QwtPlot.yRight, False) + panner.setMouseButton(Qt.MidButton) + + # Accept dropping of stats + self.setAcceptDrops(True); + self.grid = None + self.curve_en = False + self.setCanvasBackground(Qt.black) + + self.insertLegend(Qwt.QwtLegend(), Qwt.QwtPlot.TopLegend); + + self.axisEnabled(True); + + self.resize(size, size) + self.setAutoReplot(False) + self.show() + self.updateTimerInt = 500 + self.startTimer(self.updateTimerInt) + + # Set Axis on and Grid off by default + #self.toggleGrid(); + self.toggleAxes(); + + + def toggleAxes(self): + self.axisEnable = not self.axisEnable; + self.enableAxis(Qwt.QwtPlot.yLeft, self.axisEnable); + self.enableAxis(Qwt.QwtPlot.xBottom, self.axisEnable); + + + def toggleGrid(self): + # grid + if self.grid == None: + self.grid = Qwt.QwtPlotGrid() + self.grid.enableXMin(True) + self.grid.setMajPen(QtGui.QPen(Qt.gray, 0, Qt.DotLine)) + self.grid.setMinPen(QtGui.QPen(Qt.gray, 0 , Qt.DotLine)) + self.grid.attach(self) + else: + self.grid.detach() + self.grid = None + + return self + + def toggleCurve(self): + self.curve_en = not self.curve_en; + + +class DataPlotterVector(DataPlotterBase): + def __init__(self, parent, legend, title, xlabel, ylabel, size, x, y): + DataPlotterBase.__init__(self, parent, title, xlabel, ylabel, size, x, y) + self.curve = Qwt.QwtPlotCurve(legend) + self.curve.attach(self) + self.tag = None; + self.x = self.y = [0.0]; + + def offerData(self, data, tag): + if(tag == self.tag): + self.x = data[::2]; self.y = data[1::2] + + def timerEvent(self, e): + if(self.curve_en): + self.curve.setStyle(Qwt.QwtPlotCurve.Lines) + self.curve.setPen(QtGui.QPen(Qt.green)) + else: + self.curve.setStyle(Qwt.QwtPlotCurve.NoCurve) + + self.curve.setData(self.x, self.y) + self.replot() + + def enableLegend(self): + self.insertLegend(Qwt.QwtLegend(), Qwt.QwtPlot.BottomLegend); + return self + + def setSeries(self,tag,name): + self.tag = tag; + self.curve.setTitle(name) + + +class DataPlotterVectorOne(DataPlotterVector): + def __init__(self, parent, legend, title, xlabel, ylabel, size, x, y): + DataPlotterVector.__init__(self, parent, legend, title, xlabel, ylabel, size, x, y) + self.curve.setSymbol(Qwt.QwtSymbol(Qwt.QwtSymbol.XCross, + QtGui.QBrush(), QtGui.QPen(Qt.green), QtCore.QSize(2, 2))) + self.setAxisAutoScale(True) + self.axisSet = False; + + # Lines on by default + self.toggleCurve(); + + def offerData(self, data, tag): + if(tag == self.tag): + if not self.axisSet: + self.setAxisScale(1, 0, len(data)); + self.axisSet = True; + self.x = range(0,len(data)); + self.y = data; + + +class DataPlotterConst(DataPlotterVector): + def __init__(self, parent, legend, title, xlabel, ylabel, size, x, y): + DataPlotterVector.__init__(self, parent, legend, title, xlabel, ylabel, size, x, y) + self.x = arange(-2, 100.1, 2) + self.y = zeros(len(self.x), Float) + self.curve.setSymbol(Qwt.QwtSymbol(Qwt.QwtSymbol.XCross, + QtGui.QBrush(), QtGui.QPen(Qt.green), QtCore.QSize(2, 2))) + self.curve.setStyle(Qwt.QwtPlotCurve.NoCurve) + self.setAxisAutoScale(False) + +class DataPlotterEqTaps(DataPlotterVector): + def __init__(self, parent, legend, title, xlabel, ylabel, size, x, y, qtcolor): + DataPlotterVector.__init__(self, parent, legend, title, xlabel, ylabel, size, x, y) + self.x = arange(-.5, .5, 1) + self.y = zeros(len(self.x), Float) + self.curve.setPen(QtGui.QPen(qtcolor)) + +class DataPlotterTicker(DataPlotterBase): + def __init__(self, parent, title, xlabel, ylabel, size, x, y, seconds = 60): + DataPlotterBase.__init__(self, parent, title, xlabel, ylabel, size, x, y) + self.series = {} + self.setTimeScale(seconds) + +# AAAAAAAAAA - enable for legend at bottom +# self.insertLegend(Qwt.QwtLegend(), Qwt.QwtPlot.BottomLegend); + self.skipEvents=1 + + def setTimeScale(self, seconds): + intv = float(self.updateTimerInt) / 1000 + self.x = arange(0, seconds, intv) + #self.x = arange(0, seconds, 1) + return self + + def addSeries(self, tag, label, qtcolor = None, alpha = 1): + class Series: + def __init__(self, tag, label, qtcolor, x, plot): + self.vec = zeros(len(x), Float) + self.value = None + self.alpha = alpha + self.curve = Qwt.QwtPlotCurve(label) + self.curve.setPen(QtGui.QPen(qtcolor)) + self.plot = plot + + if qtcolor == None: qtcolor = self.DefaultColors[len(self.series)] + self.series[tag] = s = Series(tag, label, qtcolor, self.x, self) + self.enableSeries(tag) + return self + + def enableSeries(self, tag): + if self.hasSeries(tag): + s = self.series[tag] + s.enabled = True + s.curve.attach(s.plot) + return self + + def disableSeries(self, tag): + if self.hasSeries(tag): + s = self.series[tag] + s.enabled = False + s.curve.detach() + return self + + def toggleSeries(self,tag): + if self.seriesIsEnabled(tag): + self.disableSeries(tag) + else: + self.enableSeries(tag) + return self + + def timerEvent(self, e): + for k, v in self.series.iteritems(): + if v.value == None: continue + elif v.vec[0] == 0: v.vec = ones(len(v.vec), Float)*v.value + + prev = v.vec[0] + v.vec = concatenate((v.vec[:1], v.vec[:-1]), 1) + v.vec[0] = v.value*v.alpha + prev*(1-v.alpha) + self.series[k] = v + v.curve.setData(self.x, v.vec) + self.replot() + + def offerData(self, value, tag): + if(self.series.has_key(tag)): + if not value == NaN: + self.series[tag].value = value + #print "Data Offer %s items:"%(tag) + return self + + def hasSeries(self, tag): + return self.series.has_key(tag); + + def seriesIsEnabled(self, tag): + if self.hasSeries(tag): return self.series[tag].enabled + else: return False + +class DataPlotterTickerWithSeriesButtons(DataPlotterTicker): + def __init__(self, parent, title, xlabel, ylabel, size, x, y, seconds = 60): + DataPlotterTicker.__init__(self, parent, title, xlabel, ylabel, size, x, y, seconds) + DataPlotterTicker.setAcceptDrops(self,True); + self.buttonx = 50; self.buttony = 20; self.buttonSize = 16 + self.btns = [] + + + + + def addSeriesWithButton(self, tag, legend, qtcolor=None, alpha = 1): + self.addSeries(tag, legend, qtcolor, alpha) + lenbtns = len(self.btns) + + btn = QtGui.QToolButton(self) + btn.rank = lenbtns + btn.setText(str(btn.rank)) + btn.tag = tag + #btn.setIcon(Qt.QIcon(Qt.QPixmap(print_xpm))) + #btn.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + #btn.setForegroundColor(Qt.red) + self.btns.append(btn) + btn.setGeometry(self.buttonx, self.buttony, self.buttonSize, self.buttonSize) + self.buttonx += self.buttonSize + + if lenbtns == 0: callback = self.print_0 + if lenbtns == 1: callback = self.print_1 + if lenbtns == 2: callback = self.print_2 + if lenbtns == 3: callback = self.print_3 + if lenbtns == 4: callback = self.print_4 + if lenbtns == 5: callback = self.print_5 + if lenbtns == 6: callback = self.print_6 + if lenbtns == 7: callback = self.print_7 + self.connect(btn, QtCore.SIGNAL('clicked()'), callback) + return self + + def toggleSeriesWithButton(self,btn): + DataPlotterTicker.toggleSeries(self, btn.tag) + + if self.seriesIsEnabled(btn.tag): + # btn.setForegroundRole(Qt.QPalette.NoRole) + btn.setText(str(btn.rank)) + else: + btn.setText('x') + # btn.setForegroundRole(Qt.QPalette.Light) + + def print_0(self): self.toggleSeriesWithButton(self.btns[0]) + def print_1(self): self.toggleSeriesWithButton(self.btns[1]) + def print_2(self): self.toggleSeriesWithButton(self.btns[2]) + def print_3(self): self.toggleSeriesWithButton(self.btns[3]) + def print_4(self): self.toggleSeriesWithButton(self.btns[4]) + def print_5(self): self.toggleSeriesWithButton(self.btns[5]) + def print_6(self): self.toggleSeriesWithButton(self.btns[6]) + def print_7(self): self.toggleSeriesWithButton(self.btns[7]) + +class DataPlotterValueTable: + def __init__(self, parent, x, y, xsize, ysize, headers=['Statistic Key ( Source Block :: Stat Name ) ', 'Curent Value', 'Units', 'Description']): + # must encapsulate, cuz Qt's bases are not classes + self.treeWidget = QtGui.QTreeWidget(parent) + self.treeWidget.setColumnCount(len(headers)) + self.treeWidget.setGeometry(x,y,xsize,ysize) + self.treeWidget.setHeaderLabels(headers) + self.treeWidget.resizeColumnToContents(0) + + def updateItems(self, knobs, knobprops): + # save previous selection if exists + sel = self.treeWidget.currentItem(); + row = self.treeWidget.indexOfTopLevelItem(sel); + items = []; + self.treeWidget.clear() + for k, v in knobs.iteritems(): + items.append(QtGui.QTreeWidgetItem([str(k), str(v.value), knobprops[k].units, knobprops[k].description])) + self.treeWidget.insertTopLevelItems(0, items) + # re-set previous selection if exists + if(row != -1): + try: + self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(row)); + except: + pass diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/GrDataPlotter.py b/gnuradio-core/src/python/gnuradio/ctrlport/GrDataPlotter.py new file mode 100644 index 0000000000..f33160aca2 --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/GrDataPlotter.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python +# +# Copyright 2012 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 +import sys, time + +try: + from gnuradio import qtgui + from PyQt4 import QtGui, QtCore + import sip +except ImportError: + print "Error: Program requires PyQt4 and gr-qtgui." + sys.exit(1) + +class GrDataPlotterC(gr.top_block): + def __init__(self, name, rate, pmin=None, pmax=None): + gr.top_block.__init__(self) + + self._name = name + self._npts = 500 + samp_rate = 1.0 + + self._last_data = self._npts*[0,] + self._data_len = 0 + + self.src = gr.vector_source_c([]) + self.thr = gr.throttle(gr.sizeof_gr_complex, rate) + self.snk = qtgui.time_sink_c(self._npts, samp_rate, + self._name, 1) + + if(pmin is not None or not pmax is None): + self.snk.set_y_axis(pmin, pmax) + + self.connect(self.src, self.thr, (self.snk, 0)) + + self.snk.set_line_label(0, "Real") + self.snk.set_line_label(1, "Imag") + + self.py_window = sip.wrapinstance(self.snk.pyqwidget(), QtGui.QWidget) + + def __del__(self): + pass + + def qwidget(self): + return self.py_window + + def name(self): + return self._name + + def update(self, data): + # Ask GUI if there has been a change in nsamps + npts = self.snk.nsamps() + if(self._npts != npts): + + # Adjust buffers to accomodate new settings + if(npts < self._npts): + if(self._data_len < npts): + self._last_data = self._last_data[0:npts] + else: + self._last_data = self._last_data[self._data_len-npts:self._data_len] + self._data_len = npts + else: + self._last_data += (npts - self._npts)*[0,] + self._npts = npts + self.snk.reset() + + # Update the plot data depending on type + if(type(data) == list): + data_r = data[0::2] + data_i = data[1::2] + data = [complex(r,i) for r,i in zip(data_r, data_i)] + if(len(data) > self._npts): + self.src.set_data(data) + self._last_data = data[-self._npts:] + else: + newdata = self._last_data[-(self._npts-len(data)):] + newdata += data + self.src.set_data(newdata) + self._last_data = newdata + + else: # single value update + if(self._data_len < self._npts): + self._last_data[self._data_len] = data + self._data_len += 1 + else: + self._last_data = self._last_data[1:] + self._last_data.append(data) + self.src.set_data(self._last_data) + +class GrDataPlotterF(gr.top_block): + def __init__(self, name, rate, pmin=None, pmax=None): + gr.top_block.__init__(self) + + self._name = name + self._npts = 500 + samp_rate = 1.0 + + self._last_data = self._npts*[0,] + self._data_len = 0 + + self.src = gr.vector_source_f([]) + self.thr = gr.throttle(gr.sizeof_float, rate) + self.snk = qtgui.time_sink_f(self._npts, samp_rate, + self._name, 1) + + if(pmin is not None or not pmax is None): + self.snk.set_y_axis(pmin, pmax) + + self.connect(self.src, self.thr, (self.snk, 0)) + + self.py_window = sip.wrapinstance(self.snk.pyqwidget(), QtGui.QWidget) + + def __del__(self): + pass + + def qwidget(self): + return self.py_window + + def name(self): + return self._name + + def update(self, data): + # Ask GUI if there has been a change in nsamps + npts = self.snk.nsamps() + if(self._npts != npts): + + # Adjust buffers to accomodate new settings + if(npts < self._npts): + if(self._data_len < npts): + self._last_data = self._last_data[0:npts] + else: + self._last_data = self._last_data[self._data_len-npts:self._data_len] + self._data_len = npts + else: + self._last_data += (npts - self._npts)*[0,] + self._npts = npts + self.snk.reset() + + # Update the plot data depending on type + if(type(data) == list): + if(len(data) > self._npts): + self.src.set_data(data) + self._last_data = data[-self._npts:] + else: + newdata = self._last_data[-(self._npts-len(data)):] + newdata += data + self.src.set_data(newdata) + self._last_data = newdata + + else: # single value update + if(self._data_len < self._npts): + self._last_data[self._data_len] = data + self._data_len += 1 + else: + self._last_data = self._last_data[1:] + self._last_data.append(data) + self.src.set_data(self._last_data) + + +class GrDataPlotterConst(gr.top_block): + def __init__(self, name, rate, pmin=None, pmax=None): + gr.top_block.__init__(self) + + self._name = name + self._npts = 500 + samp_rate = 1.0 + + self._last_data = self._npts*[0,] + self._data_len = 0 + + self.src = gr.vector_source_c([]) + self.thr = gr.throttle(gr.sizeof_gr_complex, rate) + self.snk = qtgui.const_sink_c(self._npts, + self._name, 1) + + if(pmin is not None or not pmax is None): + self.snk.set_x_axis(pmin, pmax) + self.snk.set_y_axis(pmin, pmax) + + self.connect(self.src, self.thr, (self.snk, 0)) + + self.py_window = sip.wrapinstance(self.snk.pyqwidget(), QtGui.QWidget) + + def __del__(self): + pass + + def qwidget(self): + return self.py_window + + def name(self): + return self._name + + def update(self, data): + # Ask GUI if there has been a change in nsamps + npts = self.snk.nsamps() + if(self._npts != npts): + + # Adjust buffers to accomodate new settings + if(npts < self._npts): + if(self._data_len < npts): + self._last_data = self._last_data[0:npts] + else: + self._last_data = self._last_data[self._data_len-npts:self._data_len] + self._data_len = npts + else: + self._last_data += (npts - self._npts)*[0,] + self._npts = npts + self.snk.reset() + + # Update the plot data depending on type + if(type(data) == list): + data_r = data[0::2] + data_i = data[1::2] + data = [complex(r,i) for r,i in zip(data_r, data_i)] + if(len(data) > self._npts): + self.src.set_data(data) + self._last_data = data[-self._npts:] + else: + newdata = self._last_data[-(self._npts-len(data)):] + newdata += data + self.src.set_data(newdata) + self._last_data = newdata + + else: # single value update + if(self._data_len < self._npts): + self._last_data[self._data_len] = data + self._data_len += 1 + else: + self._last_data = self._last_data[1:] + self._last_data.append(data) + self.src.set_data(self._last_data) + + +class GrDataPlotterPsdC(gr.top_block): + def __init__(self, name, rate, pmin=None, pmax=None): + gr.top_block.__init__(self) + + self._name = name + self._samp_rate = 1.0 + self._fftsize = 2048 + self._wintype = gr.firdes.WIN_BLACKMAN_hARRIS + self._fc = 0 + + self._last_data = self._fftsize*[0,] + self._data_len = 0 + + self.src = gr.vector_source_c([]) + self.thr = gr.throttle(gr.sizeof_gr_complex, rate) + self.snk = qtgui.freq_sink_c(self._fftsize, self._wintype, + self._fc, self._samp_rate, + self._name, 1) + + if(pmin is not None or not pmax is None): + self.snk.set_y_axis(pmin, pmax) + + self.connect(self.src, self.thr, (self.snk, 0)) + + self.py_window = sip.wrapinstance(self.snk.pyqwidget(), QtGui.QWidget) + + def __del__(self): + pass + + def qwidget(self): + return self.py_window + + def name(self): + return self._name + + def update(self, data): + # Ask GUI if there has been a change in nsamps + fftsize = self.snk.fft_size() + if(self._fftsize != fftsize): + + # Adjust buffers to accomodate new settings + if(fftsize < self._fftsize): + if(self._data_len < fftsize): + self._last_data = self._last_data[0:fftsize] + else: + self._last_data = self._last_data[self._data_len-fftsize:self._data_len] + self._data_len = fftsize + else: + self._last_data += (fftsize - self._fftsize)*[0,] + self._fftsize = fftsize + self.snk.reset() + + # Update the plot data depending on type + if(type(data) == list): + data_r = data[0::2] + data_i = data[1::2] + data = [complex(r,i) for r,i in zip(data_r, data_i)] + if(len(data) > self._fftsize): + self.src.set_data(data) + self._last_data = data[-self._fftsize:] + else: + newdata = self._last_data[-(self._fftsize-len(data)):] + newdata += data + self.src.set_data(newdata) + self._last_data = newdata + + else: # single value update + if(self._data_len < self._fftsize): + self._last_data[self._data_len] = data + self._data_len += 1 + else: + self._last_data = self._last_data[1:] + self._last_data.append(data) + self.src.set_data(self._last_data) + +class GrDataPlotterPsdF(gr.top_block): + def __init__(self, name, rate, pmin=None, pmax=None): + gr.top_block.__init__(self) + + self._name = name + self._samp_rate = 1.0 + self._fftsize = 2048 + self._wintype = gr.firdes.WIN_BLACKMAN_hARRIS + self._fc = 0 + + self._last_data = self._fftsize*[0,] + self._data_len = 0 + + self.src = gr.vector_source_f([]) + self.thr = gr.throttle(gr.sizeof_float, rate) + self.snk = qtgui.freq_sink_f(self._fftsize, self._wintype, + self._fc, self._samp_rate, + self._name, 1) + + if(pmin is not None or not pmax is None): + self.snk.set_y_axis(pmin, pmax) + + self.connect(self.src, self.thr, (self.snk, 0)) + + self.py_window = sip.wrapinstance(self.snk.pyqwidget(), QtGui.QWidget) + + def __del__(self): + pass + + def qwidget(self): + return self.py_window + + def name(self): + return self._name + + def update(self, data): + # Ask GUI if there has been a change in nsamps + fftsize = self.snk.fft_size() + if(self._fftsize != fftsize): + + # Adjust buffers to accomodate new settings + if(fftsize < self._fftsize): + if(self._data_len < fftsize): + self._last_data = self._last_data[0:fftsize] + else: + self._last_data = self._last_data[self._data_len-fftsize:self._data_len] + self._data_len = fftsize + else: + self._last_data += (fftsize - self._fftsize)*[0,] + self._fftsize = fftsize + self.snk.reset() + + # Update the plot data depending on type + if(type(data) == list): + data_r = data[0::2] + data_i = data[1::2] + data = [complex(r,i) for r,i in zip(data_r, data_i)] + if(len(data) > self._fftsize): + self.src.set_data(data) + self._last_data = data[-self._fftsize:] + else: + newdata = self._last_data[-(self._fftsize-len(data)):] + newdata += data + self.src.set_data(newdata) + self._last_data = newdata + + else: # single value update + if(self._data_len < self._fftsize): + self._last_data[self._data_len] = data + self._data_len += 1 + else: + self._last_data = self._last_data[1:] + self._last_data.append(data) + self.src.set_data(self._last_data) + + +class GrDataPlotterValueTable: + def __init__(self, uid, parent, x, y, xsize, ysize, + headers=['Statistic Key ( Source Block :: Stat Name ) ', + 'Curent Value', 'Units', 'Description']): + # must encapsulate, cuz Qt's bases are not classes + self.uid = uid + self.treeWidget = QtGui.QTreeWidget(parent) + self.treeWidget.setColumnCount(len(headers)) + self.treeWidget.setGeometry(x,y,xsize,ysize) + self.treeWidget.setHeaderLabels(headers) + self.treeWidget.resizeColumnToContents(0) + + def updateItems(self, knobs, knobprops): + items = []; + self.treeWidget.clear() + for k, v in knobs.iteritems(): + items.append(QtGui.QTreeWidgetItem([str(k), str(v.value), + knobprops[k].units, + knobprops[k].description])) + self.treeWidget.insertTopLevelItems(0, items) diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/IceRadioClient.py b/gnuradio-core/src/python/gnuradio/ctrlport/IceRadioClient.py new file mode 100644 index 0000000000..0964b5a4ba --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/IceRadioClient.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# +# Copyright 2012 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. +# + +import Ice, Glacier2 +from PyQt4 import QtGui, QtCore +import sys, time, Ice +from gnuradio import gr +from gnuradio.ctrlport import GNURadio + +class IceRadioClient(Ice.Application): + def __init__(self, parentClass): + self.parentClass = parentClass + + def getRadio(self, host, port): + radiostr = "gnuradio -t:tcp -h " + host + " -p " + port + " -t 3000" + base = self.communicator().stringToProxy(radiostr).ice_twoway() + radio = GNURadio.ControlPortPrx.checkedCast(base) + + if not radio: + sys.stderr.write("{0} : invalid proxy.\n".format(args[0])) + return None + + return radio + + def run(self,args): + if len(args) < 2: + print "useage: [glacierinstance glacierhost glacierport] host port" + return + if len(args) == 8: + self.useglacier = True + guser = args[1] + gpass = args[2] + ginst = args[3] + ghost = args[4] + gport = args[5] + host = args[6] + port = args[7] + else: + self.useglacier = False + host = args[1] + port = args[2] + + if self.useglacier: + gstring = ginst + "/router -t:tcp -h " + ghost + " -p " + gport + print "GLACIER: {0}".format(gstring) + + setrouter = Glacier2.RouterPrx.checkedCast(self.communicator().stringToProxy(gstring)) + self.communicator().setDefaultRouter(setrouter) + defaultRouter = self.communicator().getDefaultRouter() + #defaultRouter = self.communicator().stringToProxy(gstring) + if not defaultRouter: + print self.appName() + ": no default router set" + return 1 + else: + print str(defaultRouter) + router = Glacier2.RouterPrx.checkedCast(defaultRouter) + if not router: + print self.appName() + ": configured router is not a Glacier2 router" + return 1 + + while True: + print "This demo accepts any user-id / password combination." + if not guser == '' and not gpass == '': + id = guser + pw = gpass + else: + id = raw_input("user id: ") + pw = raw_input("password: ") + + try: + router.createSession(id, pw) + break + except Glacier2.PermissionDeniedException, ex: + print "permission denied:\n" + ex.reason + + radio = self.getRadio(host, port) + if(radio is None): + return 1 + + app = QtGui.QApplication(sys.argv) + ex = self.parentClass(radio, port, self) + ex.show(); + app.exec_() diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/__init__.py b/gnuradio-core/src/python/gnuradio/ctrlport/__init__.py new file mode 100644 index 0000000000..031c3b424e --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/__init__.py @@ -0,0 +1,30 @@ +# +# Copyright 2012 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 this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +# The presence of this file turns this directory into a Python package + +import Ice, IcePy + +# import swig generated symbols into the ctrlport namespace +#from ctrlport_swig import * +from monitor import * + +# import any pure python here +#import GNURadio diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/ctrlport-monitor b/gnuradio-core/src/python/gnuradio/ctrlport/ctrlport-monitor new file mode 100755 index 0000000000..0230c4cf7a --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/ctrlport-monitor @@ -0,0 +1,477 @@ +#!/usr/bin/env python + +from gnuradio import ctrlport + +from PyQt4.QtCore import Qt; +from PyQt4 import QtCore +import PyQt4.QtGui as QtGui +import sys, time, Ice, subprocess; +from gnuradio.ctrlport.IceRadioClient import *; +from gnuradio.ctrlport.DataPlotter import *; + +_gr_prefs = gr.prefs() +ice_directory = _gr_prefs.get_string('ctrlport', 'ice_directory', '') +print ice_directory +Ice.loadSlice(ice_directory + '/gnuradio.ice') + +import GNURadio + +class MAINWindow(QtGui.QMainWindow): + def minimumSizeHint(self): + return Qtgui.QSize(800,600); + def __init__(self, radio, port): + + super(MAINWindow, self).__init__() + self.plots = []; + + 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.readSettings() + + self.setWindowTitle("GNU Radio Control Port Monitor") + self.setUnifiedTitleAndToolBarOnMac(True) + +# self.resize(QtCore.QSize(1024,768)); + + self.newCon(radio,port); +# self.mdiArea.addSubWindow(child); +# child.resize(QtCore.QSize(800,600)); + icon = QtGui.QIcon( ctrlport.__path__[0] + "/icon.png" ); + self.setWindowIcon(icon); + +# add empty plots ? +# self.plots = []; +# for i in range(3): +# self.newPlot(); + + def newCon(self,host=None,port=None): + child = MForm(host,port,self); + #child.setWindowTitle("Modem Connected :: %s:%s"%(host,port)); + child.setWindowTitle(str(host)); + self.mdiArea.addSubWindow(child); + child.resize(QtCore.QSize(800,600)); + child.showMaximized(); + return child; + + + def newUpd(self,k,r): + updater = UpdaterWindow(k,r,None); + updater.setWindowTitle("Updater: " + k); + self.mdiArea.addSubWindow(updater); + updater.show(); + + + def newSub(self,e): + tag = str(e.text(0)); + knobprop = self.knobprops[tag] + + if(type(knobprop.min) in [GNURadio.KnobVecB, GNURadio.KnobVecC, + GNURadio.KnobVecI,GNURadio.KnobVecF, + GNURadio.KnobVecD,GNURadio.KnobVecL]): + if(knobprop.display == ctrlport.DISPTIMESERIES): + #plot = self.newConstPlot(); + plot = self.newVecSeriesPlot(); + plot.setSeries(tag,tag); + plot.setWindowTitle(str(tag)); + else: # Plot others as XY for now + plot = self.newConstPlot(); + plot.setSeries(tag,tag); + plot.setWindowTitle(str(tag)); + elif(type(knobprop.min) in [GNURadio.KnobB,GNURadio.KnobC,GNURadio.KnobI, + GNURadio.KnobF,GNURadio.KnobD,GNURadio.KnobL]): + plot = self.newPlot(); + plot.addSeriesWithButton(tag, knobprop.description + ' (' + knobprop.units + ')', None, 1.0); + plot.setWindowTitle(str(tag)); + else: + print "WARNING: plotting of this knob-type not yet supported, ignoring attempt..." + + + def newVecSeriesPlot(self): + #plot = DataPlotterEqTaps(None, 'legend', 'title', 'xlabel', 'ylabel', 250, 0, 0, Qt.green) + plot = DataPlotterVectorOne(None, 'legend', 'title', 'xlabel', 'ylabel', 250, 0, 0); + self.mdiArea.addSubWindow(plot); + plot.dropSignal.connect(self.plotDropEvent ); + plot.show(); + self.plots.append(plot); + return plot; + + def newConstPlot(self): + plot = DataPlotterConst(None, 'legend', 'title', 'xlabel', 'ylabel', 250, 0, 0) + self.mdiArea.addSubWindow(plot); + plot.dropSignal.connect(self.plotDropEvent ); + plot.show(); + self.plots.append(plot); + return plot; + + def newPlot(self): + plot = DataPlotterTickerWithSeriesButtons(None,"",'units', '', 250,0,0,120); + self.mdiArea.addSubWindow(plot); + plot.dropSignal.connect(self.plotDropEvent ); + plot.show(); + self.plots.append(plot); + return plot; + + + def update(self, knobs): + for item in knobs.keys(): + for plot in self.plots: + plot.offerData( knobs[item].value, item ); + + def plotDropEvent(self, e): + model = QtGui.QStandardItemModel() + model.dropMimeData(e.mimeData(), QtCore.Qt.CopyAction,0,0,QtCore.QModelIndex()) + tag = str(QtGui.QTreeWidgetItem([model.item(0,0).text()]).text(0)); + knobprop = self.knobprops[tag] + try: + self.sender().addSeriesWithButton(tag, knobprop.description + ' (' + knobprop.units + ')', None, 1.0); + except: + print "This plot does not accept additional data items! ignoring..." + + + 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, shortcut=QtGui.QKeySequence.New, + statusTip="Create a new file", triggered=self.newPlot) + + 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) + + + 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.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 createToolBars(self): + self.fileToolBar = self.addToolBar("File") + self.fileToolBar.addAction(self.newConAct) + self.fileToolBar.addAction(self.newPlotAct) + + 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.QWidget): + def __init__(self, key, radio, parent): + QtGui.QWidget.__init__(self,parent) + + self.key = key; + self.radio = radio; + + self.resize(300,200) + self.layout = QtGui.QVBoxLayout(); + + self.textInput = QtGui.QLineEdit(); + self.setButton = QtGui.QPushButton("Set Value") + + rv = radio.get([key]); + self.textInput.setText(str(rv[key].value)); + self.sv = rv[key]; + + + self.props = radio.properties([key])[key]; + info = str(self.props); + + self.infoLabel = QtGui.QLabel(info); + self.layout.addWidget(self.infoLabel); + + self.layout.addWidget(self.textInput); + + self.setButton.connect(self.setButton, QtCore.SIGNAL('clicked()'), self._set) + + 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(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( self.props.min.value, self.props.max.value ); + self.slider.setRange( 0,10000 ); + + self.slider.setValue( self.sv.value ); + self.slider.setValue( self.steps*(self.sv.value-self.props.min.value)/self.valspan); + + self.connect(self.slider, QtCore.SIGNAL("sliderReleased()"), self._slide); + + self.layout.addLayout(self.sliderlayout); + + self.layout.addWidget(self.setButton); + + # set layout and go... + self.setLayout(self.layout); + + def _slide(self): + val = (self.slider.value()*self.valspan + self.props.min.value)/float(self.steps); + self.textInput.setText(str(val)); + + def _set(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: + print "set type not supported! (%s)"%(type(self.sv.value)); + sys.exit(-1); + #self.sv.value = int(val); + self.sv.value = val; + km = {}; + km[self.key] = self.sv; + self.radio.set(km); + +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): + # closing all of these seems to help control shutdown order. + self.parent.closeAct.trigger() + else: + sys.exit(1) + return + + tableitems = knobs.keys(); + + #UPDATE TABLE: + self.table.updateItems(knobs, self.knobprops) + + #UPDATE PLOTS + self.parent.update( knobs ); + + + def __init__(self, radio=None, port=None, parent=None): + + super(MForm, self).__init__() + + if(radio == None or port == None): + askinfo = ConInfoDialog(self); + if askinfo.exec_(): + print "connecting..." + radio = str(askinfo.host.text()); + port = str(askinfo.port.text()); + print "this is broken" + return; + else: + return; + + self.parent = parent; + self.horizontalLayout = QtGui.QVBoxLayout(self); + self.gridLayout = QtGui.QGridLayout() + + + self.radio = radio + self.knobprops = self.radio.properties([]) + self.parent.knobprops = self.knobprops; + self.resize(775,500) + self.timer = QtCore.QTimer() + self.constupdatediv = 0 + self.tableupdatediv = 0 + plotsize=250; + + + # make table + self.table = DataPlotterValueTable(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.connect(self.table.treeWidget, QtCore.SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.parent.newSub); + + # set up context menu .. + self.table.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu); + self.table.treeWidget.customContextMenuRequested.connect(self.openMenu); + + self.timer.start(2000) + + def plotDropEvent(self, e): + model = QtGui.QStandardItemModel() + model.dropMimeData(e.mimeData(), QtCore.Qt.CopyAction,0,0,QtCore.QModelIndex()) + tag = str(QtGui.QTreeWidgetItem([model.item(0,0).text()]).text(0)); + knobprop = self.knobprops[tag] + self.sender().addSeriesWithButton(tag, knobprop.description + ' (' + knobprop.units + ')', None, 1.0); + + def openMenu(self, pos): + index = self.table.treeWidget.selectedIndexes(); + item = self.table.treeWidget.itemFromIndex(index[0]); + itemname = str(item.text(0)); + self.parent.newUpd(itemname, self.radio); +# updater = UpdaterWindow(itemname, self.radio, self.parent); +# updater.setWindowTitle("Updater: " + itemname); +# self.parent.mdiArea.addSubWindow(updater); +# print "done" + + +class MyClient(IceRadioClient): + def __init__(self): IceRadioClient.__init__(self, MAINWindow) + +sys.exit(MyClient().main(sys.argv)) diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/gr-ctrlport-monitor b/gnuradio-core/src/python/gnuradio/ctrlport/gr-ctrlport-monitor new file mode 100755 index 0000000000..241b8a2043 --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/gr-ctrlport-monitor @@ -0,0 +1,581 @@ +#!/usr/bin/env python +# +# Copyright 2012 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 sys, time + +import Ice +from gnuradio.ctrlport.IceRadioClient import * +from gnuradio.ctrlport.GrDataPlotter import * +from gnuradio.ctrlport import GNURadio + +class MAINWindow(QtGui.QMainWindow): + def minimumSizeHint(self): + return Qtgui.QSize(800,600) + + def __init__(self, radio, port, interface): + + super(MAINWindow, self).__init__() + 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) + + def newCon(self, radio=None, port=None): + child = MForm(radio, port, len(self.conns), 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 + + def newUpdaterProxy(): + self.newUpdater(key, radio) + + def newPlotterFProxy(): + self.newPlotF(key, uid, title, pmin, pmax) + + def newPlotterCProxy(): + self.newPlotC(key, uid, title, pmin, pmax) + + def newPlotterConstProxy(): + self.newPlotConst(key, uid, title, pmin, pmax) + + def newPlotterPsdFProxy(): + self.newPlotPsdF(key, uid, title) + + def newPlotterPsdCProxy(): + self.newPlotPsdC(key, uid, title) + + menu = QtGui.QMenu(self) + menu.setTitle("Item Actions") + menu.setTearOffEnabled(False) + + # object properties + menu.addAction("Properties", newUpdaterProxy) + + # displays available if not complex + menu.addAction("Plot Time", newPlotterFProxy) + menu.addAction("Plot PSD", newPlotterPsdFProxy) + + # displays available if complex + menu.addAction("Plot Time (cpx)", newPlotterCProxy) + menu.addAction("Plot Constellation", newPlotterConstProxy) + menu.addAction("Plot PSD cpx", newPlotterPsdCProxy) + + 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 + + if(knobprop.display == GNURadio.DisplayType.DISPXYSCATTER): + self.newPlotConst(tag, uid, title, pmin, pmax) + elif(knobprop.display == GNURadio.DisplayType.DISPTIMESERIESF): + self.newPlotF(tag, uid, title, pmin, pmax) + elif(knobprop.display == GNURadio.DisplayType.DISPTIMESERIESC): + self.newPlotC(tag, uid, title, pmin, pmax) + + def createPlot(self, plot, uid, title): + plot.start() + self.plots[uid].append(plot) + + self.mdiArea.addSubWindow(plot.qwidget()) + plot.qwidget().setWindowTitle("{0}: {1}".format(title, plot.name())) + self.connect(plot.qwidget(), + QtCore.SIGNAL('destroyed(QObject*)'), + self.destroyPlot) + plot.qwidget().show() + + + 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): + plot = GrDataPlotterConst(tag, 32e6, pmin, pmax) + self.createPlot(plot, uid, title) + + def newPlotF(self, tag, uid, title="", pmin=None, pmax=None): + plot = GrDataPlotterF(tag, 32e6, pmin, pmax) + self.createPlot(plot, uid, title) + + def newPlotC(self, tag, uid, title="", pmin=None, pmax=None): + plot = GrDataPlotterC(tag, 32e6, pmin, pmax) + 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 update(self, knobs, uid): + #sys.stderr.write("KNOB KEYS: {0}\n".format(knobs.keys())) + for plot in self.plots[uid]: + data = knobs[plot.name()].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) + + + 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.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 createToolBars(self): + self.fileToolBar = self.addToolBar("File") + self.fileToolBar.addAction(self.newConAct) + self.fileToolBar.addAction(self.newPlotAct) + + 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, 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.timer.start(1000) + + # 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); + + 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)) diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/icon.png b/gnuradio-core/src/python/gnuradio/ctrlport/icon.png Binary files differnew file mode 100644 index 0000000000..4beb204428 --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/icon.png diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/monitor.py b/gnuradio-core/src/python/gnuradio/ctrlport/monitor.py new file mode 100644 index 0000000000..1e74a814f0 --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/monitor.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# +# Copyright 2012 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. +# + +import sys, subprocess, re, signal, time, atexit, os +from gnuradio import gr + +class monitor: + def __init__(self): + print "ControlPort Monitor running." + self.started = False + self.start() + atexit.register(self.shutdown) + + def __del__(self): + if(self.started): + self.stop() + + def start(self): + print "monitor::endpoints() = %s" % (gr.rpcmanager_get().endpoints()) + try: + self.proc = subprocess.Popen(map(lambda a: ["gr-ctrlport-monitor", + re.search("\d+\.\d+\.\d+\.\d+",a).group(0), + re.search("-p (\d+)",a).group(1)], + gr.rpcmanager_get().endpoints())[0]) + self.started = True + except: + self.proc = None + print "failed to to start ControlPort Monitor on specified port" + + def stop(self): + if(self.proc): + if(self.proc.returncode == None): + print "\tcalling stop on shutdown" + self.proc.terminate() + else: + print "\tno proc to shut down, exiting" + + def shutdown(self): + print "ctrlport.monitor received shutdown signal" + if(self.started): + self.stop() diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/qa_cpp_py_binding.py b/gnuradio-core/src/python/gnuradio/ctrlport/qa_cpp_py_binding.py new file mode 100755 index 0000000000..d4db8e76da --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/qa_cpp_py_binding.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# +# Copyright 2012 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. +# + +# +# This program tests mixed python and c++ ctrlport exports in a single app +# + +import Ice +import sys, time, random, numpy +from gnuradio import gr, gr_unittest + +from gnuradio.ctrlport import GNURadio +from gnuradio import ctrlport + +def get1(): + return "success" + +def get2(): + return "failure" + +class inc_class: + def __init__(self): + self.val = 1 + def pp(self): + self.val = self.val+1 + return self.val + +get3 = inc_class() + +def get4(): + random.seed(0) + rv = random.random() + return rv + +def get5(): + numpy.random.seed(0) + samp_t = numpy.random.randn(24)+1j*numpy.random.randn(24); + samp_f = numpy.fft.fft(samp_t); + log_pow_f = 20*numpy.log10(numpy.abs(samp_f)) + rv = list(log_pow_f) + return rv; + +def get6(): + numpy.random.seed(0) + samp_t = numpy.random.randn(1024)+1j*numpy.random.randn(1024); + rv = list(samp_t) + return rv; + +class test_cpp_py_binding(gr_unittest.TestCase): + + def setUp(self): + self.tb = gr.top_block() + + def tearDown(self): + self.tb = None + + def test_001(self): + v1 = gr.RPC_get_string("pyland", "v1", "unit_1_string", + "Python Exported String", "", "", "", + gr.DISPNULL) + v1.activate(get1) + + v2 = gr.RPC_get_string("pyland", "v2", "unit_2_string", + "Python Exported String", "", "", "", + gr.DISPNULL) + v2.activate(get2) + + v3 = gr.RPC_get_int("pyland", "v3", "unit_3_int", + "Python Exported Int", 0, 100, 1, + gr.DISPNULL) + v3.activate(get3.pp) + + v4 = gr.RPC_get_double("pyland", "time", "unit_4_time_double", + "Python Exported Double", 0, 1000, 1, + gr.DISPNULL) + v4.activate(get4) + + v5 = gr.RPC_get_vector_float("pyland", "fvec", "unit_5_float_vector", + "Python Exported Float Vector", [], [], [], + gr.DISPTIMESERIESC) + v5.activate(get5) + + v6 = gr.RPC_get_vector_gr_complex("pyland", "cvec", "unit_6_gr_complex_vector", + "Python Exported Complex Vector", [], [], [], + gr.DISPXYSCATTER) + v6.activate(get6) + + # print some variables locally + val = get1() + rval = v1.get() + self.assertEqual(val, rval) + + val = get2() + rval = v2.get() + self.assertEqual(val, rval) + + val = get3.pp() + rval = v3.get() + self.assertEqual(val+1, rval) + + val = get4() + rval = v4.get() + self.assertEqual(val, rval) + + val = get5() + rval = v5.get() + self.assertComplexTuplesAlmostEqual(val, rval, 5) + + val = get6() + rval = v6.get() + self.assertComplexTuplesAlmostEqual(val, rval, 5) + + def test_002(self): + data = range(1,9) + + self.src = gr.vector_source_c(data) + self.p1 = gr.ctrlport_probe_c("aaa","C++ exported variable") + self.p2 = gr.ctrlport_probe_c("bbb","C++ exported variable") + + self.tb.connect(self.src, self.p1) + self.tb.connect(self.src, self.p2) + self.tb.start() + + # Probes return complex values as list of floats with re, im + # Imaginary parts of this data set are 0. + expected_result = [1, 0, 2, 0, 3, 0, 4, 0, + 5, 0, 6, 0, 7, 0, 8, 0] + + # Make sure we have time for flowgraph to run + time.sleep(0.1) + + # Get available endpoint + ep = gr.rpcmanager_get().endpoints()[0] + + # Initialize a simple Ice client from endpoint + ic = Ice.initialize(sys.argv) + base = ic.stringToProxy(ep) + radio = GNURadio.ControlPortPrx.checkedCast(base) + + # Get all exported knobs + ret = radio.get([]) + for name in ret.keys(): + result = ret[name].value + self.assertEqual(result, expected_result) + + self.tb.stop() + +if __name__ == '__main__': + gr_unittest.run(test_cpp_py_binding, "test_cpp_py_binding.xml") + diff --git a/gnuradio-core/src/python/gnuradio/ctrlport/qa_cpp_py_binding_set.py b/gnuradio-core/src/python/gnuradio/ctrlport/qa_cpp_py_binding_set.py new file mode 100755 index 0000000000..fe7e5bcb60 --- /dev/null +++ b/gnuradio-core/src/python/gnuradio/ctrlport/qa_cpp_py_binding_set.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# +# Copyright 2012 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. +# + +# +# This program tests mixed python and c++ GRCP sets in a single app +# + +import Ice +import sys, time, random, numpy +from gnuradio import gr, gr_unittest + +from gnuradio.ctrlport import GNURadio +from gnuradio import ctrlport + +class inc_class: + def __init__(self,val): + self.val = val; + + def _get(self): + #print "returning get (val = %s)"%(str(self.val)); + return self.val; + + def _set(self,val): + #print "updating val to %s"%(str(val)); + self.val = val; + return; + +getset1 = inc_class(10); +getset2 = inc_class(100.0); +getset3 = inc_class("test"); + +class test_cpp_py_binding_set(gr_unittest.TestCase): + def setUp(self): + self.tb = gr.top_block() + + def tearDown(self): + self.tb = None + + def test_001(self): + + g1 = gr.RPC_get_int("pyland", "v1", "unit_1_int", + "Python Exported Int", 0, 100, 10, + gr.DISPNULL) + g1.activate(getset1._get) + s1 = gr.RPC_get_int("pyland", "v1", "unit_1_int", + "Python Exported Int", 0, 100, 10, + gr.DISPNULL) + s1.activate(getset1._set) + time.sleep(0.01) + + # test int variables + getset1._set(21) + val = getset1._get() + rval = g1.get() + self.assertEqual(val, rval) + + g2 = gr.RPC_get_float("pyland", "v2", "unit_2_float", + "Python Exported Float", -100, 1000.0, 100.0, + gr.DISPNULL) + g2.activate(getset2._get) + s2 = gr.RPC_get_float("pyland", "v2", "unit_2_float", + "Python Exported Float", -100, 1000.0, 100.0, + gr.DISPNULL) + s2.activate(getset2._set) + time.sleep(0.01) + + # test float variables + getset2._set(123.456) + val = getset2._get() + rval = g2.get() + self.assertAlmostEqual(val, rval, 4) + + g3 = gr.RPC_get_string("pyland", "v3", "unit_3_string", + "Python Exported String", "", "", "", + gr.DISPNULL) + g3.activate(getset3._get) + s3 = gr.RPC_get_string("pyland", "v3", "unit_3_string", + "Python Exported String", "", "", "", + gr.DISPNULL) + s3.activate(getset3._set) + time.sleep(0.01) + + # test string variables + getset3._set("third test") + val = getset3._get() + rval = g3.get() + self.assertEqual(val, rval) + + + def test_002(self): + data = range(1, 10) + + self.src = gr.vector_source_c(data, True) + self.p = gr.nop(gr.sizeof_gr_complex) + self.p.set_ctrlport_test(0); + probe_info = "{0}{1}".format(self.p.name(), self.p.unique_id()) + + self.tb.connect(self.src, self.p) + + # Get available endpoint + ep = gr.rpcmanager_get().endpoints()[0] + + # Initialize a simple Ice client from endpoint + ic = Ice.initialize(sys.argv) + base = ic.stringToProxy(ep) + radio = GNURadio.ControlPortPrx.checkedCast(base) + + self.tb.start() + + # Make sure we have time for flowgraph to run + time.sleep(0.1) + + # Get all exported knobs + key_name_test = probe_info+"::test" + ret = radio.get([key_name_test,]) + + ret[key_name_test].value = 10 + radio.set({key_name_test: ret[key_name_test]}) + + ret = radio.get([]) + result_test = ret[key_name_test].value + self.assertEqual(result_test, 10) + + self.tb.stop() + self.tb.wait() + +if __name__ == '__main__': + gr_unittest.run(test_cpp_py_binding_set, "test_cpp_py_binding_set.xml") + |