diff options
37 files changed, 6068 insertions, 2111 deletions
@@ -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 () |