summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AUTHORS1
-rw-r--r--config/grc_gr_wxgui.m41
-rw-r--r--gnuradio-core/src/python/gnuradio/blks2impl/logpwrfft.py7
-rwxr-xr-xgr-utils/src/python/usrp_fft.py2
-rw-r--r--gr-wxgui/Makefile.am7
-rw-r--r--gr-wxgui/README.gl20
-rw-r--r--gr-wxgui/gr-wxgui.conf1
-rw-r--r--gr-wxgui/src/python/Makefile.am21
-rw-r--r--gr-wxgui/src/python/__init__.py3
-rw-r--r--gr-wxgui/src/python/common.py302
-rw-r--r--gr-wxgui/src/python/const_window.py189
-rw-r--r--gr-wxgui/src/python/constants.py64
-rw-r--r--gr-wxgui/src/python/constsink_gl.py142
-rw-r--r--gr-wxgui/src/python/fft_window.py292
-rw-r--r--[-rwxr-xr-x]gr-wxgui/src/python/fftsink2.py608
-rw-r--r--gr-wxgui/src/python/fftsink_gl.py172
-rw-r--r--gr-wxgui/src/python/fftsink_nongl.py603
-rw-r--r--[-rwxr-xr-x]gr-wxgui/src/python/form.py0
-rw-r--r--gr-wxgui/src/python/number_window.py184
-rw-r--r--[-rwxr-xr-x]gr-wxgui/src/python/numbersink2.py600
-rw-r--r--gr-wxgui/src/python/plotter/Makefile.am37
-rw-r--r--gr-wxgui/src/python/plotter/__init__.py23
-rw-r--r--gr-wxgui/src/python/plotter/channel_plotter.py225
-rw-r--r--gr-wxgui/src/python/plotter/gltext.py503
-rw-r--r--gr-wxgui/src/python/plotter/plotter_base.py390
-rw-r--r--gr-wxgui/src/python/plotter/waterfall_plotter.py282
-rw-r--r--[-rwxr-xr-x]gr-wxgui/src/python/powermate.py0
-rw-r--r--gr-wxgui/src/python/pubsub.py153
-rw-r--r--gr-wxgui/src/python/scope_window.py482
-rw-r--r--[-rwxr-xr-x]gr-wxgui/src/python/scopesink2.py666
-rw-r--r--gr-wxgui/src/python/scopesink_gl.py183
-rw-r--r--gr-wxgui/src/python/scopesink_nongl.py661
-rw-r--r--[-rwxr-xr-x]gr-wxgui/src/python/slider.py0
-rw-r--r--gr-wxgui/src/python/waterfall_window.py301
-rw-r--r--[-rwxr-xr-x]gr-wxgui/src/python/waterfallsink2.py442
-rw-r--r--gr-wxgui/src/python/waterfallsink_gl.py175
-rw-r--r--gr-wxgui/src/python/waterfallsink_nongl.py437
37 files changed, 6068 insertions, 2111 deletions
diff --git a/AUTHORS b/AUTHORS
index 9c6e4c6c5..11fbc4598 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -13,3 +13,4 @@ Johnathan Corgan <jcorgan@corganenterprises.com> Build system, ongoing stuff, r
Bdale Garbee <bdale@gag.com> Debian release packages
Tom Rondeau <trondeau@vt.edu> Mostly digital waveforms and a little bit of trouble
Nate Goergen (UMD Student)
+Josh Blum <josh@joshknows.com> OpenGL versions of graphical sinks
diff --git a/config/grc_gr_wxgui.m4 b/config/grc_gr_wxgui.m4
index 522c62520..e1a3fe4c9 100644
--- a/config/grc_gr_wxgui.m4
+++ b/config/grc_gr_wxgui.m4
@@ -41,6 +41,7 @@ AC_DEFUN([GRC_GR_WXGUI],[
gr-wxgui/gr-wxgui.pc \
gr-wxgui/src/Makefile \
gr-wxgui/src/python/Makefile \
+ gr-wxgui/src/python/plotter/Makefile \
])
GRC_BUILD_CONDITIONAL(gr-wxgui)
diff --git a/gnuradio-core/src/python/gnuradio/blks2impl/logpwrfft.py b/gnuradio-core/src/python/gnuradio/blks2impl/logpwrfft.py
index d10246cfe..aa3931c5e 100644
--- a/gnuradio-core/src/python/gnuradio/blks2impl/logpwrfft.py
+++ b/gnuradio-core/src/python/gnuradio/blks2impl/logpwrfft.py
@@ -72,6 +72,13 @@ class _logpwrfft_base(gr.hier_block2):
"""
self._sd.set_decimation(decim)
+ def set_vec_rate(self, vec_rate):
+ """!
+ Set the vector rate on stream decimator.
+ @param vec_rate the new vector rate
+ """
+ self._sd.set_vec_rate(vec_rate)
+
def set_sample_rate(self, sample_rate):
"""!
Set the new sampling rate
diff --git a/gr-utils/src/python/usrp_fft.py b/gr-utils/src/python/usrp_fft.py
index 307ad1630..8a9008877 100755
--- a/gr-utils/src/python/usrp_fft.py
+++ b/gr-utils/src/python/usrp_fft.py
@@ -251,7 +251,7 @@ class app_top_block(stdgui2.std_top_block):
if self.show_debug_info:
self.myform['baseband'].set_value(r.baseband_freq)
self.myform['ddc'].set_value(r.dxc_freq)
- if not self.options.waterfall and not self.options.oscilloscope:
+ if not self.options.oscilloscope:
self.scope.win.set_baseband_freq(target_freq)
return True
diff --git a/gr-wxgui/Makefile.am b/gr-wxgui/Makefile.am
index f347cc691..cf9d68988 100644
--- a/gr-wxgui/Makefile.am
+++ b/gr-wxgui/Makefile.am
@@ -21,7 +21,12 @@
include $(top_srcdir)/Makefile.common
-EXTRA_DIST = gr-wxgui.conf gr-wxgui.pc.in
+EXTRA_DIST = \
+ gr-wxgui.conf \
+ gr-wxgui.pc.in \
+ README \
+ README.gl
+
SUBDIRS = src
etcdir = $(sysconfdir)/gnuradio/conf.d
diff --git a/gr-wxgui/README.gl b/gr-wxgui/README.gl
new file mode 100644
index 000000000..660758706
--- /dev/null
+++ b/gr-wxgui/README.gl
@@ -0,0 +1,20 @@
+To use the OpenGL versions of the graphical display sinks, you must ensure
+that you have Python wrappers for OpenGL installed and are using a version
+of wxPython that supports it. Then you must enable this mode by creating or
+editing an entry in the GNU Radio preferences file at:
+
+~/.gnuradio/config.conf
+
+[wxgui]
+style=gl
+
+
+The style parameter accepts 'nongl', 'gl', and 'auto', and defaults to 'auto'.
+
+'nongl' forces the use of the non-GL (current) sinks.
+
+'gl' forces the use of the new GL based sinks, and will raise an exception if the
+appropriate GL support does not exist.
+
+'auto' currently equates to 'nongl'; however, in release 3.2, this will change to
+use GL if possible and if not, fallback to the non-GL versions.
diff --git a/gr-wxgui/gr-wxgui.conf b/gr-wxgui/gr-wxgui.conf
index f6b128c68..6dcc54d2c 100644
--- a/gr-wxgui/gr-wxgui.conf
+++ b/gr-wxgui/gr-wxgui.conf
@@ -3,5 +3,6 @@
# ~/.gnuradio/config.conf
[wxgui]
+style = auto # 'gl', 'nongl', or 'auto'
fft_rate = 15 # fftsink and waterfallsink
frame_decim = 1 # scopesink
diff --git a/gr-wxgui/src/python/Makefile.am b/gr-wxgui/src/python/Makefile.am
index bda7c362c..a85bd0920 100644
--- a/gr-wxgui/src/python/Makefile.am
+++ b/gr-wxgui/src/python/Makefile.am
@@ -21,6 +21,8 @@
include $(top_srcdir)/Makefile.common
+SUBDIRS = plotter
+
# Install this stuff so that it ends up as the gnuradio.wxgui module
# This usually ends up at:
# ${prefix}/lib/python${python_version}/site-packages/gnuradio/wxgui
@@ -30,12 +32,27 @@ ourlibdir = $(grpyexecdir)/wxgui
ourpython_PYTHON = \
__init__.py \
+ common.py \
+ constants.py \
+ constsink_gl.py \
+ const_window.py \
form.py \
fftsink2.py \
+ fftsink_nongl.py \
+ fftsink_gl.py \
+ fft_window.py \
+ numbersink2.py \
+ number_window.py \
plot.py \
powermate.py \
+ pubsub.py \
scopesink2.py \
+ scopesink_nongl.py \
+ scopesink_gl.py \
+ scope_window.py \
waterfallsink2.py \
+ waterfallsink_nongl.py \
+ waterfallsink_gl.py \
+ waterfall_window.py \
slider.py \
- stdgui2.py \
- numbersink2.py
+ stdgui2.py
diff --git a/gr-wxgui/src/python/__init__.py b/gr-wxgui/src/python/__init__.py
index 027150db1..7be3c38aa 100644
--- a/gr-wxgui/src/python/__init__.py
+++ b/gr-wxgui/src/python/__init__.py
@@ -1 +1,2 @@
-# make this directory a package
+import plotter
+
diff --git a/gr-wxgui/src/python/common.py b/gr-wxgui/src/python/common.py
new file mode 100644
index 000000000..84b4c9c4c
--- /dev/null
+++ b/gr-wxgui/src/python/common.py
@@ -0,0 +1,302 @@
+#
+# Copyright 2008 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 threading
+import numpy
+import math
+import wx
+
+class prop_setter(object):
+ def _register_set_prop(self, controller, control_key, init=None):
+ def set_method(value): controller[control_key] = value
+ if init is not None: set_method(init)
+ setattr(self, 'set_%s'%control_key, set_method)
+
+##################################################
+# Input Watcher Thread
+##################################################
+class input_watcher(threading.Thread):
+ """!
+ Input watcher thread runs forever.
+ Read messages from the message queue.
+ Forward messages to the message handler.
+ """
+ def __init__ (self, msgq, handle_msg):
+ threading.Thread.__init__(self)
+ self.setDaemon(1)
+ self.msgq = msgq
+ self._handle_msg = handle_msg
+ self.keep_running = True
+ self.start()
+
+ def run(self):
+ while self.keep_running: self._handle_msg(self.msgq.delete_head().to_string())
+
+##################################################
+# WX Shared Classes
+##################################################
+class LabelText(wx.StaticText):
+ """!
+ Label text to give the wx plots a uniform look.
+ Get the default label text and set the font bold.
+ """
+ def __init__(self, parent, label):
+ wx.StaticText.__init__(self, parent, -1, label)
+ font = self.GetFont()
+ font.SetWeight(wx.FONTWEIGHT_BOLD)
+ self.SetFont(font)
+
+class IncrDecrButtons(wx.BoxSizer):
+ """!
+ A horizontal box sizer with a increment and a decrement button.
+ """
+ def __init__(self, parent, on_incr, on_decr):
+ """!
+ @param parent the parent window
+ @param on_incr the event handler for increment
+ @param on_decr the event handler for decrement
+ """
+ wx.BoxSizer.__init__(self, wx.HORIZONTAL)
+ self._incr_button = wx.Button(parent, -1, '+', style=wx.BU_EXACTFIT)
+ self._incr_button.Bind(wx.EVT_BUTTON, on_incr)
+ self.Add(self._incr_button, 0, wx.ALIGN_CENTER_VERTICAL)
+ self._decr_button = wx.Button(parent, -1, ' - ', style=wx.BU_EXACTFIT)
+ self._decr_button.Bind(wx.EVT_BUTTON, on_decr)
+ self.Add(self._decr_button, 0, wx.ALIGN_CENTER_VERTICAL)
+
+ def Disable(self, disable=True): self.Enable(not disable)
+ def Enable(self, enable=True):
+ if enable:
+ self._incr_button.Enable()
+ self._decr_button.Enable()
+ else:
+ self._incr_button.Disable()
+ self._decr_button.Disable()
+
+class ToggleButtonController(wx.Button):
+ def __init__(self, parent, controller, control_key, true_label, false_label):
+ self._controller = controller
+ self._control_key = control_key
+ wx.Button.__init__(self, parent, -1, '', style=wx.BU_EXACTFIT)
+ self.Bind(wx.EVT_BUTTON, self._evt_button)
+ controller.subscribe(control_key, lambda x: self.SetLabel(x and true_label or false_label))
+
+ def _evt_button(self, e):
+ self._controller[self._control_key] = not self._controller[self._control_key]
+
+class CheckBoxController(wx.CheckBox):
+ def __init__(self, parent, label, controller, control_key):
+ self._controller = controller
+ self._control_key = control_key
+ wx.CheckBox.__init__(self, parent, style=wx.CHK_2STATE, label=label)
+ self.Bind(wx.EVT_CHECKBOX, self._evt_checkbox)
+ controller.subscribe(control_key, lambda x: self.SetValue(bool(x)))
+
+ def _evt_checkbox(self, e):
+ self._controller[self._control_key] = bool(e.IsChecked())
+
+class LogSliderController(wx.BoxSizer):
+ """!
+ Log slider controller with display label and slider.
+ Gives logarithmic scaling to slider operation.
+ """
+ def __init__(self, parent, label, min_exp, max_exp, slider_steps, controller, control_key, formatter=lambda x: ': %.6f'%x):
+ wx.BoxSizer.__init__(self, wx.VERTICAL)
+ self._label = wx.StaticText(parent, -1, label + formatter(1/3.0))
+ self.Add(self._label, 0, wx.EXPAND)
+ self._slider = wx.Slider(parent, -1, 0, 0, slider_steps, style=wx.SL_HORIZONTAL)
+ self.Add(self._slider, 0, wx.EXPAND)
+ def _on_slider_event(event):
+ controller[control_key] = \
+ 10**(float(max_exp-min_exp)*self._slider.GetValue()/slider_steps + min_exp)
+ self._slider.Bind(wx.EVT_SLIDER, _on_slider_event)
+ def _on_controller_set(value):
+ self._label.SetLabel(label + formatter(value))
+ slider_value = slider_steps*(math.log10(value)-min_exp)/(max_exp-min_exp)
+ slider_value = min(max(0, slider_value), slider_steps)
+ if abs(slider_value - self._slider.GetValue()) > 1:
+ self._slider.SetValue(slider_value)
+ controller.subscribe(control_key, _on_controller_set)
+
+ def Disable(self, disable=True): self.Enable(not disable)
+ def Enable(self, enable=True):
+ if enable:
+ self._slider.Enable()
+ self._label.Enable()
+ else:
+ self._slider.Disable()
+ self._label.Disable()
+
+class DropDownController(wx.BoxSizer):
+ """!
+ Drop down controller with label and chooser.
+ Srop down selection from a set of choices.
+ """
+ def __init__(self, parent, label, choices, controller, control_key, size=(-1, -1)):
+ """!
+ @param parent the parent window
+ @param label the label for the drop down
+ @param choices a list of tuples -> (label, value)
+ @param controller the prop val controller
+ @param control_key the prop key for this control
+ """
+ wx.BoxSizer.__init__(self, wx.HORIZONTAL)
+ self._label = wx.StaticText(parent, -1, ' %s '%label)
+ self.Add(self._label, 1, wx.ALIGN_CENTER_VERTICAL)
+ self._chooser = wx.Choice(parent, -1, choices=[c[0] for c in choices], size=size)
+ def _on_chooser_event(event):
+ controller[control_key] = choices[self._chooser.GetSelection()][1]
+ self._chooser.Bind(wx.EVT_CHOICE, _on_chooser_event)
+ self.Add(self._chooser, 0, wx.ALIGN_CENTER_VERTICAL)
+ def _on_controller_set(value):
+ #only set the chooser if the value is a possible choice
+ for i, choice in enumerate(choices):
+ if value == choice[1]: self._chooser.SetSelection(i)
+ controller.subscribe(control_key, _on_controller_set)
+
+ def Disable(self, disable=True): self.Enable(not disable)
+ def Enable(self, enable=True):
+ if enable:
+ self._chooser.Enable()
+ self._label.Enable()
+ else:
+ self._chooser.Disable()
+ self._label.Disable()
+
+##################################################
+# Shared Functions
+##################################################
+def get_exp(num):
+ """!
+ Get the exponent of the number in base 10.
+ @param num the floating point number
+ @return the exponent as an integer
+ """
+ if num == 0: return 0
+ return int(math.floor(math.log10(abs(num))))
+
+def get_clean_num(num):
+ """!
+ Get the closest clean number match to num with bases 1, 2, 5.
+ @param num the number
+ @return the closest number
+ """
+ if num == 0: return 0
+ if num > 0: sign = 1
+ else: sign = -1
+ exp = get_exp(num)
+ nums = numpy.array((1, 2, 5, 10))*(10**exp)
+ return sign*nums[numpy.argmin(numpy.abs(nums - abs(num)))]
+
+def get_clean_incr(num):
+ """!
+ Get the next higher clean number with bases 1, 2, 5.
+ @param num the number
+ @return the next higher number
+ """
+ num = get_clean_num(num)
+ exp = get_exp(num)
+ coeff = int(round(num/10**exp))
+ return {
+ -5: -2,
+ -2: -1,
+ -1: -.5,
+ 1: 2,
+ 2: 5,
+ 5: 10,
+ }[coeff]*(10**exp)
+
+def get_clean_decr(num):
+ """!
+ Get the next lower clean number with bases 1, 2, 5.
+ @param num the number
+ @return the next lower number
+ """
+ num = get_clean_num(num)
+ exp = get_exp(num)
+ coeff = int(round(num/10**exp))
+ return {
+ -5: -10,
+ -2: -5,
+ -1: -2,
+ 1: .5,
+ 2: 1,
+ 5: 2,
+ }[coeff]*(10**exp)
+
+def get_min_max(samples):
+ """!
+ Get the minimum and maximum bounds for an array of samples.
+ @param samples the array of real values
+ @return a tuple of min, max
+ """
+ scale_factor = 3
+ mean = numpy.average(samples)
+ rms = scale_factor*((numpy.sum((samples-mean)**2)/len(samples))**.5)
+ min = mean - rms
+ max = mean + rms
+ return min, max
+
+def get_si_components(num):
+ """!
+ Get the SI units for the number.
+ Extract the coeff and exponent of the number.
+ The exponent will be a multiple of 3.
+ @param num the floating point number
+ @return the tuple coeff, exp, prefix
+ """
+ exp = get_exp(num)
+ exp -= exp%3
+ exp = min(max(exp, -24), 24) #bounds on SI table below
+ prefix = {
+ 24: 'Y', 21: 'Z',
+ 18: 'E', 15: 'P',
+ 12: 'T', 9: 'G',
+ 6: 'M', 3: 'K',
+ 0: '',
+ -3: 'm', -6: 'u',
+ -9: 'n', -12: 'p',
+ -15: 'f', -18: 'a',
+ -21: 'z', -24: 'y',
+ }[exp]
+ coeff = num/10**exp
+ return coeff, exp, prefix
+
+def label_format(num):
+ """!
+ Format a floating point number into a presentable string.
+ If the number has an small enough exponent, use regular decimal.
+ Otherwise, format the number with floating point notation.
+ Exponents are normalized to multiples of 3.
+ In the case where the exponent was found to be -3,
+ it is best to display this as a regular decimal, with a 0 to the left.
+ @param num the number to format
+ @return a label string
+ """
+ coeff, exp, prefix = get_si_components(num)
+ if -3 <= exp < 3: return '%g'%num
+ return '%se%d'%('%.3g'%coeff, exp)
+
+if __name__ == '__main__':
+ import random
+ for i in range(-25, 25):
+ num = random.random()*10**i
+ print num, ':', get_si_components(num)
diff --git a/gr-wxgui/src/python/const_window.py b/gr-wxgui/src/python/const_window.py
new file mode 100644
index 000000000..9510416be
--- /dev/null
+++ b/gr-wxgui/src/python/const_window.py
@@ -0,0 +1,189 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import plotter
+import common
+import wx
+import numpy
+import math
+import pubsub
+from constants import *
+
+##################################################
+# Constants
+##################################################
+SLIDER_STEPS = 200
+ALPHA_MIN_EXP, ALPHA_MAX_EXP = -6, -0.301
+GAIN_MU_MIN_EXP, GAIN_MU_MAX_EXP = -6, -0.301
+DEFAULT_FRAME_RATE = 5
+DEFAULT_WIN_SIZE = (500, 400)
+DEFAULT_CONST_SIZE = 2048
+CONST_PLOT_COLOR_SPEC = (0, 0, 1)
+MARKER_TYPES = (
+ ('Dot Small', 1.0),
+ ('Dot Medium', 2.0),
+ ('Dot Large', 3.0),
+ ('Line Link', None),
+)
+DEFAULT_MARKER_TYPE = MARKER_TYPES[1][1]
+
+##################################################
+# Constellation window control panel
+##################################################
+class control_panel(wx.Panel):
+ """!
+ A control panel with wx widgits to control the plotter.
+ """
+ def __init__(self, parent):
+ """!
+ Create a new control panel.
+ @param parent the wx parent window
+ """
+ self.parent = parent
+ wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+ self.marker_index = 2
+ #begin control box
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
+ #marker
+ control_box.AddStretchSpacer()
+ self.marker_chooser = common.DropDownController(self, 'Marker', MARKER_TYPES, parent, MARKER_KEY)
+ control_box.Add(self.marker_chooser, 0, wx.EXPAND)
+ #alpha
+ control_box.AddStretchSpacer()
+ self.alpha_slider = common.LogSliderController(
+ self, 'Alpha',
+ ALPHA_MIN_EXP, ALPHA_MAX_EXP, SLIDER_STEPS,
+ parent.ext_controller, parent.alpha_key,
+ )
+ control_box.Add(self.alpha_slider, 0, wx.EXPAND)
+ #gain_mu
+ control_box.AddStretchSpacer()
+ self.gain_mu_slider = common.LogSliderController(
+ self, 'Gain Mu',
+ GAIN_MU_MIN_EXP, GAIN_MU_MAX_EXP, SLIDER_STEPS,
+ parent.ext_controller, parent.gain_mu_key,
+ )
+ control_box.Add(self.gain_mu_slider, 0, wx.EXPAND)
+ #run/stop
+ control_box.AddStretchSpacer()
+ self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
+ control_box.Add(self.run_button, 0, wx.EXPAND)
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+##################################################
+# Constellation window with plotter and control panel
+##################################################
+class const_window(wx.Panel, pubsub.pubsub, common.prop_setter):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ msg_key,
+ alpha_key,
+ beta_key,
+ gain_mu_key,
+ gain_omega_key,
+ ):
+ pubsub.pubsub.__init__(self)
+ #setup
+ self.ext_controller = controller
+ self.alpha_key = alpha_key
+ self.beta_key = beta_key
+ self.gain_mu_key = gain_mu_key
+ self.gain_omega_key = gain_omega_key
+ #init panel and plot
+ wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.channel_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.set_title(title)
+ self.plotter.set_x_label('Inphase')
+ self.plotter.set_y_label('Quadrature')
+ self.plotter.enable_point_label(True)
+ #setup the box with plot and controls
+ self.control_panel = control_panel(self)
+ main_box = wx.BoxSizer(wx.HORIZONTAL)
+ main_box.Add(self.plotter, 1, wx.EXPAND)
+ main_box.Add(self.control_panel, 0, wx.EXPAND)
+ self.SetSizerAndFit(main_box)
+ #alpha and gain mu 2nd orders
+ def set_beta(alpha): self.ext_controller[self.beta_key] = .25*alpha**2
+ self.ext_controller.subscribe(self.alpha_key, set_beta)
+ def set_gain_omega(gain_mu): self.ext_controller[self.gain_omega_key] = .25*gain_mu**2
+ self.ext_controller.subscribe(self.gain_mu_key, set_gain_omega)
+ #initial setup
+ self.ext_controller[self.alpha_key] = self.ext_controller[self.alpha_key]
+ self.ext_controller[self.gain_mu_key] = self.ext_controller[self.gain_mu_key]
+ self._register_set_prop(self, RUNNING_KEY, True)
+ self._register_set_prop(self, X_DIVS_KEY, 8)
+ self._register_set_prop(self, Y_DIVS_KEY, 8)
+ self._register_set_prop(self, MARKER_KEY, DEFAULT_MARKER_TYPE)
+ #register events
+ self.ext_controller.subscribe(msg_key, self.handle_msg)
+ for key in (
+ X_DIVS_KEY, Y_DIVS_KEY,
+ ): self.subscribe(key, self.update_grid)
+ #initial update
+ self.update_grid()
+
+ def handle_msg(self, msg):
+ """!
+ Plot the samples onto the complex grid.
+ @param msg the array of complex samples
+ """
+ if not self[RUNNING_KEY]: return
+ #convert to complex floating point numbers
+ samples = numpy.fromstring(msg, numpy.complex64)
+ real = numpy.real(samples)
+ imag = numpy.imag(samples)
+ #plot
+ self.plotter.set_waveform(
+ channel=0,
+ samples=(real, imag),
+ color_spec=CONST_PLOT_COLOR_SPEC,
+ marker=self[MARKER_KEY],
+ )
+ #update the plotter
+ self.plotter.update()
+
+ def update_grid(self):
+ #grid parameters
+ x_divs = self[X_DIVS_KEY]
+ y_divs = self[Y_DIVS_KEY]
+ #update the x axis
+ x_max = 2.0
+ self.plotter.set_x_grid(-x_max, x_max, common.get_clean_num(2.0*x_max/x_divs))
+ #update the y axis
+ y_max = 2.0
+ self.plotter.set_y_grid(-y_max, y_max, common.get_clean_num(2.0*y_max/y_divs))
+ #update plotter
+ self.plotter.update()
+
+
+
+
diff --git a/gr-wxgui/src/python/constants.py b/gr-wxgui/src/python/constants.py
new file mode 100644
index 000000000..06c5a44f1
--- /dev/null
+++ b/gr-wxgui/src/python/constants.py
@@ -0,0 +1,64 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Controller Keys
+##################################################
+AC_COUPLE_KEY = 'ac_couple'
+ALPHA_KEY = 'alpha'
+AUTORANGE_KEY = 'autorange'
+AVERAGE_KEY = 'average'
+AVG_ALPHA_KEY = 'avg_alpha'
+BASEBAND_FREQ_KEY = 'baseband_freq'
+BETA_KEY = 'beta'
+COLOR_MODE_KEY = 'color_mode'
+DECIMATION_KEY = 'decimation'
+DYNAMIC_RANGE_KEY = 'dynamic_range'
+FRAME_RATE_KEY = 'frame_rate'
+GAIN_MU_KEY = 'gain_mu'
+GAIN_OMEGA_KEY = 'gain_omega'
+MARKER_KEY = 'marker'
+MSG_KEY = 'msg'
+NUM_LINES_KEY = 'num_lines'
+OMEGA_KEY = 'omega'
+PEAK_HOLD_KEY = 'peak_hold'
+REF_LEVEL_KEY = 'ref_level'
+RUNNING_KEY = 'running'
+SAMPLE_RATE_KEY = 'sample_rate'
+SCOPE_TRIGGER_CHANNEL_KEY = 'scope_trigger_channel'
+SCOPE_TRIGGER_LEVEL_KEY = 'scope_trigger_level'
+SCOPE_TRIGGER_MODE_KEY = 'scope_trigger_mode'
+SCOPE_X_CHANNEL_KEY = 'scope_x_channel'
+SCOPE_Y_CHANNEL_KEY = 'scope_y_channel'
+SCOPE_XY_MODE_KEY = 'scope_xy_mode'
+TRIGGER_CHANNEL_KEY = 'trigger_channel'
+TRIGGER_LEVEL_KEY = 'trigger_level'
+TRIGGER_MODE_KEY = 'trigger_mode'
+T_DIVS_KEY = 't_divs'
+T_OFF_KEY = 't_off'
+T_PER_DIV_KEY = 't_per_div'
+X_DIVS_KEY = 'x_divs'
+X_OFF_KEY = 'x_off'
+X_PER_DIV_KEY = 'x_per_div'
+Y_DIVS_KEY = 'y_divs'
+Y_OFF_KEY = 'y_off'
+Y_PER_DIV_KEY = 'y_per_div'
+
diff --git a/gr-wxgui/src/python/constsink_gl.py b/gr-wxgui/src/python/constsink_gl.py
new file mode 100644
index 000000000..2e1f3c5a0
--- /dev/null
+++ b/gr-wxgui/src/python/constsink_gl.py
@@ -0,0 +1,142 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import const_window
+import common
+from gnuradio import gr, blks2
+from pubsub import pubsub
+from constants import *
+
+##################################################
+# Constellation sink block (wrapper for old wxgui)
+##################################################
+class const_sink_c(gr.hier_block2, common.prop_setter):
+ """!
+ A constellation block with a gui window.
+ """
+
+ def __init__(
+ self,
+ parent,
+ title='',
+ sample_rate=1,
+ size=const_window.DEFAULT_WIN_SIZE,
+ frame_rate=const_window.DEFAULT_FRAME_RATE,
+ const_size=const_window.DEFAULT_CONST_SIZE,
+ #mpsk recv params
+ M=4,
+ theta=0,
+ alpha=0.005,
+ fmax=0.06,
+ mu=0.5,
+ gain_mu=0.005,
+ symbol_rate=1,
+ omega_limit=0.005,
+ ):
+ #init
+ gr.hier_block2.__init__(
+ self,
+ "const_sink",
+ gr.io_signature(1, 1, gr.sizeof_gr_complex),
+ gr.io_signature(0, 0, 0),
+ )
+ #blocks
+ sd = blks2.stream_to_vector_decimator(
+ item_size=gr.sizeof_gr_complex,
+ sample_rate=sample_rate,
+ vec_rate=frame_rate,
+ vec_len=const_size,
+ )
+ beta = .25*alpha**2 #redundant, will be updated
+ fmin = -fmax
+ gain_omega = .25*gain_mu**2 #redundant, will be updated
+ omega = 1 #set_sample_rate will update this
+ # Costas frequency/phase recovery loop
+ # Critically damped 2nd order PLL
+ self._costas = gr.costas_loop_cc(alpha, beta, fmax, fmin, M)
+ # Timing recovery loop
+ # Critically damped 2nd order DLL
+ self._retime = gr.clock_recovery_mm_cc(omega, gain_omega, mu, gain_mu, omega_limit)
+ #sync = gr.mpsk_receiver_cc(
+ # M, #psk order
+ # theta,
+ # alpha,
+ # beta,
+ # fmin,
+ # fmax,
+ # mu,
+ # gain_mu,
+ # omega,
+ # gain_omega,
+ # omega_limit,
+ #)
+ agc = gr.feedforward_agc_cc(16, 1)
+ msgq = gr.msg_queue(2)
+ sink = gr.message_sink(gr.sizeof_gr_complex*const_size, msgq, True)
+ #connect
+ self.connect(self, self._costas, self._retime, agc, sd, sink)
+ #controller
+ def setter(p, k, x): # lambdas can't have assignments :(
+ p[k] = x
+ self.controller = pubsub()
+ self.controller.subscribe(ALPHA_KEY, self._costas.set_alpha)
+ self.controller.publish(ALPHA_KEY, self._costas.alpha)
+ self.controller.subscribe(BETA_KEY, self._costas.set_beta)
+ self.controller.publish(BETA_KEY, self._costas.beta)
+ self.controller.subscribe(GAIN_MU_KEY, self._retime.set_gain_mu)
+ self.controller.publish(GAIN_MU_KEY, self._retime.gain_mu)
+ self.controller.subscribe(OMEGA_KEY, self._retime.set_omega)
+ self.controller.publish(OMEGA_KEY, self._retime.omega)
+ self.controller.subscribe(GAIN_OMEGA_KEY, self._retime.set_gain_omega)
+ self.controller.publish(GAIN_OMEGA_KEY, self._retime.gain_omega)
+ self.controller.subscribe(SAMPLE_RATE_KEY, sd.set_sample_rate)
+ self.controller.subscribe(SAMPLE_RATE_KEY, lambda x: setter(self.controller, OMEGA_KEY, float(x)/symbol_rate))
+ self.controller.publish(SAMPLE_RATE_KEY, sd.sample_rate)
+ #initial update
+ self.controller[SAMPLE_RATE_KEY] = sample_rate
+ #start input watcher
+ common.input_watcher(msgq, lambda x: setter(self.controller, MSG_KEY, x))
+ #create window
+ self.win = const_window.const_window(
+ parent=parent,
+ controller=self.controller,
+ size=size,
+ title=title,
+ msg_key=MSG_KEY,
+ alpha_key=ALPHA_KEY,
+ beta_key=BETA_KEY,
+ gain_mu_key=GAIN_MU_KEY,
+ gain_omega_key=GAIN_OMEGA_KEY,
+ )
+ #register callbacks from window for external use
+ for attr in filter(lambda a: a.startswith('set_'), dir(self.win)):
+ setattr(self, attr, getattr(self.win, attr))
+ self._register_set_prop(self.controller, ALPHA_KEY)
+ self._register_set_prop(self.controller, BETA_KEY)
+ self._register_set_prop(self.controller, GAIN_MU_KEY)
+ self._register_set_prop(self.controller, OMEGA_KEY)
+ self._register_set_prop(self.controller, GAIN_OMEGA_KEY)
+ self._register_set_prop(self.controller, SAMPLE_RATE_KEY)
+
+
diff --git a/gr-wxgui/src/python/fft_window.py b/gr-wxgui/src/python/fft_window.py
new file mode 100644
index 000000000..5f48e8324
--- /dev/null
+++ b/gr-wxgui/src/python/fft_window.py
@@ -0,0 +1,292 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import plotter
+import common
+import wx
+import numpy
+import math
+import pubsub
+from constants import *
+
+##################################################
+# Constants
+##################################################
+SLIDER_STEPS = 100
+AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
+DEFAULT_WIN_SIZE = (600, 300)
+DEFAULT_FRAME_RATE = 30
+DIV_LEVELS = (1, 2, 5, 10, 20)
+FFT_PLOT_COLOR_SPEC = (0, 0, 1)
+PEAK_VALS_COLOR_SPEC = (0, 1, 0)
+NO_PEAK_VALS = list()
+
+##################################################
+# FFT window control panel
+##################################################
+class control_panel(wx.Panel):
+ """!
+ A control panel with wx widgits to control the plotter and fft block chain.
+ """
+
+ def __init__(self, parent):
+ """!
+ Create a new control panel.
+ @param parent the wx parent window
+ """
+ self.parent = parent
+ wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+ #checkboxes for average and peak hold
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
+ self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key)
+ control_box.Add(self.average_check_box, 0, wx.EXPAND)
+ self.peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY)
+ control_box.Add(self.peak_hold_check_box, 0, wx.EXPAND)
+ control_box.AddSpacer(2)
+ self.avg_alpha_slider = common.LogSliderController(
+ self, 'Avg Alpha',
+ AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
+ parent.ext_controller, parent.avg_alpha_key,
+ formatter=lambda x: ': %.4f'%x,
+ )
+ parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable)
+ control_box.Add(self.avg_alpha_slider, 0, wx.EXPAND)
+ #radio buttons for div size
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Set dB/div'), 0, wx.ALIGN_CENTER)
+ radio_box = wx.BoxSizer(wx.VERTICAL)
+ self.radio_buttons = list()
+ for y_per_div in DIV_LEVELS:
+ radio_button = wx.RadioButton(self, -1, "%d dB/div"%y_per_div)
+ radio_button.Bind(wx.EVT_RADIOBUTTON, self._on_y_per_div)
+ self.radio_buttons.append(radio_button)
+ radio_box.Add(radio_button, 0, wx.ALIGN_LEFT)
+ parent.subscribe(Y_PER_DIV_KEY, self._on_set_y_per_div)
+ control_box.Add(radio_box, 0, wx.EXPAND)
+ #ref lvl buttons
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER)
+ control_box.AddSpacer(2)
+ self._ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level)
+ control_box.Add(self._ref_lvl_buttons, 0, wx.ALIGN_CENTER)
+ #autoscale
+ control_box.AddStretchSpacer()
+ self.autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT)
+ self.autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale)
+ control_box.Add(self.autoscale_button, 0, wx.EXPAND)
+ #run/stop
+ self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
+ control_box.Add(self.run_button, 0, wx.EXPAND)
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+ ##################################################
+ # Event handlers
+ ##################################################
+ def _on_set_y_per_div(self, y_per_div):
+ try:
+ index = list(DIV_LEVELS).index(y_per_div)
+ self.radio_buttons[index].SetValue(True)
+ except: pass
+ def _on_y_per_div(self, event):
+ selected_radio_button = filter(lambda rb: rb.GetValue(), self.radio_buttons)[0]
+ index = self.radio_buttons.index(selected_radio_button)
+ self.parent[Y_PER_DIV_KEY] = DIV_LEVELS[index]
+ def _on_incr_ref_level(self, event):
+ self.parent.set_ref_level(
+ self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY])
+ def _on_decr_ref_level(self, event):
+ self.parent.set_ref_level(
+ self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY])
+
+##################################################
+# FFT window with plotter and control panel
+##################################################
+class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ real,
+ fft_size,
+ baseband_freq,
+ sample_rate_key,
+ y_per_div,
+ y_divs,
+ ref_level,
+ average_key,
+ avg_alpha_key,
+ peak_hold,
+ msg_key,
+ ):
+ pubsub.pubsub.__init__(self)
+ #ensure y_per_div
+ if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0]
+ #setup
+ self.ext_controller = controller
+ self.real = real
+ self.fft_size = fft_size
+ self.sample_rate_key = sample_rate_key
+ self.average_key = average_key
+ self.avg_alpha_key = avg_alpha_key
+ self._reset_peak_vals()
+ #init panel and plot
+ wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.channel_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.set_title(title)
+ self.plotter.enable_point_label(True)
+ #setup the box with plot and controls
+ self.control_panel = control_panel(self)
+ main_box = wx.BoxSizer(wx.HORIZONTAL)
+ main_box.Add(self.plotter, 1, wx.EXPAND)
+ main_box.Add(self.control_panel, 0, wx.EXPAND)
+ self.SetSizerAndFit(main_box)
+ #initial setup
+ self.ext_controller[self.average_key] = self.ext_controller[self.average_key]
+ self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key]
+ self._register_set_prop(self, PEAK_HOLD_KEY, peak_hold)
+ self._register_set_prop(self, Y_PER_DIV_KEY, y_per_div)
+ self._register_set_prop(self, Y_DIVS_KEY, y_divs)
+ self._register_set_prop(self, X_DIVS_KEY, 8) #approximate
+ self._register_set_prop(self, REF_LEVEL_KEY, ref_level)
+ self._register_set_prop(self, BASEBAND_FREQ_KEY, baseband_freq)
+ self._register_set_prop(self, RUNNING_KEY, True)
+ #register events
+ self.subscribe(PEAK_HOLD_KEY, self.plotter.enable_legend)
+ self.ext_controller.subscribe(AVERAGE_KEY, lambda x: self._reset_peak_vals())
+ self.ext_controller.subscribe(msg_key, self.handle_msg)
+ self.ext_controller.subscribe(self.sample_rate_key, self.update_grid)
+ for key in (
+ BASEBAND_FREQ_KEY,
+ Y_PER_DIV_KEY, X_DIVS_KEY,
+ Y_DIVS_KEY, REF_LEVEL_KEY,
+ ): self.subscribe(key, self.update_grid)
+ #initial update
+ self.plotter.enable_legend(self[PEAK_HOLD_KEY])
+ self.update_grid()
+
+ def autoscale(self, *args):
+ """!
+ Autoscale the fft plot to the last frame.
+ Set the dynamic range and reference level.
+ """
+ #get the peak level (max of the samples)
+ peak_level = numpy.max(self.samples)
+ #get the noise floor (averge the smallest samples)
+ noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
+ #padding
+ noise_floor -= abs(noise_floor)*.5
+ peak_level += abs(peak_level)*.1
+ #set the reference level to a multiple of y divs
+ self.set_ref_level(self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY]))
+ #set the range to a clean number of the dynamic range
+ self.set_y_per_div(common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY]))
+
+ def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS
+
+ def handle_msg(self, msg):
+ """!
+ Handle the message from the fft sink message queue.
+ If complex, reorder the fft samples so the negative bins come first.
+ If real, keep take only the positive bins.
+ Plot the samples onto the grid as channel 1.
+ If peak hold is enabled, plot peak vals as channel 2.
+ @param msg the fft array as a character array
+ """
+ if not self[RUNNING_KEY]: return
+ #convert to floating point numbers
+ samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
+ num_samps = len(samples)
+ #reorder fft
+ if self.real: samples = samples[:num_samps/2]
+ else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
+ self.samples = samples
+ #peak hold calculation
+ if self[PEAK_HOLD_KEY]:
+ if len(self.peak_vals) != len(samples): self.peak_vals = samples
+ self.peak_vals = numpy.maximum(samples, self.peak_vals)
+ else: self._reset_peak_vals()
+ #plot the fft
+ self.plotter.set_waveform(
+ channel='FFT',
+ samples=samples,
+ color_spec=FFT_PLOT_COLOR_SPEC,
+ )
+ #plot the peak hold
+ self.plotter.set_waveform(
+ channel='Peak',
+ samples=self.peak_vals,
+ color_spec=PEAK_VALS_COLOR_SPEC,
+ )
+ #update the plotter
+ self.plotter.update()
+
+ def update_grid(self, *args):
+ """!
+ Update the plotter grid.
+ This update method is dependent on the variables below.
+ Determine the x and y axis grid parameters.
+ The x axis depends on sample rate, baseband freq, and x divs.
+ The y axis depends on y per div, y divs, and ref level.
+ """
+ #grid parameters
+ sample_rate = self.ext_controller[self.sample_rate_key]
+ baseband_freq = self[BASEBAND_FREQ_KEY]
+ y_per_div = self[Y_PER_DIV_KEY]
+ y_divs = self[Y_DIVS_KEY]
+ x_divs = self[X_DIVS_KEY]
+ ref_level = self[REF_LEVEL_KEY]
+ #determine best fitting x_per_div
+ if self.real: x_width = sample_rate/2.0
+ else: x_width = sample_rate/1.0
+ x_per_div = common.get_clean_num(x_width/x_divs)
+ coeff, exp, prefix = common.get_si_components(abs(baseband_freq) + abs(sample_rate/2.0))
+ #update the x grid
+ if self.real:
+ self.plotter.set_x_grid(
+ baseband_freq,
+ baseband_freq + sample_rate/2.0,
+ x_per_div,
+ 10**(-exp),
+ )
+ else:
+ self.plotter.set_x_grid(
+ baseband_freq - sample_rate/2.0,
+ baseband_freq + sample_rate/2.0,
+ x_per_div,
+ 10**(-exp),
+ )
+ #update x units
+ self.plotter.set_x_label('Frequency', prefix+'Hz')
+ #update y grid
+ self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
+ #update y units
+ self.plotter.set_y_label('Amplitude', 'dB')
+ #update plotter
+ self.plotter.update()
diff --git a/gr-wxgui/src/python/fftsink2.py b/gr-wxgui/src/python/fftsink2.py
index f4e9143f1..ad2a09c96 100755..100644
--- a/gr-wxgui/src/python/fftsink2.py
+++ b/gr-wxgui/src/python/fftsink2.py
@@ -1,603 +1,45 @@
-#!/usr/bin/env python
#
-# Copyright 2003,2004,2005,2006,2007 Free Software Foundation, Inc.
-#
+# Copyright 2008 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, gru, window
-from gnuradio.wxgui import stdgui2
-import wx
-import plot
-import numpy
-import threading
-import math
-
-DIV_LEVELS = (1, 2, 5, 10, 20)
-
-default_fftsink_size = (640,240)
-default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
-
-class fft_sink_base(object):
- def __init__(self, input_is_real=False, baseband_freq=0, y_per_div=10,
- y_divs=8, ref_level=50,
- sample_rate=1, fft_size=512,
- fft_rate=default_fft_rate,
- average=False, avg_alpha=None, title='', peak_hold=False):
-
- # initialize common attributes
- self.baseband_freq = baseband_freq
- self.y_per_div=y_per_div
- self.y_divs = y_divs
- self.ref_level = ref_level
- self.sample_rate = sample_rate
- self.fft_size = fft_size
- self.fft_rate = fft_rate
- self.average = average
- if avg_alpha is None:
- self.avg_alpha = 2.0 / fft_rate
- else:
- self.avg_alpha = avg_alpha
- self.title = title
- self.peak_hold = peak_hold
- self.input_is_real = input_is_real
- self.msgq = gr.msg_queue(2) # queue that holds a maximum of 2 messages
-
- def set_y_per_div(self, y_per_div):
- self.y_per_div = y_per_div
-
- def set_ref_level(self, ref_level):
- self.ref_level = ref_level
-
- def set_average(self, average):
- self.average = average
- if average:
- self.avg.set_taps(self.avg_alpha)
- else:
- self.avg.set_taps(1.0)
- self.win.peak_vals = None
-
- def set_peak_hold(self, enable):
- self.peak_hold = enable
- self.win.set_peak_hold(enable)
-
- def set_avg_alpha(self, avg_alpha):
- self.avg_alpha = avg_alpha
-
- def set_baseband_freq(self, baseband_freq):
- self.baseband_freq = baseband_freq
-
- def set_sample_rate(self, sample_rate):
- self.sample_rate = sample_rate
- self._set_n()
-
- def _set_n(self):
- self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
-
-
-class fft_sink_f(gr.hier_block2, fft_sink_base):
- def __init__(self, parent, baseband_freq=0, ref_scale=2.0,
- y_per_div=10, y_divs=8, ref_level=50, sample_rate=1, fft_size=512,
- fft_rate=default_fft_rate, average=False, avg_alpha=None,
- title='', size=default_fftsink_size, peak_hold=False):
-
- gr.hier_block2.__init__(self, "fft_sink_f",
- gr.io_signature(1, 1, gr.sizeof_float),
- gr.io_signature(0,0,0))
-
- fft_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
- y_per_div=y_per_div, y_divs=y_divs, ref_level=ref_level,
- sample_rate=sample_rate, fft_size=fft_size,
- fft_rate=fft_rate,
- average=average, avg_alpha=avg_alpha, title=title,
- peak_hold=peak_hold)
-
- self.s2p = gr.stream_to_vector(gr.sizeof_float, self.fft_size)
- self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
- max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
-
- mywindow = window.blackmanharris(self.fft_size)
- self.fft = gr.fft_vfc(self.fft_size, True, mywindow)
- power = 0
- for tap in mywindow:
- power += tap*tap
-
- self.c2mag = gr.complex_to_mag(self.fft_size)
- self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
-
- # FIXME We need to add 3dB to all bins but the DC bin
- self.log = gr.nlog10_ff(20, self.fft_size,
- -10*math.log10(self.fft_size) # Adjust for number of bins
- -10*math.log10(power/self.fft_size) # Adjust for windowing loss
- -20*math.log10(ref_scale/2)) # Adjust for reference scale
-
- self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
- self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
-
- self.win = fft_window(self, parent, size=size)
- self.set_average(self.average)
- self.set_peak_hold(self.peak_hold)
-
-class fft_sink_c(gr.hier_block2, fft_sink_base):
- def __init__(self, parent, baseband_freq=0, ref_scale=2.0,
- y_per_div=10, y_divs=8, ref_level=50, sample_rate=1, fft_size=512,
- fft_rate=default_fft_rate, average=False, avg_alpha=None,
- title='', size=default_fftsink_size, peak_hold=False):
-
- gr.hier_block2.__init__(self, "fft_sink_c",
- gr.io_signature(1, 1, gr.sizeof_gr_complex),
- gr.io_signature(0,0,0))
-
- fft_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
- y_per_div=y_per_div, y_divs=y_divs, ref_level=ref_level,
- sample_rate=sample_rate, fft_size=fft_size,
- fft_rate=fft_rate,
- average=average, avg_alpha=avg_alpha, title=title,
- peak_hold=peak_hold)
-
- self.s2p = gr.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
- self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
- max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
-
- mywindow = window.blackmanharris(self.fft_size)
- self.fft = gr.fft_vcc(self.fft_size, True, mywindow)
- power = 0
- for tap in mywindow:
- power += tap*tap
-
- self.c2mag = gr.complex_to_mag(self.fft_size)
- self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
-
- # FIXME We need to add 3dB to all bins but the DC bin
- self.log = gr.nlog10_ff(20, self.fft_size,
- -10*math.log10(self.fft_size) # Adjust for number of bins
- -10*math.log10(power/self.fft_size) # Adjust for windowing loss
- -20*math.log10(ref_scale/2)) # Adjust for reference scale
-
- self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
- self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
-
- self.win = fft_window(self, parent, size=size)
- self.set_average(self.average)
- self.set_peak_hold(self.peak_hold)
-
-
-# ------------------------------------------------------------------------
-
-myDATA_EVENT = wx.NewEventType()
-EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
-
-
-class DataEvent(wx.PyEvent):
- def __init__(self, data):
- wx.PyEvent.__init__(self)
- self.SetEventType (myDATA_EVENT)
- self.data = data
-
- def Clone (self):
- self.__class__ (self.GetId())
-
-
-class input_watcher (threading.Thread):
- def __init__ (self, msgq, fft_size, event_receiver, **kwds):
- threading.Thread.__init__ (self, **kwds)
- self.setDaemon (1)
- self.msgq = msgq
- self.fft_size = fft_size
- self.event_receiver = event_receiver
- self.keep_running = True
- self.start ()
-
- def run (self):
- while (self.keep_running):
- msg = self.msgq.delete_head() # blocking read of message queue
- itemsize = int(msg.arg1())
- nitems = int(msg.arg2())
-
- s = msg.to_string() # get the body of the msg as a string
-
- # There may be more than one FFT frame in the message.
- # If so, we take only the last one
- if nitems > 1:
- start = itemsize * (nitems - 1)
- s = s[start:start+itemsize]
-
- complex_data = numpy.fromstring (s, numpy.float32)
- de = DataEvent (complex_data)
- wx.PostEvent (self.event_receiver, de)
- del de
-
-class control_panel(wx.Panel):
-
- class LabelText(wx.StaticText):
- def __init__(self, window, label):
- wx.StaticText.__init__(self, window, -1, label)
- font = self.GetFont()
- font.SetWeight(wx.FONTWEIGHT_BOLD)
- font.SetUnderlined(True)
- self.SetFont(font)
-
- def __init__(self, parent):
- self.parent = parent
- wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER)
- control_box = wx.BoxSizer(wx.VERTICAL)
-
- #checkboxes for average and peak hold
- control_box.AddStretchSpacer()
- control_box.Add(self.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
- self.average_check_box = wx.CheckBox(parent=self, style=wx.CHK_2STATE, label="Average")
- self.average_check_box.Bind(wx.EVT_CHECKBOX, parent.on_average)
- control_box.Add(self.average_check_box, 0, wx.EXPAND)
- self.peak_hold_check_box = wx.CheckBox(parent=self, style=wx.CHK_2STATE, label="Peak Hold")
- self.peak_hold_check_box.Bind(wx.EVT_CHECKBOX, parent.on_peak_hold)
- control_box.Add(self.peak_hold_check_box, 0, wx.EXPAND)
-
- #radio buttons for div size
- control_box.AddStretchSpacer()
- control_box.Add(self.LabelText(self, 'Set dB/div'), 0, wx.ALIGN_CENTER)
- radio_box = wx.BoxSizer(wx.VERTICAL)
- self.radio_buttons = list()
- for y_per_div in DIV_LEVELS:
- radio_button = wx.RadioButton(self, -1, "%d dB/div"%y_per_div)
- radio_button.Bind(wx.EVT_RADIOBUTTON, self.on_radio_button_change)
- self.radio_buttons.append(radio_button)
- radio_box.Add(radio_button, 0, wx.ALIGN_LEFT)
- control_box.Add(radio_box, 0, wx.EXPAND)
-
- #ref lvl buttons
- control_box.AddStretchSpacer()
- control_box.Add(self.LabelText(self, 'Adj Ref Lvl'), 0, wx.ALIGN_CENTER)
- control_box.AddSpacer(2)
- button_box = wx.BoxSizer(wx.HORIZONTAL)
- self.ref_plus_button = wx.Button(self, -1, '+', style=wx.BU_EXACTFIT)
- self.ref_plus_button.Bind(wx.EVT_BUTTON, parent.on_incr_ref_level)
- button_box.Add(self.ref_plus_button, 0, wx.ALIGN_CENTER)
- self.ref_minus_button = wx.Button(self, -1, ' - ', style=wx.BU_EXACTFIT)
- self.ref_minus_button.Bind(wx.EVT_BUTTON, parent.on_decr_ref_level)
- button_box.Add(self.ref_minus_button, 0, wx.ALIGN_CENTER)
- control_box.Add(button_box, 0, wx.ALIGN_CENTER)
- control_box.AddStretchSpacer()
- #set sizer
- self.SetSizerAndFit(control_box)
- #update
- self.update()
-
- def update(self):
- """!
- Read the state of the fft plot settings and update the control panel.
- """
- #update checkboxes
- self.average_check_box.SetValue(self.parent.fftsink.average)
- self.peak_hold_check_box.SetValue(self.parent.fftsink.peak_hold)
- #update radio buttons
- try:
- index = list(DIV_LEVELS).index(self.parent.fftsink.y_per_div)
- self.radio_buttons[index].SetValue(True)
- except: pass
-
- def on_radio_button_change(self, evt):
- selected_radio_button = filter(lambda rb: rb.GetValue(), self.radio_buttons)[0]
- index = self.radio_buttons.index(selected_radio_button)
- self.parent.fftsink.set_y_per_div(DIV_LEVELS[index])
-
-class fft_window (wx.Panel):
- def __init__ (self, fftsink, parent, id = -1,
- pos = wx.DefaultPosition, size = wx.DefaultSize,
- style = wx.DEFAULT_FRAME_STYLE, name = ""):
-
- self.fftsink = fftsink
- #init panel and plot
- wx.Panel.__init__(self, parent, -1)
- self.plot = plot.PlotCanvas(self, id, pos, size, style, name)
- #setup the box with plot and controls
- self.control_panel = control_panel(self)
- main_box = wx.BoxSizer (wx.HORIZONTAL)
- main_box.Add (self.plot, 1, wx.EXPAND)
- main_box.Add (self.control_panel, 0, wx.EXPAND)
- self.SetSizerAndFit(main_box)
-
- self.peak_hold = False
- self.peak_vals = None
-
- self.plot.SetEnableGrid (True)
- # self.SetEnableZoom (True)
- # self.SetBackgroundColour ('black')
-
- self.build_popup_menu()
- self.set_baseband_freq(0.0)
-
- EVT_DATA_EVENT (self, self.set_data)
- wx.EVT_CLOSE (self, self.on_close_window)
- self.plot.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
- self.plot.Bind(wx.EVT_MOTION, self.evt_motion)
-
- self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
-
- def set_scale(self, freq):
- x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
- if x >= 1e9:
- self._scale_factor = 1e-9
- self._units = "GHz"
- self._format = "%3.6f"
- elif x >= 1e6:
- self._scale_factor = 1e-6
- self._units = "MHz"
- self._format = "%3.3f"
- else:
- self._scale_factor = 1e-3
- self._units = "kHz"
- self._format = "%3.3f"
-
- def set_baseband_freq(self, baseband_freq):
- if self.peak_hold:
- self.peak_vals = None
- self.set_scale(baseband_freq)
- self.fftsink.set_baseband_freq(baseband_freq)
-
- def on_close_window (self, event):
- print "fft_window:on_close_window"
- self.keep_running = False
-
-
- def set_data (self, evt):
- dB = evt.data
- L = len (dB)
-
- if self.peak_hold:
- if self.peak_vals is None:
- self.peak_vals = dB
- else:
- self.peak_vals = numpy.maximum(dB, self.peak_vals)
-
- if self.fftsink.input_is_real: # only plot 1/2 the points
- x_vals = ((numpy.arange (L/2) * (self.fftsink.sample_rate
- * self._scale_factor / L))
- + self.fftsink.baseband_freq * self._scale_factor)
- self._points = numpy.zeros((len(x_vals), 2), numpy.float64)
- self._points[:,0] = x_vals
- self._points[:,1] = dB[0:L/2]
- if self.peak_hold:
- self._peak_points = numpy.zeros((len(x_vals), 2), numpy.float64)
- self._peak_points[:,0] = x_vals
- self._peak_points[:,1] = self.peak_vals[0:L/2]
- else:
- # the "negative freqs" are in the second half of the array
- x_vals = ((numpy.arange (-L/2, L/2)
- * (self.fftsink.sample_rate * self._scale_factor / L))
- + self.fftsink.baseband_freq * self._scale_factor)
- self._points = numpy.zeros((len(x_vals), 2), numpy.float64)
- self._points[:,0] = x_vals
- self._points[:,1] = numpy.concatenate ((dB[L/2:], dB[0:L/2]))
- if self.peak_hold:
- self._peak_points = numpy.zeros((len(x_vals), 2), numpy.float64)
- self._peak_points[:,0] = x_vals
- self._peak_points[:,1] = numpy.concatenate ((self.peak_vals[L/2:], self.peak_vals[0:L/2]))
-
- lines = [plot.PolyLine (self._points, colour='BLUE'),]
- if self.peak_hold:
- lines.append(plot.PolyLine (self._peak_points, colour='GREEN'))
-
- graphics = plot.PlotGraphics (lines,
- title=self.fftsink.title,
- xLabel = self._units, yLabel = "dB")
- x_range = x_vals[0], x_vals[-1]
- ymax = self.fftsink.ref_level
- ymin = self.fftsink.ref_level - self.fftsink.y_per_div * self.fftsink.y_divs
- y_range = ymin, ymax
- self.plot.Draw (graphics, xAxis=x_range, yAxis=y_range, step=self.fftsink.y_per_div)
-
- def set_peak_hold(self, enable):
- self.peak_hold = enable
- self.peak_vals = None
-
- def on_average(self, evt):
- # print "on_average"
- self.fftsink.set_average(evt.IsChecked())
- self.control_panel.update()
-
- def on_peak_hold(self, evt):
- # print "on_peak_hold"
- self.fftsink.set_peak_hold(evt.IsChecked())
- self.control_panel.update()
-
- def on_incr_ref_level(self, evt):
- # print "on_incr_ref_level"
- self.fftsink.set_ref_level(self.fftsink.ref_level
- + self.fftsink.y_per_div)
-
- def on_decr_ref_level(self, evt):
- # print "on_decr_ref_level"
- self.fftsink.set_ref_level(self.fftsink.ref_level
- - self.fftsink.y_per_div)
-
- def on_incr_y_per_div(self, evt):
- # print "on_incr_y_per_div"
- self.fftsink.set_y_per_div(next_up(self.fftsink.y_per_div, DIV_LEVELS))
- self.control_panel.update()
-
- def on_decr_y_per_div(self, evt):
- # print "on_decr_y_per_div"
- self.fftsink.set_y_per_div(next_down(self.fftsink.y_per_div, DIV_LEVELS))
- self.control_panel.update()
-
- def on_y_per_div(self, evt):
- # print "on_y_per_div"
- Id = evt.GetId()
- if Id == self.id_y_per_div_1:
- self.fftsink.set_y_per_div(1)
- elif Id == self.id_y_per_div_2:
- self.fftsink.set_y_per_div(2)
- elif Id == self.id_y_per_div_5:
- self.fftsink.set_y_per_div(5)
- elif Id == self.id_y_per_div_10:
- self.fftsink.set_y_per_div(10)
- elif Id == self.id_y_per_div_20:
- self.fftsink.set_y_per_div(20)
- self.control_panel.update()
-
- def on_right_click(self, event):
- menu = self.popup_menu
- for id, pred in self.checkmarks.items():
- item = menu.FindItemById(id)
- item.Check(pred())
- self.plot.PopupMenu(menu, event.GetPosition())
-
- def evt_motion(self, event):
- if not hasattr(self, "_points"):
- return # Got here before first window data update
-
- # Clip to plotted values
- (ux, uy) = self.plot.GetXY(event) # Scaled position
- x_vals = numpy.array(self._points[:,0])
- if ux < x_vals[0] or ux > x_vals[-1]:
- tip = self.GetToolTip()
- if tip:
- tip.Enable(False)
- return
-
- # Get nearest X value (is there a better way)?
- ind = numpy.argmin(numpy.abs(x_vals-ux))
- x_val = x_vals[ind]
- db_val = self._points[ind, 1]
- text = (self._format+" %s dB=%3.3f") % (x_val, self._units, db_val)
-
- # Display the tooltip
- tip = wx.ToolTip(text)
- tip.Enable(True)
- tip.SetDelay(0)
- self.SetToolTip(tip)
-
- def build_popup_menu(self):
- self.id_incr_ref_level = wx.NewId()
- self.id_decr_ref_level = wx.NewId()
- self.id_incr_y_per_div = wx.NewId()
- self.id_decr_y_per_div = wx.NewId()
- self.id_y_per_div_1 = wx.NewId()
- self.id_y_per_div_2 = wx.NewId()
- self.id_y_per_div_5 = wx.NewId()
- self.id_y_per_div_10 = wx.NewId()
- self.id_y_per_div_20 = wx.NewId()
- self.id_average = wx.NewId()
- self.id_peak_hold = wx.NewId()
-
- self.plot.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
- self.plot.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold)
- self.plot.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
- self.plot.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
- self.plot.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
- self.plot.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
- self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
- self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
- self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
- self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
- self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
-
- # make a menu
- menu = wx.Menu()
- self.popup_menu = menu
- menu.AppendCheckItem(self.id_average, "Average")
- menu.AppendCheckItem(self.id_peak_hold, "Peak Hold")
- menu.Append(self.id_incr_ref_level, "Incr Ref Level")
- menu.Append(self.id_decr_ref_level, "Decr Ref Level")
- # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
- # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
- menu.AppendSeparator()
- # we'd use RadioItems for these, but they're not supported on Mac
- menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
- menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
- menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
- menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
- menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
-
- self.checkmarks = {
- self.id_average : lambda : self.fftsink.average,
- self.id_peak_hold : lambda : self.fftsink.peak_hold,
- self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
- self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
- self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
- self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
- self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
- }
-
-
-def next_up(v, seq):
- """
- Return the first item in seq that is > v.
- """
- for s in seq:
- if s > v:
- return s
- return v
-
-def next_down(v, seq):
- """
- Return the last item in seq that is < v.
- """
- rseq = list(seq[:])
- rseq.reverse()
-
- for s in rseq:
- if s < v:
- return s
- return v
-
-
-# ----------------------------------------------------------------
-# Standalone test app
-# ----------------------------------------------------------------
-
-class test_app_block (stdgui2.std_top_block):
- def __init__(self, frame, panel, vbox, argv):
- stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
-
- fft_size = 256
-
- # build our flow graph
- input_rate = 20.48e3
-
- # Generate a complex sinusoid
- #src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
- src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
-
- # We add these throttle blocks so that this demo doesn't
- # suck down all the CPU available. Normally you wouldn't use these.
- thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
+#
- sink1 = fft_sink_c (panel, title="Complex Data", fft_size=fft_size,
- sample_rate=input_rate, baseband_freq=100e3,
- ref_level=0, y_per_div=20, y_divs=10)
- vbox.Add (sink1.win, 1, wx.EXPAND)
+from gnuradio import gr
- self.connect(src1, thr1, sink1)
+p = gr.prefs()
+style = p.get_string('wxgui', 'style', 'auto')
- #src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
- src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
- thr2 = gr.throttle(gr.sizeof_float, input_rate)
- sink2 = fft_sink_f (panel, title="Real Data", fft_size=fft_size*2,
- sample_rate=input_rate, baseband_freq=100e3,
- ref_level=0, y_per_div=20, y_divs=10)
- vbox.Add (sink2.win, 1, wx.EXPAND)
+# In 3.2 we'll change 'auto' to mean 'gl' if possible, then fallback
+if style == 'auto':
+ style = 'nongl'
- self.connect(src2, thr2, sink2)
+if style == 'nongl':
+ from fftsink_nongl import fft_sink_f, fft_sink_c
+elif style == 'gl':
+ try:
+ import wx
+ wx.glcanvas.GLCanvas
+ except AttributeError:
+ raise RuntimeError("wxPython doesn't support glcanvas")
-def main ():
- app = stdgui2.stdapp (test_app_block, "FFT Sink Test App")
- app.MainLoop ()
+ try:
+ from OpenGL.GL import *
+ except ImportError:
+ raise RuntimeError("Unable to import OpenGL. Are Python wrappers for OpenGL installed?")
-if __name__ == '__main__':
- main ()
+ from fftsink_gl import fft_sink_f, fft_sink_c
diff --git a/gr-wxgui/src/python/fftsink_gl.py b/gr-wxgui/src/python/fftsink_gl.py
new file mode 100644
index 000000000..db402618e
--- /dev/null
+++ b/gr-wxgui/src/python/fftsink_gl.py
@@ -0,0 +1,172 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import fft_window
+import common
+from gnuradio import gr, blks2
+from pubsub import pubsub
+from constants import *
+
+##################################################
+# FFT sink block (wrapper for old wxgui)
+##################################################
+class _fft_sink_base(gr.hier_block2, common.prop_setter):
+ """!
+ An fft block with real/complex inputs and a gui window.
+ """
+
+ def __init__(
+ self,
+ parent,
+ baseband_freq=0,
+ ref_scale=2.0,
+ y_per_div=10,
+ y_divs=8,
+ ref_level=50,
+ sample_rate=1,
+ fft_size=512,
+ fft_rate=gr.prefs().get_long('wxgui', 'fft_rate', fft_window.DEFAULT_FRAME_RATE),
+ average=False,
+ avg_alpha=None,
+ title='',
+ size=fft_window.DEFAULT_WIN_SIZE,
+ peak_hold=False,
+ ):
+ #ensure avg alpha
+ if avg_alpha is None: avg_alpha = 2.0/fft_rate
+ #init
+ gr.hier_block2.__init__(
+ self,
+ "fft_sink",
+ gr.io_signature(1, 1, self._item_size),
+ gr.io_signature(0, 0, 0),
+ )
+ #blocks
+ copy = gr.kludge_copy(self._item_size)
+ fft = self._fft_chain(
+ sample_rate=sample_rate,
+ fft_size=fft_size,
+ frame_rate=fft_rate,
+ ref_scale=ref_scale,
+ avg_alpha=avg_alpha,
+ average=average,
+ )
+ msgq = gr.msg_queue(2)
+ sink = gr.message_sink(gr.sizeof_float*fft_size, msgq, True)
+ #connect
+ self.connect(self, copy, fft, sink)
+ #controller
+ self.controller = pubsub()
+ self.controller.subscribe(AVERAGE_KEY, fft.set_average)
+ self.controller.publish(AVERAGE_KEY, fft.average)
+ self.controller.subscribe(AVG_ALPHA_KEY, fft.set_avg_alpha)
+ self.controller.publish(AVG_ALPHA_KEY, fft.avg_alpha)
+ self.controller.subscribe(SAMPLE_RATE_KEY, fft.set_sample_rate)
+ self.controller.publish(SAMPLE_RATE_KEY, fft.sample_rate)
+ #start input watcher
+ def setter(p, k, x): # lambdas can't have assignments :(
+ p[k] = x
+ common.input_watcher(msgq, lambda x: setter(self.controller, MSG_KEY, x))
+ #create window
+ self.win = fft_window.fft_window(
+ parent=parent,
+ controller=self.controller,
+ size=size,
+ title=title,
+ real=self._real,
+ fft_size=fft_size,
+ baseband_freq=baseband_freq,
+ sample_rate_key=SAMPLE_RATE_KEY,
+ y_per_div=y_per_div,
+ y_divs=y_divs,
+ ref_level=ref_level,
+ average_key=AVERAGE_KEY,
+ avg_alpha_key=AVG_ALPHA_KEY,
+ peak_hold=peak_hold,
+ msg_key=MSG_KEY,
+ )
+ #register callbacks from window for external use
+ for attr in filter(lambda a: a.startswith('set_'), dir(self.win)):
+ setattr(self, attr, getattr(self.win, attr))
+ self._register_set_prop(self.controller, SAMPLE_RATE_KEY)
+ self._register_set_prop(self.controller, AVERAGE_KEY)
+ self._register_set_prop(self.controller, AVG_ALPHA_KEY)
+
+class fft_sink_f(_fft_sink_base):
+ _fft_chain = blks2.logpwrfft_f
+ _item_size = gr.sizeof_float
+ _real = True
+
+class fft_sink_c(_fft_sink_base):
+ _fft_chain = blks2.logpwrfft_c
+ _item_size = gr.sizeof_gr_complex
+ _real = False
+
+# ----------------------------------------------------------------
+# Standalone test app
+# ----------------------------------------------------------------
+
+import wx
+from gnuradio.wxgui import stdgui2
+
+class test_app_block (stdgui2.std_top_block):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
+
+ fft_size = 256
+
+ # build our flow graph
+ input_rate = 20.48e3
+
+ # Generate a complex sinusoid
+ #src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
+ src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
+
+ # We add these throttle blocks so that this demo doesn't
+ # suck down all the CPU available. Normally you wouldn't use these.
+ thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ sink1 = fft_sink_c (panel, title="Complex Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3,
+ ref_level=0, y_per_div=20, y_divs=10)
+ vbox.Add (sink1.win, 1, wx.EXPAND)
+
+ self.connect(src1, thr1, sink1)
+
+ #src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
+ src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
+ thr2 = gr.throttle(gr.sizeof_float, input_rate)
+ sink2 = fft_sink_f (panel, title="Real Data", fft_size=fft_size*2,
+ sample_rate=input_rate, baseband_freq=100e3,
+ ref_level=0, y_per_div=20, y_divs=10)
+ vbox.Add (sink2.win, 1, wx.EXPAND)
+
+ self.connect(src2, thr2, sink2)
+
+def main ():
+ app = stdgui2.stdapp (test_app_block, "FFT Sink Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
diff --git a/gr-wxgui/src/python/fftsink_nongl.py b/gr-wxgui/src/python/fftsink_nongl.py
new file mode 100644
index 000000000..f4e9143f1
--- /dev/null
+++ b/gr-wxgui/src/python/fftsink_nongl.py
@@ -0,0 +1,603 @@
+#!/usr/bin/env python
+#
+# Copyright 2003,2004,2005,2006,2007 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, gru, window
+from gnuradio.wxgui import stdgui2
+import wx
+import plot
+import numpy
+import threading
+import math
+
+DIV_LEVELS = (1, 2, 5, 10, 20)
+
+default_fftsink_size = (640,240)
+default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
+
+class fft_sink_base(object):
+ def __init__(self, input_is_real=False, baseband_freq=0, y_per_div=10,
+ y_divs=8, ref_level=50,
+ sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate,
+ average=False, avg_alpha=None, title='', peak_hold=False):
+
+ # initialize common attributes
+ self.baseband_freq = baseband_freq
+ self.y_per_div=y_per_div
+ self.y_divs = y_divs
+ self.ref_level = ref_level
+ self.sample_rate = sample_rate
+ self.fft_size = fft_size
+ self.fft_rate = fft_rate
+ self.average = average
+ if avg_alpha is None:
+ self.avg_alpha = 2.0 / fft_rate
+ else:
+ self.avg_alpha = avg_alpha
+ self.title = title
+ self.peak_hold = peak_hold
+ self.input_is_real = input_is_real
+ self.msgq = gr.msg_queue(2) # queue that holds a maximum of 2 messages
+
+ def set_y_per_div(self, y_per_div):
+ self.y_per_div = y_per_div
+
+ def set_ref_level(self, ref_level):
+ self.ref_level = ref_level
+
+ def set_average(self, average):
+ self.average = average
+ if average:
+ self.avg.set_taps(self.avg_alpha)
+ else:
+ self.avg.set_taps(1.0)
+ self.win.peak_vals = None
+
+ def set_peak_hold(self, enable):
+ self.peak_hold = enable
+ self.win.set_peak_hold(enable)
+
+ def set_avg_alpha(self, avg_alpha):
+ self.avg_alpha = avg_alpha
+
+ def set_baseband_freq(self, baseband_freq):
+ self.baseband_freq = baseband_freq
+
+ def set_sample_rate(self, sample_rate):
+ self.sample_rate = sample_rate
+ self._set_n()
+
+ def _set_n(self):
+ self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+
+class fft_sink_f(gr.hier_block2, fft_sink_base):
+ def __init__(self, parent, baseband_freq=0, ref_scale=2.0,
+ y_per_div=10, y_divs=8, ref_level=50, sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate, average=False, avg_alpha=None,
+ title='', size=default_fftsink_size, peak_hold=False):
+
+ gr.hier_block2.__init__(self, "fft_sink_f",
+ gr.io_signature(1, 1, gr.sizeof_float),
+ gr.io_signature(0,0,0))
+
+ fft_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
+ y_per_div=y_per_div, y_divs=y_divs, ref_level=ref_level,
+ sample_rate=sample_rate, fft_size=fft_size,
+ fft_rate=fft_rate,
+ average=average, avg_alpha=avg_alpha, title=title,
+ peak_hold=peak_hold)
+
+ self.s2p = gr.stream_to_vector(gr.sizeof_float, self.fft_size)
+ self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
+ max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+ mywindow = window.blackmanharris(self.fft_size)
+ self.fft = gr.fft_vfc(self.fft_size, True, mywindow)
+ power = 0
+ for tap in mywindow:
+ power += tap*tap
+
+ self.c2mag = gr.complex_to_mag(self.fft_size)
+ self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
+
+ # FIXME We need to add 3dB to all bins but the DC bin
+ self.log = gr.nlog10_ff(20, self.fft_size,
+ -10*math.log10(self.fft_size) # Adjust for number of bins
+ -10*math.log10(power/self.fft_size) # Adjust for windowing loss
+ -20*math.log10(ref_scale/2)) # Adjust for reference scale
+
+ self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
+ self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
+
+ self.win = fft_window(self, parent, size=size)
+ self.set_average(self.average)
+ self.set_peak_hold(self.peak_hold)
+
+class fft_sink_c(gr.hier_block2, fft_sink_base):
+ def __init__(self, parent, baseband_freq=0, ref_scale=2.0,
+ y_per_div=10, y_divs=8, ref_level=50, sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate, average=False, avg_alpha=None,
+ title='', size=default_fftsink_size, peak_hold=False):
+
+ gr.hier_block2.__init__(self, "fft_sink_c",
+ gr.io_signature(1, 1, gr.sizeof_gr_complex),
+ gr.io_signature(0,0,0))
+
+ fft_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
+ y_per_div=y_per_div, y_divs=y_divs, ref_level=ref_level,
+ sample_rate=sample_rate, fft_size=fft_size,
+ fft_rate=fft_rate,
+ average=average, avg_alpha=avg_alpha, title=title,
+ peak_hold=peak_hold)
+
+ self.s2p = gr.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
+ self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
+ max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+ mywindow = window.blackmanharris(self.fft_size)
+ self.fft = gr.fft_vcc(self.fft_size, True, mywindow)
+ power = 0
+ for tap in mywindow:
+ power += tap*tap
+
+ self.c2mag = gr.complex_to_mag(self.fft_size)
+ self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
+
+ # FIXME We need to add 3dB to all bins but the DC bin
+ self.log = gr.nlog10_ff(20, self.fft_size,
+ -10*math.log10(self.fft_size) # Adjust for number of bins
+ -10*math.log10(power/self.fft_size) # Adjust for windowing loss
+ -20*math.log10(ref_scale/2)) # Adjust for reference scale
+
+ self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
+ self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
+
+ self.win = fft_window(self, parent, size=size)
+ self.set_average(self.average)
+ self.set_peak_hold(self.peak_hold)
+
+
+# ------------------------------------------------------------------------
+
+myDATA_EVENT = wx.NewEventType()
+EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
+
+
+class DataEvent(wx.PyEvent):
+ def __init__(self, data):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (myDATA_EVENT)
+ self.data = data
+
+ def Clone (self):
+ self.__class__ (self.GetId())
+
+
+class input_watcher (threading.Thread):
+ def __init__ (self, msgq, fft_size, event_receiver, **kwds):
+ threading.Thread.__init__ (self, **kwds)
+ self.setDaemon (1)
+ self.msgq = msgq
+ self.fft_size = fft_size
+ self.event_receiver = event_receiver
+ self.keep_running = True
+ self.start ()
+
+ def run (self):
+ while (self.keep_running):
+ msg = self.msgq.delete_head() # blocking read of message queue
+ itemsize = int(msg.arg1())
+ nitems = int(msg.arg2())
+
+ s = msg.to_string() # get the body of the msg as a string
+
+ # There may be more than one FFT frame in the message.
+ # If so, we take only the last one
+ if nitems > 1:
+ start = itemsize * (nitems - 1)
+ s = s[start:start+itemsize]
+
+ complex_data = numpy.fromstring (s, numpy.float32)
+ de = DataEvent (complex_data)
+ wx.PostEvent (self.event_receiver, de)
+ del de
+
+class control_panel(wx.Panel):
+
+ class LabelText(wx.StaticText):
+ def __init__(self, window, label):
+ wx.StaticText.__init__(self, window, -1, label)
+ font = self.GetFont()
+ font.SetWeight(wx.FONTWEIGHT_BOLD)
+ font.SetUnderlined(True)
+ self.SetFont(font)
+
+ def __init__(self, parent):
+ self.parent = parent
+ wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+
+ #checkboxes for average and peak hold
+ control_box.AddStretchSpacer()
+ control_box.Add(self.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
+ self.average_check_box = wx.CheckBox(parent=self, style=wx.CHK_2STATE, label="Average")
+ self.average_check_box.Bind(wx.EVT_CHECKBOX, parent.on_average)
+ control_box.Add(self.average_check_box, 0, wx.EXPAND)
+ self.peak_hold_check_box = wx.CheckBox(parent=self, style=wx.CHK_2STATE, label="Peak Hold")
+ self.peak_hold_check_box.Bind(wx.EVT_CHECKBOX, parent.on_peak_hold)
+ control_box.Add(self.peak_hold_check_box, 0, wx.EXPAND)
+
+ #radio buttons for div size
+ control_box.AddStretchSpacer()
+ control_box.Add(self.LabelText(self, 'Set dB/div'), 0, wx.ALIGN_CENTER)
+ radio_box = wx.BoxSizer(wx.VERTICAL)
+ self.radio_buttons = list()
+ for y_per_div in DIV_LEVELS:
+ radio_button = wx.RadioButton(self, -1, "%d dB/div"%y_per_div)
+ radio_button.Bind(wx.EVT_RADIOBUTTON, self.on_radio_button_change)
+ self.radio_buttons.append(radio_button)
+ radio_box.Add(radio_button, 0, wx.ALIGN_LEFT)
+ control_box.Add(radio_box, 0, wx.EXPAND)
+
+ #ref lvl buttons
+ control_box.AddStretchSpacer()
+ control_box.Add(self.LabelText(self, 'Adj Ref Lvl'), 0, wx.ALIGN_CENTER)
+ control_box.AddSpacer(2)
+ button_box = wx.BoxSizer(wx.HORIZONTAL)
+ self.ref_plus_button = wx.Button(self, -1, '+', style=wx.BU_EXACTFIT)
+ self.ref_plus_button.Bind(wx.EVT_BUTTON, parent.on_incr_ref_level)
+ button_box.Add(self.ref_plus_button, 0, wx.ALIGN_CENTER)
+ self.ref_minus_button = wx.Button(self, -1, ' - ', style=wx.BU_EXACTFIT)
+ self.ref_minus_button.Bind(wx.EVT_BUTTON, parent.on_decr_ref_level)
+ button_box.Add(self.ref_minus_button, 0, wx.ALIGN_CENTER)
+ control_box.Add(button_box, 0, wx.ALIGN_CENTER)
+ control_box.AddStretchSpacer()
+ #set sizer
+ self.SetSizerAndFit(control_box)
+ #update
+ self.update()
+
+ def update(self):
+ """!
+ Read the state of the fft plot settings and update the control panel.
+ """
+ #update checkboxes
+ self.average_check_box.SetValue(self.parent.fftsink.average)
+ self.peak_hold_check_box.SetValue(self.parent.fftsink.peak_hold)
+ #update radio buttons
+ try:
+ index = list(DIV_LEVELS).index(self.parent.fftsink.y_per_div)
+ self.radio_buttons[index].SetValue(True)
+ except: pass
+
+ def on_radio_button_change(self, evt):
+ selected_radio_button = filter(lambda rb: rb.GetValue(), self.radio_buttons)[0]
+ index = self.radio_buttons.index(selected_radio_button)
+ self.parent.fftsink.set_y_per_div(DIV_LEVELS[index])
+
+class fft_window (wx.Panel):
+ def __init__ (self, fftsink, parent, id = -1,
+ pos = wx.DefaultPosition, size = wx.DefaultSize,
+ style = wx.DEFAULT_FRAME_STYLE, name = ""):
+
+ self.fftsink = fftsink
+ #init panel and plot
+ wx.Panel.__init__(self, parent, -1)
+ self.plot = plot.PlotCanvas(self, id, pos, size, style, name)
+ #setup the box with plot and controls
+ self.control_panel = control_panel(self)
+ main_box = wx.BoxSizer (wx.HORIZONTAL)
+ main_box.Add (self.plot, 1, wx.EXPAND)
+ main_box.Add (self.control_panel, 0, wx.EXPAND)
+ self.SetSizerAndFit(main_box)
+
+ self.peak_hold = False
+ self.peak_vals = None
+
+ self.plot.SetEnableGrid (True)
+ # self.SetEnableZoom (True)
+ # self.SetBackgroundColour ('black')
+
+ self.build_popup_menu()
+ self.set_baseband_freq(0.0)
+
+ EVT_DATA_EVENT (self, self.set_data)
+ wx.EVT_CLOSE (self, self.on_close_window)
+ self.plot.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
+ self.plot.Bind(wx.EVT_MOTION, self.evt_motion)
+
+ self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
+
+ def set_scale(self, freq):
+ x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
+ if x >= 1e9:
+ self._scale_factor = 1e-9
+ self._units = "GHz"
+ self._format = "%3.6f"
+ elif x >= 1e6:
+ self._scale_factor = 1e-6
+ self._units = "MHz"
+ self._format = "%3.3f"
+ else:
+ self._scale_factor = 1e-3
+ self._units = "kHz"
+ self._format = "%3.3f"
+
+ def set_baseband_freq(self, baseband_freq):
+ if self.peak_hold:
+ self.peak_vals = None
+ self.set_scale(baseband_freq)
+ self.fftsink.set_baseband_freq(baseband_freq)
+
+ def on_close_window (self, event):
+ print "fft_window:on_close_window"
+ self.keep_running = False
+
+
+ def set_data (self, evt):
+ dB = evt.data
+ L = len (dB)
+
+ if self.peak_hold:
+ if self.peak_vals is None:
+ self.peak_vals = dB
+ else:
+ self.peak_vals = numpy.maximum(dB, self.peak_vals)
+
+ if self.fftsink.input_is_real: # only plot 1/2 the points
+ x_vals = ((numpy.arange (L/2) * (self.fftsink.sample_rate
+ * self._scale_factor / L))
+ + self.fftsink.baseband_freq * self._scale_factor)
+ self._points = numpy.zeros((len(x_vals), 2), numpy.float64)
+ self._points[:,0] = x_vals
+ self._points[:,1] = dB[0:L/2]
+ if self.peak_hold:
+ self._peak_points = numpy.zeros((len(x_vals), 2), numpy.float64)
+ self._peak_points[:,0] = x_vals
+ self._peak_points[:,1] = self.peak_vals[0:L/2]
+ else:
+ # the "negative freqs" are in the second half of the array
+ x_vals = ((numpy.arange (-L/2, L/2)
+ * (self.fftsink.sample_rate * self._scale_factor / L))
+ + self.fftsink.baseband_freq * self._scale_factor)
+ self._points = numpy.zeros((len(x_vals), 2), numpy.float64)
+ self._points[:,0] = x_vals
+ self._points[:,1] = numpy.concatenate ((dB[L/2:], dB[0:L/2]))
+ if self.peak_hold:
+ self._peak_points = numpy.zeros((len(x_vals), 2), numpy.float64)
+ self._peak_points[:,0] = x_vals
+ self._peak_points[:,1] = numpy.concatenate ((self.peak_vals[L/2:], self.peak_vals[0:L/2]))
+
+ lines = [plot.PolyLine (self._points, colour='BLUE'),]
+ if self.peak_hold:
+ lines.append(plot.PolyLine (self._peak_points, colour='GREEN'))
+
+ graphics = plot.PlotGraphics (lines,
+ title=self.fftsink.title,
+ xLabel = self._units, yLabel = "dB")
+ x_range = x_vals[0], x_vals[-1]
+ ymax = self.fftsink.ref_level
+ ymin = self.fftsink.ref_level - self.fftsink.y_per_div * self.fftsink.y_divs
+ y_range = ymin, ymax
+ self.plot.Draw (graphics, xAxis=x_range, yAxis=y_range, step=self.fftsink.y_per_div)
+
+ def set_peak_hold(self, enable):
+ self.peak_hold = enable
+ self.peak_vals = None
+
+ def on_average(self, evt):
+ # print "on_average"
+ self.fftsink.set_average(evt.IsChecked())
+ self.control_panel.update()
+
+ def on_peak_hold(self, evt):
+ # print "on_peak_hold"
+ self.fftsink.set_peak_hold(evt.IsChecked())
+ self.control_panel.update()
+
+ def on_incr_ref_level(self, evt):
+ # print "on_incr_ref_level"
+ self.fftsink.set_ref_level(self.fftsink.ref_level
+ + self.fftsink.y_per_div)
+
+ def on_decr_ref_level(self, evt):
+ # print "on_decr_ref_level"
+ self.fftsink.set_ref_level(self.fftsink.ref_level
+ - self.fftsink.y_per_div)
+
+ def on_incr_y_per_div(self, evt):
+ # print "on_incr_y_per_div"
+ self.fftsink.set_y_per_div(next_up(self.fftsink.y_per_div, DIV_LEVELS))
+ self.control_panel.update()
+
+ def on_decr_y_per_div(self, evt):
+ # print "on_decr_y_per_div"
+ self.fftsink.set_y_per_div(next_down(self.fftsink.y_per_div, DIV_LEVELS))
+ self.control_panel.update()
+
+ def on_y_per_div(self, evt):
+ # print "on_y_per_div"
+ Id = evt.GetId()
+ if Id == self.id_y_per_div_1:
+ self.fftsink.set_y_per_div(1)
+ elif Id == self.id_y_per_div_2:
+ self.fftsink.set_y_per_div(2)
+ elif Id == self.id_y_per_div_5:
+ self.fftsink.set_y_per_div(5)
+ elif Id == self.id_y_per_div_10:
+ self.fftsink.set_y_per_div(10)
+ elif Id == self.id_y_per_div_20:
+ self.fftsink.set_y_per_div(20)
+ self.control_panel.update()
+
+ def on_right_click(self, event):
+ menu = self.popup_menu
+ for id, pred in self.checkmarks.items():
+ item = menu.FindItemById(id)
+ item.Check(pred())
+ self.plot.PopupMenu(menu, event.GetPosition())
+
+ def evt_motion(self, event):
+ if not hasattr(self, "_points"):
+ return # Got here before first window data update
+
+ # Clip to plotted values
+ (ux, uy) = self.plot.GetXY(event) # Scaled position
+ x_vals = numpy.array(self._points[:,0])
+ if ux < x_vals[0] or ux > x_vals[-1]:
+ tip = self.GetToolTip()
+ if tip:
+ tip.Enable(False)
+ return
+
+ # Get nearest X value (is there a better way)?
+ ind = numpy.argmin(numpy.abs(x_vals-ux))
+ x_val = x_vals[ind]
+ db_val = self._points[ind, 1]
+ text = (self._format+" %s dB=%3.3f") % (x_val, self._units, db_val)
+
+ # Display the tooltip
+ tip = wx.ToolTip(text)
+ tip.Enable(True)
+ tip.SetDelay(0)
+ self.SetToolTip(tip)
+
+ def build_popup_menu(self):
+ self.id_incr_ref_level = wx.NewId()
+ self.id_decr_ref_level = wx.NewId()
+ self.id_incr_y_per_div = wx.NewId()
+ self.id_decr_y_per_div = wx.NewId()
+ self.id_y_per_div_1 = wx.NewId()
+ self.id_y_per_div_2 = wx.NewId()
+ self.id_y_per_div_5 = wx.NewId()
+ self.id_y_per_div_10 = wx.NewId()
+ self.id_y_per_div_20 = wx.NewId()
+ self.id_average = wx.NewId()
+ self.id_peak_hold = wx.NewId()
+
+ self.plot.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
+ self.plot.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold)
+ self.plot.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
+ self.plot.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
+ self.plot.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
+ self.plot.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
+ self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
+ self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
+ self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
+ self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
+ self.plot.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
+
+ # make a menu
+ menu = wx.Menu()
+ self.popup_menu = menu
+ menu.AppendCheckItem(self.id_average, "Average")
+ menu.AppendCheckItem(self.id_peak_hold, "Peak Hold")
+ menu.Append(self.id_incr_ref_level, "Incr Ref Level")
+ menu.Append(self.id_decr_ref_level, "Decr Ref Level")
+ # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
+ # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
+ menu.AppendSeparator()
+ # we'd use RadioItems for these, but they're not supported on Mac
+ menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
+ menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
+ menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
+ menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
+ menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
+
+ self.checkmarks = {
+ self.id_average : lambda : self.fftsink.average,
+ self.id_peak_hold : lambda : self.fftsink.peak_hold,
+ self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
+ self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
+ self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
+ self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
+ self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
+ }
+
+
+def next_up(v, seq):
+ """
+ Return the first item in seq that is > v.
+ """
+ for s in seq:
+ if s > v:
+ return s
+ return v
+
+def next_down(v, seq):
+ """
+ Return the last item in seq that is < v.
+ """
+ rseq = list(seq[:])
+ rseq.reverse()
+
+ for s in rseq:
+ if s < v:
+ return s
+ return v
+
+
+# ----------------------------------------------------------------
+# Standalone test app
+# ----------------------------------------------------------------
+
+class test_app_block (stdgui2.std_top_block):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
+
+ fft_size = 256
+
+ # build our flow graph
+ input_rate = 20.48e3
+
+ # Generate a complex sinusoid
+ #src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
+ src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
+
+ # We add these throttle blocks so that this demo doesn't
+ # suck down all the CPU available. Normally you wouldn't use these.
+ thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ sink1 = fft_sink_c (panel, title="Complex Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3,
+ ref_level=0, y_per_div=20, y_divs=10)
+ vbox.Add (sink1.win, 1, wx.EXPAND)
+
+ self.connect(src1, thr1, sink1)
+
+ #src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
+ src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
+ thr2 = gr.throttle(gr.sizeof_float, input_rate)
+ sink2 = fft_sink_f (panel, title="Real Data", fft_size=fft_size*2,
+ sample_rate=input_rate, baseband_freq=100e3,
+ ref_level=0, y_per_div=20, y_divs=10)
+ vbox.Add (sink2.win, 1, wx.EXPAND)
+
+ self.connect(src2, thr2, sink2)
+
+def main ():
+ app = stdgui2.stdapp (test_app_block, "FFT Sink Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
diff --git a/gr-wxgui/src/python/form.py b/gr-wxgui/src/python/form.py
index 3e303554d..3e303554d 100755..100644
--- a/gr-wxgui/src/python/form.py
+++ b/gr-wxgui/src/python/form.py
diff --git a/gr-wxgui/src/python/number_window.py b/gr-wxgui/src/python/number_window.py
new file mode 100644
index 000000000..8572b7c4a
--- /dev/null
+++ b/gr-wxgui/src/python/number_window.py
@@ -0,0 +1,184 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import common
+import numpy
+import wx
+import pubsub
+from constants import *
+
+##################################################
+# Constants
+##################################################
+NEG_INF = float('-inf')
+SLIDER_STEPS = 100
+AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
+DEFAULT_NUMBER_RATE = 5
+DEFAULT_WIN_SIZE = (300, 300)
+DEFAULT_GAUGE_RANGE = 1000
+
+##################################################
+# Number window control panel
+##################################################
+class control_panel(wx.Panel):
+ """!
+ A control panel with wx widgits to control the averaging.
+ """
+
+ def __init__(self, parent):
+ """!
+ Create a new control panel.
+ @param parent the wx parent window
+ """
+ self.parent = parent
+ wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+ #checkboxes for average and peak hold
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
+ self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key)
+ control_box.Add(self.average_check_box, 0, wx.EXPAND)
+ self.peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY)
+ control_box.Add(self.peak_hold_check_box, 0, wx.EXPAND)
+ control_box.AddSpacer(2)
+ self.avg_alpha_slider = common.LogSliderController(
+ self, 'Avg Alpha',
+ AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
+ parent.ext_controller, parent.avg_alpha_key,
+ formatter=lambda x: ': %.4f'%x,
+ )
+ parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable)
+ control_box.Add(self.avg_alpha_slider, 0, wx.EXPAND)
+ #run/stop
+ control_box.AddStretchSpacer()
+ self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
+ control_box.Add(self.run_button, 0, wx.EXPAND)
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+##################################################
+# Numbersink window with label and gauges
+##################################################
+class number_window(wx.Panel, pubsub.pubsub, common.prop_setter):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ units,
+ show_gauge,
+ real,
+ minval,
+ maxval,
+ decimal_places,
+ average_key,
+ avg_alpha_key,
+ peak_hold,
+ msg_key,
+ ):
+ pubsub.pubsub.__init__(self)
+ wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER)
+ #setup
+ self.peak_val_real = NEG_INF
+ self.peak_val_imag = NEG_INF
+ self.ext_controller = controller
+ self.real = real
+ self.units = units
+ self.minval = minval
+ self.maxval = maxval
+ self.decimal_places = decimal_places
+ self.average_key = average_key
+ self.avg_alpha_key = avg_alpha_key
+ #setup the box with display and controls
+ self.control_panel = control_panel(self)
+ main_box = wx.BoxSizer(wx.HORIZONTAL)
+ sizer = wx.BoxSizer(wx.VERTICAL)
+ main_box.Add(sizer, 1, wx.EXPAND)
+ main_box.Add(self.control_panel, 0, wx.EXPAND)
+ sizer.Add(common.LabelText(self, title), 1, wx.ALIGN_CENTER)
+ self.text = wx.StaticText(self, size=(size[0], -1))
+ sizer.Add(self.text, 1, wx.EXPAND)
+ self.gauge_real = wx.Gauge(self, range=DEFAULT_GAUGE_RANGE, style=wx.GA_HORIZONTAL)
+ self.gauge_imag = wx.Gauge(self, range=DEFAULT_GAUGE_RANGE, style=wx.GA_HORIZONTAL)
+ #hide/show gauges
+ self.show_gauges(show_gauge)
+ sizer.Add(self.gauge_real, 1, wx.EXPAND)
+ sizer.Add(self.gauge_imag, 1, wx.EXPAND)
+ self.SetSizerAndFit(main_box)
+ #initial setup
+ self.ext_controller[self.average_key] = self.ext_controller[self.average_key]
+ self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key]
+ self._register_set_prop(self, PEAK_HOLD_KEY, peak_hold)
+ self._register_set_prop(self, RUNNING_KEY, True)
+ #register events
+ self.ext_controller.subscribe(msg_key, self.handle_msg)
+
+ def show_gauges(self, show_gauge):
+ """!
+ Show or hide the gauges.
+ If this is real, never show the imaginary gauge.
+ @param show_gauge true to show
+ """
+ if show_gauge: self.gauge_real.Show()
+ else: self.gauge_real.Hide()
+ if show_gauge and not self.real: self.gauge_imag.Show()
+ else: self.gauge_imag.Hide()
+
+ def handle_msg(self, msg):
+ """!
+ Handle a message from the message queue.
+ Convert the string based message into a float or complex.
+ If more than one number was read, only take the last number.
+ Perform peak hold operations, set the gauges and display.
+ @param msg the number sample as a character array
+ """
+ if not self[RUNNING_KEY]: return
+ #set gauge
+ def set_gauge_value(gauge, value):
+ gauge_val = DEFAULT_GAUGE_RANGE*(value-self.minval)/(self.maxval-self.minval)
+ gauge_val = max(0, gauge_val) #clip
+ gauge_val = min(DEFAULT_GAUGE_RANGE, gauge_val) #clip
+ gauge.SetValue(gauge_val)
+ format_string = "%%.%df"%self.decimal_places
+ if self.real:
+ sample = numpy.fromstring(msg, numpy.float32)[-1]
+ if self[PEAK_HOLD_KEY]: sample = self.peak_val_real = max(self.peak_val_real, sample)
+ label_text = "%s %s"%(format_string%sample, self.units)
+ set_gauge_value(self.gauge_real, sample)
+ else:
+ sample = numpy.fromstring(msg, numpy.complex64)[-1]
+ if self[PEAK_HOLD_KEY]:
+ self.peak_val_real = max(self.peak_val_real, sample.real)
+ self.peak_val_imag = max(self.peak_val_imag, sample.imag)
+ sample = self.peak_val_real + self.peak_val_imag*1j
+ label_text = "%s + %sj %s"%(format_string%sample.real, format_string%sample.imag, self.units)
+ set_gauge_value(self.gauge_real, sample.real)
+ set_gauge_value(self.gauge_imag, sample.imag)
+ #set label text
+ self.text.SetLabel(label_text)
+ #clear peak hold
+ if not self[PEAK_HOLD_KEY]:
+ self.peak_val_real = NEG_INF
+ self.peak_val_imag = NEG_INF
diff --git a/gr-wxgui/src/python/numbersink2.py b/gr-wxgui/src/python/numbersink2.py
index 63bfe25c9..43eb9e493 100755..100644
--- a/gr-wxgui/src/python/numbersink2.py
+++ b/gr-wxgui/src/python/numbersink2.py
@@ -1,481 +1,150 @@
-#!/usr/bin/env python
#
-# Copyright 2003,2004,2005,2006,2007,2008 Free Software Foundation, Inc.
-#
+# Copyright 2008 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., 59 Temple Place - Suite 330,
-# Boston, MA 02111-1307, USA.
-#
-
-from gnuradio import gr, gru, window
-from gnuradio.wxgui import stdgui2
-import wx
-import gnuradio.wxgui.plot as plot
-import numpy
-import threading
-import math
-
-default_numbersink_size = (640,240)
-default_number_rate = gr.prefs().get_long('wxgui', 'number_rate', 15)
-
-class number_sink_base(object):
- def __init__(self, input_is_real=False, unit='',base_value=0, minval=-100.0,maxval=100.0,factor=1.0,decimal_places=10, ref_level=50,
- sample_rate=1,
- number_rate=default_number_rate,
- average=False, avg_alpha=None, label='', peak_hold=False):
-
- # initialize common attributes
- self.unit=unit
- self.base_value = base_value
- self.minval=minval
- self.maxval=maxval
- self.factor=factor
- self.y_divs = 8
- self.decimal_places=decimal_places
- self.ref_level = ref_level
- self.sample_rate = sample_rate
- number_size=1
- self.number_size = number_size
- self.number_rate = number_rate
- self.average = average
- if avg_alpha is None:
- self.avg_alpha = 2.0 / number_rate
- else:
- self.avg_alpha = avg_alpha
- self.label = label
- self.peak_hold = peak_hold
- self.show_gauge = True
- self.input_is_real = input_is_real
- self.msgq = gr.msg_queue(2) # queue that holds a maximum of 2 messages
-
- def set_decimal_places(self, decimal_places):
- self.decimal_places = decimal_places
-
- def set_ref_level(self, ref_level):
- self.ref_level = ref_level
-
- def print_current_value(self, comment):
- print comment,self.win.current_value
-
- def set_average(self, average):
- self.average = average
- if average:
- self.avg.set_taps(self.avg_alpha)
- self.set_peak_hold(False)
- else:
- self.avg.set_taps(1.0)
-
- def set_peak_hold(self, enable):
- self.peak_hold = enable
- if enable:
- self.set_average(False)
- self.win.set_peak_hold(enable)
-
- def set_show_gauge(self, enable):
- self.show_gauge = enable
- self.win.set_show_gauge(enable)
-
- def set_avg_alpha(self, avg_alpha):
- self.avg_alpha = avg_alpha
-
- def set_base_value(self, base_value):
- self.base_value = base_value
-
-
-class number_sink_f(gr.hier_block2, number_sink_base):
- def __init__(self, parent, unit='',base_value=0,minval=-100.0,maxval=100.0,factor=1.0,
- decimal_places=10, ref_level=50, sample_rate=1,
- number_rate=default_number_rate, average=False, avg_alpha=None,
- label='', size=default_numbersink_size, peak_hold=False):
-
- gr.hier_block2.__init__(self, "number_sink_f",
- gr.io_signature(1, 1, gr.sizeof_float), # Input signature
- gr.io_signature(0, 0, 0)) # Output signature
-
- number_sink_base.__init__(self, unit=unit, input_is_real=True, base_value=base_value,
- minval=minval,maxval=maxval,factor=factor,
- decimal_places=decimal_places, ref_level=ref_level,
- sample_rate=sample_rate, number_rate=number_rate,
- average=average, avg_alpha=avg_alpha, label=label,
- peak_hold=peak_hold)
-
- number_size=1
- one_in_n = gr.keep_one_in_n(gr.sizeof_float,
- max(1, int(sample_rate/number_rate)))
-
- self.avg = gr.single_pole_iir_filter_ff(1.0, number_size)
- sink = gr.message_sink(gr.sizeof_float , self.msgq, True)
- self.connect(self, self.avg, one_in_n, sink)
-
- self.win = number_window(self, parent, size=size,label=label)
- self.set_average(self.average)
- self.set_peak_hold(self.peak_hold)
-
-class number_sink_c(gr.hier_block2, number_sink_base):
- def __init__(self, parent, unit='',base_value=0,minval=-100.0,maxval=100.0,factor=1.0,
- decimal_places=10, ref_level=50, sample_rate=1,
- number_rate=default_number_rate, average=False, avg_alpha=None,
- label='', size=default_numbersink_size, peak_hold=False):
-
- gr.hier_block2.__init__(self, "number_sink_c",
- gr.io_signature(1, 1, gr.sizeof_gr_complex), # Input signature
- gr.io_signature(0, 0, 0)) # Output signature
-
- number_sink_base.__init__(self, unit=unit, input_is_real=False, base_value=base_value,factor=factor,
- minval=minval,maxval=maxval,decimal_places=decimal_places, ref_level=ref_level,
- sample_rate=sample_rate, number_rate=number_rate,
- average=average, avg_alpha=avg_alpha, label=label,
- peak_hold=peak_hold)
-
- number_size=1
- one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex,
- max(1, int(sample_rate/number_rate)))
-
- self.avg = gr.single_pole_iir_filter_cc(1.0, number_size)
- sink = gr.message_sink(gr.sizeof_gr_complex , self.msgq, True)
- self.connect(self, self.avg, one_in_n, sink)
-
- self.win = number_window(self, parent, size=size,label=label)
- self.set_average(self.average)
- self.set_peak_hold(self.peak_hold)
-
-
-# ------------------------------------------------------------------------
-
-myDATA_EVENT = wx.NewEventType()
-EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
-
-
-class DataEvent(wx.PyEvent):
- def __init__(self, data):
- wx.PyEvent.__init__(self)
- self.SetEventType (myDATA_EVENT)
- self.data = data
-
- def Clone (self):
- self.__class__ (self.GetId())
-
-
-class input_watcher (threading.Thread):
- def __init__ (self, msgq, number_size, event_receiver, **kwds):
- threading.Thread.__init__ (self, **kwds)
- self.setDaemon (1)
- self.msgq = msgq
- self.number_size = number_size
- self.event_receiver = event_receiver
- self.keep_running = True
- self.start ()
-
- def run (self):
- while (self.keep_running):
- msg = self.msgq.delete_head() # blocking read of message queue
- itemsize = int(msg.arg1())
- nitems = int(msg.arg2())
-
- s = msg.to_string() # get the body of the msg as a string
-
- # There may be more than one number in the message.
- # If so, we take only the last one
- if nitems > 1:
- start = itemsize * (nitems - 1)
- s = s[start:start+itemsize]
-
- complex_data = numpy.fromstring (s, numpy.float32)
- de = DataEvent (complex_data)
- wx.PostEvent (self.event_receiver, de)
- del de
-
-#========================================================================================
-class static_text_window (wx.StaticText): #plot.PlotCanvas):
- def __init__ (self, parent, numbersink,id = -1,label="number",
- pos = wx.DefaultPosition, size = wx.DefaultSize,
- style = wx.DEFAULT_FRAME_STYLE, name = ""):
- wx.StaticText.__init__(self, parent, id, label, pos, size, style, name)
- self.parent=parent
- self.label=label
- self.numbersink = numbersink
- self.peak_hold = False
- self.peak_vals = None
- self.build_popup_menu()
- self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
-
- def on_close_window (self, event):
- print "number_window:on_close_window"
- self.keep_running = False
-
- def set_peak_hold(self, enable):
- self.peak_hold = enable
- self.peak_vals = None
-
- def update_y_range (self):
- ymax = self.numbersink.ref_level
- ymin = self.numbersink.ref_level - self.numbersink.decimal_places * self.numbersink.y_divs
- self.y_range = self._axisInterval ('min', ymin, ymax)
-
- def on_average(self, evt):
- # print "on_average"
- self.numbersink.set_average(evt.IsChecked())
-
- def on_peak_hold(self, evt):
- # print "on_peak_hold"
- self.numbersink.set_peak_hold(evt.IsChecked())
-
- def on_show_gauge(self, evt):
- # print "on_show_gauge"
- self.numbersink.set_show_gauge(evt.IsChecked())
- print evt.IsChecked()
-
- def on_incr_ref_level(self, evt):
- # print "on_incr_ref_level"
- self.numbersink.set_ref_level(self.numbersink.ref_level
- + self.numbersink.decimal_places)
-
- def on_decr_ref_level(self, evt):
- # print "on_decr_ref_level"
- self.numbersink.set_ref_level(self.numbersink.ref_level
- - self.numbersink.decimal_places)
-
- def on_incr_decimal_places(self, evt):
- # print "on_incr_decimal_places"
- self.numbersink.set_decimal_places(self.numbersink.decimal_places+1)
-
- def on_decr_decimal_places(self, evt):
- # print "on_decr_decimal_places"
- self.numbersink.set_decimal_places(max(self.numbersink.decimal_places-1,0))
-
- def on_decimal_places(self, evt):
- # print "on_decimal_places"
- Id = evt.GetId()
- if Id == self.id_decimal_places_0:
- self.numbersink.set_decimal_places(0)
- elif Id == self.id_decimal_places_1:
- self.numbersink.set_decimal_places(1)
- elif Id == self.id_decimal_places_2:
- self.numbersink.set_decimal_places(2)
- elif Id == self.id_decimal_places_3:
- self.numbersink.set_decimal_places(3)
- elif Id == self.id_decimal_places_6:
- self.numbersink.set_decimal_places(6)
- elif Id == self.id_decimal_places_9:
- self.numbersink.set_decimal_places(9)
-
- def on_right_click(self, event):
- menu = self.popup_menu
- for id, pred in self.checkmarks.items():
- item = menu.FindItemById(id)
- item.Check(pred())
- self.PopupMenu(menu, event.GetPosition())
-
- def build_popup_menu(self):
- self.id_show_gauge = wx.NewId()
- self.id_incr_ref_level = wx.NewId()
- self.id_decr_ref_level = wx.NewId()
- self.id_incr_decimal_places = wx.NewId()
- self.id_decr_decimal_places = wx.NewId()
- self.id_decimal_places_0 = wx.NewId()
- self.id_decimal_places_1 = wx.NewId()
- self.id_decimal_places_2 = wx.NewId()
- self.id_decimal_places_3 = wx.NewId()
- self.id_decimal_places_6 = wx.NewId()
- self.id_decimal_places_9 = wx.NewId()
- self.id_average = wx.NewId()
- self.id_peak_hold = wx.NewId()
-
- self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
- self.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold)
- #self.Bind(wx.EVT_MENU, self.on_hide_gauge, id=self.id_hide_gauge)
- self.Bind(wx.EVT_MENU, self.on_show_gauge, id=self.id_show_gauge)
- self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
- self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
- self.Bind(wx.EVT_MENU, self.on_incr_decimal_places, id=self.id_incr_decimal_places)
- self.Bind(wx.EVT_MENU, self.on_decr_decimal_places, id=self.id_decr_decimal_places)
- self.Bind(wx.EVT_MENU, self.on_decimal_places, id=self.id_decimal_places_0)
- self.Bind(wx.EVT_MENU, self.on_decimal_places, id=self.id_decimal_places_1)
- self.Bind(wx.EVT_MENU, self.on_decimal_places, id=self.id_decimal_places_2)
- self.Bind(wx.EVT_MENU, self.on_decimal_places, id=self.id_decimal_places_3)
- self.Bind(wx.EVT_MENU, self.on_decimal_places, id=self.id_decimal_places_6)
- self.Bind(wx.EVT_MENU, self.on_decimal_places, id=self.id_decimal_places_9)
-
- # make a menu
- menu = wx.Menu()
- self.popup_menu = menu
- menu.AppendCheckItem(self.id_average, "Average")
- menu.AppendCheckItem(self.id_peak_hold, "Peak Hold")
- menu.AppendCheckItem(self.id_show_gauge, "Show gauge")
- menu.Append(self.id_incr_ref_level, "Incr Ref Level")
- menu.Append(self.id_decr_ref_level, "Decr Ref Level")
- menu.Append(self.id_incr_decimal_places, "Incr decimal places")
- menu.Append(self.id_decr_decimal_places, "Decr decimal places")
- menu.AppendSeparator()
- # we'd use RadioItems for these, but they're not supported on Mac
- menu.AppendCheckItem(self.id_decimal_places_0, "0 decimal places")
- menu.AppendCheckItem(self.id_decimal_places_1, "1 decimal places")
- menu.AppendCheckItem(self.id_decimal_places_2, "2 decimal places")
- menu.AppendCheckItem(self.id_decimal_places_3, "3 decimal places")
- menu.AppendCheckItem(self.id_decimal_places_6, "6 decimal places")
- menu.AppendCheckItem(self.id_decimal_places_9, "9 decimal places")
-
- self.checkmarks = {
- self.id_average : lambda : self.numbersink.average,
- self.id_peak_hold : lambda : self.numbersink.peak_hold,
- self.id_show_gauge : lambda : self.numbersink.show_gauge,
- self.id_decimal_places_0 : lambda : self.numbersink.decimal_places == 0,
- self.id_decimal_places_1 : lambda : self.numbersink.decimal_places == 1,
- self.id_decimal_places_2 : lambda : self.numbersink.decimal_places == 2,
- self.id_decimal_places_3 : lambda : self.numbersink.decimal_places == 3,
- self.id_decimal_places_6 : lambda : self.numbersink.decimal_places == 6,
- self.id_decimal_places_9 : lambda : self.numbersink.decimal_places == 9,
- }
-
-def next_up(v, seq):
- """
- Return the first item in seq that is > v.
- """
- for s in seq:
- if s > v:
- return s
- return v
-
-def next_down(v, seq):
- """
- Return the last item in seq that is < v.
- """
- rseq = list(seq[:])
- rseq.reverse()
-
- for s in rseq:
- if s < v:
- return s
- return v
-
-
-#========================================================================================
-class number_window (plot.PlotCanvas):
- def __init__ (self, numbersink, parent, id = -1,label="number",
- pos = wx.DefaultPosition, size = wx.DefaultSize,
- style = wx.DEFAULT_FRAME_STYLE, name = ""):
- plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
- self.static_text=static_text_window( self, numbersink,id, label, pos, (size[0]/2,size[1]/2), style, name)
- gauge_style = wx.GA_HORIZONTAL
- vbox=wx.BoxSizer(wx.VERTICAL)
- vbox.Add (self.static_text, 0, wx.EXPAND)
- self.current_value=None
- if numbersink.input_is_real:
- self.gauge=wx.Gauge( self, id, range=1000, pos=(pos[0],pos[1]+size[1]/2),size=(size[0]/2,size[1]/2), style=gauge_style, name = "gauge")
- vbox.Add (self.gauge, 1, wx.EXPAND)
- else:
- self.gauge=wx.Gauge( self, id, range=1000, pos=(pos[0],pos[1]+size[1]/3),size=(size[0]/2,size[1]/3), style=gauge_style, name = "gauge")
- self.gauge_imag=wx.Gauge( self, id, range=1000, pos=(pos[0],pos[1]+size[1]*2/3),size=(size[0]/2,size[1]/3), style=gauge_style, name = "gauge_imag")
- vbox.Add (self.gauge, 1, wx.EXPAND)
- vbox.Add (self.gauge_imag, 1, wx.EXPAND)
- self.sizer = vbox
- self.SetSizer (self.sizer)
- self.SetAutoLayout (True)
- self.sizer.Fit (self)
-
- self.label=label
- self.numbersink = numbersink
- self.peak_hold = False
- self.peak_vals = None
-
- EVT_DATA_EVENT (self, self.set_data)
- wx.EVT_CLOSE (self, self.on_close_window)
- self.input_watcher = input_watcher(numbersink.msgq, numbersink.number_size, self)
-
- def on_close_window (self, event):
- # print "number_window:on_close_window"
- self.keep_running = False
-
- def set_show_gauge(self, enable):
- self.show_gauge = enable
- if enable:
- self.gauge.Show()
- if not self.numbersink.input_is_real:
- self.gauge_imag.Show()
- #print 'show'
- else:
- self.gauge.Hide()
- if not self.numbersink.input_is_real:
- self.gauge_imag.Hide()
- #print 'hide'
-
- def set_data (self, evt):
- numbers = evt.data
- L = len (numbers)
-
- if self.peak_hold:
- if self.peak_vals is None:
- self.peak_vals = numbers
- else:
- self.peak_vals = numpy.maximum(numbers, self.peak_vals)
- numbers = self.peak_vals
-
- if self.numbersink.input_is_real:
- real_value=numbers[0]*self.numbersink.factor + self.numbersink.base_value
- imag_value=0.0
- self.current_value=real_value
- else:
- real_value=numbers[0]*self.numbersink.factor + self.numbersink.base_value
- imag_value=numbers[1]*self.numbersink.factor + self.numbersink.base_value
- self.current_value=complex(real_value,imag_value)
- x = max(real_value, imag_value)
- if x >= 1e9:
- sf = 1e-9
- unit_prefix = "G"
- elif x >= 1e6:
- sf = 1e-6
- unit_prefix = "M"
- elif x>= 1e3:
- sf = 1e-3
- unit_prefix = "k"
- else :
- sf = 1
- unit_prefix = ""
- if self.numbersink.input_is_real:
- showtext = "%s: %.*f %s%s" % (self.label, self.numbersink.decimal_places,real_value*sf,unit_prefix,self.numbersink.unit)
- else:
- showtext = "%s: %.*f,%.*f %s%s" % (self.label, self.numbersink.decimal_places,real_value*sf,
- self.numbersink.decimal_places,imag_value*sf,unit_prefix,self.numbersink.unit)
- self.static_text.SetLabel(showtext)
- self.gauge.SetValue(int(float((real_value-self.numbersink.base_value)*1000.0/(self.numbersink.maxval-self.numbersink.minval)))+500)
- if not self.numbersink.input_is_real:
- self.gauge.SetValue(int(float((imag_value-self.numbersink.base_value)*1000.0/(self.numbersink.maxval-self.numbersink.minval)))+500)
-
- def set_peak_hold(self, enable):
- self.peak_hold = enable
- self.peak_vals = None
-
- def update_y_range (self):
- ymax = self.numbersink.ref_level
- ymin = self.numbersink.ref_level - self.numbersink.decimal_places * self.numbersink.y_divs
- self.y_range = self._axisInterval ('min', ymin, ymax)
-
- def on_average(self, evt):
- # print "on_average"
- self.numbersink.set_average(evt.IsChecked())
-
- def on_peak_hold(self, evt):
- # print "on_peak_hold"
- self.numbersink.set_peak_hold(evt.IsChecked())
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+##################################################
+# Imports
+##################################################
+import number_window
+import common
+from gnuradio import gr, blks2
+from pubsub import pubsub
+from constants import *
+
+##################################################
+# Number sink block (wrapper for old wxgui)
+##################################################
+class _number_sink_base(gr.hier_block2, common.prop_setter):
+ """!
+ An decimator block with a number window display
+ """
+
+ def __init__(
+ self,
+ parent,
+ unit='units',
+ base_value=None, #ignore (old wrapper)
+ minval=0,
+ maxval=1,
+ factor=1,
+ decimal_places=3,
+ ref_level=0,
+ sample_rate=1,
+ number_rate=number_window.DEFAULT_NUMBER_RATE,
+ average=False,
+ avg_alpha=None,
+ label='Number Plot',
+ size=number_window.DEFAULT_WIN_SIZE,
+ peak_hold=False,
+ show_gauge=True,
+ ):
+ #ensure avg alpha
+ if avg_alpha is None: avg_alpha = 2.0/number_rate
+ #init
+ gr.hier_block2.__init__(
+ self,
+ "number_sink",
+ gr.io_signature(1, 1, self._item_size),
+ gr.io_signature(0, 0, 0),
+ )
+ #blocks
+ sd = blks2.stream_to_vector_decimator(
+ item_size=self._item_size,
+ sample_rate=sample_rate,
+ vec_rate=number_rate,
+ vec_len=1,
+ )
+ if self._real:
+ mult = gr.multiply_const_ff(factor)
+ add = gr.add_const_ff(ref_level)
+ self._avg = gr.single_pole_iir_filter_ff(1.0)
+ else:
+ mult = gr.multiply_const_cc(factor)
+ add = gr.add_const_cc(ref_level)
+ self._avg = gr.single_pole_iir_filter_cc(1.0)
+ msgq = gr.msg_queue(2)
+ sink = gr.message_sink(self._item_size, msgq, True)
+ #connect
+ self.connect(self, sd, mult, add, self._avg, sink)
+ #setup averaging
+ self._avg_alpha = avg_alpha
+ self.set_average(average)
+ self.set_avg_alpha(avg_alpha)
+ #controller
+ self.controller = pubsub()
+ self.controller.subscribe(SAMPLE_RATE_KEY, sd.set_sample_rate)
+ self.controller.subscribe(AVERAGE_KEY, self.set_average)
+ self.controller.publish(AVERAGE_KEY, self.get_average)
+ self.controller.subscribe(AVG_ALPHA_KEY, self.set_avg_alpha)
+ self.controller.publish(AVG_ALPHA_KEY, self.get_avg_alpha)
+ #start input watcher
+ def set_msg(msg): self.controller[MSG_KEY] = msg
+ common.input_watcher(msgq, set_msg)
+ #create window
+ self.win = number_window.number_window(
+ parent=parent,
+ controller=self.controller,
+ size=size,
+ title=label,
+ units=unit,
+ real=self._real,
+ minval=minval,
+ maxval=maxval,
+ decimal_places=decimal_places,
+ show_gauge=show_gauge,
+ average_key=AVERAGE_KEY,
+ avg_alpha_key=AVG_ALPHA_KEY,
+ peak_hold=peak_hold,
+ msg_key=MSG_KEY,
+ )
+ #register callbacks from window for external use
+ for attr in filter(lambda a: a.startswith('set_'), dir(self.win)):
+ setattr(self, attr, getattr(self.win, attr))
+ self._register_set_prop(self.controller, SAMPLE_RATE_KEY)
+
+ def get_average(self): return self._average
+ def set_average(self, average):
+ self._average = average
+ if self.get_average(): self._avg.set_taps(self.get_avg_alpha())
+ else: self._avg.set_taps(1.0)
+
+ def get_avg_alpha(self): return self._avg_alpha
+ def set_avg_alpha(self, avg_alpha):
+ self._avg_alpha = avg_alpha
+ self.set_average(self.get_average())
+
+class number_sink_f(_number_sink_base):
+ _item_size = gr.sizeof_float
+ _real = True
+
+class number_sink_c(_number_sink_base):
+ _item_size = gr.sizeof_gr_complex
+ _real = False
# ----------------------------------------------------------------
# Standalone test app
# ----------------------------------------------------------------
+import wx
+from gnuradio.wxgui import stdgui2
+
class test_app_flow_graph (stdgui2.std_top_block):
def __init__(self, frame, panel, vbox, argv):
stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
@@ -484,20 +153,20 @@ class test_app_flow_graph (stdgui2.std_top_block):
input_rate = 20.48e3
# Generate a real and complex sinusoids
- src1 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
- src2 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
+ src1 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2.21e3, 1)
+ src2 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2.21e3, 1)
# We add these throttle blocks so that this demo doesn't
# suck down all the CPU available. Normally you wouldn't use these.
thr1 = gr.throttle(gr.sizeof_float, input_rate)
thr2 = gr.throttle(gr.sizeof_gr_complex, input_rate)
- sink1 = number_sink_f (panel, unit='Hz',label="Real Data", avg_alpha=0.001,
- sample_rate=input_rate, base_value=100e3,
+ sink1 = number_sink_f (panel, unit='V',label="Real Data", avg_alpha=0.001,
+ sample_rate=input_rate, minval=-1, maxval=1,
ref_level=0, decimal_places=3)
vbox.Add (sink1.win, 1, wx.EXPAND)
sink2 = number_sink_c (panel, unit='V',label="Complex Data", avg_alpha=0.001,
- sample_rate=input_rate, base_value=0,
+ sample_rate=input_rate, minval=-1, maxval=1,
ref_level=0, decimal_places=3)
vbox.Add (sink2.win, 1, wx.EXPAND)
@@ -510,3 +179,4 @@ def main ():
if __name__ == '__main__':
main ()
+
diff --git a/gr-wxgui/src/python/plotter/Makefile.am b/gr-wxgui/src/python/plotter/Makefile.am
new file mode 100644
index 000000000..ada506794
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/Makefile.am
@@ -0,0 +1,37 @@
+#
+# Copyright 2004,2005,2008 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 $(top_srcdir)/Makefile.common
+
+# Install this stuff so that it ends up as the gnuradio.wxgui module
+# This usually ends up at:
+# ${prefix}/lib/python${python_version}/site-packages/gnuradio/wxgui
+
+ourpythondir = $(grpythondir)/wxgui/plotter
+ourlibdir = $(grpyexecdir)/wxgui/plotter
+
+ourpython_PYTHON = \
+ __init__.py \
+ channel_plotter.py \
+ gltext.py \
+ plotter_base.py \
+ waterfall_plotter.py
+
diff --git a/gr-wxgui/src/python/plotter/__init__.py b/gr-wxgui/src/python/plotter/__init__.py
new file mode 100644
index 000000000..12f8b3450
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/__init__.py
@@ -0,0 +1,23 @@
+#
+# Copyright 2008 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 channel_plotter import channel_plotter
+from waterfall_plotter import waterfall_plotter
diff --git a/gr-wxgui/src/python/plotter/channel_plotter.py b/gr-wxgui/src/python/plotter/channel_plotter.py
new file mode 100644
index 000000000..22126bd0b
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/channel_plotter.py
@@ -0,0 +1,225 @@
+#
+# Copyright 2008 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 wx
+from plotter_base import grid_plotter_base
+from OpenGL.GL import *
+from gnuradio.wxgui import common
+import numpy
+import gltext
+import math
+
+LEGEND_TEXT_FONT_SIZE = 8
+LEGEND_BOX_PADDING = 3
+PADDING = 35, 15, 40, 60 #top, right, bottom, left
+#constants for the waveform storage
+SAMPLES_KEY = 'samples'
+COLOR_SPEC_KEY = 'color_spec'
+MARKERY_KEY = 'marker'
+
+##################################################
+# Channel Plotter for X Y Waveforms
+##################################################
+class channel_plotter(grid_plotter_base):
+
+ def __init__(self, parent):
+ """!
+ Create a new channel plotter.
+ """
+ #init
+ grid_plotter_base.__init__(self, parent, PADDING)
+ self._channels = dict()
+ self.enable_legend(False)
+
+ def _gl_init(self):
+ """!
+ Run gl initialization tasks.
+ """
+ glEnableClientState(GL_VERTEX_ARRAY)
+ self._grid_compiled_list_id = glGenLists(1)
+
+ def enable_legend(self, enable=None):
+ """!
+ Enable/disable the legend.
+ @param enable true to enable
+ @return the enable state when None
+ """
+ if enable is None: return self._enable_legend
+ self.lock()
+ self._enable_legend = enable
+ self.changed(True)
+ self.unlock()
+
+ def draw(self):
+ """!
+ Draw the grid and waveforms.
+ """
+ self.lock()
+ self.clear()
+ #store the grid drawing operations
+ if self.changed():
+ glNewList(self._grid_compiled_list_id, GL_COMPILE)
+ self._draw_grid()
+ self._draw_legend()
+ glEndList()
+ self.changed(False)
+ #draw the grid
+ glCallList(self._grid_compiled_list_id)
+ #use scissor to prevent drawing outside grid
+ glEnable(GL_SCISSOR_TEST)
+ glScissor(
+ self.padding_left+1,
+ self.padding_bottom+1,
+ self.width-self.padding_left-self.padding_right-1,
+ self.height-self.padding_top-self.padding_bottom-1,
+ )
+ #draw the waveforms
+ self._draw_waveforms()
+ glDisable(GL_SCISSOR_TEST)
+ self._draw_point_label()
+ #swap buffer into display
+ self.SwapBuffers()
+ self.unlock()
+
+ def _draw_waveforms(self):
+ """!
+ Draw the waveforms for each channel.
+ Scale the waveform data to the grid using gl matrix operations.
+ """
+ for channel in reversed(sorted(self._channels.keys())):
+ samples = self._channels[channel][SAMPLES_KEY]
+ num_samps = len(samples)
+ #use opengl to scale the waveform
+ glPushMatrix()
+ glTranslatef(self.padding_left, self.padding_top, 0)
+ glScalef(
+ (self.width-self.padding_left-self.padding_right),
+ (self.height-self.padding_top-self.padding_bottom),
+ 1,
+ )
+ glTranslatef(0, 1, 0)
+ if isinstance(samples, tuple):
+ x_scale, x_trans = 1.0/(self.x_max-self.x_min), -self.x_min
+ points = zip(*samples)
+ else:
+ x_scale, x_trans = 1.0/(num_samps-1), 0
+ points = zip(numpy.arange(0, num_samps), samples)
+ glScalef(x_scale, -1.0/(self.y_max-self.y_min), 1)
+ glTranslatef(x_trans, -self.y_min, 0)
+ #draw the points/lines
+ glColor3f(*self._channels[channel][COLOR_SPEC_KEY])
+ marker = self._channels[channel][MARKERY_KEY]
+ if marker: glPointSize(marker)
+ glVertexPointer(2, GL_FLOAT, 0, points)
+ glDrawArrays(marker is None and GL_LINE_STRIP or GL_POINTS, 0, len(points))
+ glPopMatrix()
+
+ def _populate_point_label(self, x_val, y_val):
+ """!
+ Get the text the will populate the point label.
+ Give X and Y values for the current point.
+ Give values for the channel at the X coordinate.
+ @param x_val the current x value
+ @param y_val the current y value
+ @return a string with newlines
+ """
+ #create text
+ label_str = '%s: %s %s\n%s: %s %s'%(
+ self.x_label,
+ common.label_format(x_val),
+ self.x_units, self.y_label,
+ common.label_format(y_val),
+ self.y_units,
+ )
+ for channel in sorted(self._channels.keys()):
+ samples = self._channels[channel][SAMPLES_KEY]
+ num_samps = len(samples)
+ if not num_samps: continue
+ if isinstance(samples, tuple): continue
+ #linear interpolation
+ x_index = (num_samps-1)*(x_val/self.x_scalar-self.x_min)/(self.x_max-self.x_min)
+ x_index_low = int(math.floor(x_index))
+ x_index_high = int(math.ceil(x_index))
+ y_value = (samples[x_index_high] - samples[x_index_low])*(x_index - x_index_low) + samples[x_index_low]
+ label_str += '\n%s: %s %s'%(channel, common.label_format(y_value), self.y_units)
+ return label_str
+
+ def _draw_legend(self):
+ """!
+ Draw the legend in the upper right corner.
+ For each channel, draw a rectangle out of the channel color,
+ and overlay the channel text on top of the rectangle.
+ """
+ if not self.enable_legend(): return
+ x_off = self.width - self.padding_right - LEGEND_BOX_PADDING
+ for i, channel in enumerate(reversed(sorted(self._channels.keys()))):
+ samples = self._channels[channel][SAMPLES_KEY]
+ if not len(samples): continue
+ color_spec = self._channels[channel][COLOR_SPEC_KEY]
+ txt = gltext.Text(channel, font_size=LEGEND_TEXT_FONT_SIZE)
+ w, h = txt.get_size()
+ #draw rect + text
+ glColor3f(*color_spec)
+ self._draw_rect(
+ x_off - w - LEGEND_BOX_PADDING,
+ self.padding_top/2 - h/2 - LEGEND_BOX_PADDING,
+ w+2*LEGEND_BOX_PADDING,
+ h+2*LEGEND_BOX_PADDING,
+ )
+ txt.draw_text(wx.Point(x_off - w, self.padding_top/2 - h/2))
+ x_off -= w + 4*LEGEND_BOX_PADDING
+
+ def set_waveform(self, channel, samples, color_spec, marker=None):
+ """!
+ Set the waveform for a given channel.
+ @param channel the channel key
+ @param samples the waveform samples
+ @param color_spec the 3-tuple for line color
+ @param marker None for line
+ """
+ self.lock()
+ if channel not in self._channels.keys(): self.changed(True)
+ self._channels[channel] = {
+ SAMPLES_KEY: samples,
+ COLOR_SPEC_KEY: color_spec,
+ MARKERY_KEY: marker,
+ }
+ self.unlock()
+
+if __name__ == '__main__':
+ app = wx.PySimpleApp()
+ frame = wx.Frame(None, -1, 'Demo', wx.DefaultPosition)
+ vbox = wx.BoxSizer(wx.VERTICAL)
+
+ plotter = channel_plotter(frame)
+ plotter.set_x_grid(-1, 1, .2)
+ plotter.set_y_grid(-1, 1, .4)
+ vbox.Add(plotter, 1, wx.EXPAND)
+
+ plotter = channel_plotter(frame)
+ plotter.set_x_grid(-1, 1, .2)
+ plotter.set_y_grid(-1, 1, .4)
+ vbox.Add(plotter, 1, wx.EXPAND)
+
+ frame.SetSizerAndFit(vbox)
+ frame.SetSize(wx.Size(800, 600))
+ frame.Show()
+ app.MainLoop()
diff --git a/gr-wxgui/src/python/plotter/gltext.py b/gr-wxgui/src/python/plotter/gltext.py
new file mode 100644
index 000000000..67f62ca56
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/gltext.py
@@ -0,0 +1,503 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+#
+# Provides some text display functions for wx + ogl
+# Copyright (C) 2007 Christian Brugger, Stefan Hacker
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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.
+
+import wx
+from OpenGL.GL import *
+
+"""
+Optimize with psyco if possible, this gains us about 50% speed when
+creating our textures in trade for about 4MBytes of additional memory usage for
+psyco. If you don't like loosing the memory you have to turn the lines following
+"enable psyco" into a comment while uncommenting the line after "Disable psyco".
+"""
+#Try to enable psyco
+try:
+ import psyco
+ psyco_optimized = False
+except ImportError:
+ psyco = None
+
+#Disable psyco
+#psyco = None
+
+class TextElement(object):
+ """
+ A simple class for using system Fonts to display
+ text in an OpenGL scene
+ """
+ def __init__(self,
+ text = '',
+ font = None,
+ foreground = wx.BLACK,
+ centered = False):
+ """
+ text (String) - Text
+ font (wx.Font) - Font to draw with (None = System default)
+ foreground (wx.Color) - Color of the text
+ or (wx.Bitmap)- Bitmap to overlay the text with
+ centered (bool) - Center the text
+
+ Initializes the TextElement
+ """
+ # save given variables
+ self._text = text
+ self._lines = text.split('\n')
+ self._font = font
+ self._foreground = foreground
+ self._centered = centered
+
+ # init own variables
+ self._owner_cnt = 0 #refcounter
+ self._texture = None #OpenGL texture ID
+ self._text_size = None #x/y size tuple of the text
+ self._texture_size= None #x/y Texture size tuple
+
+ # create Texture
+ self.createTexture()
+
+
+ #---Internal helpers
+
+ def _getUpper2Base(self, value):
+ """
+ Returns the lowest value with the power of
+ 2 greater than 'value' (2^n>value)
+ """
+ base2 = 1
+ while base2 < value:
+ base2 *= 2
+ return base2
+
+ #---Functions
+
+ def draw_text(self, position = wx.Point(0,0), scale = 1.0, rotation = 0):
+ """
+ position (wx.Point) - x/y Position to draw in scene
+ scale (float) - Scale
+ rotation (int) - Rotation in degree
+
+ Draws the text to the scene
+ """
+ #Enable necessary functions
+ glColor(1,1,1,1)
+ glEnable(GL_TEXTURE_2D)
+ glEnable(GL_ALPHA_TEST) #Enable alpha test
+ glAlphaFunc(GL_GREATER, 0)
+ glEnable(GL_BLEND) #Enable blending
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+ #Bind texture
+ glBindTexture(GL_TEXTURE_2D, self._texture)
+
+ ow, oh = self._text_size
+ w , h = self._texture_size
+ #Perform transformations
+ glPushMatrix()
+ glTranslated(position.x, position.y, 0)
+ glRotate(-rotation, 0, 0, 1)
+ glScaled(scale, scale, scale)
+ if self._centered:
+ glTranslate(-w/2, -oh/2, 0)
+ #Draw vertices
+ glBegin(GL_QUADS)
+ glTexCoord2f(0,0); glVertex2f(0,0)
+ glTexCoord2f(0,1); glVertex2f(0,h)
+ glTexCoord2f(1,1); glVertex2f(w,h)
+ glTexCoord2f(1,0); glVertex2f(w,0)
+ glEnd()
+ glPopMatrix()
+
+ #Disable features
+ glDisable(GL_BLEND)
+ glDisable(GL_ALPHA_TEST)
+ glDisable(GL_TEXTURE_2D)
+
+ def createTexture(self):
+ """
+ Creates a texture from the settings saved in TextElement, to be able to use normal
+ system fonts conviently a wx.MemoryDC is used to draw on a wx.Bitmap. As wxwidgets
+ device contexts don't support alpha at all it is necessary to apply a little hack
+ to preserve antialiasing without sticking to a fixed background color:
+
+ We draw the bmp in b/w mode so we can use its data as a alpha channel for a solid
+ color bitmap which after GL_ALPHA_TEST and GL_BLEND will show a nicely antialiased
+ text on any surface.
+
+ To access the raw pixel data the bmp gets converted to a wx.Image. Now we just have
+ to merge our foreground color with the alpha data we just created and push it all
+ into a OpenGL texture and we are DONE *inhalesdelpy*
+
+ DRAWBACK of the whole conversion thing is a really long time for creating the
+ texture. If you see any optimizations that could save time PLEASE CREATE A PATCH!!!
+ """
+ # get a memory dc
+ dc = wx.MemoryDC()
+
+ # set our font
+ dc.SetFont(self._font)
+
+ # Approximate extend to next power of 2 and create our bitmap
+ # REMARK: You wouldn't believe how much fucking speed this little
+ # sucker gains compared to sizes not of the power of 2. It's like
+ # 500ms --> 0.5ms (on my ATI-GPU powered Notebook). On Sams nvidia
+ # machine there don't seem to occur any losses...bad drivers?
+ ow, oh = dc.GetMultiLineTextExtent(self._text)[:2]
+ w, h = self._getUpper2Base(ow), self._getUpper2Base(oh)
+
+ self._text_size = wx.Size(ow,oh)
+ self._texture_size = wx.Size(w,h)
+ bmp = wx.EmptyBitmap(w,h)
+
+
+ #Draw in b/w mode to bmp so we can use it as alpha channel
+ dc.SelectObject(bmp)
+ dc.SetBackground(wx.BLACK_BRUSH)
+ dc.Clear()
+ dc.SetTextForeground(wx.WHITE)
+ x,y = 0,0
+ centered = self.centered
+ for line in self._lines:
+ if not line: line = ' '
+ tw, th = dc.GetTextExtent(line)
+ if centered:
+ x = int(round((w-tw)/2))
+ dc.DrawText(line, x, y)
+ x = 0
+ y += th
+ #Release the dc
+ dc.SelectObject(wx.NullBitmap)
+ del dc
+
+ #Generate a correct RGBA data string from our bmp
+ """
+ NOTE: You could also use wx.AlphaPixelData to access the pixel data
+ in 'bmp' directly, but the iterator given by it is much slower than
+ first converting to an image and using wx.Image.GetData().
+ """
+ img = wx.ImageFromBitmap(bmp)
+ alpha = img.GetData()
+
+ if isinstance(self._foreground, wx.Color):
+ """
+ If we have a static color...
+ """
+ r,g,b = self._foreground.Get()
+ color = "%c%c%c" % (chr(r), chr(g), chr(b))
+
+ data = ''
+ for i in xrange(0, len(alpha)-1, 3):
+ data += color + alpha[i]
+
+ elif isinstance(self._foreground, wx.Bitmap):
+ """
+ If we have a bitmap...
+ """
+ bg_img = wx.ImageFromBitmap(self._foreground)
+ bg = bg_img.GetData()
+ bg_width = self._foreground.GetWidth()
+ bg_height = self._foreground.GetHeight()
+
+ data = ''
+
+ for y in xrange(0, h):
+ for x in xrange(0, w):
+ if (y > (bg_height-1)) or (x > (bg_width-1)):
+ color = "%c%c%c" % (chr(0),chr(0),chr(0))
+ else:
+ pos = (x+y*bg_width) * 3
+ color = bg[pos:pos+3]
+ data += color + alpha[(x+y*w)*3]
+
+
+ # now convert it to ogl texture
+ self._texture = glGenTextures(1)
+ glBindTexture(GL_TEXTURE_2D, self._texture)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 2)
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
+
+ def deleteTexture(self):
+ """
+ Deletes the OpenGL texture object
+ """
+ if self._texture:
+ if glIsTexture(self._texture):
+ glDeleteTextures(self._texture)
+ else:
+ self._texture = None
+
+ def bind(self):
+ """
+ Increase refcount
+ """
+ self._owner_cnt += 1
+
+ def release(self):
+ """
+ Decrease refcount
+ """
+ self._owner_cnt -= 1
+
+ def isBound(self):
+ """
+ Return refcount
+ """
+ return self._owner_cnt
+
+ def __del__(self):
+ """
+ Destructor
+ """
+ self.deleteTexture()
+
+ #---Getters/Setters
+
+ def getText(self): return self._text
+ def getFont(self): return self._font
+ def getForeground(self): return self._foreground
+ def getCentered(self): return self._centered
+ def getTexture(self): return self._texture
+ def getTexture_size(self): return self._texture_size
+
+ def getOwner_cnt(self): return self._owner_cnt
+ def setOwner_cnt(self, value):
+ self._owner_cnt = value
+
+ #---Properties
+
+ text = property(getText, None, None, "Text of the object")
+ font = property(getFont, None, None, "Font of the object")
+ foreground = property(getForeground, None, None, "Color of the text")
+ centered = property(getCentered, None, None, "Is text centered")
+ owner_cnt = property(getOwner_cnt, setOwner_cnt, None, "Owner count")
+ texture = property(getTexture, None, None, "Used texture")
+ texture_size = property(getTexture_size, None, None, "Size of the used texture")
+
+
+class Text(object):
+ """
+ A simple class for using System Fonts to display text in
+ an OpenGL scene. The Text adds a global Cache of already
+ created text elements to TextElement's base functionality
+ so you can save some memory and increase speed
+ """
+ _texts = [] #Global cache for TextElements
+
+ def __init__(self,
+ text = 'Text',
+ font = None,
+ font_size = 8,
+ foreground = wx.BLACK,
+ centered = False,
+ bold = False):
+ """
+ text (string) - displayed text
+ font (wx.Font) - if None, system default font will be used with font_size
+ font_size (int) - font size in points
+ foreground (wx.Color) - Color of the text
+ or (wx.Bitmap) - Bitmap to overlay the text with
+ centered (bool) - should the text drawn centered towards position?
+
+ Initializes the text object
+ """
+ #Init/save variables
+ self._aloc_text = None
+ self._text = text
+ self._font_size = font_size
+ self._foreground= foreground
+ self._centered = centered
+
+ #Check if we are offered a font
+ if not font:
+ #if not use the system default
+ self._font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
+ else:
+ #save it
+ self._font = font
+
+ if bold: self._font.SetWeight(wx.FONTWEIGHT_BOLD)
+
+ #Bind us to our texture
+ self._initText()
+
+ #---Internal helpers
+
+ def _initText(self):
+ """
+ Initializes/Reinitializes the Text object by binding it
+ to a TextElement suitable for its current settings
+ """
+ #Check if we already bound to a texture
+ if self._aloc_text:
+ #if so release it
+ self._aloc_text.release()
+ if not self._aloc_text.isBound():
+ self._texts.remove(self._aloc_text)
+ self._aloc_text = None
+
+ #Adjust our font
+ self._font.SetPointSize(self._font_size)
+
+ #Search for existing element in our global buffer
+ for element in self._texts:
+ if element.text == self._text and\
+ element.font == self._font and\
+ element.foreground == self._foreground and\
+ element.centered == self._centered:
+ # We already exist in global buffer ;-)
+ element.bind()
+ self._aloc_text = element
+ break
+
+ if not self._aloc_text:
+ # We are not in the global buffer, let's create ourselves
+ aloc_text = self._aloc_text = TextElement(self._text,
+ self._font,
+ self._foreground,
+ self._centered)
+ aloc_text.bind()
+ self._texts.append(aloc_text)
+
+ def __del__(self):
+ """
+ Destructor
+ """
+ aloc_text = self._aloc_text
+ aloc_text.release()
+ if not aloc_text.isBound():
+ self._texts.remove(aloc_text)
+
+ #---Functions
+
+ def draw_text(self, position = wx.Point(0,0), scale = 1.0, rotation = 0):
+ """
+ position (wx.Point) - x/y Position to draw in scene
+ scale (float) - Scale
+ rotation (int) - Rotation in degree
+
+ Draws the text to the scene
+ """
+
+ self._aloc_text.draw_text(position, scale, rotation)
+
+ #---Setter/Getter
+
+ def getText(self): return self._text
+ def setText(self, value, reinit = True):
+ """
+ value (bool) - New Text
+ reinit (bool) - Create a new texture
+
+ Sets a new text
+ """
+ self._text = value
+ if reinit:
+ self._initText()
+
+ def getFont(self): return self._font
+ def setFont(self, value, reinit = True):
+ """
+ value (bool) - New Font
+ reinit (bool) - Create a new texture
+
+ Sets a new font
+ """
+ self._font = value
+ if reinit:
+ self._initText()
+
+ def getFont_size(self): return self._font_size
+ def setFont_size(self, value, reinit = True):
+ """
+ value (bool) - New font size
+ reinit (bool) - Create a new texture
+
+ Sets a new font size
+ """
+ self._font_size = value
+ if reinit:
+ self._initText()
+
+ def getForeground(self): return self._foreground
+ def setForeground(self, value, reinit = True):
+ """
+ value (bool) - New centered value
+ reinit (bool) - Create a new texture
+
+ Sets a new value for 'centered'
+ """
+ self._foreground = value
+ if reinit:
+ self._initText()
+
+ def getCentered(self): return self._centered
+ def setCentered(self, value, reinit = True):
+ """
+ value (bool) - New centered value
+ reinit (bool) - Create a new texture
+
+ Sets a new value for 'centered'
+ """
+ self._centered = value
+ if reinit:
+ self._initText()
+
+ def get_size(self):
+ """
+ Returns a text size tuple
+ """
+ return self._aloc_text._text_size
+
+ def getTexture_size(self):
+ """
+ Returns a texture size tuple
+ """
+ return self._aloc_text.texture_size
+
+ def getTextElement(self):
+ """
+ Returns the text element bound to the Text class
+ """
+ return self._aloc_text
+
+ def getTexture(self):
+ """
+ Returns the texture of the bound TextElement
+ """
+ return self._aloc_text.texture
+
+
+ #---Properties
+
+ text = property(getText, setText, None, "Text of the object")
+ font = property(getFont, setFont, None, "Font of the object")
+ font_size = property(getFont_size, setFont_size, None, "Font size")
+ foreground = property(getForeground, setForeground, None, "Color/Overlay bitmap of the text")
+ centered = property(getCentered, setCentered, None, "Display the text centered")
+ texture_size = property(getTexture_size, None, None, "Size of the used texture")
+ texture = property(getTexture, None, None, "Texture of bound TextElement")
+ text_element = property(getTextElement,None , None, "TextElement bound to this class")
+
+#Optimize critical functions
+if psyco and not psyco_optimized:
+ psyco.bind(TextElement.createTexture)
+ psyco_optimized = True
diff --git a/gr-wxgui/src/python/plotter/plotter_base.py b/gr-wxgui/src/python/plotter/plotter_base.py
new file mode 100644
index 000000000..96a1869da
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/plotter_base.py
@@ -0,0 +1,390 @@
+#
+# Copyright 2008 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 wx
+import wx.glcanvas
+from OpenGL.GL import *
+from gnuradio.wxgui import common
+import threading
+import gltext
+import math
+import time
+
+BACKGROUND_COLOR_SPEC = (1, 0.976, 1, 1) #creamy white
+GRID_LINE_COLOR_SPEC = (0, 0, 0) #black
+TICK_TEXT_FONT_SIZE = 9
+TITLE_TEXT_FONT_SIZE = 13
+UNITS_TEXT_FONT_SIZE = 9
+TICK_LABEL_PADDING = 5
+POINT_LABEL_FONT_SIZE = 8
+POINT_LABEL_COLOR_SPEC = (1, 1, .5)
+POINT_LABEL_PADDING = 3
+
+##################################################
+# OpenGL WX Plotter Canvas
+##################################################
+class _plotter_base(wx.glcanvas.GLCanvas):
+ """!
+ Plotter base class for all plot types.
+ """
+
+ def __init__(self, parent):
+ """!
+ Create a new plotter base.
+ Initialize GL and register events.
+ @param parent the parent widgit
+ """
+ self._semaphore = threading.Semaphore(1)
+ wx.glcanvas.GLCanvas.__init__(self, parent, -1)
+ self.changed(False)
+ self._gl_init_flag = False
+ self._resized_flag = True
+ self._update_ts = 0
+ self.Bind(wx.EVT_PAINT, self._on_paint)
+ self.Bind(wx.EVT_SIZE, self._on_size)
+
+ def lock(self): self._semaphore.acquire(True)
+ def unlock(self): self._semaphore.release()
+
+ def _on_size(self, event):
+ """!
+ Flag the resize event.
+ The paint event will handle the actual resizing.
+ """
+ self._resized_flag = True
+
+ def _on_paint(self, event):
+ """!
+ Respond to paint events, call update.
+ Initialize GL if this is the first paint event.
+ """
+ self.SetCurrent()
+ #check if gl was initialized
+ if not self._gl_init_flag:
+ glClearColor(*BACKGROUND_COLOR_SPEC)
+ self._gl_init()
+ self._gl_init_flag = True
+ #check for a change in window size
+ if self._resized_flag:
+ self.lock()
+ self.width, self.height = self.GetSize()
+ glViewport(0, 0, self.width, self.height)
+ glMatrixMode(GL_PROJECTION)
+ glLoadIdentity()
+ glOrtho(0, self.width, self.height, 0, 1, 0)
+ glMatrixMode(GL_MODELVIEW)
+ glLoadIdentity()
+ glViewport(0, 0, self.width, self.height)
+ self._resized_flag = False
+ self.changed(True)
+ self.unlock()
+ self.draw()
+
+ def update(self):
+ """!
+ Force a paint event.
+ Record the timestamp.
+ """
+ wx.PostEvent(self, wx.PaintEvent())
+ self._update_ts = time.time()
+
+ def clear(self): glClear(GL_COLOR_BUFFER_BIT)
+
+ def changed(self, state=None):
+ """!
+ Set the changed flag if state is not None.
+ Otherwise return the changed flag.
+ """
+ if state is not None: self._changed = state
+ else: return self._changed
+
+##################################################
+# Grid Plotter Base Class
+##################################################
+class grid_plotter_base(_plotter_base):
+
+ def __init__(self, parent, padding):
+ _plotter_base.__init__(self, parent)
+ self.padding_top, self.padding_right, self.padding_bottom, self.padding_left = padding
+ #store title and unit strings
+ self.set_title('Title')
+ self.set_x_label('X Label')
+ self.set_y_label('Y Label')
+ #init the grid to some value
+ self.set_x_grid(-1, 1, 1)
+ self.set_y_grid(-1, 1, 1)
+ #setup point label
+ self.enable_point_label(False)
+ self._mouse_coordinate = None
+ self.Bind(wx.EVT_MOTION, self._on_motion)
+ self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave_window)
+
+ def _on_motion(self, event):
+ """!
+ Mouse motion, record the position X, Y.
+ """
+ self.lock()
+ self._mouse_coordinate = event.GetPosition()
+ #update based on last known update time
+ if time.time() - self._update_ts > 0.03: self.update()
+ self.unlock()
+
+ def _on_leave_window(self, event):
+ """!
+ Mouse leave window, set the position to None.
+ """
+ self.lock()
+ self._mouse_coordinate = None
+ self.update()
+ self.unlock()
+
+ def enable_point_label(self, enable=None):
+ """!
+ Enable/disable the point label.
+ @param enable true to enable
+ @return the enable state when None
+ """
+ if enable is None: return self._enable_point_label
+ self.lock()
+ self._enable_point_label = enable
+ self.changed(True)
+ self.unlock()
+
+ def set_title(self, title):
+ """!
+ Set the title.
+ @param title the title string
+ """
+ self.lock()
+ self.title = title
+ self.changed(True)
+ self.unlock()
+
+ def set_x_label(self, x_label, x_units=''):
+ """!
+ Set the x label and units.
+ @param x_label the x label string
+ @param x_units the x units string
+ """
+ self.lock()
+ self.x_label = x_label
+ self.x_units = x_units
+ self.changed(True)
+ self.unlock()
+
+ def set_y_label(self, y_label, y_units=''):
+ """!
+ Set the y label and units.
+ @param y_label the y label string
+ @param y_units the y units string
+ """
+ self.lock()
+ self.y_label = y_label
+ self.y_units = y_units
+ self.changed(True)
+ self.unlock()
+
+ def set_x_grid(self, x_min, x_max, x_step, x_scalar=1.0):
+ """!
+ Set the x grid parameters.
+ @param x_min the left-most value
+ @param x_max the right-most value
+ @param x_step the grid spacing
+ @param x_scalar the scalar factor
+ """
+ self.lock()
+ self.x_min = float(x_min)
+ self.x_max = float(x_max)
+ self.x_step = float(x_step)
+ self.x_scalar = float(x_scalar)
+ self.changed(True)
+ self.unlock()
+
+ def set_y_grid(self, y_min, y_max, y_step, y_scalar=1.0):
+ """!
+ Set the y grid parameters.
+ @param y_min the bottom-most value
+ @param y_max the top-most value
+ @param y_step the grid spacing
+ @param y_scalar the scalar factor
+ """
+ self.lock()
+ self.y_min = float(y_min)
+ self.y_max = float(y_max)
+ self.y_step = float(y_step)
+ self.y_scalar = float(y_scalar)
+ self.changed(True)
+ self.unlock()
+
+ def _draw_grid(self):
+ """!
+ Draw the border, grid, title, and units.
+ """
+ ##################################################
+ # Draw Border
+ ##################################################
+ glColor3f(*GRID_LINE_COLOR_SPEC)
+ glBegin(GL_LINE_LOOP)
+ glVertex3f(self.padding_left, self.padding_top, 0)
+ glVertex3f(self.width - self.padding_right, self.padding_top, 0)
+ glVertex3f(self.width - self.padding_right, self.height - self.padding_bottom, 0)
+ glVertex3f(self.padding_left, self.height - self.padding_bottom, 0)
+ glEnd()
+ ##################################################
+ # Draw Grid X
+ ##################################################
+ for tick in self._get_ticks(self.x_min, self.x_max, self.x_step, self.x_scalar):
+ scaled_tick = (self.width-self.padding_left-self.padding_right)*\
+ (tick/self.x_scalar-self.x_min)/(self.x_max-self.x_min) + self.padding_left
+ glColor3f(*GRID_LINE_COLOR_SPEC)
+ self._draw_line(
+ (scaled_tick, self.padding_top, 0),
+ (scaled_tick, self.height-self.padding_bottom, 0),
+ )
+ txt = self._get_tick_label(tick)
+ w, h = txt.get_size()
+ txt.draw_text(wx.Point(scaled_tick-w/2, self.height-self.padding_bottom+TICK_LABEL_PADDING))
+ ##################################################
+ # Draw Grid Y
+ ##################################################
+ for tick in self._get_ticks(self.y_min, self.y_max, self.y_step, self.y_scalar):
+ scaled_tick = (self.height-self.padding_top-self.padding_bottom)*\
+ (1 - (tick/self.y_scalar-self.y_min)/(self.y_max-self.y_min)) + self.padding_top
+ glColor3f(*GRID_LINE_COLOR_SPEC)
+ self._draw_line(
+ (self.padding_left, scaled_tick, 0),
+ (self.width-self.padding_right, scaled_tick, 0),
+ )
+ txt = self._get_tick_label(tick)
+ w, h = txt.get_size()
+ txt.draw_text(wx.Point(self.padding_left-w-TICK_LABEL_PADDING, scaled_tick-h/2))
+ ##################################################
+ # Draw Title
+ ##################################################
+ #draw x units
+ txt = gltext.Text(self.title, bold=True, font_size=TITLE_TEXT_FONT_SIZE, centered=True)
+ txt.draw_text(wx.Point(self.width/2.0, .5*self.padding_top))
+ ##################################################
+ # Draw Labels
+ ##################################################
+ #draw x labels
+ x_label_str = self.x_units and "%s (%s)"%(self.x_label, self.x_units) or self.x_label
+ txt = gltext.Text(x_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True)
+ txt.draw_text(wx.Point(
+ (self.width-self.padding_left-self.padding_right)/2.0 + self.padding_left,
+ self.height-.25*self.padding_bottom,
+ )
+ )
+ #draw y labels
+ y_label_str = self.y_units and "%s (%s)"%(self.y_label, self.y_units) or self.y_label
+ txt = gltext.Text(y_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True)
+ txt.draw_text(wx.Point(
+ .25*self.padding_left,
+ (self.height-self.padding_top-self.padding_bottom)/2.0 + self.padding_top,
+ ), rotation=90,
+ )
+
+ def _get_tick_label(self, tick):
+ """!
+ Format the tick value and create a gl text.
+ @param tick the floating point tick value
+ @return the tick label text
+ """
+ tick_str = common.label_format(tick)
+ return gltext.Text(tick_str, font_size=TICK_TEXT_FONT_SIZE)
+
+ def _get_ticks(self, min, max, step, scalar):
+ """!
+ Determine the positions for the ticks.
+ @param min the lower bound
+ @param max the upper bound
+ @param step the grid spacing
+ @param scalar the grid scaling
+ @return a list of tick positions between min and max
+ """
+ #cast to float
+ min = float(min)
+ max = float(max)
+ step = float(step)
+ #check for valid numbers
+ assert step > 0
+ assert max > min
+ assert max - min > step
+ #determine the start and stop value
+ start = int(math.ceil(min/step))
+ stop = int(math.floor(max/step))
+ return [i*step*scalar for i in range(start, stop+1)]
+
+ def _draw_line(self, coor1, coor2):
+ """!
+ Draw a line from coor1 to coor2.
+ @param corr1 a tuple of x, y, z
+ @param corr2 a tuple of x, y, z
+ """
+ glBegin(GL_LINES)
+ glVertex3f(*coor1)
+ glVertex3f(*coor2)
+ glEnd()
+
+ def _draw_rect(self, x, y, width, height, fill=True):
+ """!
+ Draw a rectangle on the x, y plane.
+ X and Y are the top-left corner.
+ @param x the left position of the rectangle
+ @param y the top position of the rectangle
+ @param width the width of the rectangle
+ @param height the height of the rectangle
+ @param fill true to color inside of rectangle
+ """
+ glBegin(fill and GL_QUADS or GL_LINE_LOOP)
+ glVertex2f(x, y)
+ glVertex2f(x+width, y)
+ glVertex2f(x+width, y+height)
+ glVertex2f(x, y+height)
+ glEnd()
+
+ def _draw_point_label(self):
+ """!
+ Draw the point label for the last mouse motion coordinate.
+ The mouse coordinate must be an X, Y tuple.
+ The label will be drawn at the X, Y coordinate.
+ The values of the X, Y coordinate will be scaled to the current X, Y bounds.
+ """
+ if not self.enable_point_label(): return
+ if not self._mouse_coordinate: return
+ x, y = self._mouse_coordinate
+ if x < self.padding_left or x > self.width-self.padding_right: return
+ if y < self.padding_top or y > self.height-self.padding_bottom: return
+ #scale to window bounds
+ x_win_scalar = float(x - self.padding_left)/(self.width-self.padding_left-self.padding_right)
+ y_win_scalar = float((self.height - y) - self.padding_bottom)/(self.height-self.padding_top-self.padding_bottom)
+ #scale to grid bounds
+ x_val = self.x_scalar*(x_win_scalar*(self.x_max-self.x_min) + self.x_min)
+ y_val = self.y_scalar*(y_win_scalar*(self.y_max-self.y_min) + self.y_min)
+ #create text
+ label_str = self._populate_point_label(x_val, y_val)
+ txt = gltext.Text(label_str, font_size=POINT_LABEL_FONT_SIZE)
+ w, h = txt.get_size()
+ #draw rect + text
+ glColor3f(*POINT_LABEL_COLOR_SPEC)
+ if x > self.width/2: x -= w+2*POINT_LABEL_PADDING
+ self._draw_rect(x, y-h-2*POINT_LABEL_PADDING, w+2*POINT_LABEL_PADDING, h+2*POINT_LABEL_PADDING)
+ txt.draw_text(wx.Point(x+POINT_LABEL_PADDING, y-h-POINT_LABEL_PADDING))
diff --git a/gr-wxgui/src/python/plotter/waterfall_plotter.py b/gr-wxgui/src/python/plotter/waterfall_plotter.py
new file mode 100644
index 000000000..88e2b4dc1
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/waterfall_plotter.py
@@ -0,0 +1,282 @@
+#
+# Copyright 2008 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 wx
+from plotter_base import grid_plotter_base
+from OpenGL.GL import *
+from gnuradio.wxgui import common
+import numpy
+import gltext
+import math
+
+LEGEND_LEFT_PAD = 7
+LEGEND_NUM_BLOCKS = 256
+LEGEND_NUM_LABELS = 9
+LEGEND_WIDTH = 8
+LEGEND_FONT_SIZE = 8
+LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
+PADDING = 35, 60, 40, 60 #top, right, bottom, left
+
+ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))
+
+def _get_rbga(red_pts, green_pts, blue_pts, alpha_pts=[(0, 0), (1, 0)]):
+ """!
+ Get an array of 256 rgba values where each index maps to a color.
+ The scaling for red, green, blue, alpha are specified in piece-wise functions.
+ The piece-wise functions consist of a set of x, y coordinates.
+ The x and y values of the coordinates range from 0 to 1.
+ The coordinates must be specified so that x increases with the index value.
+ Resulting values are calculated along the line formed between 2 coordinates.
+ @param *_pts an array of x,y coordinates for each color element
+ @return array of rbga values (4 bytes) each
+ """
+ def _fcn(x, pw):
+ for (x1, y1), (x2, y2) in zip(pw, pw[1:]):
+ #linear interpolation
+ if x <= x2: return float(y1 - y2)/(x1 - x2)*(x - x1) + y1
+ raise Exception
+ return [numpy.array(map(
+ lambda pw: int(255*_fcn(i/255.0, pw)),
+ (red_pts, green_pts, blue_pts, alpha_pts),
+ ), numpy.uint8).tostring() for i in range(0, 256)
+ ]
+
+COLORS = {
+ 'rgb1': _get_rbga( #http://www.ks.uiuc.edu/Research/vmd/vmd-1.7.1/ug/img47.gif
+ red_pts = [(0, 0), (.5, 0), (1, 1)],
+ green_pts = [(0, 0), (.5, 1), (1, 0)],
+ blue_pts = [(0, 1), (.5, 0), (1, 0)],
+ ),
+ 'rgb2': _get_rbga( #http://xtide.ldeo.columbia.edu/~krahmann/coledit/screen.jpg
+ red_pts = [(0, 0), (3.0/8, 0), (5.0/8, 1), (7.0/8, 1), (1, .5)],
+ green_pts = [(0, 0), (1.0/8, 0), (3.0/8, 1), (5.0/8, 1), (7.0/8, 0), (1, 0)],
+ blue_pts = [(0, .5), (1.0/8, 1), (3.0/8, 1), (5.0/8, 0), (1, 0)],
+ ),
+ 'rgb3': _get_rbga(
+ red_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 0), (1, 1)],
+ green_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 1), (1, 0)],
+ blue_pts = [(0, 0), (1.0/3.0, 1), (2.0/3.0, 0), (1, 0)],
+ ),
+ 'gray': _get_rbga(
+ red_pts = [(0, 0), (1, 1)],
+ green_pts = [(0, 0), (1, 1)],
+ blue_pts = [(0, 0), (1, 1)],
+ ),
+}
+
+##################################################
+# Waterfall Plotter
+##################################################
+class waterfall_plotter(grid_plotter_base):
+ def __init__(self, parent):
+ """!
+ Create a new channel plotter.
+ """
+ #init
+ grid_plotter_base.__init__(self, parent, PADDING)
+ self._resize_texture(False)
+ self._minimum = 0
+ self._maximum = 0
+ self._fft_size = 1
+ self._buffer = list()
+ self._pointer = 0
+ self._counter = 0
+ self.set_num_lines(0)
+ self.set_color_mode(COLORS.keys()[0])
+
+ def _gl_init(self):
+ """!
+ Run gl initialization tasks.
+ """
+ self._grid_compiled_list_id = glGenLists(1)
+ self._waterfall_texture = glGenTextures(1)
+
+ def draw(self):
+ """!
+ Draw the grid and waveforms.
+ """
+ self.lock()
+ #resize texture
+ self._resize_texture()
+ #store the grid drawing operations
+ if self.changed():
+ glNewList(self._grid_compiled_list_id, GL_COMPILE)
+ self._draw_grid()
+ self._draw_legend()
+ glEndList()
+ self.changed(False)
+ self.clear()
+ #draw the grid
+ glCallList(self._grid_compiled_list_id)
+ self._draw_waterfall()
+ self._draw_point_label()
+ #swap buffer into display
+ self.SwapBuffers()
+ self.unlock()
+
+ def _draw_waterfall(self):
+ """!
+ Draw the waterfall from the texture.
+ The texture is circularly filled and will wrap around.
+ Use matrix modeling to shift and scale the texture onto the coordinate plane.
+ """
+ #setup texture
+ glBindTexture(GL_TEXTURE_2D, self._waterfall_texture)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
+ glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
+ #write the buffer to the texture
+ while self._buffer:
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, self._pointer, self._fft_size, 1, GL_RGBA, GL_UNSIGNED_BYTE, self._buffer.pop(0))
+ self._pointer = (self._pointer + 1)%self._num_lines
+ #begin drawing
+ glEnable(GL_TEXTURE_2D)
+ glPushMatrix()
+ #matrix scaling
+ glTranslatef(self.padding_left+1, self.padding_top, 0)
+ glScalef(
+ float(self.width-self.padding_left-self.padding_right-1),
+ float(self.height-self.padding_top-self.padding_bottom-1),
+ 1.0,
+ )
+ #draw texture with wrapping
+ glBegin(GL_QUADS)
+ prop_y = float(self._pointer)/(self._num_lines-1)
+ prop_x = float(self._fft_size)/ceil_log2(self._fft_size)
+ off = 1.0/(self._num_lines-1)
+ glTexCoord2f(0, prop_y+1-off)
+ glVertex2f(0, 1)
+ glTexCoord2f(prop_x, prop_y+1-off)
+ glVertex2f(1, 1)
+ glTexCoord2f(prop_x, prop_y)
+ glVertex2f(1, 0)
+ glTexCoord2f(0, prop_y)
+ glVertex2f(0, 0)
+ glEnd()
+ glPopMatrix()
+ glDisable(GL_TEXTURE_2D)
+
+ def _populate_point_label(self, x_val, y_val):
+ """!
+ Get the text the will populate the point label.
+ Give the X value for the current point.
+ @param x_val the current x value
+ @param y_val the current y value
+ @return a value string with units
+ """
+ return '%s: %s %s'%(self.x_label, common.label_format(x_val), self.x_units)
+
+ def _draw_legend(self):
+ """!
+ Draw the color scale legend.
+ """
+ if not self._color_mode: return
+ legend_height = self.height-self.padding_top-self.padding_bottom
+ #draw each legend block
+ block_height = float(legend_height)/LEGEND_NUM_BLOCKS
+ x = self.width - self.padding_right + LEGEND_LEFT_PAD
+ for i in range(LEGEND_NUM_BLOCKS):
+ color = COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))]
+ glColor4f(*map(lambda c: ord(c)/255.0, color))
+ y = self.height - (i+1)*block_height - self.padding_bottom
+ self._draw_rect(x, y, LEGEND_WIDTH, block_height)
+ #draw rectangle around color scale border
+ glColor3f(*LEGEND_BORDER_COLOR_SPEC)
+ self._draw_rect(x, self.padding_top, LEGEND_WIDTH, legend_height, fill=False)
+ #draw each legend label
+ label_spacing = float(legend_height)/(LEGEND_NUM_LABELS-1)
+ x = self.width - (self.padding_right - LEGEND_LEFT_PAD - LEGEND_WIDTH)/2
+ for i in range(LEGEND_NUM_LABELS):
+ proportion = i/float(LEGEND_NUM_LABELS-1)
+ dB = proportion*(self._maximum - self._minimum) + self._minimum
+ y = self.height - i*label_spacing - self.padding_bottom
+ txt = gltext.Text('%ddB'%int(dB), font_size=LEGEND_FONT_SIZE, centered=True)
+ txt.draw_text(wx.Point(x, y))
+
+ def _resize_texture(self, flag=None):
+ """!
+ Create the texture to fit the fft_size X num_lines.
+ @param flag the set/unset or update flag
+ """
+ if flag is not None:
+ self._resize_texture_flag = flag
+ return
+ if not self._resize_texture_flag: return
+ self._buffer = list()
+ self._pointer = 0
+ if self._num_lines and self._fft_size:
+ glBindTexture(GL_TEXTURE_2D, self._waterfall_texture)
+ data = numpy.zeros(self._num_lines*self._fft_size*4, numpy.uint8).tostring()
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ceil_log2(self._fft_size), self._num_lines, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
+ self._resize_texture_flag = False
+
+ def set_color_mode(self, color_mode):
+ """!
+ Set the color mode.
+ New samples will be converted to the new color mode.
+ Old samples will not be recolorized.
+ @param color_mode the new color mode string
+ """
+ self.lock()
+ if color_mode in COLORS.keys():
+ self._color_mode = color_mode
+ self.changed(True)
+ self.update()
+ self.unlock()
+
+ def set_num_lines(self, num_lines):
+ """!
+ Set number of lines.
+ Powers of two only.
+ @param num_lines the new number of lines
+ """
+ self.lock()
+ self._num_lines = num_lines
+ self._resize_texture(True)
+ self.update()
+ self.unlock()
+
+ def set_samples(self, samples, minimum, maximum):
+ """!
+ Set the samples to the waterfall.
+ Convert the samples to color data.
+ @param samples the array of floats
+ @param minimum the minimum value to scale
+ @param maximum the maximum value to scale
+ """
+ self.lock()
+ #set the min, max values
+ if self._minimum != minimum or self._maximum != maximum:
+ self._minimum = minimum
+ self._maximum = maximum
+ self.changed(True)
+ if self._fft_size != len(samples):
+ self._fft_size = len(samples)
+ self._resize_texture(True)
+ #normalize the samples to min/max
+ samples = (samples - minimum)*float(255/(maximum-minimum))
+ samples = numpy.clip(samples, 0, 255) #clip
+ samples = numpy.array(samples, numpy.uint8)
+ #convert the samples to RGBA data
+ data = numpy.choose(samples, COLORS[self._color_mode]).tostring()
+ self._buffer.append(data)
+ self.unlock()
diff --git a/gr-wxgui/src/python/powermate.py b/gr-wxgui/src/python/powermate.py
index 7f54dff33..7f54dff33 100755..100644
--- a/gr-wxgui/src/python/powermate.py
+++ b/gr-wxgui/src/python/powermate.py
diff --git a/gr-wxgui/src/python/pubsub.py b/gr-wxgui/src/python/pubsub.py
new file mode 100644
index 000000000..18aa60603
--- /dev/null
+++ b/gr-wxgui/src/python/pubsub.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python
+#
+# Copyright 2008 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.
+#
+
+"""!
+Abstract GNU Radio publisher/subscriber interface
+
+This is a proof of concept implementation, will likely change significantly.
+"""
+
+class pubsub(dict):
+ def __init__(self):
+ self._publishers = { }
+ self._subscribers = { }
+ self._proxies = { }
+
+ def __missing__(self, key, value=None):
+ dict.__setitem__(self, key, value)
+ self._publishers[key] = None
+ self._subscribers[key] = []
+ self._proxies[key] = None
+
+ def __setitem__(self, key, val):
+ if not self.has_key(key):
+ self.__missing__(key, val)
+ elif self._proxies[key] is not None:
+ (p, newkey) = self._proxies[key]
+ p[newkey] = val
+ else:
+ dict.__setitem__(self, key, val)
+ for sub in self._subscribers[key]:
+ # Note this means subscribers will get called in the thread
+ # context of the 'set' caller.
+ sub(val)
+
+ def __getitem__(self, key):
+ if not self.has_key(key): self.__missing__(key)
+ if self._proxies[key] is not None:
+ (p, newkey) = self._proxies[key]
+ return p[newkey]
+ elif self._publishers[key] is not None:
+ return self._publishers[key]()
+ else:
+ return dict.__getitem__(self, key)
+
+ def publish(self, key, publisher):
+ if not self.has_key(key): self.__missing__(key)
+ if self._proxies[key] is not None:
+ (p, newkey) = self._proxies[key]
+ p.publish(newkey, publisher)
+ else:
+ self._publishers[key] = publisher
+
+ def subscribe(self, key, subscriber):
+ if not self.has_key(key): self.__missing__(key)
+ if self._proxies[key] is not None:
+ (p, newkey) = self._proxies[key]
+ p.subscribe(newkey, subscriber)
+ else:
+ self._subscribers[key].append(subscriber)
+
+ def unpublish(self, key):
+ if self._proxies[key] is not None:
+ (p, newkey) = self._proxies[key]
+ p.unpublish(newkey)
+ else:
+ self._publishers[key] = None
+
+ def unsubscribe(self, key, subscriber):
+ if self._proxies[key] is not None:
+ (p, newkey) = self._proxies[key]
+ p.unsubscribe(newkey, subscriber)
+ else:
+ self._subscribers[key].remove(subscriber)
+
+ def proxy(self, key, p, newkey=None):
+ if not self.has_key(key): self.__missing__(key)
+ if newkey is None: newkey = key
+ self._proxies[key] = (p, newkey)
+
+ def unproxy(self, key):
+ self._proxies[key] = None
+
+# Test code
+if __name__ == "__main__":
+ import sys
+ o = pubsub()
+
+ # Non-existent key gets auto-created with None value
+ print "Auto-created key 'foo' value:", o['foo']
+
+ # Add some subscribers
+ # First is a bare function
+ def print_len(x):
+ print "len=%i" % (len(x), )
+ o.subscribe('foo', print_len)
+
+ # The second is a class member function
+ class subber(object):
+ def __init__(self, param):
+ self._param = param
+ def printer(self, x):
+ print self._param, `x`
+ s = subber('param')
+ o.subscribe('foo', s.printer)
+
+ # The third is a lambda function
+ o.subscribe('foo', lambda x: sys.stdout.write('val='+`x`+'\n'))
+
+ # Update key 'foo', will notify subscribers
+ print "Updating 'foo' with three subscribers:"
+ o['foo'] = 'bar';
+
+ # Remove first subscriber
+ o.unsubscribe('foo', print_len)
+
+ # Update now will only trigger second and third subscriber
+ print "Updating 'foo' after removing a subscriber:"
+ o['foo'] = 'bar2';
+
+ # Publish a key as a function, in this case, a lambda function
+ o.publish('baz', lambda : 42)
+ print "Published value of 'baz':", o['baz']
+
+ # Unpublish the key
+ o.unpublish('baz')
+
+ # This will return None, as there is no publisher
+ print "Value of 'baz' with no publisher:", o['baz']
+
+ # Set 'baz' key, it gets cached
+ o['baz'] = 'bazzz'
+
+ # Now will return cached value, since no provider
+ print "Cached value of 'baz' after being set:", o['baz']
diff --git a/gr-wxgui/src/python/scope_window.py b/gr-wxgui/src/python/scope_window.py
new file mode 100644
index 000000000..0c7326b1d
--- /dev/null
+++ b/gr-wxgui/src/python/scope_window.py
@@ -0,0 +1,482 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import plotter
+import common
+import wx
+import numpy
+import time
+import pubsub
+from constants import *
+
+##################################################
+# Constants
+##################################################
+DEFAULT_FRAME_RATE = 30
+DEFAULT_WIN_SIZE = (600, 300)
+DEFAULT_V_SCALE = 1000
+TRIGGER_MODES = (
+ ('Off', 0),
+ ('Neg', -1),
+ ('Pos', +1),
+)
+TRIGGER_LEVELS = (
+ ('Auto', None),
+ ('+High', 0.75),
+ ('+Med', 0.5),
+ ('+Low', 0.25),
+ ('Zero', 0.0),
+ ('-Low', -0.25),
+ ('-Med', -0.5),
+ ('-High', -0.75),
+)
+CHANNEL_COLOR_SPECS = (
+ (0, 0, 1),
+ (0, 1, 0),
+ (1, 0, 0),
+ (1, 0, 1),
+)
+AUTORANGE_UPDATE_RATE = 0.5 #sec
+
+##################################################
+# Scope window control panel
+##################################################
+class control_panel(wx.Panel):
+ """!
+ A control panel with wx widgits to control the plotter and scope block.
+ """
+ def __init__(self, parent):
+ """!
+ Create a new control panel.
+ @param parent the wx parent window
+ """
+ self.parent = parent
+ wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
+ self.control_box = control_box = wx.BoxSizer(wx.VERTICAL)
+ #trigger options
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Trigger Options'), 0, wx.ALIGN_CENTER)
+ control_box.AddSpacer(2)
+ #trigger mode
+ self.trigger_mode_chooser = common.DropDownController(self, 'Mode', TRIGGER_MODES, parent, TRIGGER_MODE_KEY)
+ control_box.Add(self.trigger_mode_chooser, 0, wx.EXPAND)
+ #trigger level
+ self.trigger_level_chooser = common.DropDownController(self, 'Level', TRIGGER_LEVELS, parent, TRIGGER_LEVEL_KEY)
+ parent.subscribe(TRIGGER_MODE_KEY, lambda x: self.trigger_level_chooser.Disable(x==0))
+ control_box.Add(self.trigger_level_chooser, 0, wx.EXPAND)
+ #trigger channel
+ choices = [('Ch%d'%(i+1), i) for i in range(parent.num_inputs)]
+ self.trigger_channel_chooser = common.DropDownController(self, 'Channel', choices, parent, TRIGGER_CHANNEL_KEY)
+ parent.subscribe(TRIGGER_MODE_KEY, lambda x: self.trigger_channel_chooser.Disable(x==0))
+ control_box.Add(self.trigger_channel_chooser, 0, wx.EXPAND)
+ #axes options
+ SPACING = 15
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Axes Options'), 0, wx.ALIGN_CENTER)
+ control_box.AddSpacer(2)
+ ##################################################
+ # Scope Mode Box
+ ##################################################
+ self.scope_mode_box = wx.BoxSizer(wx.VERTICAL)
+ control_box.Add(self.scope_mode_box, 0, wx.EXPAND)
+ #x axis divs
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ self.scope_mode_box.Add(hbox, 0, wx.EXPAND)
+ hbox.Add(wx.StaticText(self, -1, ' Secs/Div '), 1, wx.ALIGN_CENTER_VERTICAL)
+ x_buttons = common.IncrDecrButtons(self, self._on_incr_t_divs, self._on_decr_t_divs)
+ hbox.Add(x_buttons, 0, wx.ALIGN_CENTER_VERTICAL)
+ hbox.AddSpacer(SPACING)
+ #y axis divs
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ self.scope_mode_box.Add(hbox, 0, wx.EXPAND)
+ hbox.Add(wx.StaticText(self, -1, ' Units/Div '), 1, wx.ALIGN_CENTER_VERTICAL)
+ y_buttons = common.IncrDecrButtons(self, self._on_incr_y_divs, self._on_decr_y_divs)
+ parent.subscribe(AUTORANGE_KEY, y_buttons.Disable)
+ hbox.Add(y_buttons, 0, wx.ALIGN_CENTER_VERTICAL)
+ hbox.AddSpacer(SPACING)
+ #y axis ref lvl
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ self.scope_mode_box.Add(hbox, 0, wx.EXPAND)
+ hbox.Add(wx.StaticText(self, -1, ' Y Offset '), 1, wx.ALIGN_CENTER_VERTICAL)
+ y_off_buttons = common.IncrDecrButtons(self, self._on_incr_y_off, self._on_decr_y_off)
+ parent.subscribe(AUTORANGE_KEY, y_off_buttons.Disable)
+ hbox.Add(y_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL)
+ hbox.AddSpacer(SPACING)
+ ##################################################
+ # XY Mode Box
+ ##################################################
+ self.xy_mode_box = wx.BoxSizer(wx.VERTICAL)
+ control_box.Add(self.xy_mode_box, 0, wx.EXPAND)
+ #x and y channel
+ CHOOSER_WIDTH = 60
+ CENTER_SPACING = 10
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ self.xy_mode_box.Add(hbox, 0, wx.EXPAND)
+ choices = [('Ch%d'%(i+1), i) for i in range(parent.num_inputs)]
+ self.channel_x_chooser = common.DropDownController(self, 'X Ch', choices, parent, SCOPE_X_CHANNEL_KEY, (CHOOSER_WIDTH, -1))
+ hbox.Add(self.channel_x_chooser, 0, wx.EXPAND)
+ hbox.AddSpacer(CENTER_SPACING)
+ self.channel_y_chooser = common.DropDownController(self, 'Y Ch', choices, parent, SCOPE_Y_CHANNEL_KEY, (CHOOSER_WIDTH, -1))
+ hbox.Add(self.channel_y_chooser, 0, wx.EXPAND)
+ #div controls
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ self.xy_mode_box.Add(hbox, 0, wx.EXPAND)
+ hbox.Add(wx.StaticText(self, -1, ' X/Div '), 1, wx.ALIGN_CENTER_VERTICAL)
+ x_buttons = common.IncrDecrButtons(self, self._on_incr_x_divs, self._on_decr_x_divs)
+ parent.subscribe(AUTORANGE_KEY, x_buttons.Disable)
+ hbox.Add(x_buttons, 0, wx.ALIGN_CENTER_VERTICAL)
+ hbox.AddSpacer(CENTER_SPACING)
+ hbox.Add(wx.StaticText(self, -1, ' Y/Div '), 1, wx.ALIGN_CENTER_VERTICAL)
+ y_buttons = common.IncrDecrButtons(self, self._on_incr_y_divs, self._on_decr_y_divs)
+ parent.subscribe(AUTORANGE_KEY, y_buttons.Disable)
+ hbox.Add(y_buttons, 0, wx.ALIGN_CENTER_VERTICAL)
+ #offset controls
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ self.xy_mode_box.Add(hbox, 0, wx.EXPAND)
+ hbox.Add(wx.StaticText(self, -1, ' X Off '), 1, wx.ALIGN_CENTER_VERTICAL)
+ x_off_buttons = common.IncrDecrButtons(self, self._on_incr_x_off, self._on_decr_x_off)
+ parent.subscribe(AUTORANGE_KEY, x_off_buttons.Disable)
+ hbox.Add(x_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL)
+ hbox.AddSpacer(CENTER_SPACING)
+ hbox.Add(wx.StaticText(self, -1, ' Y Off '), 1, wx.ALIGN_CENTER_VERTICAL)
+ y_off_buttons = common.IncrDecrButtons(self, self._on_incr_y_off, self._on_decr_y_off)
+ parent.subscribe(AUTORANGE_KEY, y_off_buttons.Disable)
+ hbox.Add(y_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL)
+ ##################################################
+ # End Special Boxes
+ ##################################################
+ #misc options
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Range Options'), 0, wx.ALIGN_CENTER)
+ #ac couple check box
+ self.ac_couple_check_box = common.CheckBoxController(self, 'AC Couple', parent, AC_COUPLE_KEY)
+ control_box.Add(self.ac_couple_check_box, 0, wx.ALIGN_LEFT)
+ #autorange check box
+ self.autorange_check_box = common.CheckBoxController(self, 'Autorange', parent, AUTORANGE_KEY)
+ control_box.Add(self.autorange_check_box, 0, wx.ALIGN_LEFT)
+ #run/stop
+ control_box.AddStretchSpacer()
+ self.scope_xy_mode_button = common.ToggleButtonController(self, parent, SCOPE_XY_MODE_KEY, 'Scope Mode', 'X:Y Mode')
+ parent.subscribe(SCOPE_XY_MODE_KEY, self._on_scope_xy_mode)
+ control_box.Add(self.scope_xy_mode_button, 0, wx.EXPAND)
+ #run/stop
+ self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
+ control_box.Add(self.run_button, 0, wx.EXPAND)
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+ ##################################################
+ # Event handlers
+ ##################################################
+ def _on_scope_xy_mode(self, mode):
+ self.scope_mode_box.ShowItems(not mode)
+ self.xy_mode_box.ShowItems(mode)
+ self.control_box.Layout()
+ #incr/decr divs
+ def _on_incr_t_divs(self, event):
+ self.parent.set_t_per_div(
+ common.get_clean_incr(self.parent[T_PER_DIV_KEY]))
+ def _on_decr_t_divs(self, event):
+ self.parent.set_t_per_div(
+ common.get_clean_decr(self.parent[T_PER_DIV_KEY]))
+ def _on_incr_x_divs(self, event):
+ self.parent.set_x_per_div(
+ common.get_clean_incr(self.parent[X_PER_DIV_KEY]))
+ def _on_decr_x_divs(self, event):
+ self.parent.set_x_per_div(
+ common.get_clean_decr(self.parent[X_PER_DIV_KEY]))
+ def _on_incr_y_divs(self, event):
+ self.parent.set_y_per_div(
+ common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
+ def _on_decr_y_divs(self, event):
+ self.parent.set_y_per_div(
+ common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
+ #incr/decr offset
+ def _on_incr_t_off(self, event):
+ self.parent.set_t_off(
+ self.parent[T_OFF_KEY] + self.parent[T_PER_DIV_KEY])
+ def _on_decr_t_off(self, event):
+ self.parent.set_t_off(
+ self.parent[T_OFF_KEY] - self.parent[T_PER_DIV_KEY])
+ def _on_incr_x_off(self, event):
+ self.parent.set_x_off(
+ self.parent[X_OFF_KEY] + self.parent[X_PER_DIV_KEY])
+ def _on_decr_x_off(self, event):
+ self.parent.set_x_off(
+ self.parent[X_OFF_KEY] - self.parent[X_PER_DIV_KEY])
+ def _on_incr_y_off(self, event):
+ self.parent.set_y_off(
+ self.parent[Y_OFF_KEY] + self.parent[Y_PER_DIV_KEY])
+ def _on_decr_y_off(self, event):
+ self.parent.set_y_off(
+ self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY])
+
+##################################################
+# Scope window with plotter and control panel
+##################################################
+class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ frame_rate,
+ num_inputs,
+ sample_rate_key,
+ t_scale,
+ v_scale,
+ ac_couple,
+ xy_mode,
+ scope_trigger_level_key,
+ scope_trigger_mode_key,
+ scope_trigger_channel_key,
+ msg_key,
+ ):
+ pubsub.pubsub.__init__(self)
+ #check num inputs
+ assert num_inputs <= len(CHANNEL_COLOR_SPECS)
+ #setup
+ self.ext_controller = controller
+ self.num_inputs = num_inputs
+ self.sample_rate_key = sample_rate_key
+ autorange = v_scale is None
+ self.autorange_ts = 0
+ if v_scale is None: v_scale = 1
+ self.frame_rate_ts = 0
+ self._init = False #HACK
+ #scope keys
+ self.scope_trigger_level_key = scope_trigger_level_key
+ self.scope_trigger_mode_key = scope_trigger_mode_key
+ self.scope_trigger_channel_key = scope_trigger_channel_key
+ #init panel and plot
+ wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.channel_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.set_title(title)
+ self.plotter.enable_legend(True)
+ self.plotter.enable_point_label(True)
+ #setup the box with plot and controls
+ self.control_panel = control_panel(self)
+ main_box = wx.BoxSizer(wx.HORIZONTAL)
+ main_box.Add(self.plotter, 1, wx.EXPAND)
+ main_box.Add(self.control_panel, 0, wx.EXPAND)
+ self.SetSizerAndFit(main_box)
+ #initial setup
+ self._register_set_prop(self, RUNNING_KEY, True)
+ self._register_set_prop(self, AC_COUPLE_KEY, ac_couple)
+ self._register_set_prop(self, SCOPE_XY_MODE_KEY, xy_mode)
+ self._register_set_prop(self, AUTORANGE_KEY, autorange)
+ self._register_set_prop(self, T_PER_DIV_KEY, t_scale)
+ self._register_set_prop(self, X_PER_DIV_KEY, v_scale)
+ self._register_set_prop(self, Y_PER_DIV_KEY, v_scale)
+ self._register_set_prop(self, T_OFF_KEY, 0)
+ self._register_set_prop(self, X_OFF_KEY, 0)
+ self._register_set_prop(self, Y_OFF_KEY, 0)
+ self._register_set_prop(self, T_DIVS_KEY, 8)
+ self._register_set_prop(self, X_DIVS_KEY, 8)
+ self._register_set_prop(self, Y_DIVS_KEY, 8)
+ self._register_set_prop(self, SCOPE_X_CHANNEL_KEY, 0)
+ self._register_set_prop(self, SCOPE_Y_CHANNEL_KEY, num_inputs-1)
+ self._register_set_prop(self, FRAME_RATE_KEY, frame_rate)
+ self._register_set_prop(self, TRIGGER_CHANNEL_KEY, 0)
+ self._register_set_prop(self, TRIGGER_MODE_KEY, 1)
+ self._register_set_prop(self, TRIGGER_LEVEL_KEY, None)
+ #register events
+ self.ext_controller.subscribe(msg_key, self.handle_msg)
+ for key in (
+ T_PER_DIV_KEY, X_PER_DIV_KEY, Y_PER_DIV_KEY,
+ T_OFF_KEY, X_OFF_KEY, Y_OFF_KEY,
+ T_DIVS_KEY, X_DIVS_KEY, Y_DIVS_KEY,
+ SCOPE_XY_MODE_KEY,
+ SCOPE_X_CHANNEL_KEY,
+ SCOPE_Y_CHANNEL_KEY,
+ AUTORANGE_KEY,
+ AC_COUPLE_KEY,
+ ): self.subscribe(key, self.update_grid)
+ #initial update, dont do this here, wait for handle_msg #HACK
+ #self.update_grid()
+
+ def handle_msg(self, msg):
+ """!
+ Handle the message from the scope sink message queue.
+ Plot the list of arrays of samples onto the grid.
+ Each samples array gets its own channel.
+ @param msg the time domain data as a character array
+ """
+ if not self[RUNNING_KEY]: return
+ #check time elapsed
+ if time.time() - self.frame_rate_ts < 1.0/self[FRAME_RATE_KEY]: return
+ #convert to floating point numbers
+ samples = numpy.fromstring(msg, numpy.float32)
+ samps_per_ch = len(samples)/self.num_inputs
+ self.sampleses = [samples[samps_per_ch*i:samps_per_ch*(i+1)] for i in range(self.num_inputs)]
+ if not self._init: #HACK
+ self._init = True
+ self.update_grid()
+ #handle samples
+ self.handle_samples()
+ self.frame_rate_ts = time.time()
+
+ def handle_samples(self):
+ """!
+ Handle the cached samples from the scope input.
+ Perform ac coupling, triggering, and auto ranging.
+ """
+ sampleses = self.sampleses
+ #trigger level (must do before ac coupling)
+ self.ext_controller[self.scope_trigger_channel_key] = self[TRIGGER_CHANNEL_KEY]
+ self.ext_controller[self.scope_trigger_mode_key] = self[TRIGGER_MODE_KEY]
+ trigger_level = self[TRIGGER_LEVEL_KEY]
+ if trigger_level is None: self.ext_controller[self.scope_trigger_level_key] = ''
+ else:
+ samples = sampleses[self[TRIGGER_CHANNEL_KEY]]
+ self.ext_controller[self.scope_trigger_level_key] = \
+ trigger_level*(numpy.max(samples)-numpy.min(samples))/2 + numpy.average(samples)
+ #ac coupling
+ if self[AC_COUPLE_KEY]:
+ sampleses = [samples - numpy.average(samples) for samples in sampleses]
+ if self[SCOPE_XY_MODE_KEY]:
+ x_samples = sampleses[self[SCOPE_X_CHANNEL_KEY]]
+ y_samples = sampleses[self[SCOPE_Y_CHANNEL_KEY]]
+ #autorange
+ if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
+ x_min, x_max = common.get_min_max(x_samples)
+ y_min, y_max = common.get_min_max(y_samples)
+ #adjust the x per div
+ x_per_div = common.get_clean_num((x_max-x_min)/self[X_DIVS_KEY])
+ if x_per_div != self[X_PER_DIV_KEY]: self.set_x_per_div(x_per_div)
+ #adjust the x offset
+ x_off = x_per_div*round((x_max+x_min)/2/x_per_div)
+ if x_off != self[X_OFF_KEY]: self.set_x_off(x_off)
+ #adjust the y per div
+ y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
+ if y_per_div != self[Y_PER_DIV_KEY]: self.set_y_per_div(y_per_div)
+ #adjust the y offset
+ y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
+ if y_off != self[Y_OFF_KEY]: self.set_y_off(y_off)
+ self.autorange_ts = time.time()
+ #plot xy channel
+ self.plotter.set_waveform(
+ channel='XY',
+ samples=(x_samples, y_samples),
+ color_spec=CHANNEL_COLOR_SPECS[0],
+ )
+ #turn off each waveform
+ for i, samples in enumerate(sampleses):
+ self.plotter.set_waveform(
+ channel='Ch%d'%(i+1),
+ samples=[],
+ color_spec=CHANNEL_COLOR_SPECS[i],
+ )
+ else:
+ #autorange
+ if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
+ bounds = [common.get_min_max(samples) for samples in sampleses]
+ y_min = numpy.min(*[bound[0] for bound in bounds])
+ y_max = numpy.max(*[bound[1] for bound in bounds])
+ #adjust the y per div
+ y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
+ if y_per_div != self[Y_PER_DIV_KEY]: self.set_y_per_div(y_per_div)
+ #adjust the y offset
+ y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
+ if y_off != self[Y_OFF_KEY]: self.set_y_off(y_off)
+ self.autorange_ts = time.time()
+ #plot each waveform
+ for i, samples in enumerate(sampleses):
+ #number of samples to scale to the screen
+ num_samps = int(self[T_PER_DIV_KEY]*self[T_DIVS_KEY]*self.ext_controller[self.sample_rate_key])
+ #handle num samps out of bounds
+ if num_samps > len(samples):
+ self.set_t_per_div(
+ common.get_clean_decr(self[T_PER_DIV_KEY]))
+ elif num_samps < 2:
+ self.set_t_per_div(
+ common.get_clean_incr(self[T_PER_DIV_KEY]))
+ num_samps = 0
+ else:
+ #plot samples
+ self.plotter.set_waveform(
+ channel='Ch%d'%(i+1),
+ samples=samples[:num_samps],
+ color_spec=CHANNEL_COLOR_SPECS[i],
+ )
+ #turn XY channel off
+ self.plotter.set_waveform(
+ channel='XY',
+ samples=[],
+ color_spec=CHANNEL_COLOR_SPECS[0],
+ )
+ #update the plotter
+ self.plotter.update()
+
+ def update_grid(self, *args):
+ """!
+ Update the grid to reflect the current settings:
+ xy divisions, xy offset, xy mode setting
+ """
+ #grid parameters
+ t_per_div = self[T_PER_DIV_KEY]
+ x_per_div = self[X_PER_DIV_KEY]
+ y_per_div = self[Y_PER_DIV_KEY]
+ t_off = self[T_OFF_KEY]
+ x_off = self[X_OFF_KEY]
+ y_off = self[Y_OFF_KEY]
+ t_divs = self[T_DIVS_KEY]
+ x_divs = self[X_DIVS_KEY]
+ y_divs = self[Y_DIVS_KEY]
+ if self[SCOPE_XY_MODE_KEY]:
+ #update the x axis
+ self.plotter.set_x_label('Ch%d'%(self[SCOPE_X_CHANNEL_KEY]+1))
+ self.plotter.set_x_grid(
+ -1*x_per_div*x_divs/2.0 + x_off,
+ x_per_div*x_divs/2.0 + x_off,
+ x_per_div,
+ )
+ #update the y axis
+ self.plotter.set_y_label('Ch%d'%(self[SCOPE_Y_CHANNEL_KEY]+1))
+ self.plotter.set_y_grid(
+ -1*y_per_div*y_divs/2.0 + y_off,
+ y_per_div*y_divs/2.0 + y_off,
+ y_per_div,
+ )
+ else:
+ #update the t axis
+ coeff, exp, prefix = common.get_si_components(t_per_div*t_divs + t_off)
+ self.plotter.set_x_label('Time', prefix+'s')
+ self.plotter.set_x_grid(
+ t_off,
+ t_per_div*t_divs + t_off,
+ t_per_div,
+ 10**(-exp),
+ )
+ #update the y axis
+ self.plotter.set_y_label('Counts')
+ self.plotter.set_y_grid(
+ -1*y_per_div*y_divs/2.0 + y_off,
+ y_per_div*y_divs/2.0 + y_off,
+ y_per_div,
+ )
+ #redraw current sample
+ self.handle_samples()
diff --git a/gr-wxgui/src/python/scopesink2.py b/gr-wxgui/src/python/scopesink2.py
index 71fd7e128..5eee3efd5 100755..100644
--- a/gr-wxgui/src/python/scopesink2.py
+++ b/gr-wxgui/src/python/scopesink2.py
@@ -1,661 +1,45 @@
-#!/usr/bin/env python
#
-# Copyright 2003,2004,2006,2007 Free Software Foundation, Inc.
-#
+# Copyright 2008 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, gru, eng_notation
-from gnuradio.wxgui import stdgui2
-import wx
-import gnuradio.wxgui.plot as plot
-import numpy
-import threading
-import struct
-
-default_scopesink_size = (640, 240)
-default_v_scale = 1000
-default_frame_decim = gr.prefs().get_long('wxgui', 'frame_decim', 1)
-
-class scope_sink_f(gr.hier_block2):
- def __init__(self, parent, title='', sample_rate=1,
- size=default_scopesink_size, frame_decim=default_frame_decim,
- v_scale=default_v_scale, t_scale=None, num_inputs=1):
-
- gr.hier_block2.__init__(self, "scope_sink_f",
- gr.io_signature(num_inputs, num_inputs, gr.sizeof_float),
- gr.io_signature(0,0,0))
-
- msgq = gr.msg_queue(2) # message queue that holds at most 2 messages
- self.guts = gr.oscope_sink_f(sample_rate, msgq)
- for i in range(num_inputs):
- self.connect((self, i), (self.guts, i))
-
- self.win = scope_window(win_info (msgq, sample_rate, frame_decim,
- v_scale, t_scale, self.guts, title), parent)
-
- def set_sample_rate(self, sample_rate):
- self.guts.set_sample_rate(sample_rate)
- self.win.info.set_sample_rate(sample_rate)
-
-class scope_sink_c(gr.hier_block2):
- def __init__(self, parent, title='', sample_rate=1,
- size=default_scopesink_size, frame_decim=default_frame_decim,
- v_scale=default_v_scale, t_scale=None, num_inputs=1):
-
- gr.hier_block2.__init__(self, "scope_sink_c",
- gr.io_signature(num_inputs, num_inputs, gr.sizeof_gr_complex),
- gr.io_signature(0,0,0))
-
- msgq = gr.msg_queue(2) # message queue that holds at most 2 messages
- self.guts = gr.oscope_sink_f(sample_rate, msgq)
- for i in range(num_inputs):
- c2f = gr.complex_to_float()
- self.connect((self, i), c2f)
- self.connect((c2f, 0), (self.guts, 2*i+0))
- self.connect((c2f, 1), (self.guts, 2*i+1))
-
- self.win = scope_window(win_info(msgq, sample_rate, frame_decim,
- v_scale, t_scale, self.guts, title), parent)
-
- def set_sample_rate(self, sample_rate):
- self.guts.set_sample_rate(sample_rate)
- self.win.info.set_sample_rate(sample_rate)
-
-class constellation_sink(scope_sink_c):
- def __init__(self, parent, title='Constellation', sample_rate=1,
- size=default_scopesink_size, frame_decim=default_frame_decim):
- scope_sink_c.__init__(self, parent=parent, title=title, sample_rate=sample_rate,
- size=size, frame_decim=frame_decim)
- self.win.info.xy = True #constellation mode
-
-# ========================================================================
-
-
-time_base_list = [ # time / division
- 1.0e-7, # 100ns / div
- 2.5e-7,
- 5.0e-7,
- 1.0e-6, # 1us / div
- 2.5e-6,
- 5.0e-6,
- 1.0e-5, # 10us / div
- 2.5e-5,
- 5.0e-5,
- 1.0e-4, # 100us / div
- 2.5e-4,
- 5.0e-4,
- 1.0e-3, # 1ms / div
- 2.5e-3,
- 5.0e-3,
- 1.0e-2, # 10ms / div
- 2.5e-2,
- 5.0e-2
- ]
-
-v_scale_list = [ # counts / div, LARGER gains are SMALLER /div, appear EARLIER
- 2.0e-3, # 2m / div, don't call it V/div it's actually counts/div
- 5.0e-3,
- 1.0e-2,
- 2.0e-2,
- 5.0e-2,
- 1.0e-1,
- 2.0e-1,
- 5.0e-1,
- 1.0e+0,
- 2.0e+0,
- 5.0e+0,
- 1.0e+1,
- 2.0e+1,
- 5.0e+1,
- 1.0e+2,
- 2.0e+2,
- 5.0e+2,
- 1.0e+3,
- 2.0e+3,
- 5.0e+3,
- 1.0e+4 # 10000 /div, USRP full scale is -/+ 32767
- ]
-
-
-wxDATA_EVENT = wx.NewEventType()
-
-def EVT_DATA_EVENT(win, func):
- win.Connect(-1, -1, wxDATA_EVENT, func)
-
-class DataEvent(wx.PyEvent):
- def __init__(self, data):
- wx.PyEvent.__init__(self)
- self.SetEventType (wxDATA_EVENT)
- self.data = data
-
- def Clone (self):
- self.__class__ (self.GetId())
-
-
-class win_info (object):
- __slots__ = ['msgq', 'sample_rate', 'frame_decim', 'v_scale',
- 'scopesink', 'title',
- 'time_scale_cursor', 'v_scale_cursor', 'marker', 'xy',
- 'autorange', 'running']
-
- def __init__ (self, msgq, sample_rate, frame_decim, v_scale, t_scale,
- scopesink, title = "Oscilloscope", xy=False):
- self.msgq = msgq
- self.sample_rate = sample_rate
- self.frame_decim = frame_decim
- self.scopesink = scopesink
- self.title = title;
-
- self.time_scale_cursor = gru.seq_with_cursor(time_base_list, initial_value = t_scale)
- self.v_scale_cursor = gru.seq_with_cursor(v_scale_list, initial_value = v_scale)
-
- self.marker = 'line'
- self.xy = xy
- if v_scale == None: # 0 and None are both False, but 0 != None
- self.autorange = True
- else:
- self.autorange = False # 0 is a valid v_scale
- self.running = True
-
- def get_time_per_div (self):
- return self.time_scale_cursor.current ()
-
- def get_volts_per_div (self):
- return self.v_scale_cursor.current ()
-
- def set_sample_rate(self, sample_rate):
- self.sample_rate = sample_rate
-
- def get_sample_rate (self):
- return self.sample_rate
-
- def get_decimation_rate (self):
- return 1.0
-
- def set_marker (self, s):
- self.marker = s
-
- def get_marker (self):
- return self.marker
-
-
-class input_watcher (threading.Thread):
- def __init__ (self, msgq, event_receiver, frame_decim, **kwds):
- threading.Thread.__init__ (self, **kwds)
- self.setDaemon (1)
- self.msgq = msgq
- self.event_receiver = event_receiver
- self.frame_decim = frame_decim
- self.iscan = 0
- self.keep_running = True
- self.start ()
-
- def run (self):
- # print "input_watcher: pid = ", os.getpid ()
- while (self.keep_running):
- msg = self.msgq.delete_head() # blocking read of message queue
- if self.iscan == 0: # only display at frame_decim
- self.iscan = self.frame_decim
-
- nchan = int(msg.arg1()) # number of channels of data in msg
- nsamples = int(msg.arg2()) # number of samples in each channel
-
- s = msg.to_string() # get the body of the msg as a string
-
- bytes_per_chan = nsamples * gr.sizeof_float
-
- records = []
- for ch in range (nchan):
-
- start = ch * bytes_per_chan
- chan_data = s[start:start+bytes_per_chan]
- rec = numpy.fromstring (chan_data, numpy.float32)
- records.append (rec)
-
- # print "nrecords = %d, reclen = %d" % (len (records),nsamples)
-
- de = DataEvent (records)
- wx.PostEvent (self.event_receiver, de)
- records = []
- del de
-
- # end if iscan == 0
- self.iscan -= 1
-
-
-class scope_window (wx.Panel):
-
- def __init__ (self, info, parent, id = -1,
- pos = wx.DefaultPosition, size = wx.DefaultSize, name = ""):
- wx.Panel.__init__ (self, parent, -1)
- self.info = info
-
- vbox = wx.BoxSizer (wx.VERTICAL)
-
- self.graph = graph_window (info, self, -1)
-
- vbox.Add (self.graph, 1, wx.EXPAND)
- vbox.Add (self.make_control_box(), 0, wx.EXPAND)
- vbox.Add (self.make_control2_box(), 0, wx.EXPAND)
-
- self.sizer = vbox
- self.SetSizer (self.sizer)
- self.SetAutoLayout (True)
- self.sizer.Fit (self)
- self.set_autorange(self.info.autorange)
-
-
- # second row of control buttons etc. appears BELOW control_box
- def make_control2_box (self):
- ctrlbox = wx.BoxSizer (wx.HORIZONTAL)
-
- self.inc_v_button = wx.Button (self, 1101, " < ", style=wx.BU_EXACTFIT)
- self.inc_v_button.SetToolTipString ("Increase vertical range")
- wx.EVT_BUTTON (self, 1101, self.incr_v_scale) # ID matches button ID above
-
- self.dec_v_button = wx.Button (self, 1100, " > ", style=wx.BU_EXACTFIT)
- self.dec_v_button.SetToolTipString ("Decrease vertical range")
- wx.EVT_BUTTON (self, 1100, self.decr_v_scale)
-
- self.v_scale_label = wx.StaticText (self, 1002, "None") # vertical /div
- self.update_v_scale_label ()
-
- self.autorange_checkbox = wx.CheckBox (self, 1102, "Autorange")
- self.autorange_checkbox.SetToolTipString ("Select autorange on/off")
- wx.EVT_CHECKBOX(self, 1102, self.autorange_checkbox_event)
-
- ctrlbox.Add ((5,0) ,0) # left margin space
- ctrlbox.Add (self.inc_v_button, 0, wx.EXPAND)
- ctrlbox.Add (self.dec_v_button, 0, wx.EXPAND)
- ctrlbox.Add (self.v_scale_label, 0, wx.ALIGN_CENTER)
- ctrlbox.Add ((20,0) ,0) # spacer
- ctrlbox.Add (self.autorange_checkbox, 0, wx.ALIGN_CENTER)
-
- return ctrlbox
-
- def make_control_box (self):
- ctrlbox = wx.BoxSizer (wx.HORIZONTAL)
-
- tb_left = wx.Button (self, 1001, " < ", style=wx.BU_EXACTFIT)
- tb_left.SetToolTipString ("Increase time base")
- wx.EVT_BUTTON (self, 1001, self.incr_timebase)
-
-
- tb_right = wx.Button (self, 1000, " > ", style=wx.BU_EXACTFIT)
- tb_right.SetToolTipString ("Decrease time base")
- wx.EVT_BUTTON (self, 1000, self.decr_timebase)
-
- self.time_base_label = wx.StaticText (self, 1002, "")
- self.update_timebase_label ()
-
- ctrlbox.Add ((5,0) ,0)
- # ctrlbox.Add (wx.StaticText (self, -1, "Horiz Scale: "), 0, wx.ALIGN_CENTER)
- ctrlbox.Add (tb_left, 0, wx.EXPAND)
- ctrlbox.Add (tb_right, 0, wx.EXPAND)
- ctrlbox.Add (self.time_base_label, 0, wx.ALIGN_CENTER)
-
- ctrlbox.Add ((10,0) ,1) # stretchy space
-
- ctrlbox.Add (wx.StaticText (self, -1, "Trig: "), 0, wx.ALIGN_CENTER)
- self.trig_chan_choice = wx.Choice (self, 1004,
- choices = ['Ch1', 'Ch2', 'Ch3', 'Ch4'])
- self.trig_chan_choice.SetToolTipString ("Select channel for trigger")
- wx.EVT_CHOICE (self, 1004, self.trig_chan_choice_event)
- ctrlbox.Add (self.trig_chan_choice, 0, wx.ALIGN_CENTER)
-
- self.trig_mode_choice = wx.Choice (self, 1005,
- choices = ['Auto', 'Pos', 'Neg'])
- self.trig_mode_choice.SetToolTipString ("Select trigger slope or Auto (untriggered roll)")
- wx.EVT_CHOICE (self, 1005, self.trig_mode_choice_event)
- ctrlbox.Add (self.trig_mode_choice, 0, wx.ALIGN_CENTER)
-
- trig_level50 = wx.Button (self, 1006, "50%")
- trig_level50.SetToolTipString ("Set trigger level to 50%")
- wx.EVT_BUTTON (self, 1006, self.set_trig_level50)
- ctrlbox.Add (trig_level50, 0, wx.EXPAND)
-
- run_stop = wx.Button (self, 1007, "Run/Stop")
- run_stop.SetToolTipString ("Toggle Run/Stop mode")
- wx.EVT_BUTTON (self, 1007, self.run_stop)
- ctrlbox.Add (run_stop, 0, wx.EXPAND)
-
- ctrlbox.Add ((10, 0) ,1) # stretchy space
-
- ctrlbox.Add (wx.StaticText (self, -1, "Fmt: "), 0, wx.ALIGN_CENTER)
- self.marker_choice = wx.Choice (self, 1002, choices = self._marker_choices)
- self.marker_choice.SetToolTipString ("Select plotting with lines, pluses or dots")
- wx.EVT_CHOICE (self, 1002, self.marker_choice_event)
- ctrlbox.Add (self.marker_choice, 0, wx.ALIGN_CENTER)
-
- self.xy_choice = wx.Choice (self, 1003, choices = ['X:t', 'X:Y'])
- self.xy_choice.SetToolTipString ("Select X vs time or X vs Y display")
- wx.EVT_CHOICE (self, 1003, self.xy_choice_event)
- ctrlbox.Add (self.xy_choice, 0, wx.ALIGN_CENTER)
-
- return ctrlbox
-
- _marker_choices = ['line', 'plus', 'dot']
-
- def update_timebase_label (self):
- time_per_div = self.info.get_time_per_div ()
- s = ' ' + eng_notation.num_to_str (time_per_div) + 's/div'
- self.time_base_label.SetLabel (s)
-
- def decr_timebase (self, evt):
- self.info.time_scale_cursor.prev ()
- self.update_timebase_label ()
-
- def incr_timebase (self, evt):
- self.info.time_scale_cursor.next ()
- self.update_timebase_label ()
-
- def update_v_scale_label (self):
- volts_per_div = self.info.get_volts_per_div ()
- s = ' ' + eng_notation.num_to_str (volts_per_div) + '/div' # Not V/div
- self.v_scale_label.SetLabel (s)
-
- def decr_v_scale (self, evt):
- self.info.v_scale_cursor.prev ()
- self.update_v_scale_label ()
-
- def incr_v_scale (self, evt):
- self.info.v_scale_cursor.next ()
- self.update_v_scale_label ()
-
- def marker_choice_event (self, evt):
- s = evt.GetString ()
- self.set_marker (s)
-
- def set_autorange(self, on):
- if on:
- self.v_scale_label.SetLabel(" (auto)")
- self.info.autorange = True
- self.autorange_checkbox.SetValue(True)
- self.inc_v_button.Enable(False)
- self.dec_v_button.Enable(False)
- else:
- if self.graph.y_range:
- (l,u) = self.graph.y_range # found by autorange
- self.info.v_scale_cursor.set_index_by_value((u-l)/8.0)
- self.update_v_scale_label()
- self.info.autorange = False
- self.autorange_checkbox.SetValue(False)
- self.inc_v_button.Enable(True)
- self.dec_v_button.Enable(True)
-
- def autorange_checkbox_event(self, evt):
- if evt.Checked():
- self.set_autorange(True)
- else:
- self.set_autorange(False)
-
- def set_marker (self, s):
- self.info.set_marker (s) # set info for drawing routines
- i = self.marker_choice.FindString (s)
- assert i >= 0, "Hmmm, set_marker problem"
- self.marker_choice.SetSelection (i)
-
- def set_format_line (self):
- self.set_marker ('line')
-
- def set_format_dot (self):
- self.set_marker ('dot')
-
- def set_format_plus (self):
- self.set_marker ('plus')
-
- def xy_choice_event (self, evt):
- s = evt.GetString ()
- self.info.xy = s == 'X:Y'
-
- def trig_chan_choice_event (self, evt):
- s = evt.GetString ()
- ch = int (s[-1]) - 1
- self.info.scopesink.set_trigger_channel (ch)
-
- def trig_mode_choice_event (self, evt):
- sink = self.info.scopesink
- s = evt.GetString ()
- if s == 'Pos':
- sink.set_trigger_mode (gr.gr_TRIG_POS_SLOPE)
- elif s == 'Neg':
- sink.set_trigger_mode (gr.gr_TRIG_NEG_SLOPE)
- elif s == 'Auto':
- sink.set_trigger_mode (gr.gr_TRIG_AUTO)
- else:
- assert 0, "Bad trig_mode_choice string"
-
- def set_trig_level50 (self, evt):
- self.info.scopesink.set_trigger_level_auto ()
-
- def run_stop (self, evt):
- self.info.running = not self.info.running
-
-
-class graph_window (plot.PlotCanvas):
-
- channel_colors = ['BLUE', 'RED',
- 'CYAN', 'MAGENTA', 'GREEN', 'YELLOW']
-
- def __init__ (self, info, parent, id = -1,
- pos = wx.DefaultPosition, size = (640, 240),
- style = wx.DEFAULT_FRAME_STYLE, name = ""):
- plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
-
- self.SetXUseScopeTicks (True)
- self.SetEnableGrid (True)
- self.SetEnableZoom (True)
- self.SetEnableLegend(True)
- # self.SetBackgroundColour ('black')
-
- self.info = info;
- self.y_range = None
- self.x_range = None
- self.avg_y_min = None
- self.avg_y_max = None
- self.avg_x_min = None
- self.avg_x_max = None
-
- EVT_DATA_EVENT (self, self.format_data)
-
- self.input_watcher = input_watcher (info.msgq, self, info.frame_decim)
-
- def channel_color (self, ch):
- return self.channel_colors[ch % len(self.channel_colors)]
-
- def format_data (self, evt):
- if not self.info.running:
- return
-
- if self.info.xy:
- self.format_xy_data (evt)
- return
-
- info = self.info
- records = evt.data
- nchannels = len (records)
- npoints = len (records[0])
-
- objects = []
-
- Ts = 1.0 / (info.get_sample_rate () / info.get_decimation_rate ())
- x_vals = Ts * numpy.arange (-npoints/2, npoints/2)
-
- # preliminary clipping based on time axis here, instead of in graphics code
- time_per_window = self.info.get_time_per_div () * 10
- n = int (time_per_window / Ts + 0.5)
- n = n & ~0x1 # make even
- n = max (2, min (n, npoints))
-
- self.SetXUseScopeTicks (True) # use 10 divisions, no labels
-
- for ch in range(nchannels):
- r = records[ch]
-
- # plot middle n points of record
-
- lb = npoints/2 - n/2
- ub = npoints/2 + n/2
- # points = zip (x_vals[lb:ub], r[lb:ub])
- points = numpy.zeros ((ub-lb, 2), numpy.float64)
- points[:,0] = x_vals[lb:ub]
- points[:,1] = r[lb:ub]
-
- m = info.get_marker ()
- if m == 'line':
- objects.append (plot.PolyLine (points,
- colour=self.channel_color (ch),
- legend=('Ch%d' % (ch+1,))))
- else:
- objects.append (plot.PolyMarker (points,
- marker=m,
- colour=self.channel_color (ch),
- legend=('Ch%d' % (ch+1,))))
-
- graphics = plot.PlotGraphics (objects,
- title=self.info.title,
- xLabel = '', yLabel = '')
-
- time_per_div = info.get_time_per_div ()
- x_range = (-5.0 * time_per_div, 5.0 * time_per_div) # ranges are tuples!
- volts_per_div = info.get_volts_per_div ()
- if not self.info.autorange:
- self.y_range = (-4.0 * volts_per_div, 4.0 * volts_per_div)
- self.Draw (graphics, xAxis=x_range, yAxis=self.y_range)
- self.update_y_range () # autorange to self.y_range
-
-
- def format_xy_data (self, evt):
- info = self.info
- records = evt.data
- nchannels = len (records)
- npoints = len (records[0])
-
- if nchannels < 2:
- return
-
- objects = []
- # points = zip (records[0], records[1])
- points = numpy.zeros ((len(records[0]), 2), numpy.float32)
- points[:,0] = records[0]
- points[:,1] = records[1]
-
- self.SetXUseScopeTicks (False)
-
- m = info.get_marker ()
- if m == 'line':
- objects.append (plot.PolyLine (points,
- colour=self.channel_color (0)))
- else:
- objects.append (plot.PolyMarker (points,
- marker=m,
- colour=self.channel_color (0)))
-
- graphics = plot.PlotGraphics (objects,
- title=self.info.title,
- xLabel = 'I', yLabel = 'Q')
-
- self.Draw (graphics, xAxis=self.x_range, yAxis=self.y_range)
- self.update_y_range ()
- self.update_x_range ()
-
-
- def update_y_range (self):
- alpha = 1.0/25
- graphics = self.last_draw[0]
- p1, p2 = graphics.boundingBox () # min, max points of graphics
-
- if self.avg_y_min: # prevent vertical scale from jumping abruptly --?
- self.avg_y_min = p1[1] * alpha + self.avg_y_min * (1 - alpha)
- self.avg_y_max = p2[1] * alpha + self.avg_y_max * (1 - alpha)
- else: # initial guess
- self.avg_y_min = p1[1] # -500.0 workaround, sometimes p1 is ~ 10^35
- self.avg_y_max = p2[1] # 500.0
-
- self.y_range = self._axisInterval ('auto', self.avg_y_min, self.avg_y_max)
- # print "p1 %s p2 %s y_min %s y_max %s y_range %s" \
- # % (p1, p2, self.avg_y_min, self.avg_y_max, self.y_range)
-
-
- def update_x_range (self):
- alpha = 1.0/25
- graphics = self.last_draw[0]
- p1, p2 = graphics.boundingBox () # min, max points of graphics
-
- if self.avg_x_min:
- self.avg_x_min = p1[0] * alpha + self.avg_x_min * (1 - alpha)
- self.avg_x_max = p2[0] * alpha + self.avg_x_max * (1 - alpha)
- else:
- self.avg_x_min = p1[0]
- self.avg_x_max = p2[0]
-
- self.x_range = self._axisInterval ('auto', self.avg_x_min, self.avg_x_max)
-
-
-# ----------------------------------------------------------------
-# Stand-alone test application
-# ----------------------------------------------------------------
-
-class test_top_block (stdgui2.std_top_block):
- def __init__(self, frame, panel, vbox, argv):
- stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
-
- if len(argv) > 1:
- frame_decim = int(argv[1])
- else:
- frame_decim = 1
-
- if len(argv) > 2:
- v_scale = float(argv[2]) # start up at this v_scale value
- else:
- v_scale = None # start up in autorange mode, default
-
- if len(argv) > 3:
- t_scale = float(argv[3]) # start up at this t_scale value
- else:
- t_scale = None # old behavior
-
- print "frame decim %s v_scale %s t_scale %s" % (frame_decim,v_scale,t_scale)
-
- input_rate = 1e6
-
- # Generate a complex sinusoid
- self.src0 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 25.1e3, 1e3)
+#
- # We add this throttle block so that this demo doesn't suck down
- # all the CPU available. You normally wouldn't use it...
- self.thr = gr.throttle(gr.sizeof_gr_complex, input_rate)
+from gnuradio import gr
- scope = scope_sink_c (panel,"Secret Data",sample_rate=input_rate,
- frame_decim=frame_decim,
- v_scale=v_scale, t_scale=t_scale)
- vbox.Add (scope.win, 1, wx.EXPAND)
+p = gr.prefs()
+style = p.get_string('wxgui', 'style', 'auto')
- # Ultimately this will be
- # self.connect("src0 throttle scope")
- self.connect(self.src0, self.thr, scope)
+# In 3.2 we'll change 'auto' to mean 'gl' if possible, then fallback
+if style == 'auto':
+ style = 'nongl'
-def main ():
- app = stdgui2.stdapp (test_top_block, "O'Scope Test App")
- app.MainLoop ()
+if style == 'nongl':
+ from scopesink_nongl import scope_sink_f, scope_sink_c
+elif style == 'gl':
+ try:
+ import wx
+ wx.glcanvas.GLCanvas
+ except AttributeError:
+ raise RuntimeError("wxPython doesn't support glcanvas")
-if __name__ == '__main__':
- main ()
+ try:
+ from OpenGL.GL import *
+ except ImportError:
+ raise RuntimeError("Unable to import OpenGL. Are Python wrappers for OpenGL installed?")
-# ----------------------------------------------------------------
+ from scopesink_gl import scope_sink_f, scope_sink_c
diff --git a/gr-wxgui/src/python/scopesink_gl.py b/gr-wxgui/src/python/scopesink_gl.py
new file mode 100644
index 000000000..727e1dc0a
--- /dev/null
+++ b/gr-wxgui/src/python/scopesink_gl.py
@@ -0,0 +1,183 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import scope_window
+import common
+from gnuradio import gr
+from pubsub import pubsub
+from constants import *
+
+##################################################
+# Scope sink block (wrapper for old wxgui)
+##################################################
+class _scope_sink_base(gr.hier_block2, common.prop_setter):
+ """!
+ A scope block with a gui window.
+ """
+
+ def __init__(
+ self,
+ parent,
+ title='',
+ sample_rate=1,
+ size=scope_window.DEFAULT_WIN_SIZE,
+ frame_decim=None, #ignore (old wrapper)
+ v_scale=scope_window.DEFAULT_V_SCALE,
+ t_scale=None,
+ num_inputs=1,
+ ac_couple=False,
+ xy_mode=False,
+ frame_rate=scope_window.DEFAULT_FRAME_RATE,
+ ):
+ if t_scale is None: t_scale = 0.001
+ #init
+ gr.hier_block2.__init__(
+ self,
+ "scope_sink",
+ gr.io_signature(num_inputs, num_inputs, self._item_size),
+ gr.io_signature(0, 0, 0),
+ )
+ #scope
+ msgq = gr.msg_queue(2)
+ scope = gr.oscope_sink_f(sample_rate, msgq)
+ #connect
+ if self._real:
+ for i in range(num_inputs):
+ self.connect((self, i), (scope, i))
+ else:
+ for i in range(num_inputs):
+ c2f = gr.complex_to_float()
+ self.connect((self, i), c2f)
+ self.connect((c2f, 0), (scope, 2*i+0))
+ self.connect((c2f, 1), (scope, 2*i+1))
+ num_inputs *= 2
+ #controller
+ self.controller = pubsub()
+ self.controller.subscribe(SAMPLE_RATE_KEY, scope.set_sample_rate)
+ self.controller.publish(SAMPLE_RATE_KEY, scope.sample_rate)
+ def set_trigger_level(level):
+ if level == '': scope.set_trigger_level_auto()
+ else: scope.set_trigger_level(level)
+ self.controller.subscribe(SCOPE_TRIGGER_LEVEL_KEY, set_trigger_level)
+ def set_trigger_mode(mode):
+ if mode == 0: mode = gr.gr_TRIG_AUTO
+ elif mode < 0: mode = gr.gr_TRIG_NEG_SLOPE
+ elif mode > 0: mode = gr.gr_TRIG_POS_SLOPE
+ else: return
+ scope.set_trigger_mode(mode)
+ self.controller.subscribe(SCOPE_TRIGGER_MODE_KEY, set_trigger_mode)
+ self.controller.subscribe(SCOPE_TRIGGER_CHANNEL_KEY, scope.set_trigger_channel)
+ #start input watcher
+ def setter(p, k, x): # lambdas can't have assignments :(
+ p[k] = x
+ common.input_watcher(msgq, lambda x: setter(self.controller, MSG_KEY, x))
+ #create window
+ self.win = scope_window.scope_window(
+ parent=parent,
+ controller=self.controller,
+ size=size,
+ title=title,
+ frame_rate=frame_rate,
+ num_inputs=num_inputs,
+ sample_rate_key=SAMPLE_RATE_KEY,
+ t_scale=t_scale,
+ v_scale=v_scale,
+ ac_couple=ac_couple,
+ xy_mode=xy_mode,
+ scope_trigger_level_key=SCOPE_TRIGGER_LEVEL_KEY,
+ scope_trigger_mode_key=SCOPE_TRIGGER_MODE_KEY,
+ scope_trigger_channel_key=SCOPE_TRIGGER_CHANNEL_KEY,
+ msg_key=MSG_KEY,
+ )
+ #register callbacks from window for external use
+ for attr in filter(lambda a: a.startswith('set_'), dir(self.win)):
+ setattr(self, attr, getattr(self.win, attr))
+ self._register_set_prop(self.controller, SAMPLE_RATE_KEY)
+
+class scope_sink_f(_scope_sink_base):
+ _item_size = gr.sizeof_float
+ _real = True
+
+class scope_sink_c(_scope_sink_base):
+ _item_size = gr.sizeof_gr_complex
+ _real = False
+
+#backwards compadible wrapper (maybe only grc uses this)
+class constellation_sink(scope_sink_c):
+ def __init__(self, **kwargs):
+ kwargs['xy_mode'] = True
+ scope_sink_c.__init__(self, **kwargs)
+
+# ----------------------------------------------------------------
+# Stand-alone test application
+# ----------------------------------------------------------------
+
+import wx
+from gnuradio.wxgui import stdgui2
+
+class test_top_block (stdgui2.std_top_block):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
+
+ if len(argv) > 1:
+ frame_decim = int(argv[1])
+ else:
+ frame_decim = 1
+
+ if len(argv) > 2:
+ v_scale = float(argv[2]) # start up at this v_scale value
+ else:
+ v_scale = None # start up in autorange mode, default
+
+ if len(argv) > 3:
+ t_scale = float(argv[3]) # start up at this t_scale value
+ else:
+ t_scale = .00003 # old behavior
+
+ print "frame decim %s v_scale %s t_scale %s" % (frame_decim,v_scale,t_scale)
+
+ input_rate = 1e6
+
+ # Generate a complex sinusoid
+ self.src0 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 25.1e3, 1e3)
+
+ # We add this throttle block so that this demo doesn't suck down
+ # all the CPU available. You normally wouldn't use it...
+ self.thr = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ scope = scope_sink_c (panel,"Secret Data",sample_rate=input_rate,
+ frame_decim=frame_decim,
+ v_scale=v_scale, t_scale=t_scale)
+ vbox.Add (scope.win, 1, wx.EXPAND)
+
+ # Ultimately this will be
+ # self.connect("src0 throttle scope")
+ self.connect(self.src0, self.thr, scope)
+
+def main ():
+ app = stdgui2.stdapp (test_top_block, "O'Scope Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
diff --git a/gr-wxgui/src/python/scopesink_nongl.py b/gr-wxgui/src/python/scopesink_nongl.py
new file mode 100644
index 000000000..71fd7e128
--- /dev/null
+++ b/gr-wxgui/src/python/scopesink_nongl.py
@@ -0,0 +1,661 @@
+#!/usr/bin/env python
+#
+# Copyright 2003,2004,2006,2007 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, gru, eng_notation
+from gnuradio.wxgui import stdgui2
+import wx
+import gnuradio.wxgui.plot as plot
+import numpy
+import threading
+import struct
+
+default_scopesink_size = (640, 240)
+default_v_scale = 1000
+default_frame_decim = gr.prefs().get_long('wxgui', 'frame_decim', 1)
+
+class scope_sink_f(gr.hier_block2):
+ def __init__(self, parent, title='', sample_rate=1,
+ size=default_scopesink_size, frame_decim=default_frame_decim,
+ v_scale=default_v_scale, t_scale=None, num_inputs=1):
+
+ gr.hier_block2.__init__(self, "scope_sink_f",
+ gr.io_signature(num_inputs, num_inputs, gr.sizeof_float),
+ gr.io_signature(0,0,0))
+
+ msgq = gr.msg_queue(2) # message queue that holds at most 2 messages
+ self.guts = gr.oscope_sink_f(sample_rate, msgq)
+ for i in range(num_inputs):
+ self.connect((self, i), (self.guts, i))
+
+ self.win = scope_window(win_info (msgq, sample_rate, frame_decim,
+ v_scale, t_scale, self.guts, title), parent)
+
+ def set_sample_rate(self, sample_rate):
+ self.guts.set_sample_rate(sample_rate)
+ self.win.info.set_sample_rate(sample_rate)
+
+class scope_sink_c(gr.hier_block2):
+ def __init__(self, parent, title='', sample_rate=1,
+ size=default_scopesink_size, frame_decim=default_frame_decim,
+ v_scale=default_v_scale, t_scale=None, num_inputs=1):
+
+ gr.hier_block2.__init__(self, "scope_sink_c",
+ gr.io_signature(num_inputs, num_inputs, gr.sizeof_gr_complex),
+ gr.io_signature(0,0,0))
+
+ msgq = gr.msg_queue(2) # message queue that holds at most 2 messages
+ self.guts = gr.oscope_sink_f(sample_rate, msgq)
+ for i in range(num_inputs):
+ c2f = gr.complex_to_float()
+ self.connect((self, i), c2f)
+ self.connect((c2f, 0), (self.guts, 2*i+0))
+ self.connect((c2f, 1), (self.guts, 2*i+1))
+
+ self.win = scope_window(win_info(msgq, sample_rate, frame_decim,
+ v_scale, t_scale, self.guts, title), parent)
+
+ def set_sample_rate(self, sample_rate):
+ self.guts.set_sample_rate(sample_rate)
+ self.win.info.set_sample_rate(sample_rate)
+
+class constellation_sink(scope_sink_c):
+ def __init__(self, parent, title='Constellation', sample_rate=1,
+ size=default_scopesink_size, frame_decim=default_frame_decim):
+ scope_sink_c.__init__(self, parent=parent, title=title, sample_rate=sample_rate,
+ size=size, frame_decim=frame_decim)
+ self.win.info.xy = True #constellation mode
+
+# ========================================================================
+
+
+time_base_list = [ # time / division
+ 1.0e-7, # 100ns / div
+ 2.5e-7,
+ 5.0e-7,
+ 1.0e-6, # 1us / div
+ 2.5e-6,
+ 5.0e-6,
+ 1.0e-5, # 10us / div
+ 2.5e-5,
+ 5.0e-5,
+ 1.0e-4, # 100us / div
+ 2.5e-4,
+ 5.0e-4,
+ 1.0e-3, # 1ms / div
+ 2.5e-3,
+ 5.0e-3,
+ 1.0e-2, # 10ms / div
+ 2.5e-2,
+ 5.0e-2
+ ]
+
+v_scale_list = [ # counts / div, LARGER gains are SMALLER /div, appear EARLIER
+ 2.0e-3, # 2m / div, don't call it V/div it's actually counts/div
+ 5.0e-3,
+ 1.0e-2,
+ 2.0e-2,
+ 5.0e-2,
+ 1.0e-1,
+ 2.0e-1,
+ 5.0e-1,
+ 1.0e+0,
+ 2.0e+0,
+ 5.0e+0,
+ 1.0e+1,
+ 2.0e+1,
+ 5.0e+1,
+ 1.0e+2,
+ 2.0e+2,
+ 5.0e+2,
+ 1.0e+3,
+ 2.0e+3,
+ 5.0e+3,
+ 1.0e+4 # 10000 /div, USRP full scale is -/+ 32767
+ ]
+
+
+wxDATA_EVENT = wx.NewEventType()
+
+def EVT_DATA_EVENT(win, func):
+ win.Connect(-1, -1, wxDATA_EVENT, func)
+
+class DataEvent(wx.PyEvent):
+ def __init__(self, data):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (wxDATA_EVENT)
+ self.data = data
+
+ def Clone (self):
+ self.__class__ (self.GetId())
+
+
+class win_info (object):
+ __slots__ = ['msgq', 'sample_rate', 'frame_decim', 'v_scale',
+ 'scopesink', 'title',
+ 'time_scale_cursor', 'v_scale_cursor', 'marker', 'xy',
+ 'autorange', 'running']
+
+ def __init__ (self, msgq, sample_rate, frame_decim, v_scale, t_scale,
+ scopesink, title = "Oscilloscope", xy=False):
+ self.msgq = msgq
+ self.sample_rate = sample_rate
+ self.frame_decim = frame_decim
+ self.scopesink = scopesink
+ self.title = title;
+
+ self.time_scale_cursor = gru.seq_with_cursor(time_base_list, initial_value = t_scale)
+ self.v_scale_cursor = gru.seq_with_cursor(v_scale_list, initial_value = v_scale)
+
+ self.marker = 'line'
+ self.xy = xy
+ if v_scale == None: # 0 and None are both False, but 0 != None
+ self.autorange = True
+ else:
+ self.autorange = False # 0 is a valid v_scale
+ self.running = True
+
+ def get_time_per_div (self):
+ return self.time_scale_cursor.current ()
+
+ def get_volts_per_div (self):
+ return self.v_scale_cursor.current ()
+
+ def set_sample_rate(self, sample_rate):
+ self.sample_rate = sample_rate
+
+ def get_sample_rate (self):
+ return self.sample_rate
+
+ def get_decimation_rate (self):
+ return 1.0
+
+ def set_marker (self, s):
+ self.marker = s
+
+ def get_marker (self):
+ return self.marker
+
+
+class input_watcher (threading.Thread):
+ def __init__ (self, msgq, event_receiver, frame_decim, **kwds):
+ threading.Thread.__init__ (self, **kwds)
+ self.setDaemon (1)
+ self.msgq = msgq
+ self.event_receiver = event_receiver
+ self.frame_decim = frame_decim
+ self.iscan = 0
+ self.keep_running = True
+ self.start ()
+
+ def run (self):
+ # print "input_watcher: pid = ", os.getpid ()
+ while (self.keep_running):
+ msg = self.msgq.delete_head() # blocking read of message queue
+ if self.iscan == 0: # only display at frame_decim
+ self.iscan = self.frame_decim
+
+ nchan = int(msg.arg1()) # number of channels of data in msg
+ nsamples = int(msg.arg2()) # number of samples in each channel
+
+ s = msg.to_string() # get the body of the msg as a string
+
+ bytes_per_chan = nsamples * gr.sizeof_float
+
+ records = []
+ for ch in range (nchan):
+
+ start = ch * bytes_per_chan
+ chan_data = s[start:start+bytes_per_chan]
+ rec = numpy.fromstring (chan_data, numpy.float32)
+ records.append (rec)
+
+ # print "nrecords = %d, reclen = %d" % (len (records),nsamples)
+
+ de = DataEvent (records)
+ wx.PostEvent (self.event_receiver, de)
+ records = []
+ del de
+
+ # end if iscan == 0
+ self.iscan -= 1
+
+
+class scope_window (wx.Panel):
+
+ def __init__ (self, info, parent, id = -1,
+ pos = wx.DefaultPosition, size = wx.DefaultSize, name = ""):
+ wx.Panel.__init__ (self, parent, -1)
+ self.info = info
+
+ vbox = wx.BoxSizer (wx.VERTICAL)
+
+ self.graph = graph_window (info, self, -1)
+
+ vbox.Add (self.graph, 1, wx.EXPAND)
+ vbox.Add (self.make_control_box(), 0, wx.EXPAND)
+ vbox.Add (self.make_control2_box(), 0, wx.EXPAND)
+
+ self.sizer = vbox
+ self.SetSizer (self.sizer)
+ self.SetAutoLayout (True)
+ self.sizer.Fit (self)
+ self.set_autorange(self.info.autorange)
+
+
+ # second row of control buttons etc. appears BELOW control_box
+ def make_control2_box (self):
+ ctrlbox = wx.BoxSizer (wx.HORIZONTAL)
+
+ self.inc_v_button = wx.Button (self, 1101, " < ", style=wx.BU_EXACTFIT)
+ self.inc_v_button.SetToolTipString ("Increase vertical range")
+ wx.EVT_BUTTON (self, 1101, self.incr_v_scale) # ID matches button ID above
+
+ self.dec_v_button = wx.Button (self, 1100, " > ", style=wx.BU_EXACTFIT)
+ self.dec_v_button.SetToolTipString ("Decrease vertical range")
+ wx.EVT_BUTTON (self, 1100, self.decr_v_scale)
+
+ self.v_scale_label = wx.StaticText (self, 1002, "None") # vertical /div
+ self.update_v_scale_label ()
+
+ self.autorange_checkbox = wx.CheckBox (self, 1102, "Autorange")
+ self.autorange_checkbox.SetToolTipString ("Select autorange on/off")
+ wx.EVT_CHECKBOX(self, 1102, self.autorange_checkbox_event)
+
+ ctrlbox.Add ((5,0) ,0) # left margin space
+ ctrlbox.Add (self.inc_v_button, 0, wx.EXPAND)
+ ctrlbox.Add (self.dec_v_button, 0, wx.EXPAND)
+ ctrlbox.Add (self.v_scale_label, 0, wx.ALIGN_CENTER)
+ ctrlbox.Add ((20,0) ,0) # spacer
+ ctrlbox.Add (self.autorange_checkbox, 0, wx.ALIGN_CENTER)
+
+ return ctrlbox
+
+ def make_control_box (self):
+ ctrlbox = wx.BoxSizer (wx.HORIZONTAL)
+
+ tb_left = wx.Button (self, 1001, " < ", style=wx.BU_EXACTFIT)
+ tb_left.SetToolTipString ("Increase time base")
+ wx.EVT_BUTTON (self, 1001, self.incr_timebase)
+
+
+ tb_right = wx.Button (self, 1000, " > ", style=wx.BU_EXACTFIT)
+ tb_right.SetToolTipString ("Decrease time base")
+ wx.EVT_BUTTON (self, 1000, self.decr_timebase)
+
+ self.time_base_label = wx.StaticText (self, 1002, "")
+ self.update_timebase_label ()
+
+ ctrlbox.Add ((5,0) ,0)
+ # ctrlbox.Add (wx.StaticText (self, -1, "Horiz Scale: "), 0, wx.ALIGN_CENTER)
+ ctrlbox.Add (tb_left, 0, wx.EXPAND)
+ ctrlbox.Add (tb_right, 0, wx.EXPAND)
+ ctrlbox.Add (self.time_base_label, 0, wx.ALIGN_CENTER)
+
+ ctrlbox.Add ((10,0) ,1) # stretchy space
+
+ ctrlbox.Add (wx.StaticText (self, -1, "Trig: "), 0, wx.ALIGN_CENTER)
+ self.trig_chan_choice = wx.Choice (self, 1004,
+ choices = ['Ch1', 'Ch2', 'Ch3', 'Ch4'])
+ self.trig_chan_choice.SetToolTipString ("Select channel for trigger")
+ wx.EVT_CHOICE (self, 1004, self.trig_chan_choice_event)
+ ctrlbox.Add (self.trig_chan_choice, 0, wx.ALIGN_CENTER)
+
+ self.trig_mode_choice = wx.Choice (self, 1005,
+ choices = ['Auto', 'Pos', 'Neg'])
+ self.trig_mode_choice.SetToolTipString ("Select trigger slope or Auto (untriggered roll)")
+ wx.EVT_CHOICE (self, 1005, self.trig_mode_choice_event)
+ ctrlbox.Add (self.trig_mode_choice, 0, wx.ALIGN_CENTER)
+
+ trig_level50 = wx.Button (self, 1006, "50%")
+ trig_level50.SetToolTipString ("Set trigger level to 50%")
+ wx.EVT_BUTTON (self, 1006, self.set_trig_level50)
+ ctrlbox.Add (trig_level50, 0, wx.EXPAND)
+
+ run_stop = wx.Button (self, 1007, "Run/Stop")
+ run_stop.SetToolTipString ("Toggle Run/Stop mode")
+ wx.EVT_BUTTON (self, 1007, self.run_stop)
+ ctrlbox.Add (run_stop, 0, wx.EXPAND)
+
+ ctrlbox.Add ((10, 0) ,1) # stretchy space
+
+ ctrlbox.Add (wx.StaticText (self, -1, "Fmt: "), 0, wx.ALIGN_CENTER)
+ self.marker_choice = wx.Choice (self, 1002, choices = self._marker_choices)
+ self.marker_choice.SetToolTipString ("Select plotting with lines, pluses or dots")
+ wx.EVT_CHOICE (self, 1002, self.marker_choice_event)
+ ctrlbox.Add (self.marker_choice, 0, wx.ALIGN_CENTER)
+
+ self.xy_choice = wx.Choice (self, 1003, choices = ['X:t', 'X:Y'])
+ self.xy_choice.SetToolTipString ("Select X vs time or X vs Y display")
+ wx.EVT_CHOICE (self, 1003, self.xy_choice_event)
+ ctrlbox.Add (self.xy_choice, 0, wx.ALIGN_CENTER)
+
+ return ctrlbox
+
+ _marker_choices = ['line', 'plus', 'dot']
+
+ def update_timebase_label (self):
+ time_per_div = self.info.get_time_per_div ()
+ s = ' ' + eng_notation.num_to_str (time_per_div) + 's/div'
+ self.time_base_label.SetLabel (s)
+
+ def decr_timebase (self, evt):
+ self.info.time_scale_cursor.prev ()
+ self.update_timebase_label ()
+
+ def incr_timebase (self, evt):
+ self.info.time_scale_cursor.next ()
+ self.update_timebase_label ()
+
+ def update_v_scale_label (self):
+ volts_per_div = self.info.get_volts_per_div ()
+ s = ' ' + eng_notation.num_to_str (volts_per_div) + '/div' # Not V/div
+ self.v_scale_label.SetLabel (s)
+
+ def decr_v_scale (self, evt):
+ self.info.v_scale_cursor.prev ()
+ self.update_v_scale_label ()
+
+ def incr_v_scale (self, evt):
+ self.info.v_scale_cursor.next ()
+ self.update_v_scale_label ()
+
+ def marker_choice_event (self, evt):
+ s = evt.GetString ()
+ self.set_marker (s)
+
+ def set_autorange(self, on):
+ if on:
+ self.v_scale_label.SetLabel(" (auto)")
+ self.info.autorange = True
+ self.autorange_checkbox.SetValue(True)
+ self.inc_v_button.Enable(False)
+ self.dec_v_button.Enable(False)
+ else:
+ if self.graph.y_range:
+ (l,u) = self.graph.y_range # found by autorange
+ self.info.v_scale_cursor.set_index_by_value((u-l)/8.0)
+ self.update_v_scale_label()
+ self.info.autorange = False
+ self.autorange_checkbox.SetValue(False)
+ self.inc_v_button.Enable(True)
+ self.dec_v_button.Enable(True)
+
+ def autorange_checkbox_event(self, evt):
+ if evt.Checked():
+ self.set_autorange(True)
+ else:
+ self.set_autorange(False)
+
+ def set_marker (self, s):
+ self.info.set_marker (s) # set info for drawing routines
+ i = self.marker_choice.FindString (s)
+ assert i >= 0, "Hmmm, set_marker problem"
+ self.marker_choice.SetSelection (i)
+
+ def set_format_line (self):
+ self.set_marker ('line')
+
+ def set_format_dot (self):
+ self.set_marker ('dot')
+
+ def set_format_plus (self):
+ self.set_marker ('plus')
+
+ def xy_choice_event (self, evt):
+ s = evt.GetString ()
+ self.info.xy = s == 'X:Y'
+
+ def trig_chan_choice_event (self, evt):
+ s = evt.GetString ()
+ ch = int (s[-1]) - 1
+ self.info.scopesink.set_trigger_channel (ch)
+
+ def trig_mode_choice_event (self, evt):
+ sink = self.info.scopesink
+ s = evt.GetString ()
+ if s == 'Pos':
+ sink.set_trigger_mode (gr.gr_TRIG_POS_SLOPE)
+ elif s == 'Neg':
+ sink.set_trigger_mode (gr.gr_TRIG_NEG_SLOPE)
+ elif s == 'Auto':
+ sink.set_trigger_mode (gr.gr_TRIG_AUTO)
+ else:
+ assert 0, "Bad trig_mode_choice string"
+
+ def set_trig_level50 (self, evt):
+ self.info.scopesink.set_trigger_level_auto ()
+
+ def run_stop (self, evt):
+ self.info.running = not self.info.running
+
+
+class graph_window (plot.PlotCanvas):
+
+ channel_colors = ['BLUE', 'RED',
+ 'CYAN', 'MAGENTA', 'GREEN', 'YELLOW']
+
+ def __init__ (self, info, parent, id = -1,
+ pos = wx.DefaultPosition, size = (640, 240),
+ style = wx.DEFAULT_FRAME_STYLE, name = ""):
+ plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
+
+ self.SetXUseScopeTicks (True)
+ self.SetEnableGrid (True)
+ self.SetEnableZoom (True)
+ self.SetEnableLegend(True)
+ # self.SetBackgroundColour ('black')
+
+ self.info = info;
+ self.y_range = None
+ self.x_range = None
+ self.avg_y_min = None
+ self.avg_y_max = None
+ self.avg_x_min = None
+ self.avg_x_max = None
+
+ EVT_DATA_EVENT (self, self.format_data)
+
+ self.input_watcher = input_watcher (info.msgq, self, info.frame_decim)
+
+ def channel_color (self, ch):
+ return self.channel_colors[ch % len(self.channel_colors)]
+
+ def format_data (self, evt):
+ if not self.info.running:
+ return
+
+ if self.info.xy:
+ self.format_xy_data (evt)
+ return
+
+ info = self.info
+ records = evt.data
+ nchannels = len (records)
+ npoints = len (records[0])
+
+ objects = []
+
+ Ts = 1.0 / (info.get_sample_rate () / info.get_decimation_rate ())
+ x_vals = Ts * numpy.arange (-npoints/2, npoints/2)
+
+ # preliminary clipping based on time axis here, instead of in graphics code
+ time_per_window = self.info.get_time_per_div () * 10
+ n = int (time_per_window / Ts + 0.5)
+ n = n & ~0x1 # make even
+ n = max (2, min (n, npoints))
+
+ self.SetXUseScopeTicks (True) # use 10 divisions, no labels
+
+ for ch in range(nchannels):
+ r = records[ch]
+
+ # plot middle n points of record
+
+ lb = npoints/2 - n/2
+ ub = npoints/2 + n/2
+ # points = zip (x_vals[lb:ub], r[lb:ub])
+ points = numpy.zeros ((ub-lb, 2), numpy.float64)
+ points[:,0] = x_vals[lb:ub]
+ points[:,1] = r[lb:ub]
+
+ m = info.get_marker ()
+ if m == 'line':
+ objects.append (plot.PolyLine (points,
+ colour=self.channel_color (ch),
+ legend=('Ch%d' % (ch+1,))))
+ else:
+ objects.append (plot.PolyMarker (points,
+ marker=m,
+ colour=self.channel_color (ch),
+ legend=('Ch%d' % (ch+1,))))
+
+ graphics = plot.PlotGraphics (objects,
+ title=self.info.title,
+ xLabel = '', yLabel = '')
+
+ time_per_div = info.get_time_per_div ()
+ x_range = (-5.0 * time_per_div, 5.0 * time_per_div) # ranges are tuples!
+ volts_per_div = info.get_volts_per_div ()
+ if not self.info.autorange:
+ self.y_range = (-4.0 * volts_per_div, 4.0 * volts_per_div)
+ self.Draw (graphics, xAxis=x_range, yAxis=self.y_range)
+ self.update_y_range () # autorange to self.y_range
+
+
+ def format_xy_data (self, evt):
+ info = self.info
+ records = evt.data
+ nchannels = len (records)
+ npoints = len (records[0])
+
+ if nchannels < 2:
+ return
+
+ objects = []
+ # points = zip (records[0], records[1])
+ points = numpy.zeros ((len(records[0]), 2), numpy.float32)
+ points[:,0] = records[0]
+ points[:,1] = records[1]
+
+ self.SetXUseScopeTicks (False)
+
+ m = info.get_marker ()
+ if m == 'line':
+ objects.append (plot.PolyLine (points,
+ colour=self.channel_color (0)))
+ else:
+ objects.append (plot.PolyMarker (points,
+ marker=m,
+ colour=self.channel_color (0)))
+
+ graphics = plot.PlotGraphics (objects,
+ title=self.info.title,
+ xLabel = 'I', yLabel = 'Q')
+
+ self.Draw (graphics, xAxis=self.x_range, yAxis=self.y_range)
+ self.update_y_range ()
+ self.update_x_range ()
+
+
+ def update_y_range (self):
+ alpha = 1.0/25
+ graphics = self.last_draw[0]
+ p1, p2 = graphics.boundingBox () # min, max points of graphics
+
+ if self.avg_y_min: # prevent vertical scale from jumping abruptly --?
+ self.avg_y_min = p1[1] * alpha + self.avg_y_min * (1 - alpha)
+ self.avg_y_max = p2[1] * alpha + self.avg_y_max * (1 - alpha)
+ else: # initial guess
+ self.avg_y_min = p1[1] # -500.0 workaround, sometimes p1 is ~ 10^35
+ self.avg_y_max = p2[1] # 500.0
+
+ self.y_range = self._axisInterval ('auto', self.avg_y_min, self.avg_y_max)
+ # print "p1 %s p2 %s y_min %s y_max %s y_range %s" \
+ # % (p1, p2, self.avg_y_min, self.avg_y_max, self.y_range)
+
+
+ def update_x_range (self):
+ alpha = 1.0/25
+ graphics = self.last_draw[0]
+ p1, p2 = graphics.boundingBox () # min, max points of graphics
+
+ if self.avg_x_min:
+ self.avg_x_min = p1[0] * alpha + self.avg_x_min * (1 - alpha)
+ self.avg_x_max = p2[0] * alpha + self.avg_x_max * (1 - alpha)
+ else:
+ self.avg_x_min = p1[0]
+ self.avg_x_max = p2[0]
+
+ self.x_range = self._axisInterval ('auto', self.avg_x_min, self.avg_x_max)
+
+
+# ----------------------------------------------------------------
+# Stand-alone test application
+# ----------------------------------------------------------------
+
+class test_top_block (stdgui2.std_top_block):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
+
+ if len(argv) > 1:
+ frame_decim = int(argv[1])
+ else:
+ frame_decim = 1
+
+ if len(argv) > 2:
+ v_scale = float(argv[2]) # start up at this v_scale value
+ else:
+ v_scale = None # start up in autorange mode, default
+
+ if len(argv) > 3:
+ t_scale = float(argv[3]) # start up at this t_scale value
+ else:
+ t_scale = None # old behavior
+
+ print "frame decim %s v_scale %s t_scale %s" % (frame_decim,v_scale,t_scale)
+
+ input_rate = 1e6
+
+ # Generate a complex sinusoid
+ self.src0 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 25.1e3, 1e3)
+
+ # We add this throttle block so that this demo doesn't suck down
+ # all the CPU available. You normally wouldn't use it...
+ self.thr = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ scope = scope_sink_c (panel,"Secret Data",sample_rate=input_rate,
+ frame_decim=frame_decim,
+ v_scale=v_scale, t_scale=t_scale)
+ vbox.Add (scope.win, 1, wx.EXPAND)
+
+ # Ultimately this will be
+ # self.connect("src0 throttle scope")
+ self.connect(self.src0, self.thr, scope)
+
+def main ():
+ app = stdgui2.stdapp (test_top_block, "O'Scope Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
+
+# ----------------------------------------------------------------
diff --git a/gr-wxgui/src/python/slider.py b/gr-wxgui/src/python/slider.py
index e8cdcfcac..e8cdcfcac 100755..100644
--- a/gr-wxgui/src/python/slider.py
+++ b/gr-wxgui/src/python/slider.py
diff --git a/gr-wxgui/src/python/waterfall_window.py b/gr-wxgui/src/python/waterfall_window.py
new file mode 100644
index 000000000..3831fca90
--- /dev/null
+++ b/gr-wxgui/src/python/waterfall_window.py
@@ -0,0 +1,301 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import plotter
+import common
+import wx
+import numpy
+import math
+import pubsub
+from constants import *
+
+##################################################
+# Constants
+##################################################
+SLIDER_STEPS = 100
+AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
+DEFAULT_FRAME_RATE = 30
+DEFAULT_WIN_SIZE = (600, 300)
+DIV_LEVELS = (1, 2, 5, 10, 20)
+MIN_DYNAMIC_RANGE, MAX_DYNAMIC_RANGE = 10, 200
+COLOR_MODES = (
+ ('RGB1', 'rgb1'),
+ ('RGB2', 'rgb2'),
+ ('RGB3', 'rgb3'),
+ ('Gray', 'gray'),
+)
+
+##################################################
+# Waterfall window control panel
+##################################################
+class control_panel(wx.Panel):
+ """!
+ A control panel with wx widgits to control the plotter and fft block chain.
+ """
+
+ def __init__(self, parent):
+ """!
+ Create a new control panel.
+ @param parent the wx parent window
+ """
+ self.parent = parent
+ wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
+ #color mode
+ control_box.AddStretchSpacer()
+ self.color_mode_chooser = common.DropDownController(self, 'Color', COLOR_MODES, parent, COLOR_MODE_KEY)
+ control_box.Add(self.color_mode_chooser, 0, wx.EXPAND)
+ #average
+ control_box.AddStretchSpacer()
+ self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key)
+ control_box.Add(self.average_check_box, 0, wx.EXPAND)
+ control_box.AddSpacer(2)
+ self.avg_alpha_slider = common.LogSliderController(
+ self, 'Avg Alpha',
+ AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
+ parent.ext_controller, parent.avg_alpha_key,
+ formatter=lambda x: ': %.4f'%x,
+ )
+ parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable)
+ control_box.Add(self.avg_alpha_slider, 0, wx.EXPAND)
+ #dyanmic range buttons
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Dynamic Range'), 0, wx.ALIGN_CENTER)
+ control_box.AddSpacer(2)
+ self._dynamic_range_buttons = common.IncrDecrButtons(self, self._on_incr_dynamic_range, self._on_decr_dynamic_range)
+ control_box.Add(self._dynamic_range_buttons, 0, wx.ALIGN_CENTER)
+ #ref lvl buttons
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER)
+ control_box.AddSpacer(2)
+ self._ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level)
+ control_box.Add(self._ref_lvl_buttons, 0, wx.ALIGN_CENTER)
+ #num lines buttons
+ control_box.AddStretchSpacer()
+ control_box.Add(common.LabelText(self, 'Set Time Scale'), 0, wx.ALIGN_CENTER)
+ control_box.AddSpacer(2)
+ self._time_scale_buttons = common.IncrDecrButtons(self, self._on_incr_time_scale, self._on_decr_time_scale)
+ control_box.Add(self._time_scale_buttons, 0, wx.ALIGN_CENTER)
+ #autoscale
+ control_box.AddStretchSpacer()
+ self.autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT)
+ self.autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale)
+ control_box.Add(self.autoscale_button, 0, wx.EXPAND)
+ #clear
+ self.clear_button = wx.Button(self, label='Clear', style=wx.BU_EXACTFIT)
+ self.clear_button.Bind(wx.EVT_BUTTON, self._on_clear_button)
+ control_box.Add(self.clear_button, 0, wx.EXPAND)
+ #run/stop
+ self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
+ control_box.Add(self.run_button, 0, wx.EXPAND)
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+ ##################################################
+ # Event handlers
+ ##################################################
+ def _on_clear_button(self, event):
+ self.parent.set_num_lines(self.parent[NUM_LINES_KEY])
+ def _on_incr_dynamic_range(self, event):
+ self.parent.set_dynamic_range(
+ min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE))
+ def _on_decr_dynamic_range(self, event):
+ self.parent.set_dynamic_range(
+ max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE))
+ def _on_incr_ref_level(self, event):
+ self.parent.set_ref_level(
+ self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1)
+ def _on_decr_ref_level(self, event):
+ self.parent.set_ref_level(
+ self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1)
+ def _on_incr_time_scale(self, event):
+ old_rate = self.parent.ext_controller[self.parent.frame_rate_key]
+ self.parent.ext_controller[self.parent.frame_rate_key] *= 0.75
+ if self.parent.ext_controller[self.parent.frame_rate_key] == old_rate:
+ self.parent.ext_controller[self.parent.decimation_key] += 1
+ def _on_decr_time_scale(self, event):
+ old_rate = self.parent.ext_controller[self.parent.frame_rate_key]
+ self.parent.ext_controller[self.parent.frame_rate_key] *= 1.25
+ if self.parent.ext_controller[self.parent.frame_rate_key] == old_rate:
+ self.parent.ext_controller[self.parent.decimation_key] -= 1
+
+##################################################
+# Waterfall window with plotter and control panel
+##################################################
+class waterfall_window(wx.Panel, pubsub.pubsub, common.prop_setter):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ real,
+ fft_size,
+ num_lines,
+ decimation_key,
+ baseband_freq,
+ sample_rate_key,
+ frame_rate_key,
+ dynamic_range,
+ ref_level,
+ average_key,
+ avg_alpha_key,
+ msg_key,
+ ):
+ pubsub.pubsub.__init__(self)
+ #setup
+ self.ext_controller = controller
+ self.real = real
+ self.fft_size = fft_size
+ self.decimation_key = decimation_key
+ self.sample_rate_key = sample_rate_key
+ self.frame_rate_key = frame_rate_key
+ self.average_key = average_key
+ self.avg_alpha_key = avg_alpha_key
+ #init panel and plot
+ wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.waterfall_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.set_title(title)
+ self.plotter.enable_point_label(True)
+ #setup the box with plot and controls
+ self.control_panel = control_panel(self)
+ main_box = wx.BoxSizer(wx.HORIZONTAL)
+ main_box.Add(self.plotter, 1, wx.EXPAND)
+ main_box.Add(self.control_panel, 0, wx.EXPAND)
+ self.SetSizerAndFit(main_box)
+ #plotter listeners
+ self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
+ self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
+ #initial setup
+ self.ext_controller[self.average_key] = self.ext_controller[self.average_key]
+ self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key]
+ self._register_set_prop(self, DYNAMIC_RANGE_KEY, dynamic_range)
+ self._register_set_prop(self, NUM_LINES_KEY, num_lines)
+ self._register_set_prop(self, Y_DIVS_KEY, 8)
+ self._register_set_prop(self, X_DIVS_KEY, 8) #approximate
+ self._register_set_prop(self, REF_LEVEL_KEY, ref_level)
+ self._register_set_prop(self, BASEBAND_FREQ_KEY, baseband_freq)
+ self._register_set_prop(self, COLOR_MODE_KEY, COLOR_MODES[0][1])
+ self._register_set_prop(self, RUNNING_KEY, True)
+ #register events
+ self.ext_controller.subscribe(msg_key, self.handle_msg)
+ self.ext_controller.subscribe(self.decimation_key, self.update_grid)
+ self.ext_controller.subscribe(self.sample_rate_key, self.update_grid)
+ self.ext_controller.subscribe(self.frame_rate_key, self.update_grid)
+ self.subscribe(BASEBAND_FREQ_KEY, self.update_grid)
+ self.subscribe(NUM_LINES_KEY, self.update_grid)
+ self.subscribe(Y_DIVS_KEY, self.update_grid)
+ self.subscribe(X_DIVS_KEY, self.update_grid)
+ #initial update
+ self.update_grid()
+
+ def autoscale(self, *args):
+ """!
+ Autoscale the waterfall plot to the last frame.
+ Set the dynamic range and reference level.
+ Does not affect the current data in the waterfall.
+ """
+ #get the peak level (max of the samples)
+ peak_level = numpy.max(self.samples)
+ #get the noise floor (averge the smallest samples)
+ noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
+ #padding
+ noise_floor -= abs(noise_floor)*.5
+ peak_level += abs(peak_level)*.1
+ #set the range and level
+ self.set_ref_level(peak_level)
+ self.set_dynamic_range(peak_level - noise_floor)
+
+ def handle_msg(self, msg):
+ """!
+ Handle the message from the fft sink message queue.
+ If complex, reorder the fft samples so the negative bins come first.
+ If real, keep take only the positive bins.
+ Send the data to the plotter.
+ @param msg the fft array as a character array
+ """
+ if not self[RUNNING_KEY]: return
+ #convert to floating point numbers
+ self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
+ num_samps = len(samples)
+ #reorder fft
+ if self.real: samples = samples[:num_samps/2]
+ else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
+ #plot the fft
+ self.plotter.set_samples(
+ samples=samples,
+ minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY],
+ maximum=self[REF_LEVEL_KEY],
+ )
+ #update the plotter
+ self.plotter.update()
+
+ def update_grid(self, *args):
+ """!
+ Update the plotter grid.
+ This update method is dependent on the variables below.
+ Determine the x and y axis grid parameters.
+ The x axis depends on sample rate, baseband freq, and x divs.
+ The y axis depends on y per div, y divs, and ref level.
+ """
+ #grid parameters
+ sample_rate = self.ext_controller[self.sample_rate_key]
+ frame_rate = self.ext_controller[self.frame_rate_key]
+ baseband_freq = self[BASEBAND_FREQ_KEY]
+ num_lines = self[NUM_LINES_KEY]
+ y_divs = self[Y_DIVS_KEY]
+ x_divs = self[X_DIVS_KEY]
+ #determine best fitting x_per_div
+ if self.real: x_width = sample_rate/2.0
+ else: x_width = sample_rate/1.0
+ x_per_div = common.get_clean_num(x_width/x_divs)
+ coeff, exp, prefix = common.get_si_components(abs(baseband_freq) + abs(sample_rate/2.0))
+ #update the x grid
+ if self.real:
+ self.plotter.set_x_grid(
+ baseband_freq,
+ baseband_freq + sample_rate/2.0,
+ x_per_div,
+ 10**(-exp),
+ )
+ else:
+ self.plotter.set_x_grid(
+ baseband_freq - sample_rate/2.0,
+ baseband_freq + sample_rate/2.0,
+ x_per_div,
+ 10**(-exp),
+ )
+ #update x units
+ self.plotter.set_x_label('Frequency', prefix+'Hz')
+ #update y grid
+ duration = float(num_lines)/frame_rate
+ y_per_div = common.get_clean_num(duration/y_divs)
+ self.plotter.set_y_grid(0, duration, y_per_div)
+ #update y units
+ self.plotter.set_y_label('Time', 's')
+ #update plotter
+ self.plotter.update()
diff --git a/gr-wxgui/src/python/waterfallsink2.py b/gr-wxgui/src/python/waterfallsink2.py
index fb91d26e0..215193044 100755..100644
--- a/gr-wxgui/src/python/waterfallsink2.py
+++ b/gr-wxgui/src/python/waterfallsink2.py
@@ -1,437 +1,45 @@
-#!/usr/bin/env python
#
-# Copyright 2003,2004,2005,2007,2008 Free Software Foundation, Inc.
-#
+# Copyright 2008 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, gru, window
-from gnuradio.wxgui import stdgui2
-import wx
-import gnuradio.wxgui.plot as plot
-import numpy
-import os
-import threading
-import math
-
-default_fftsink_size = (640,240)
-default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
-
-class waterfall_sink_base(object):
- def __init__(self, input_is_real=False, baseband_freq=0,
- sample_rate=1, fft_size=512,
- fft_rate=default_fft_rate,
- average=False, avg_alpha=None, title=''):
-
- # initialize common attributes
- self.baseband_freq = baseband_freq
- self.sample_rate = sample_rate
- self.fft_size = fft_size
- self.fft_rate = fft_rate
- self.average = average
- if avg_alpha is None:
- self.avg_alpha = 2.0 / fft_rate
- else:
- self.avg_alpha = avg_alpha
- self.title = title
- self.input_is_real = input_is_real
- self.msgq = gr.msg_queue(2) # queue up to 2 messages
-
- def set_average(self, average):
- self.average = average
- if average:
- self.avg.set_taps(self.avg_alpha)
- else:
- self.avg.set_taps(1.0)
-
- def set_avg_alpha(self, avg_alpha):
- self.avg_alpha = avg_alpha
-
- def set_baseband_freq(self, baseband_freq):
- self.baseband_freq = baseband_freq
-
- def set_sample_rate(self, sample_rate):
- self.sample_rate = sample_rate
- self._set_n()
-
- def _set_n(self):
- self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
-
-class waterfall_sink_f(gr.hier_block2, waterfall_sink_base):
- def __init__(self, parent, baseband_freq=0,
- y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
- fft_rate=default_fft_rate, average=False, avg_alpha=None,
- title='', size=default_fftsink_size):
-
- gr.hier_block2.__init__(self, "waterfall_sink_f",
- gr.io_signature(1, 1, gr.sizeof_float),
- gr.io_signature(0,0,0))
-
- waterfall_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
- sample_rate=sample_rate, fft_size=fft_size,
- fft_rate=fft_rate,
- average=average, avg_alpha=avg_alpha, title=title)
-
- self.s2p = gr.serial_to_parallel(gr.sizeof_float, self.fft_size)
- self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
- max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
-
- mywindow = window.blackmanharris(self.fft_size)
- self.fft = gr.fft_vfc(self.fft_size, True, mywindow)
- self.c2mag = gr.complex_to_mag(self.fft_size)
- self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
- self.log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
- self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
- self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
-
- self.win = waterfall_window(self, parent, size=size)
- self.set_average(self.average)
-
-
-class waterfall_sink_c(gr.hier_block2, waterfall_sink_base):
- def __init__(self, parent, baseband_freq=0,
- y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
- fft_rate=default_fft_rate, average=False, avg_alpha=None,
- title='', size=default_fftsink_size):
-
- gr.hier_block2.__init__(self, "waterfall_sink_f",
- gr.io_signature(1, 1, gr.sizeof_gr_complex),
- gr.io_signature(0,0,0))
-
- waterfall_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
- sample_rate=sample_rate, fft_size=fft_size,
- fft_rate=fft_rate,
- average=average, avg_alpha=avg_alpha, title=title)
-
- self.s2p = gr.serial_to_parallel(gr.sizeof_gr_complex, self.fft_size)
- self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
- max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
-
- mywindow = window.blackmanharris(self.fft_size)
- self.fft = gr.fft_vcc(self.fft_size, True, mywindow)
- self.c2mag = gr.complex_to_mag(self.fft_size)
- self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
- self.log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
- self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
- self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
-
- self.win = waterfall_window(self, parent, size=size)
- self.set_average(self.average)
-
-
-# ------------------------------------------------------------------------
-
-myDATA_EVENT = wx.NewEventType()
-EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
-
-
-class DataEvent(wx.PyEvent):
- def __init__(self, data):
- wx.PyEvent.__init__(self)
- self.SetEventType (myDATA_EVENT)
- self.data = data
-
- def Clone (self):
- self.__class__ (self.GetId())
-
-
-class input_watcher (threading.Thread):
- def __init__ (self, msgq, fft_size, event_receiver, **kwds):
- threading.Thread.__init__ (self, **kwds)
- self.setDaemon (1)
- self.msgq = msgq
- self.fft_size = fft_size
- self.event_receiver = event_receiver
- self.keep_running = True
- self.start ()
-
- def run (self):
- while (self.keep_running):
- msg = self.msgq.delete_head() # blocking read of message queue
- itemsize = int(msg.arg1())
- nitems = int(msg.arg2())
-
- s = msg.to_string() # get the body of the msg as a string
-
- # There may be more than one FFT frame in the message.
- # If so, we take only the last one
- if nitems > 1:
- start = itemsize * (nitems - 1)
- s = s[start:start+itemsize]
-
- complex_data = numpy.fromstring (s, numpy.float32)
- de = DataEvent (complex_data)
- wx.PostEvent (self.event_receiver, de)
- del de
-
-
-class waterfall_window (wx.Panel):
- def __init__ (self, fftsink, parent, id = -1,
- pos = wx.DefaultPosition, size = wx.DefaultSize,
- style = wx.DEFAULT_FRAME_STYLE, name = ""):
- wx.Panel.__init__(self, parent, id, pos, size, style, name)
-
- self.fftsink = fftsink
- self.bm = wx.EmptyBitmap(self.fftsink.fft_size, 300, -1)
-
- self.scale_factor = 5.0 # FIXME should autoscale, or set this
-
- dc1 = wx.MemoryDC()
- dc1.SelectObject(self.bm)
- dc1.Clear()
-
- self.pens = self.make_pens()
-
- wx.EVT_PAINT( self, self.OnPaint )
- wx.EVT_CLOSE (self, self.on_close_window)
- EVT_DATA_EVENT (self, self.set_data)
-
- self.build_popup_menu()
-
- wx.EVT_CLOSE (self, self.on_close_window)
- self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
-
- self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
-
-
- def on_close_window (self, event):
- print "waterfall_window: on_close_window"
- self.keep_running = False
-
- def const_list(self,const,len):
- return [const] * len
-
- def make_colormap(self):
- r = []
- r.extend(self.const_list(0,96))
- r.extend(range(0,255,4))
- r.extend(self.const_list(255,64))
- r.extend(range(255,128,-4))
-
- g = []
- g.extend(self.const_list(0,32))
- g.extend(range(0,255,4))
- g.extend(self.const_list(255,64))
- g.extend(range(255,0,-4))
- g.extend(self.const_list(0,32))
-
- b = range(128,255,4)
- b.extend(self.const_list(255,64))
- b.extend(range(255,0,-4))
- b.extend(self.const_list(0,96))
- return (r,g,b)
-
- def make_pens(self):
- (r,g,b) = self.make_colormap()
- pens = []
- for i in range(0,256):
- colour = wx.Colour(r[i], g[i], b[i])
- pens.append( wx.Pen(colour, 2, wx.SOLID))
- return pens
-
- def OnPaint(self, event):
- dc = wx.PaintDC(self)
- self.DoDrawing(dc)
-
- def DoDrawing(self, dc=None):
- if dc is None:
- dc = wx.ClientDC(self)
- dc.DrawBitmap(self.bm, 0, 0, False )
-
-
- def const_list(self,const,len):
- a = [const]
- for i in range(1,len):
- a.append(const)
- return a
-
-
- def set_data (self, evt):
- dB = evt.data
- L = len (dB)
-
- dc1 = wx.MemoryDC()
- dc1.SelectObject(self.bm)
- dc1.Blit(0,1,self.fftsink.fft_size,300,dc1,0,0,wx.COPY,False,-1,-1)
-
- x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
- if x >= 1e9:
- sf = 1e-9
- units = "GHz"
- elif x >= 1e6:
- sf = 1e-6
- units = "MHz"
- else:
- sf = 1e-3
- units = "kHz"
-
-
- if self.fftsink.input_is_real: # only plot 1/2 the points
- d_max = L/2
- p_width = 2
- else:
- d_max = L/2
- p_width = 1
-
- scale_factor = self.scale_factor
- if self.fftsink.input_is_real: # real fft
- for x_pos in range(0, d_max):
- value = int(dB[x_pos] * scale_factor)
- value = min(255, max(0, value))
- dc1.SetPen(self.pens[value])
- dc1.DrawRectangle(x_pos*p_width, 0, p_width, 2)
- else: # complex fft
- for x_pos in range(0, d_max): # positive freqs
- value = int(dB[x_pos] * scale_factor)
- value = min(255, max(0, value))
- dc1.SetPen(self.pens[value])
- dc1.DrawRectangle(x_pos*p_width + d_max, 0, p_width, 2)
- for x_pos in range(0 , d_max): # negative freqs
- value = int(dB[x_pos+d_max] * scale_factor)
- value = min(255, max(0, value))
- dc1.SetPen(self.pens[value])
- dc1.DrawRectangle(x_pos*p_width, 0, p_width, 2)
-
- del dc1
- self.DoDrawing (None)
-
- def on_average(self, evt):
- # print "on_average"
- self.fftsink.set_average(evt.IsChecked())
-
- def on_right_click(self, event):
- menu = self.popup_menu
- for id, pred in self.checkmarks.items():
- item = menu.FindItemById(id)
- item.Check(pred())
- self.PopupMenu(menu, event.GetPosition())
-
-
- def build_popup_menu(self):
- self.id_incr_ref_level = wx.NewId()
- self.id_decr_ref_level = wx.NewId()
- self.id_incr_y_per_div = wx.NewId()
- self.id_decr_y_per_div = wx.NewId()
- self.id_y_per_div_1 = wx.NewId()
- self.id_y_per_div_2 = wx.NewId()
- self.id_y_per_div_5 = wx.NewId()
- self.id_y_per_div_10 = wx.NewId()
- self.id_y_per_div_20 = wx.NewId()
- self.id_average = wx.NewId()
-
- self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
- #self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
- #self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
- #self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
- #self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
- #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
- #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
- #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
- #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
- #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
-
-
- # make a menu
- menu = wx.Menu()
- self.popup_menu = menu
- menu.AppendCheckItem(self.id_average, "Average")
- # menu.Append(self.id_incr_ref_level, "Incr Ref Level")
- # menu.Append(self.id_decr_ref_level, "Decr Ref Level")
- # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
- # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
- # menu.AppendSeparator()
- # we'd use RadioItems for these, but they're not supported on Mac
- #menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
- #menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
- #menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
- #menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
- #menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
-
- self.checkmarks = {
- self.id_average : lambda : self.fftsink.average
- #self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
- #self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
- #self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
- #self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
- #self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
- }
-
-
-def next_up(v, seq):
- """
- Return the first item in seq that is > v.
- """
- for s in seq:
- if s > v:
- return s
- return v
-
-def next_down(v, seq):
- """
- Return the last item in seq that is < v.
- """
- rseq = list(seq[:])
- rseq.reverse()
-
- for s in rseq:
- if s < v:
- return s
- return v
-
-
-# ----------------------------------------------------------------
-# Standalone test app
-# ----------------------------------------------------------------
-
-class test_top_block (stdgui2.std_top_block):
- def __init__(self, frame, panel, vbox, argv):
- stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
-
- fft_size = 512
-
- # build our flow graph
- input_rate = 20.000e3
-
- # Generate a complex sinusoid
- self.src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
- #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
+#
- # We add these throttle blocks so that this demo doesn't
- # suck down all the CPU available. Normally you wouldn't use these.
- self.thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
+from gnuradio import gr
- sink1 = waterfall_sink_c (panel, title="Complex Data", fft_size=fft_size,
- sample_rate=input_rate, baseband_freq=100e3)
- self.connect(self.src1, self.thr1, sink1)
- vbox.Add (sink1.win, 1, wx.EXPAND)
+p = gr.prefs()
+style = p.get_string('wxgui', 'style', 'auto')
- # generate a real sinusoid
- self.src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
- self.thr2 = gr.throttle(gr.sizeof_float, input_rate)
- sink2 = waterfall_sink_f (panel, title="Real Data", fft_size=fft_size,
- sample_rate=input_rate, baseband_freq=100e3)
- self.connect(self.src2, self.thr2, sink2)
- vbox.Add (sink2.win, 1, wx.EXPAND)
+# In 3.2 we'll change 'auto' to mean 'gl' if possible, then fallback
+if style == 'auto':
+ style = 'nongl'
+if style == 'nongl':
+ from waterfallsink_nongl import waterfall_sink_f, waterfall_sink_c
+elif style == 'gl':
+ try:
+ import wx
+ wx.glcanvas.GLCanvas
+ except AttributeError:
+ raise RuntimeError("wxPython doesn't support glcanvas")
-def main ():
- app = stdgui2.stdapp (test_top_block, "Waterfall Sink Test App")
- app.MainLoop ()
+ try:
+ from OpenGL.GL import *
+ except ImportError:
+ raise RuntimeError("Unable to import OpenGL. Are Python wrappers for OpenGL installed?")
-if __name__ == '__main__':
- main ()
+ from waterfallsink_gl import waterfall_sink_f, waterfall_sink_c
diff --git a/gr-wxgui/src/python/waterfallsink_gl.py b/gr-wxgui/src/python/waterfallsink_gl.py
new file mode 100644
index 000000000..0b36e1048
--- /dev/null
+++ b/gr-wxgui/src/python/waterfallsink_gl.py
@@ -0,0 +1,175 @@
+#
+# Copyright 2008 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.
+#
+
+##################################################
+# Imports
+##################################################
+import waterfall_window
+import common
+from gnuradio import gr, blks2
+from pubsub import pubsub
+from constants import *
+
+##################################################
+# Waterfall sink block (wrapper for old wxgui)
+##################################################
+class _waterfall_sink_base(gr.hier_block2, common.prop_setter):
+ """!
+ An fft block with real/complex inputs and a gui window.
+ """
+
+ def __init__(
+ self,
+ parent,
+ baseband_freq=0,
+ y_per_div=None, #ignore (old wrapper)
+ ref_level=50,
+ sample_rate=1,
+ fft_size=512,
+ fft_rate=waterfall_window.DEFAULT_FRAME_RATE,
+ average=False,
+ avg_alpha=None,
+ title='',
+ size=waterfall_window.DEFAULT_WIN_SIZE,
+ ref_scale=2.0,
+ dynamic_range=80,
+ num_lines=256,
+ ):
+ #ensure avg alpha
+ if avg_alpha is None: avg_alpha = 2.0/fft_rate
+ #init
+ gr.hier_block2.__init__(
+ self,
+ "waterfall_sink",
+ gr.io_signature(1, 1, self._item_size),
+ gr.io_signature(0, 0, 0),
+ )
+ #blocks
+ copy = gr.kludge_copy(self._item_size)
+ fft = self._fft_chain(
+ sample_rate=sample_rate,
+ fft_size=fft_size,
+ frame_rate=fft_rate,
+ ref_scale=ref_scale,
+ avg_alpha=avg_alpha,
+ average=average,
+ )
+ msgq = gr.msg_queue(2)
+ sink = gr.message_sink(gr.sizeof_float*fft_size, msgq, True)
+ #connect
+ self.connect(self, copy, fft, sink)
+ #controller
+ self.controller = pubsub()
+ self.controller.subscribe(AVERAGE_KEY, fft.set_average)
+ self.controller.publish(AVERAGE_KEY, fft.average)
+ self.controller.subscribe(AVG_ALPHA_KEY, fft.set_avg_alpha)
+ self.controller.publish(AVG_ALPHA_KEY, fft.avg_alpha)
+ self.controller.subscribe(SAMPLE_RATE_KEY, fft.set_sample_rate)
+ self.controller.publish(SAMPLE_RATE_KEY, fft.sample_rate)
+ self.controller.subscribe(DECIMATION_KEY, fft.set_decimation)
+ self.controller.publish(DECIMATION_KEY, fft.decimation)
+ self.controller.subscribe(FRAME_RATE_KEY, fft.set_vec_rate)
+ self.controller.publish(FRAME_RATE_KEY, fft.frame_rate)
+ #start input watcher
+ def setter(p, k, x): # lambdas can't have assignments :(
+ p[k] = x
+ common.input_watcher(msgq, lambda x: setter(self.controller, MSG_KEY, x))
+ #create window
+ self.win = waterfall_window.waterfall_window(
+ parent=parent,
+ controller=self.controller,
+ size=size,
+ title=title,
+ real=self._real,
+ fft_size=fft_size,
+ num_lines=num_lines,
+ baseband_freq=baseband_freq,
+ decimation_key=DECIMATION_KEY,
+ sample_rate_key=SAMPLE_RATE_KEY,
+ frame_rate_key=FRAME_RATE_KEY,
+ dynamic_range=dynamic_range,
+ ref_level=ref_level,
+ average_key=AVERAGE_KEY,
+ avg_alpha_key=AVG_ALPHA_KEY,
+ msg_key=MSG_KEY,
+ )
+ #register callbacks from window for external use
+ for attr in filter(lambda a: a.startswith('set_'), dir(self.win)):
+ setattr(self, attr, getattr(self.win, attr))
+ self._register_set_prop(self.controller, SAMPLE_RATE_KEY)
+ self._register_set_prop(self.controller, AVERAGE_KEY)
+ self._register_set_prop(self.controller, AVG_ALPHA_KEY)
+
+class waterfall_sink_f(_waterfall_sink_base):
+ _fft_chain = blks2.logpwrfft_f
+ _item_size = gr.sizeof_float
+ _real = True
+
+class waterfall_sink_c(_waterfall_sink_base):
+ _fft_chain = blks2.logpwrfft_c
+ _item_size = gr.sizeof_gr_complex
+ _real = False
+
+# ----------------------------------------------------------------
+# Standalone test app
+# ----------------------------------------------------------------
+
+import wx
+from gnuradio.wxgui import stdgui2
+
+class test_top_block (stdgui2.std_top_block):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
+
+ fft_size = 512
+
+ # build our flow graph
+ input_rate = 20.000e3
+
+ # Generate a complex sinusoid
+ self.src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
+ #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
+
+ # We add these throttle blocks so that this demo doesn't
+ # suck down all the CPU available. Normally you wouldn't use these.
+ self.thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ sink1 = waterfall_sink_c (panel, title="Complex Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3)
+ self.connect(self.src1, self.thr1, sink1)
+ vbox.Add (sink1.win, 1, wx.EXPAND)
+
+ # generate a real sinusoid
+ self.src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
+ self.thr2 = gr.throttle(gr.sizeof_float, input_rate)
+ sink2 = waterfall_sink_f (panel, title="Real Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3)
+ self.connect(self.src2, self.thr2, sink2)
+ vbox.Add (sink2.win, 1, wx.EXPAND)
+
+
+def main ():
+ app = stdgui2.stdapp (test_top_block, "Waterfall Sink Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
+
diff --git a/gr-wxgui/src/python/waterfallsink_nongl.py b/gr-wxgui/src/python/waterfallsink_nongl.py
new file mode 100644
index 000000000..9d97c4e3c
--- /dev/null
+++ b/gr-wxgui/src/python/waterfallsink_nongl.py
@@ -0,0 +1,437 @@
+#!/usr/bin/env python
+#
+# Copyright 2003,2004,2005,2007,2008 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, gru, window
+from gnuradio.wxgui import stdgui2
+import wx
+import gnuradio.wxgui.plot as plot
+import numpy
+import os
+import threading
+import math
+
+default_fftsink_size = (640,240)
+default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
+
+class waterfall_sink_base(object):
+ def __init__(self, input_is_real=False, baseband_freq=0,
+ sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate,
+ average=False, avg_alpha=None, title=''):
+
+ # initialize common attributes
+ self.baseband_freq = baseband_freq
+ self.sample_rate = sample_rate
+ self.fft_size = fft_size
+ self.fft_rate = fft_rate
+ self.average = average
+ if avg_alpha is None:
+ self.avg_alpha = 2.0 / fft_rate
+ else:
+ self.avg_alpha = avg_alpha
+ self.title = title
+ self.input_is_real = input_is_real
+ self.msgq = gr.msg_queue(2) # queue up to 2 messages
+
+ def set_average(self, average):
+ self.average = average
+ if average:
+ self.avg.set_taps(self.avg_alpha)
+ else:
+ self.avg.set_taps(1.0)
+
+ def set_avg_alpha(self, avg_alpha):
+ self.avg_alpha = avg_alpha
+
+ def set_baseband_freq(self, baseband_freq):
+ self.baseband_freq = baseband_freq
+
+ def set_sample_rate(self, sample_rate):
+ self.sample_rate = sample_rate
+ self._set_n()
+
+ def _set_n(self):
+ self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+class waterfall_sink_f(gr.hier_block2, waterfall_sink_base):
+ def __init__(self, parent, baseband_freq=0,
+ y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate, average=False, avg_alpha=None,
+ title='', size=default_fftsink_size):
+
+ gr.hier_block2.__init__(self, "waterfall_sink_f",
+ gr.io_signature(1, 1, gr.sizeof_float),
+ gr.io_signature(0,0,0))
+
+ waterfall_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
+ sample_rate=sample_rate, fft_size=fft_size,
+ fft_rate=fft_rate,
+ average=average, avg_alpha=avg_alpha, title=title)
+
+ self.s2p = gr.serial_to_parallel(gr.sizeof_float, self.fft_size)
+ self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
+ max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+ mywindow = window.blackmanharris(self.fft_size)
+ self.fft = gr.fft_vfc(self.fft_size, True, mywindow)
+ self.c2mag = gr.complex_to_mag(self.fft_size)
+ self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
+ self.log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
+ self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
+ self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
+
+ self.win = waterfall_window(self, parent, size=size)
+ self.set_average(self.average)
+
+
+class waterfall_sink_c(gr.hier_block2, waterfall_sink_base):
+ def __init__(self, parent, baseband_freq=0,
+ y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate, average=False, avg_alpha=None,
+ title='', size=default_fftsink_size):
+
+ gr.hier_block2.__init__(self, "waterfall_sink_f",
+ gr.io_signature(1, 1, gr.sizeof_gr_complex),
+ gr.io_signature(0,0,0))
+
+ waterfall_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
+ sample_rate=sample_rate, fft_size=fft_size,
+ fft_rate=fft_rate,
+ average=average, avg_alpha=avg_alpha, title=title)
+
+ self.s2p = gr.serial_to_parallel(gr.sizeof_gr_complex, self.fft_size)
+ self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
+ max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+ mywindow = window.blackmanharris(self.fft_size)
+ self.fft = gr.fft_vcc(self.fft_size, True, mywindow)
+ self.c2mag = gr.complex_to_mag(self.fft_size)
+ self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
+ self.log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
+ self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
+ self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
+
+ self.win = waterfall_window(self, parent, size=size)
+ self.set_average(self.average)
+
+
+# ------------------------------------------------------------------------
+
+myDATA_EVENT = wx.NewEventType()
+EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
+
+
+class DataEvent(wx.PyEvent):
+ def __init__(self, data):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (myDATA_EVENT)
+ self.data = data
+
+ def Clone (self):
+ self.__class__ (self.GetId())
+
+
+class input_watcher (threading.Thread):
+ def __init__ (self, msgq, fft_size, event_receiver, **kwds):
+ threading.Thread.__init__ (self, **kwds)
+ self.setDaemon (1)
+ self.msgq = msgq
+ self.fft_size = fft_size
+ self.event_receiver = event_receiver
+ self.keep_running = True
+ self.start ()
+
+ def run (self):
+ while (self.keep_running):
+ msg = self.msgq.delete_head() # blocking read of message queue
+ itemsize = int(msg.arg1())
+ nitems = int(msg.arg2())
+
+ s = msg.to_string() # get the body of the msg as a string
+
+ # There may be more than one FFT frame in the message.
+ # If so, we take only the last one
+ if nitems > 1:
+ start = itemsize * (nitems - 1)
+ s = s[start:start+itemsize]
+
+ complex_data = numpy.fromstring (s, numpy.float32)
+ de = DataEvent (complex_data)
+ wx.PostEvent (self.event_receiver, de)
+ del de
+
+
+class waterfall_window (wx.Panel):
+ def __init__ (self, fftsink, parent, id = -1,
+ pos = wx.DefaultPosition, size = wx.DefaultSize,
+ style = wx.DEFAULT_FRAME_STYLE, name = ""):
+ wx.Panel.__init__(self, parent, id, pos, size, style, name)
+ self.set_baseband_freq = fftsink.set_baseband_freq
+ self.fftsink = fftsink
+ self.bm = wx.EmptyBitmap(self.fftsink.fft_size, 300, -1)
+
+ self.scale_factor = 5.0 # FIXME should autoscale, or set this
+
+ dc1 = wx.MemoryDC()
+ dc1.SelectObject(self.bm)
+ dc1.Clear()
+
+ self.pens = self.make_pens()
+
+ wx.EVT_PAINT( self, self.OnPaint )
+ wx.EVT_CLOSE (self, self.on_close_window)
+ EVT_DATA_EVENT (self, self.set_data)
+
+ self.build_popup_menu()
+
+ wx.EVT_CLOSE (self, self.on_close_window)
+ self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
+
+ self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
+
+
+ def on_close_window (self, event):
+ print "waterfall_window: on_close_window"
+ self.keep_running = False
+
+ def const_list(self,const,len):
+ return [const] * len
+
+ def make_colormap(self):
+ r = []
+ r.extend(self.const_list(0,96))
+ r.extend(range(0,255,4))
+ r.extend(self.const_list(255,64))
+ r.extend(range(255,128,-4))
+
+ g = []
+ g.extend(self.const_list(0,32))
+ g.extend(range(0,255,4))
+ g.extend(self.const_list(255,64))
+ g.extend(range(255,0,-4))
+ g.extend(self.const_list(0,32))
+
+ b = range(128,255,4)
+ b.extend(self.const_list(255,64))
+ b.extend(range(255,0,-4))
+ b.extend(self.const_list(0,96))
+ return (r,g,b)
+
+ def make_pens(self):
+ (r,g,b) = self.make_colormap()
+ pens = []
+ for i in range(0,256):
+ colour = wx.Colour(r[i], g[i], b[i])
+ pens.append( wx.Pen(colour, 2, wx.SOLID))
+ return pens
+
+ def OnPaint(self, event):
+ dc = wx.PaintDC(self)
+ self.DoDrawing(dc)
+
+ def DoDrawing(self, dc=None):
+ if dc is None:
+ dc = wx.ClientDC(self)
+ dc.DrawBitmap(self.bm, 0, 0, False )
+
+
+ def const_list(self,const,len):
+ a = [const]
+ for i in range(1,len):
+ a.append(const)
+ return a
+
+
+ def set_data (self, evt):
+ dB = evt.data
+ L = len (dB)
+
+ dc1 = wx.MemoryDC()
+ dc1.SelectObject(self.bm)
+ dc1.Blit(0,1,self.fftsink.fft_size,300,dc1,0,0,wx.COPY,False,-1,-1)
+
+ x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
+ if x >= 1e9:
+ sf = 1e-9
+ units = "GHz"
+ elif x >= 1e6:
+ sf = 1e-6
+ units = "MHz"
+ else:
+ sf = 1e-3
+ units = "kHz"
+
+
+ if self.fftsink.input_is_real: # only plot 1/2 the points
+ d_max = L/2
+ p_width = 2
+ else:
+ d_max = L/2
+ p_width = 1
+
+ scale_factor = self.scale_factor
+ if self.fftsink.input_is_real: # real fft
+ for x_pos in range(0, d_max):
+ value = int(dB[x_pos] * scale_factor)
+ value = min(255, max(0, value))
+ dc1.SetPen(self.pens[value])
+ dc1.DrawRectangle(x_pos*p_width, 0, p_width, 2)
+ else: # complex fft
+ for x_pos in range(0, d_max): # positive freqs
+ value = int(dB[x_pos] * scale_factor)
+ value = min(255, max(0, value))
+ dc1.SetPen(self.pens[value])
+ dc1.DrawRectangle(x_pos*p_width + d_max, 0, p_width, 2)
+ for x_pos in range(0 , d_max): # negative freqs
+ value = int(dB[x_pos+d_max] * scale_factor)
+ value = min(255, max(0, value))
+ dc1.SetPen(self.pens[value])
+ dc1.DrawRectangle(x_pos*p_width, 0, p_width, 2)
+
+ del dc1
+ self.DoDrawing (None)
+
+ def on_average(self, evt):
+ # print "on_average"
+ self.fftsink.set_average(evt.IsChecked())
+
+ def on_right_click(self, event):
+ menu = self.popup_menu
+ for id, pred in self.checkmarks.items():
+ item = menu.FindItemById(id)
+ item.Check(pred())
+ self.PopupMenu(menu, event.GetPosition())
+
+
+ def build_popup_menu(self):
+ self.id_incr_ref_level = wx.NewId()
+ self.id_decr_ref_level = wx.NewId()
+ self.id_incr_y_per_div = wx.NewId()
+ self.id_decr_y_per_div = wx.NewId()
+ self.id_y_per_div_1 = wx.NewId()
+ self.id_y_per_div_2 = wx.NewId()
+ self.id_y_per_div_5 = wx.NewId()
+ self.id_y_per_div_10 = wx.NewId()
+ self.id_y_per_div_20 = wx.NewId()
+ self.id_average = wx.NewId()
+
+ self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
+ #self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
+ #self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
+ #self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
+ #self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
+
+
+ # make a menu
+ menu = wx.Menu()
+ self.popup_menu = menu
+ menu.AppendCheckItem(self.id_average, "Average")
+ # menu.Append(self.id_incr_ref_level, "Incr Ref Level")
+ # menu.Append(self.id_decr_ref_level, "Decr Ref Level")
+ # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
+ # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
+ # menu.AppendSeparator()
+ # we'd use RadioItems for these, but they're not supported on Mac
+ #menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
+ #menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
+ #menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
+ #menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
+ #menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
+
+ self.checkmarks = {
+ self.id_average : lambda : self.fftsink.average
+ #self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
+ #self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
+ #self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
+ #self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
+ #self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
+ }
+
+
+def next_up(v, seq):
+ """
+ Return the first item in seq that is > v.
+ """
+ for s in seq:
+ if s > v:
+ return s
+ return v
+
+def next_down(v, seq):
+ """
+ Return the last item in seq that is < v.
+ """
+ rseq = list(seq[:])
+ rseq.reverse()
+
+ for s in rseq:
+ if s < v:
+ return s
+ return v
+
+
+# ----------------------------------------------------------------
+# Standalone test app
+# ----------------------------------------------------------------
+
+class test_top_block (stdgui2.std_top_block):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
+
+ fft_size = 512
+
+ # build our flow graph
+ input_rate = 20.000e3
+
+ # Generate a complex sinusoid
+ self.src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
+ #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
+
+ # We add these throttle blocks so that this demo doesn't
+ # suck down all the CPU available. Normally you wouldn't use these.
+ self.thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ sink1 = waterfall_sink_c (panel, title="Complex Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3)
+ self.connect(self.src1, self.thr1, sink1)
+ vbox.Add (sink1.win, 1, wx.EXPAND)
+
+ # generate a real sinusoid
+ self.src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
+ self.thr2 = gr.throttle(gr.sizeof_float, input_rate)
+ sink2 = waterfall_sink_f (panel, title="Real Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3)
+ self.connect(self.src2, self.thr2, sink2)
+ vbox.Add (sink2.win, 1, wx.EXPAND)
+
+
+def main ():
+ app = stdgui2.stdapp (test_top_block, "Waterfall Sink Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()